本項目隸屬於 HslCommunication 項目的SDK套件,如果不清楚HslCommunication組件的話,可以先了解那個項目,源代碼地址:https://github.com/dathlin/HslCommunication
聯系作者及加群方式(激活碼在群里發放):http://www.hslcommunication.cn/Cooperation
本項目源代碼地址:https://github.com/dathlin/SharpNodeSettings
本項目的主要實現的功能主要有2個:
- 實現單個設備信息的可配置,可存儲,采用一個相對標准的Xml存儲機制實現,適用的場景是:如果你有20個西門子PLC(種類需要一致),但是PLC的ip地址不一致,或是具體的型號不一致,需要進行可視化的存儲
- 實現一個數據網關中心,內置了一個自身協議的網絡,當然您也可以實現其他的,比如示例項目里的Redis數據網關,OPC UA數據網關。
本項目的所有的核心構建,都是圍繞一定格式的Xml文件展開的,以 NodeClass 作為節點的基類,賦予每個節點 Name 值,Description 值,節點下可以跟隨子節點,或是跟隨設備節點,設備下可以跟隨請求節點,多說無益,直接上代碼
<?xml version="1.0" encoding="utf-8"?>
<Settings>
<NodeClass Name="Devices" Description="所有的設備的集合對象">
<NodeClass Name="分廠一" Description="">
<NodeClass Name="車間一" Description="" />
<NodeClass Name="車間二" Description="">
<DeviceNode Name="ModbusTcp客戶端" Description="這是描述" DeviceType="10" ConnectTimeOut="1000" CreateTime="2018/8/9 19:58:49" InstallationDate="2018/8/9 19:58:49" IpAddress="127.0.0.1" Port="502" Station="1" IsAddressStartWithZero="true" IsWordReverse="false" IsStringReverse="false">
<DeviceRequest Name="數據請求" Description="一次完整的數據請求" Address="0" Length="30" CaptureInterval="1000" PraseRegularCode="ABCD" />
</DeviceNode>
</NodeClass>
</NodeClass>
<NodeClass Name="分廠二" Description="位於西南方">
<NodeClass Name="車間三" Description="">
<DeviceNode Name="測試設備二" Description="這是測試設備二的描述" DeviceType="10" ConnectTimeOut="1000" CreateTime="2018/8/10 23:01:28" InstallationDate="2018/8/10 23:01:28" IpAddress="127.0.0.1" Port="502" Station="1" IsAddressStartWithZero="true" IsWordReverse="false" IsStringReverse="false">
<DeviceRequest Name="數據請求" Description="一次完整的數據請求" Address="100" Length="10" CaptureInterval="500" PraseRegularCode="B" />
</DeviceNode>
</NodeClass>
</NodeClass>
</NodeClass>
<NodeClass Name="Server" Description="所有掛載的服務器">
<ServerNode Name="異形服務器" Description="這是一個異形服務器" CreateTime="2018/8/8 13:29:30" Port="1234" ServerType="2" Password="" />
</NodeClass>
<NodeClass Name="Regular" Description="所有的解析規則的信息">
<RegularNode Name="ABCD" Description="">
<RegularItemNode Name="溫度" Description="" Index="0" TypeCode="3" TypeLength="1" />
<RegularItemNode Name="風俗" Description="" Index="2" TypeCode="9" TypeLength="1" />
<RegularItemNode Name="轉速" Description="" Index="14" TypeCode="9" TypeLength="1" />
<RegularItemNode Name="機器人關節" Description="" Index="18" TypeCode="9" TypeLength="6" />
<RegularItemNode Name="cvsdf" Description="" Index="42" TypeCode="9" TypeLength="1" />
<RegularItemNode Name="條碼" Description="條碼信息" Index="6" TypeCode="11" TypeLength="8" />
<RegularItemNode Name="開關量" Description="設備的開關量信息" Index="368" TypeCode="1" TypeLength="8" />
</RegularNode>
<RegularNode Name="B" Description="">
<RegularItemNode Name="溫度" Description="" Index="0" TypeCode="3" TypeLength="1" />
<RegularItemNode Name="壓力" Description="" Index="2" TypeCode="3" TypeLength="1" />
</RegularNode>
</NodeClass>
</Settings>
以上就是一個示例的XML文件,手動創建這樣的一個數據表將會是難以想象的,所以本組件提供了可視化的數據創建中心,
Form nodeSettings = new SharpNodeSettings.View.FormNodeSetting( "settings.xml" ) nodeSettings.ShowDialog();
這樣就可以顯示一個窗體,顯示節點配置信息了。

不僅可以配置左側的節點,設備信息,還支持配置解析規則和可視化的顯示,輔助你找到正確的字節索引。點擊保存,即可生成上述示例的一個xml配置表。
我們有了這個配置文件后,如何才能解析出來,並且生成相應的設備呢?
我們可以調用 SharpNodeServer 來創建服務器應用,可以生成相應的節點信息,並且根據配置信息來請求設備,更新對應的數據。創建服務器的代碼如下:
SharpNodeServer sharpNodeServer = new SharpNodeServer( ); sharpNodeServer.LoadByXmlFile( "settings.xml" ); sharpNodeServer.ServerStart( 12345 );
這樣就啟動了一個最簡單的服務器,主要包含實例化,加載配置,啟動服務器,注意:加載配置必須放置到服務器啟動之前。
怎樣查看服務器的數據呢?內置了一個默認的 SimplifyNet 服務器,想要知道更多的這個服務器的內容,可以參照下面的博客:https://www.cnblogs.com/dathlin/p/7697782.html
基於 NetSimplifyClient 實現了一個通用的數據節點查看器,需要指定服務器的Ip地址和端口號:
SharpNodeSettings.View.FormNodeView form = new SharpNodeSettings.View.FormNodeView( "127.0.0.1",12345 ); form.ShowDialog();
如果你想實現訪問單個的數據,可以使用 NetSimplifyClient 創建的Demo來訪問,需要注意的是,此處請求的數據都是序列化的JSON字符串。 
在實際開發中,可能你不需要上述的配置功能,你就想實現某個PLC的設備信息是可配置的,那么也可以通過本組件實現:
SharpNodeSettings.View.FormSelectDevice selectDevice = new View.FormSelectDevice( );
if (selectDevice.ShowDialog( ) == DialogResult.OK)
{
XElement xmlDevice = selectDevice.DeviceXml;
// 設備的配置對象可用於存儲,網絡傳輸等等操作
// 如果想要通過xml信息創建設備
SharpNodeSettings.Device.DeviceCore deviceCore = SharpNodeSettings.Util.CreateFromXElement( xmlDevice );
// 演示讀取數據,此處有個問題在於如果是相同種類的PLC,應用還是很方便的,如果是不同種類的,地址模型就比較麻煩。
HslCommunication.OperateResult<short> read = deviceCore.ReadWriteDevice.ReadInt16( "D100" );
}
Quick Start
按照如下的步驟走,就可以急速體驗本項目所傳達的核心功能價值,就可以明白本項目是否符合您的需求。啟動測試之前,你需要准備個真實的設備:
- 西門子PLC
- 三菱PLC
- 歐姆龍PLC
- ModbusTcp設備
如果您沒有真實的設備,也可以從網上下載個Modbus服務器軟件,這里也提供一個下載地址:ModbusTcpServer.zip
下載完成后啟動服務器即可。
配置Xml信息
去本項目的目錄下配置設備的信息: \SharpNodeSettings\XmlFile 運行 SharpNodeSettings.Tools.exe 進行配置,已經配置了一部分,如果想要快速開始,忽略本步驟也可以。
SampleServer
本示例直接重新生成 SampleServer 項目,啟動程序即可。如果想要看實際的數據信息,啟動 SharpNodeSettings.NodeView項目查看 
RedisServer
本示例是在 SampleServer 的基礎上添加了Redis服務器,所以需要先安裝好Redis服務器,windows版本下載地址:https://github.com/MicrosoftArchive/redis/releases
當然,最好再下載安裝一個redis服務器的可視化工具,此處推薦 RedisDesktopManagerhttps://github.com/uglide/RedisDesktopManager/releases
然后基於本項目,重新生成 SharpNodeSettings.RedisServer 項目,啟動服務器
上述的 SharpNodeSettings.NodeView 項目依然可以查看,然后下圖演示Redis
OpcUaServer
本示例是演示從PLC采集數據並且寫入到OPC UA服務器中的示例,重新生成 SharpNodeSettings.OpcUaServer 項目,啟動它,如果顯示是否增加信任證書時,選擇是即可。
首先創建OPC UA服務器項目的時候,需要根據xml文件創建對應的OPC UA節點,這部分還是比較麻煩的
#region INodeManager Members
/// <summary>
/// Does any initialization required before the address space can be used.
/// </summary>
/// <remarks>
/// The externalReferences is an out parameter that allows the node manager to link to nodes
/// in other node managers. For example, the 'Objects' node is managed by the CoreNodeManager and
/// should have a reference to the root folder node(s) exposed by this node manager.
/// </remarks>
public override void CreateAddressSpace( IDictionary<NodeId, IList<IReference>> externalReferences )
{
lock (Lock)
{
LoadPredefinedNodes( SystemContext, externalReferences );
IList<IReference> references = null;
if (!externalReferences.TryGetValue( ObjectIds.ObjectsFolder, out references ))
{
externalReferences[ObjectIds.ObjectsFolder] = references = new List<IReference>( );
}
dict_BaseDataVariableState = new Dictionary<string, BaseDataVariableState>( );
try
{
// =========================================================================================
//
// 此處需要加載本地文件,並且創建對應的節點信息,
//
// =========================================================================================
sharpNodeServer = new SharpNodeServer( );
sharpNodeServer.WriteCustomerData = ( Device.DeviceCore deviceCore, string name ) =>
{
string opcNode = "ns=2;s=" + string.Join( "/", deviceCore.DeviceNodes ) + "/" + name;
lock (Lock)
{
if (dict_BaseDataVariableState.ContainsKey( opcNode ))
{
dict_BaseDataVariableState[opcNode].Value = deviceCore.GetDynamicValueByName( name );
dict_BaseDataVariableState[opcNode].ClearChangeMasks( SystemContext, false );
}
}
};
XElement element = XElement.Load( "settings.xml" );
dicRegularItemNode = SharpNodeSettings.Util.ParesRegular( element );
AddNodeClass( null, element, references );
// 加載配置文件之前設置寫入方法
sharpNodeServer.LoadByXmlFile( "settings.xml" );
// 最后再啟動服務器信息
sharpNodeServer.ServerStart( 12345 );
}
catch (Exception e)
{
Utils.Trace( e, "Error creating the address space." );
}
}
}
private void AddNodeClass( NodeState parent, XElement nodeClass, IList<IReference> references )
{
foreach (var xmlNode in nodeClass.Elements( ))
{
if (xmlNode.Name == "NodeClass")
{
SharpNodeSettings.Node.NodeBase.NodeClass nClass = new SharpNodeSettings.Node.NodeBase.NodeClass( );
nClass.LoadByXmlElement( xmlNode );
FolderState son;
if (parent == null)
{
son = CreateFolder( null, nClass.Name );
son.Description = nClass.Description;
son.AddReference( ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder );
references.Add( new NodeStateReference( ReferenceTypes.Organizes, false, son.NodeId ) );
son.EventNotifier = EventNotifiers.SubscribeToEvents;
AddRootNotifier( son );
AddNodeClass( son, xmlNode, references );
AddPredefinedNode( SystemContext, son );
}
else
{
son = CreateFolder( parent, nClass.Name, nClass.Description );
AddNodeClass( son, xmlNode, references );
}
}
else if (xmlNode.Name == "DeviceNode")
{
AddDeviceCore( parent, xmlNode );
}
else if (xmlNode.Name == "Server")
{
AddServer( parent, xmlNode, references );
}
}
}
private void AddDeviceCore( NodeState parent, XElement device )
{
if (device.Name == "DeviceNode")
{
// 提取名稱和描述信息
string name = device.Attribute( "Name" ).Value;
string description = device.Attribute( "Description" ).Value;
// 創建OPC節點
FolderState deviceFolder = CreateFolder( parent, device.Attribute( "Name" ).Value, device.Attribute( "Description" ).Value );
// 添加Request
foreach (var requestXml in device.Elements( "DeviceRequest" ))
{
DeviceRequest deviceRequest = new DeviceRequest( );
deviceRequest.LoadByXmlElement( requestXml );
AddDeviceRequest( deviceFolder, deviceRequest );
}
}
}
private void AddServer( NodeState parent, XElement xmlNode, IList<IReference> references )
{
int serverType = int.Parse( xmlNode.Attribute( "ServerType" ).Value );
if (serverType == ServerNode.ModbusServer)
{
NodeModbusServer serverNode = new NodeModbusServer( );
serverNode.LoadByXmlElement( xmlNode );
FolderState son = CreateFolder( parent, serverNode.Name, serverNode.Description );
AddNodeClass( son, xmlNode, references );
}
else if (serverType == ServerNode.AlienServer)
{
AlienServerNode alienNode = new AlienServerNode( );
alienNode.LoadByXmlElement( xmlNode );
FolderState son = CreateFolder( parent, alienNode.Name, alienNode.Description );
AddNodeClass( son, xmlNode, references );
}
}
private void AddDeviceRequest( NodeState parent, DeviceRequest deviceRequest )
{
// 提煉真正的數據節點
if (!dicRegularItemNode.ContainsKey( deviceRequest.PraseRegularCode )) return;
List<RegularItemNode> regularNodes = dicRegularItemNode[deviceRequest.PraseRegularCode];
foreach (var regularNode in regularNodes)
{
if (regularNode.RegularCode == RegularNodeTypeItem.Bool.Code)
{
if (regularNode.TypeLength == 1)
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Boolean, ValueRanks.Scalar, default( bool ) );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
else
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Boolean, ValueRanks.OneDimension, new bool[regularNode.TypeLength] );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
}
else if (regularNode.RegularCode == RegularNodeTypeItem.Byte.Code)
{
if (regularNode.TypeLength == 1)
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Byte, ValueRanks.Scalar, default( byte ) );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
else
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Byte, ValueRanks.OneDimension, new byte[regularNode.TypeLength] );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
}
else if (regularNode.RegularCode == RegularNodeTypeItem.Int16.Code)
{
if (regularNode.TypeLength == 1)
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Int16, ValueRanks.Scalar, default( short ) );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
else
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Int16, ValueRanks.OneDimension, new short[regularNode.TypeLength] );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
}
else if (regularNode.RegularCode == RegularNodeTypeItem.UInt16.Code)
{
if (regularNode.TypeLength == 1)
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.UInt16, ValueRanks.Scalar, default( ushort ) );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
else
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.UInt16, ValueRanks.OneDimension, new ushort[regularNode.TypeLength] );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
}
else if (regularNode.RegularCode == RegularNodeTypeItem.Int32.Code)
{
if (regularNode.TypeLength == 1)
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Int32, ValueRanks.Scalar, default( int ) );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
else
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Int32, ValueRanks.OneDimension, new int[regularNode.TypeLength] );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
}
else if (regularNode.RegularCode == RegularNodeTypeItem.UInt32.Code)
{
if (regularNode.TypeLength == 1)
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.UInt32, ValueRanks.Scalar, default( uint ) );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
else
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.UInt32, ValueRanks.OneDimension, new uint[regularNode.TypeLength] );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
}
else if (regularNode.RegularCode == RegularNodeTypeItem.Float.Code)
{
if (regularNode.TypeLength == 1)
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Float, ValueRanks.Scalar, default( float ) );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
else
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Float, ValueRanks.OneDimension, new float[regularNode.TypeLength] );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
}
else if (regularNode.RegularCode == RegularNodeTypeItem.Int64.Code)
{
if (regularNode.TypeLength == 1)
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Int64, ValueRanks.Scalar, default( long ) );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
else
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Int64, ValueRanks.OneDimension, new long[regularNode.TypeLength] );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
}
else if (regularNode.RegularCode == RegularNodeTypeItem.UInt64.Code)
{
if (regularNode.TypeLength == 1)
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.UInt64, ValueRanks.Scalar, default( ulong ) );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
else
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.UInt64, ValueRanks.OneDimension, new ulong[regularNode.TypeLength] );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
}
else if (regularNode.RegularCode == RegularNodeTypeItem.Double.Code)
{
if (regularNode.TypeLength == 1)
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Double, ValueRanks.Scalar, default( double ) );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
else
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.Double, ValueRanks.OneDimension, new double[regularNode.TypeLength] );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
}
else if (regularNode.RegularCode == RegularNodeTypeItem.StringAscii.Code ||
regularNode.RegularCode == RegularNodeTypeItem.StringUnicode.Code ||
regularNode.RegularCode == RegularNodeTypeItem.StringUtf8.Code)
{
var dataVariableState = CreateBaseVariable( parent, regularNode.Name, regularNode.Description, DataTypeIds.String, ValueRanks.OneDimension, string.Empty );
dict_BaseDataVariableState.Add( dataVariableState.NodeId.ToString( ), dataVariableState );
}
}
}
/// <summary>
/// 創建一個新的節點,節點名稱為字符串
/// </summary>
protected FolderState CreateFolder( NodeState parent, string name )
{
return CreateFolder( parent, name, string.Empty );
}
/// <summary>
/// 創建一個新的節點,節點名稱為字符串
/// </summary>
protected FolderState CreateFolder( NodeState parent, string name, string description )
{
FolderState folder = new FolderState( parent );
folder.SymbolicName = name;
folder.ReferenceTypeId = ReferenceTypes.Organizes;
folder.TypeDefinitionId = ObjectTypeIds.FolderType;
folder.Description = description;
if (parent == null)
{
folder.NodeId = new NodeId( name, NamespaceIndex );
}
else
{
folder.NodeId = new NodeId( parent.NodeId.ToString( ) + "/" + name );
}
folder.BrowseName = new QualifiedName( name, NamespaceIndex );
folder.DisplayName = new LocalizedText( name );
folder.WriteMask = AttributeWriteMask.None;
folder.UserWriteMask = AttributeWriteMask.None;
folder.EventNotifier = EventNotifiers.None;
if (parent != null)
{
parent.AddChild( folder );
}
return folder;
}
/// <summary>
/// 創建一個值節點,類型需要在創建的時候指定
/// </summary>
protected BaseDataVariableState CreateBaseVariable( NodeState parent, string name, string description, NodeId dataType, int valueRank, object defaultValue )
{
BaseDataVariableState variable = new BaseDataVariableState( parent );
variable.SymbolicName = name;
variable.ReferenceTypeId = ReferenceTypes.Organizes;
variable.TypeDefinitionId = VariableTypeIds.BaseDataVariableType;
if (parent == null)
{
variable.NodeId = new NodeId( name, NamespaceIndex );
}
else
{
variable.NodeId = new NodeId( parent.NodeId.ToString( ) + "/" + name );
}
variable.Description = description;
variable.BrowseName = new QualifiedName( name, NamespaceIndex );
variable.DisplayName = new LocalizedText( name );
variable.WriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description;
variable.UserWriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description;
variable.DataType = dataType;
variable.ValueRank = valueRank;
variable.AccessLevel = AccessLevels.CurrentReadOrWrite;
variable.UserAccessLevel = AccessLevels.CurrentReadOrWrite;
variable.Historizing = false;
variable.Value = defaultValue;
variable.StatusCode = StatusCodes.Good;
variable.Timestamp = DateTime.Now;
if (parent != null)
{
parent.AddChild( variable );
}
return variable;
}
然后再啟動一個 OPC UA Client的示例項目



