From 4acff71c77f18771630b2488f4ebc4f5b05c6cb5 Mon Sep 17 00:00:00 2001 From: luosheng Date: Wed, 28 Jun 2023 09:10:50 +0800 Subject: [PATCH] Several function enchancement --- .../AddressFormaterNA200H.cs | 2 +- .../Modbus.Net.Modbus.NA200H.csproj | 2 +- ...Modbus.Net.Modbus.SelfDefinedSample.csproj | 2 +- .../AddressFormaterModbus.cs | 2 +- .../Modbus.Net.Modbus.csproj | 2 +- Modbus.Net/Modbus.Net.Modbus/ModbusMachine.cs | 4 +- .../Modbus.Net.Opc/AddressFormaterOpc.cs | 25 +- Modbus.Net/Modbus.Net.Opc/ClientExtend.cs | 13 +- .../Modbus.Net.Opc/Modbus.Net.Opc.csproj | 13 +- Modbus.Net/Modbus.Net.Opc/OpcConnector.cs | 2 +- Modbus.Net/Modbus.Net.Opc/OpcMachine.cs | 9 +- .../Modbus.Net.Opc/OpcProtocolLinker.cs | 15 +- Modbus.Net/Modbus.Net.Opc/README.md | 7 +- .../AddressFormaterSiemens.cs | 4 +- .../Modbus.Net.Siemens.csproj | 2 +- .../Modbus.Net.Siemens/SiemensMachine.cs | 4 +- .../Modbus.Net.Siemens/SiemensPpiProtocol.cs | 10 - .../SiemensPpiProtocolLinker.cs | 4 +- .../Modbus.Net.Siemens/SiemensTcpProtocol.cs | 21 - Modbus.Net/Modbus.Net.sln | 6 + .../Modbus.Net/Configuration/MachineReader.cs | 26 +- Modbus.Net/Modbus.Net/Helper/AsyncHelper.cs | 71 -- Modbus.Net/Modbus.Net/Helper/ValueHelper.cs | 18 +- .../Modbus.Net/Interface/IMachineMethod.cs | 14 - Modbus.Net/Modbus.Net/Interface/IProtocol.cs | 25 - .../Modbus.Net/Interface/IProtocolLinker.cs | 28 - .../Modbus.Net/Interface/IUtilityMethod.cs | 41 -- .../Modbus.Net/Linker/ComProtocolLinker.cs | 12 +- .../Modbus.Net/Linker/ProtocolLinker.cs | 51 +- .../Modbus.Net/Machine/AddressCombiner.cs | 44 +- .../Modbus.Net/Machine/AddressFormater.cs | 12 +- Modbus.Net/Modbus.Net/Machine/BaseMachine.cs | 592 ++++++++++++---- .../Modbus.Net/Machine/BaseMachineExtend.cs | 8 +- Modbus.Net/Modbus.Net/Modbus.Net.csproj | 3 +- .../Modbus.Net/Protocol/BaseProtocol.cs | 34 - Modbus.Net/Modbus.Net/Utility/BaseUtility.cs | 60 -- README.md | 1 + Samples/AnyType/Controllers/HomeController.cs | 1 + Samples/MachineJob/MachineJob.csproj | 1 + Samples/MachineJob/appsettings.json | 35 +- .../TripleAdd/Controllers/HomeController.cs | 1 + Tests/Modbus.Net.Tests/BaseTest.cs | 33 +- Tests/Modbus.Net.Tests/EndianTest.cs | 1 + Tests/Modbus.Net.Tests/MachineMethodTest.cs | 5 +- .../Modbus.Net.Tests/Modbus.Net.Tests.csproj | 1 + .../ModbusMultiStationTest.cs | 1 + Tests/Modbus.Net.Tests/ModbusTest.cs | 1 + Tests/Modbus.Net.Tests/SiemensTest.cs | 1 + h-opc/h-opc/Common/ClientExtensions.cs | 32 + h-opc/h-opc/Common/IClient.cs | 99 +++ h-opc/h-opc/Common/Node.cs | 46 ++ h-opc/h-opc/Common/OpcException.cs | 69 ++ h-opc/h-opc/Common/OpcStatus.cs | 18 + h-opc/h-opc/Common/Quality.cs | 28 + h-opc/h-opc/Common/ReadEvent.cs | 32 + h-opc/h-opc/Da/DaClient.cs | 298 ++++++++ h-opc/h-opc/Da/DaClient_async.cs | 42 ++ h-opc/h-opc/Da/DaNode.cs | 22 + h-opc/h-opc/Ua/ClientUtils.cs | 98 +++ h-opc/h-opc/Ua/NodeExtensions.cs | 24 + h-opc/h-opc/Ua/UaClient.cs | 642 ++++++++++++++++++ h-opc/h-opc/Ua/UaClientOptions.cs | 120 ++++ h-opc/h-opc/Ua/UaNode.cs | 29 + h-opc/h-opc/h-opc.csproj | 15 + 64 files changed, 2254 insertions(+), 630 deletions(-) delete mode 100644 Modbus.Net/Modbus.Net/Helper/AsyncHelper.cs create mode 100644 h-opc/h-opc/Common/ClientExtensions.cs create mode 100644 h-opc/h-opc/Common/IClient.cs create mode 100644 h-opc/h-opc/Common/Node.cs create mode 100644 h-opc/h-opc/Common/OpcException.cs create mode 100644 h-opc/h-opc/Common/OpcStatus.cs create mode 100644 h-opc/h-opc/Common/Quality.cs create mode 100644 h-opc/h-opc/Common/ReadEvent.cs create mode 100644 h-opc/h-opc/Da/DaClient.cs create mode 100644 h-opc/h-opc/Da/DaClient_async.cs create mode 100644 h-opc/h-opc/Da/DaNode.cs create mode 100644 h-opc/h-opc/Ua/ClientUtils.cs create mode 100644 h-opc/h-opc/Ua/NodeExtensions.cs create mode 100644 h-opc/h-opc/Ua/UaClient.cs create mode 100644 h-opc/h-opc/Ua/UaClientOptions.cs create mode 100644 h-opc/h-opc/Ua/UaNode.cs create mode 100644 h-opc/h-opc/h-opc.csproj diff --git a/Modbus.Net/Modbus.Net.Modbus.NA200H/AddressFormaterNA200H.cs b/Modbus.Net/Modbus.Net.Modbus.NA200H/AddressFormaterNA200H.cs index 21d8afe..54001c7 100644 --- a/Modbus.Net/Modbus.Net.Modbus.NA200H/AddressFormaterNA200H.cs +++ b/Modbus.Net/Modbus.Net.Modbus.NA200H/AddressFormaterNA200H.cs @@ -3,7 +3,7 @@ /// /// 南大奥拓NA200H专用AddressFormater /// - public class AddressFormaterNA200H : AddressFormater + public class AddressFormaterNA200H : AddressFormater { /// /// 格式化地址 diff --git a/Modbus.Net/Modbus.Net.Modbus.NA200H/Modbus.Net.Modbus.NA200H.csproj b/Modbus.Net/Modbus.Net.Modbus.NA200H/Modbus.Net.Modbus.NA200H.csproj index a8f57a4..2f1ae6e 100644 --- a/Modbus.Net/Modbus.Net.Modbus.NA200H/Modbus.Net.Modbus.NA200H.csproj +++ b/Modbus.Net/Modbus.Net.Modbus.NA200H/Modbus.Net.Modbus.NA200H.csproj @@ -1,7 +1,7 @@  - net6.0;net462 + net6.0 10.0 Modbus.Net.Modbus.NA200H Modbus.Net.Modbus.NA200H diff --git a/Modbus.Net/Modbus.Net.Modbus.SelfDefinedSample/Modbus.Net.Modbus.SelfDefinedSample.csproj b/Modbus.Net/Modbus.Net.Modbus.SelfDefinedSample/Modbus.Net.Modbus.SelfDefinedSample.csproj index 9ac0e69..6e88179 100644 --- a/Modbus.Net/Modbus.Net.Modbus.SelfDefinedSample/Modbus.Net.Modbus.SelfDefinedSample.csproj +++ b/Modbus.Net/Modbus.Net.Modbus.SelfDefinedSample/Modbus.Net.Modbus.SelfDefinedSample.csproj @@ -1,7 +1,7 @@  - net6.0;net462 + net6.0 10.0 Modbus.Net.Modbus.SelfDefinedSample Modbus.Net.Modbus.SelfDefinedSample diff --git a/Modbus.Net/Modbus.Net.Modbus/AddressFormaterModbus.cs b/Modbus.Net/Modbus.Net.Modbus/AddressFormaterModbus.cs index 06000c5..5b21c40 100644 --- a/Modbus.Net/Modbus.Net.Modbus/AddressFormaterModbus.cs +++ b/Modbus.Net/Modbus.Net.Modbus/AddressFormaterModbus.cs @@ -3,7 +3,7 @@ /// /// Modbus标准AddressFormater /// - public class AddressFormaterModbus : AddressFormater + public class AddressFormaterModbus : AddressFormater { /// /// 格式化地址 diff --git a/Modbus.Net/Modbus.Net.Modbus/Modbus.Net.Modbus.csproj b/Modbus.Net/Modbus.Net.Modbus/Modbus.Net.Modbus.csproj index af9952c..3a5ecb1 100644 --- a/Modbus.Net/Modbus.Net.Modbus/Modbus.Net.Modbus.csproj +++ b/Modbus.Net/Modbus.Net.Modbus/Modbus.Net.Modbus.csproj @@ -1,7 +1,7 @@  - net6.0;net462 + net6.0 10.0 Modbus.Net.Modbus Modbus.Net.Modbus diff --git a/Modbus.Net/Modbus.Net.Modbus/ModbusMachine.cs b/Modbus.Net/Modbus.Net.Modbus/ModbusMachine.cs index 76c3601..8a4f1db 100644 --- a/Modbus.Net/Modbus.Net.Modbus/ModbusMachine.cs +++ b/Modbus.Net/Modbus.Net.Modbus/ModbusMachine.cs @@ -21,7 +21,7 @@ namespace Modbus.Net.Modbus /// 主站号 /// 端格式 public ModbusMachine(TKey id, ModbusType connectionType, string connectionString, - IEnumerable> getAddresses, bool keepConnect, byte slaveAddress, byte masterAddress, + IEnumerable> getAddresses, bool keepConnect, byte slaveAddress, byte masterAddress, Endian endian) : base(id, getAddresses, keepConnect, slaveAddress, masterAddress) { @@ -42,7 +42,7 @@ namespace Modbus.Net.Modbus /// 主站号 /// 端格式 public ModbusMachine(TKey id, ModbusType connectionType, string connectionString, - IEnumerable> getAddresses, byte slaveAddress, byte masterAddress, + IEnumerable> getAddresses, byte slaveAddress, byte masterAddress, Endian endian) : this(id, connectionType, connectionString, getAddresses, true, slaveAddress, masterAddress, endian) { diff --git a/Modbus.Net/Modbus.Net.Opc/AddressFormaterOpc.cs b/Modbus.Net/Modbus.Net.Opc/AddressFormaterOpc.cs index 2b0e8c6..4b0a697 100644 --- a/Modbus.Net/Modbus.Net.Opc/AddressFormaterOpc.cs +++ b/Modbus.Net/Modbus.Net.Opc/AddressFormaterOpc.cs @@ -6,16 +6,16 @@ namespace Modbus.Net.Opc /// /// Opc地址编码器 /// - public class AddressFormaterOpc : AddressFormater where TMachineKey : IEquatable - where TUnitKey : IEquatable + public class AddressFormaterOpc : AddressFormater where TMachineKey : IEquatable + where TUnitKey : IEquatable where TAddressKey : IEquatable where TSubAddressKey : IEquatable { /// /// 协议构造器 /// /// 如何通过BaseMachine和AddressUnit构造Opc的标签 /// 调用这个编码器的设备 - public AddressFormaterOpc(Func, AddressUnit, string[]> tagGeter, - BaseMachine machine) + public AddressFormaterOpc(Func, AddressUnit, string> tagGeter, + BaseMachine machine) { Machine = machine; TagGeter = tagGeter; @@ -24,13 +24,13 @@ namespace Modbus.Net.Opc /// /// 设备 /// - public BaseMachine Machine { get; set; } + public BaseMachine Machine { get; set; } /// /// 标签构造器 /// (设备,地址)->不具备分隔符的标签数组 /// - protected Func, AddressUnit, string[]> TagGeter { get; set; } + protected Func, AddressUnit, string> TagGeter { get; set; } /// /// 编码地址 @@ -38,15 +38,12 @@ namespace Modbus.Net.Opc /// 地址所在的数据区域 /// 地址 /// 编码后的地址 - public override string FormatAddress(string area, int address) + public override string FormatAddress(string area, TAddressKey address) { - var findAddress = Machine?.GetAddresses.FirstOrDefault(p => p.Area == area && p.Address == address); + var findAddress = Machine?.GetAddresses.FirstOrDefault(p => p.Area == area && p.Address.Equals(address)); if (findAddress == null) return null; - var strings = TagGeter(Machine, findAddress); - var ans = ""; - for (var i = 0; i < strings.Length; i++) - ans += strings[i].Trim().Replace(" ", "") + '\r'; - ans = ans.Substring(0, ans.Length - 1); + var ans = TagGeter(Machine, findAddress); + ans = ans.Trim().Replace(" ", ""); return ans; } @@ -57,7 +54,7 @@ namespace Modbus.Net.Opc /// 地址 /// 子地址(忽略) /// 编码后的地址 - public override string FormatAddress(string area, int address, int subAddress) + public override string FormatAddress(string area, TAddressKey address, TSubAddressKey subAddress) { return FormatAddress(area, address); } diff --git a/Modbus.Net/Modbus.Net.Opc/ClientExtend.cs b/Modbus.Net/Modbus.Net.Opc/ClientExtend.cs index 38c9027..d67ee40 100644 --- a/Modbus.Net/Modbus.Net.Opc/ClientExtend.cs +++ b/Modbus.Net/Modbus.Net.Opc/ClientExtend.cs @@ -3,9 +3,7 @@ using Hylasoft.Opc.Da; using Hylasoft.Opc.Ua; using System; using System.Collections.Generic; -using System.Reflection; using System.Threading.Tasks; -using URL = Opc.URL; namespace Modbus.Net.Opc { @@ -73,18 +71,11 @@ namespace Modbus.Net.Opc public class MyDaClient : DaClient, IClientExtend { /// - /// UaClient Extend + /// 构造函数 /// - /// Url address of Opc UA server + /// OpcDa服务端Url public MyDaClient(Uri serverUrl) : base(serverUrl) { - var url = new URL(serverUrl.OriginalString) - { - Scheme = serverUrl.Scheme, - HostName = serverUrl.Host - }; - - typeof(DaClient).GetField("_url", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(this, url); } /// diff --git a/Modbus.Net/Modbus.Net.Opc/Modbus.Net.Opc.csproj b/Modbus.Net/Modbus.Net.Opc/Modbus.Net.Opc.csproj index 4432066..6a765e5 100644 --- a/Modbus.Net/Modbus.Net.Opc/Modbus.Net.Opc.csproj +++ b/Modbus.Net/Modbus.Net.Opc/Modbus.Net.Opc.csproj @@ -1,7 +1,7 @@  - net462 + net6.0 10.0 Modbus.Net.Opc Modbus.Net.Opc @@ -23,22 +23,15 @@ MIT README.md snupkg - AnyCPU;x86 + AnyCPU bin\Debug\Modbus.Net.Opc.xml - - - bin\Debug\Modbus.Net.Opc.xml - - - - - + diff --git a/Modbus.Net/Modbus.Net.Opc/OpcConnector.cs b/Modbus.Net/Modbus.Net.Opc/OpcConnector.cs index ee871ab..0deff3f 100644 --- a/Modbus.Net/Modbus.Net.Opc/OpcConnector.cs +++ b/Modbus.Net/Modbus.Net.Opc/OpcConnector.cs @@ -97,7 +97,7 @@ namespace Modbus.Net.Opc { var result = await Client.ReadAsync(tag); object resultTrans; - if (result.Value.ToString() == "False") + if (result.Value?.ToString() == "False") { resultTrans = (byte)0; } diff --git a/Modbus.Net/Modbus.Net.Opc/OpcMachine.cs b/Modbus.Net/Modbus.Net.Opc/OpcMachine.cs index 17a9fd1..c95c066 100644 --- a/Modbus.Net/Modbus.Net.Opc/OpcMachine.cs +++ b/Modbus.Net/Modbus.Net.Opc/OpcMachine.cs @@ -6,7 +6,7 @@ namespace Modbus.Net.Opc /// /// Opc设备 /// - public class OpcMachine : BaseMachine where TKey : IEquatable + public class OpcMachine : BaseMachine where TKey : IEquatable where TUnitKey : IEquatable { /// @@ -16,13 +16,12 @@ namespace Modbus.Net.Opc /// 连接类型 /// 连接地址 /// 需要读写的地址 - public OpcMachine(TKey id, OpcType connectionType, string connectionString, IEnumerable> getAddresses) + /// 连接各个字段用的符号 + public OpcMachine(TKey id, OpcType connectionType, string connectionString, IEnumerable> getAddresses, string tagSpliter) : base(id, getAddresses, true) { BaseUtility = new OpcUtility(connectionType, connectionString); - AddressFormater = new AddressFormaterOpc((machine, unit) => { return new string[] { unit.Area }; }, this); - AddressCombiner = new AddressCombinerSingle(); - AddressCombinerSet = new AddressCombinerSingle(); + AddressFormater = new AddressFormaterOpc((machine, unit) => { var ans = unit.Area; if (unit.Address != null) ans += tagSpliter + unit.Address; if (unit.SubAddress != null) ans += tagSpliter + unit.SubAddress; return ans; }, this); } } } \ No newline at end of file diff --git a/Modbus.Net/Modbus.Net.Opc/OpcProtocolLinker.cs b/Modbus.Net/Modbus.Net.Opc/OpcProtocolLinker.cs index dd1c778..3177c31 100644 --- a/Modbus.Net/Modbus.Net.Opc/OpcProtocolLinker.cs +++ b/Modbus.Net/Modbus.Net.Opc/OpcProtocolLinker.cs @@ -8,20 +8,6 @@ namespace Modbus.Net.Opc /// public abstract class OpcProtocolLinker : ProtocolLinker { - /// - /// 发送并接收数据 - /// - /// 发送协议的内容 - /// 接收协议的内容 - public override async Task SendReceiveAsync(OpcParamIn content) - { - var extBytes = BytesExtend(content); - var receiveBytes = await SendReceiveWithoutExtAndDecAsync(extBytes); - return receiveBytes == null - ? null - : receiveBytes.Value.Length == 0 ? receiveBytes : BytesDecact(receiveBytes); - } - /// /// 发送并接收数据,不进行协议扩展和收缩,用于特殊协议 /// @@ -47,6 +33,7 @@ namespace Modbus.Net.Opc public override bool? CheckRight(OpcParamOut content) { if (content == null || !content.Success) return false; + if (content.Success && content.Value == null) { content.Value = Encoding.ASCII.GetBytes("Success"); return true; } if (content.Value.Length == 6 && Encoding.ASCII.GetString(content.Value) == "NoData") return null; return base.CheckRight(content); diff --git a/Modbus.Net/Modbus.Net.Opc/README.md b/Modbus.Net/Modbus.Net.Opc/README.md index 79b6955..19d5db1 100644 --- a/Modbus.Net/Modbus.Net.Opc/README.md +++ b/Modbus.Net/Modbus.Net.Opc/README.md @@ -4,4 +4,9 @@ OPC Implementation of Modbus.Net -Doc has been moved to wiki. \ No newline at end of file +Doc has been moved to wiki. + +## Caution + +Do not use this module in commercial environment.
+Altered h-opc uses Technosoftware.DaAeHdaSolution that will not abey for non paid commercial use. diff --git a/Modbus.Net/Modbus.Net.Siemens/AddressFormaterSiemens.cs b/Modbus.Net/Modbus.Net.Siemens/AddressFormaterSiemens.cs index e2ba25b..4b78e02 100644 --- a/Modbus.Net/Modbus.Net.Siemens/AddressFormaterSiemens.cs +++ b/Modbus.Net/Modbus.Net.Siemens/AddressFormaterSiemens.cs @@ -3,7 +3,7 @@ /// /// Siemens地址格式化(Modbus.Net专用格式) /// - public class AddressFormaterSiemens : AddressFormater + public class AddressFormaterSiemens : AddressFormater { /// /// 编码地址 @@ -32,7 +32,7 @@ /// /// Siemens地址格式化(Siemens格式) /// - public class AddressFormaterSimenseStandard : AddressFormater + public class AddressFormaterSimenseStandard : AddressFormater { /// /// 编码地址 diff --git a/Modbus.Net/Modbus.Net.Siemens/Modbus.Net.Siemens.csproj b/Modbus.Net/Modbus.Net.Siemens/Modbus.Net.Siemens.csproj index 200e553..8573f74 100644 --- a/Modbus.Net/Modbus.Net.Siemens/Modbus.Net.Siemens.csproj +++ b/Modbus.Net/Modbus.Net.Siemens/Modbus.Net.Siemens.csproj @@ -1,7 +1,7 @@  - net6.0;net462 + net6.0 10.0 Modbus.Net.Siemens Modbus.Net.Siemens diff --git a/Modbus.Net/Modbus.Net.Siemens/SiemensMachine.cs b/Modbus.Net/Modbus.Net.Siemens/SiemensMachine.cs index 49c87fb..4bfd5ed 100644 --- a/Modbus.Net/Modbus.Net.Siemens/SiemensMachine.cs +++ b/Modbus.Net/Modbus.Net.Siemens/SiemensMachine.cs @@ -23,7 +23,7 @@ namespace Modbus.Net.Siemens /// 本机模块位,0到7,仅200使用,其它型号不要填写 /// PLC模块位,0到7,仅200使用,其它型号不要填写 public SiemensMachine(TKey id, SiemensType connectionType, string connectionString, SiemensMachineModel model, - IEnumerable> getAddresses, bool keepConnect, byte slaveAddress, byte masterAddress, byte src = 1, byte dst = 0) + IEnumerable> getAddresses, bool keepConnect, byte slaveAddress, byte masterAddress, byte src = 1, byte dst = 0) : base(id, getAddresses, keepConnect, slaveAddress, masterAddress) { BaseUtility = new SiemensUtility(connectionType, connectionString, model, slaveAddress, masterAddress, src, dst); @@ -45,7 +45,7 @@ namespace Modbus.Net.Siemens /// 本机模块位,0到7,仅200使用,其它型号不要填写 /// PLC模块位,0到7,仅200使用,其它型号不要填写 public SiemensMachine(TKey id, SiemensType connectionType, string connectionString, SiemensMachineModel model, - IEnumerable> getAddresses, byte slaveAddress, byte masterAddress, byte src = 1, byte dst = 0) + IEnumerable> getAddresses, byte slaveAddress, byte masterAddress, byte src = 1, byte dst = 0) : this(id, connectionType, connectionString, model, getAddresses, true, slaveAddress, masterAddress, src, dst) { } diff --git a/Modbus.Net/Modbus.Net.Siemens/SiemensPpiProtocol.cs b/Modbus.Net/Modbus.Net.Siemens/SiemensPpiProtocol.cs index 2ddf9d5..422e378 100644 --- a/Modbus.Net/Modbus.Net.Siemens/SiemensPpiProtocol.cs +++ b/Modbus.Net/Modbus.Net.Siemens/SiemensPpiProtocol.cs @@ -34,16 +34,6 @@ namespace Modbus.Net.Siemens ProtocolLinker = new SiemensPpiProtocolLinker(_com, SlaveAddress); } - /// - /// 发送协议内容并接收,一般方法 - /// - /// 写入的内容,使用对象数组描述 - /// 从设备获取的字节流 - public override PipeUnit SendReceive(params object[] content) - { - return AsyncHelper.RunSync(() => SendReceiveAsync(Endian, content)); - } - /// /// 发送协议内容并接收,一般方法 /// diff --git a/Modbus.Net/Modbus.Net.Siemens/SiemensPpiProtocolLinker.cs b/Modbus.Net/Modbus.Net.Siemens/SiemensPpiProtocolLinker.cs index 12f3782..7c25c67 100644 --- a/Modbus.Net/Modbus.Net.Siemens/SiemensPpiProtocolLinker.cs +++ b/Modbus.Net/Modbus.Net.Siemens/SiemensPpiProtocolLinker.cs @@ -1,4 +1,4 @@ -using FastEnumUtility; +using System; using System.Collections.Generic; using System.IO.Ports; using System.Threading; @@ -19,7 +19,7 @@ namespace Modbus.Net.Siemens public SiemensPpiProtocolLinker(string com, int slaveAddress) : base(com, slaveAddress, parity: ConfigurationReader.GetValue("COM:Siemens", "Parity") != null - ? FastEnum.Parse(ConfigurationReader.GetValue("COM:Siemens", "Parity")) + ? Enum.Parse(ConfigurationReader.GetValue("COM:Siemens", "Parity")) : null ) { diff --git a/Modbus.Net/Modbus.Net.Siemens/SiemensTcpProtocol.cs b/Modbus.Net/Modbus.Net.Siemens/SiemensTcpProtocol.cs index 23a6ad2..597fe69 100644 --- a/Modbus.Net/Modbus.Net.Siemens/SiemensTcpProtocol.cs +++ b/Modbus.Net/Modbus.Net.Siemens/SiemensTcpProtocol.cs @@ -78,16 +78,6 @@ namespace Modbus.Net.Siemens ProtocolLinker = new SiemensTcpProtocolLinker(_ip, _port); } - /// - /// 发送数据并接收 - /// - /// 发送的数据 - /// 返回的数据 - public override PipeUnit SendReceive(params object[] content) - { - return AsyncHelper.RunSync(() => SendReceiveAsync(Endian, content)); - } - /// /// 发送数据并接收 /// @@ -100,17 +90,6 @@ namespace Modbus.Net.Siemens return await base.SendReceiveAsync(Endian, content); } - /// - /// 发送数据并接收 - /// - /// 协议的核心 - /// 协议的参数 - /// 返回的数据 - public override PipeUnit SendReceive(ProtocolUnit unit, IInputStruct content) - { - return AsyncHelper.RunSync(() => SendReceiveAsync(unit, content)); - } - /// /// 发送数据并接收 /// diff --git a/Modbus.Net/Modbus.Net.sln b/Modbus.Net/Modbus.Net.sln index dad642e..346f06b 100644 --- a/Modbus.Net/Modbus.Net.sln +++ b/Modbus.Net/Modbus.Net.sln @@ -34,6 +34,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TripleAdd", "..\Samples\Tri EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modbus.Net.Modbus.SelfDefinedSample", "Modbus.Net.Modbus.SelfDefinedSample\Modbus.Net.Modbus.SelfDefinedSample.csproj", "{C4FA55AF-80ED-4467-948F-8EF865C8A5A5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "h-opc", "..\h-opc\h-opc\h-opc.csproj", "{DC6425E4-1409-488D-A014-4DCC909CF542}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -84,6 +86,10 @@ Global {C4FA55AF-80ED-4467-948F-8EF865C8A5A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {C4FA55AF-80ED-4467-948F-8EF865C8A5A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {C4FA55AF-80ED-4467-948F-8EF865C8A5A5}.Release|Any CPU.Build.0 = Release|Any CPU + {DC6425E4-1409-488D-A014-4DCC909CF542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC6425E4-1409-488D-A014-4DCC909CF542}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC6425E4-1409-488D-A014-4DCC909CF542}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC6425E4-1409-488D-A014-4DCC909CF542}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Modbus.Net/Modbus.Net/Configuration/MachineReader.cs b/Modbus.Net/Modbus.Net/Configuration/MachineReader.cs index ff8a0be..34004ea 100644 --- a/Modbus.Net/Modbus.Net/Configuration/MachineReader.cs +++ b/Modbus.Net/Modbus.Net/Configuration/MachineReader.cs @@ -1,5 +1,4 @@ -using FastEnumUtility; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; using System.IO; @@ -62,8 +61,19 @@ namespace Modbus.Net } case "addressMap": { - paramsSet.Add(AddressReader.ReadAddresses(dic["addressMap"])); - break; + var machineTypeTemp = Assembly.Load("Modbus.Net." + dic["protocol"]).GetType("Modbus.Net." + dic["protocol"] + "." + dic["protocol"] + "Machine`2"); + var addressTypes = machineTypeTemp.GetProperty("GetAddresses").PropertyType.GenericTypeArguments[0].GenericTypeArguments; + if (addressTypes[1] == typeof(int) && addressTypes[2] == typeof(int)) + { + paramsSet.Add(AddressReader.ReadAddresses(dic["addressMap"])); + break; + } + else if (addressTypes[1] == typeof(string) && addressTypes[2] == typeof(string)) + { + paramsSet.Add(AddressReader.ReadAddresses(dic["addressMap"])); + break; + } + throw new NotSupportedException("AddressUnit type not supported for AddressReader"); } case "endian": { @@ -105,7 +115,7 @@ namespace Modbus.Net } } - class AddressReader + internal class AddressReader where TUnitKey : IEquatable where TAddressKey : IEquatable where TSubAddressKey : IEquatable { private static readonly IConfigurationRoot configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) @@ -114,14 +124,14 @@ namespace Modbus.Net .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true, reloadOnChange: true) .Build(); - public static IEnumerable> ReadAddresses(string addressMapName) + public static IEnumerable> ReadAddresses(string addressMapName) { - var ans = new List>(); + var ans = new List>(); var addressesRoot = configuration.GetSection("Modbus.Net").GetSection("AddressMap").GetSection(addressMapName).GetChildren(); foreach (var address in addressesRoot) { - var addressNew = address.Get>(); + var addressNew = address.Get>(); addressNew.DataType = "System." + address["DataType"] != null ? Type.GetType("System." + address["DataType"]) : throw new ArgumentNullException("DataType is null"); if (addressNew.DataType == null) throw new ArgumentNullException(string.Format("DataType define error {0} {1} {2}", addressMapName, addressNew.Id, address["DataType"])); ans.Add(addressNew); diff --git a/Modbus.Net/Modbus.Net/Helper/AsyncHelper.cs b/Modbus.Net/Modbus.Net/Helper/AsyncHelper.cs deleted file mode 100644 index ea16dce..0000000 --- a/Modbus.Net/Modbus.Net/Helper/AsyncHelper.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Modbus.Net -{ - /// - /// AsyncHelper Class - /// - public static class AsyncHelper - { - private static readonly TaskFactory _myTaskFactory = new - TaskFactory(CancellationToken.None, - TaskCreationOptions.None, - TaskContinuationOptions.None, - TaskScheduler.Default); - - /// - /// Run async method syncronized - /// - /// Return type - /// Async method with return - /// Return value - public static TResult RunSync(Func> func) - { - return _myTaskFactory - .StartNew(func) - .Unwrap() - .GetAwaiter() - .GetResult(); - } - - /// - /// Run async method syncronized. - /// - /// Async method - public static void RunSync(Func func) - { - _myTaskFactory - .StartNew(func) - .Unwrap() - .GetAwaiter() - .GetResult(); - } - - /// - /// Change async task to async task with cancellation token - /// - /// Async task - /// Cancellation Token - /// Task with Cancellation token - public static Task WithCancellation(this Task task, - CancellationToken token) - { - return task.ContinueWith(t => t.GetAwaiter().GetResult(), token); - } - - /// - /// Add a CancellationToken to async task - /// - /// type of a task - /// Task - /// Cancellation token - /// Task with cancellation token - public static Task WithCancellation(this Task task, - CancellationToken token) - { - return task.ContinueWith(t => t.GetAwaiter().GetResult(), token); - } - } -} \ No newline at end of file diff --git a/Modbus.Net/Modbus.Net/Helper/ValueHelper.cs b/Modbus.Net/Modbus.Net/Helper/ValueHelper.cs index 76b9e38..c8635b4 100644 --- a/Modbus.Net/Modbus.Net/Helper/ValueHelper.cs +++ b/Modbus.Net/Modbus.Net/Helper/ValueHelper.cs @@ -307,13 +307,27 @@ namespace Modbus.Net /// 对应的ValueHelper实例 public static ValueHelper GetInstance(Endian endian) { + if (EndianInstanceCache.ContainsKey(endian.ToString())) + { + return EndianInstanceCache[endian.ToString()]; + } foreach (var assembly in AssemblyHelper.GetAllLibraryAssemblies()) { var valueHelper = assembly.GetType("Modbus.Net."+endian.ToString() + "ValueHelper")?.GetProperty("Instance")?.GetValue(null, null) as ValueHelper; - if (valueHelper != null) return valueHelper; + if (valueHelper != null) + { + EndianInstanceCache[endian.ToString()] = valueHelper; + return valueHelper; + } + } - return null; + throw new NotImplementedException("ValueHelper " + endian.ToString() + " doesn't exist."); } + + /// + /// ValueHelper实例的缓存 + /// + protected static Dictionary EndianInstanceCache = new Dictionary(); } /// diff --git a/Modbus.Net/Modbus.Net/Interface/IMachineMethod.cs b/Modbus.Net/Modbus.Net/Interface/IMachineMethod.cs index 676fdcd..25158b7 100644 --- a/Modbus.Net/Modbus.Net/Interface/IMachineMethod.cs +++ b/Modbus.Net/Modbus.Net/Interface/IMachineMethod.cs @@ -15,26 +15,12 @@ namespace Modbus.Net /// public interface IMachineMethodDatas : IMachineMethod { - /// - /// ȡ - /// - /// 豸ȡ - ReturnStruct>> GetDatas(MachineDataType getDataType); - /// /// ȡ /// /// 豸ȡ Task>>> GetDatasAsync(MachineDataType getDataType); - /// - /// д - /// - /// д - /// Ҫдֵ䣬дΪAddressʱΪҪдĵַдΪCommunicationTagʱΪҪдĵԪ - /// Ƿдɹ - ReturnStruct SetDatas(MachineDataType setDataType, Dictionary values); - /// /// д /// diff --git a/Modbus.Net/Modbus.Net/Interface/IProtocol.cs b/Modbus.Net/Modbus.Net/Interface/IProtocol.cs index 57a1d05..1d68227 100644 --- a/Modbus.Net/Modbus.Net/Interface/IProtocol.cs +++ b/Modbus.Net/Modbus.Net/Interface/IProtocol.cs @@ -39,13 +39,6 @@ namespace Modbus.Net /// bool Disconnect(); - /// - /// 发送协议内容并接收,一般方法 - /// - /// 写入的内容,使用对象数组描述 - /// 从设备获取的字节流 - TPipeUnit SendReceive(params object[] content); - /// /// 发送协议内容并接收,一般方法 /// @@ -53,14 +46,6 @@ namespace Modbus.Net /// 从设备获取的字节流 Task SendReceiveAsync(params object[] content); - /// - /// 发送协议,通过传入需要使用的协议内容和输入结构 - /// - /// 协议的实例 - /// 输入信息的结构化描述 - /// 输出信息的结构化描述 - TPipeUnit SendReceive(TProtocolUnit unit, IInputStruct content); - /// /// 发送协议,通过传入需要使用的协议内容和输入结构 /// @@ -69,16 +54,6 @@ namespace Modbus.Net /// 输出信息的结构化描述 Task SendReceiveAsync(TProtocolUnit unit, IInputStruct content); - /// - /// 发送协议,通过传入需要使用的协议内容和输入结构 - /// - /// 协议的实例 - /// 输入信息的结构化描述 - /// 输出信息的结构化描述 - /// IOutputStruct的具体类型 - T SendReceive( - TProtocolUnit unit, IInputStruct content) where T : class, IOutputStruct; - /// /// 发送协议,通过传入需要使用的协议内容和输入结构 /// diff --git a/Modbus.Net/Modbus.Net/Interface/IProtocolLinker.cs b/Modbus.Net/Modbus.Net/Interface/IProtocolLinker.cs index 867dde9..c3b525a 100644 --- a/Modbus.Net/Modbus.Net/Interface/IProtocolLinker.cs +++ b/Modbus.Net/Modbus.Net/Interface/IProtocolLinker.cs @@ -31,13 +31,6 @@ namespace Modbus.Net /// 设备是否断开成功 bool Disconnect(); - /// - /// 发送并接收数据 - /// - /// 发送协议的内容 - /// 接收协议的内容 - TParamOut SendReceive(TParamIn content); - /// /// 发送并接收数据 /// @@ -45,13 +38,6 @@ namespace Modbus.Net /// 接收协议的内容 Task SendReceiveAsync(TParamIn content); - /// - /// 发送并接收数据,不进行协议扩展和收缩,用于特殊协议 - /// - /// 发送协议的内容 - /// 接收协议的内容 - TParamOut SendReceiveWithoutExtAndDec(TParamIn content); - /// /// 发送并接收数据,不进行协议扩展和收缩,用于特殊协议 /// @@ -65,19 +51,5 @@ namespace Modbus.Net /// 接收协议的内容 /// 协议是否是正确的 bool? CheckRight(TParamOut content); - - /// - /// 协议内容扩展,发送时根据需要扩展 - /// - /// 扩展前的基本协议内容 - /// 扩展后的协议内容 - TParamIn BytesExtend(TParamIn content); - - /// - /// 协议内容缩减,接收时根据需要缩减 - /// - /// 缩减前的完整协议内容 - /// 缩减后的协议内容 - TParamOut BytesDecact(TParamOut content); } } \ No newline at end of file diff --git a/Modbus.Net/Modbus.Net/Interface/IUtilityMethod.cs b/Modbus.Net/Modbus.Net/Interface/IUtilityMethod.cs index b74e26b..5e5de59 100644 --- a/Modbus.Net/Modbus.Net/Interface/IUtilityMethod.cs +++ b/Modbus.Net/Modbus.Net/Interface/IUtilityMethod.cs @@ -16,14 +16,6 @@ namespace Modbus.Net /// public interface IUtilityMethodDatas : IUtilityMethod { - /// - /// 获取数据 - /// - /// 开始地址 - /// 获取字节数个数 - /// 接收到的byte数据 - ReturnStruct GetDatas(string startAddress, int getByteCount); - /// /// 获取数据 /// @@ -32,14 +24,6 @@ namespace Modbus.Net /// 接收到的byte数据 Task> GetDatasAsync(string startAddress, int getByteCount); - /// - /// 获取数据 - /// - /// 开始地址 - /// 获取类型和个数 - /// 接收到的对应的类型和数据 - ReturnStruct GetDatas(string startAddress, KeyValuePair getTypeAndCount); - /// /// 获取数据 /// @@ -48,15 +32,6 @@ namespace Modbus.Net /// 接收到的对应的类型和数据 Task> GetDatasAsync(string startAddress, KeyValuePair getTypeAndCount); - /// - /// 获取数据 - /// - /// 需要接收的类型 - /// 开始地址 - /// 获取字节数个数 - /// 接收到的对应的类型和数据 - ReturnStruct GetDatas(string startAddress, int getByteCount); - /// /// 获取数据 /// @@ -66,14 +41,6 @@ namespace Modbus.Net /// 接收到的对应的类型和数据 Task> GetDatasAsync(string startAddress, int getByteCount); - /// - /// 获取数据 - /// - /// 开始地址 - /// 获取类型和个数的队列 - /// 获取数据的对象数组,请强制转换成相应类型 - ReturnStruct GetDatas(string startAddress, IEnumerable> getTypeAndCountList); - /// /// 获取数据 /// @@ -81,14 +48,6 @@ namespace Modbus.Net /// 获取类型和个数的队列 Task> GetDatasAsync(string startAddress, IEnumerable> getTypeAndCountList); - /// - /// 设置数据 - /// - /// 开始地址 - /// 设置数据 - /// 是否设置成功 - ReturnStruct SetDatas(string startAddress, object[] setContents); - /// /// 设置数据 /// diff --git a/Modbus.Net/Modbus.Net/Linker/ComProtocolLinker.cs b/Modbus.Net/Modbus.Net/Linker/ComProtocolLinker.cs index e84f972..fca0bed 100644 --- a/Modbus.Net/Modbus.Net/Linker/ComProtocolLinker.cs +++ b/Modbus.Net/Modbus.Net/Linker/ComProtocolLinker.cs @@ -1,4 +1,4 @@ -using FastEnumUtility; +using System; using System.IO.Ports; namespace Modbus.Net @@ -23,11 +23,11 @@ namespace Modbus.Net protected ComProtocolLinker(string com, int slaveAddress, BaudRate? baudRate = null, Parity? parity = null, StopBits? stopBits = null, DataBits? dataBits = null, Handshake? handshake = null, int? connectionTimeout = null, bool? isFullDuplex = null) { - baudRate = FastEnum.Parse(baudRate != null ? baudRate.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "BaudRate")); - parity = FastEnum.Parse(parity != null ? parity.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "Parity")); - stopBits = FastEnum.Parse(stopBits != null ? stopBits.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "StopBits")); - dataBits = FastEnum.Parse(dataBits != null ? dataBits.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "DataBits")); - handshake = FastEnum.Parse(handshake != null ? handshake.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "Handshake")); + baudRate = Enum.Parse(baudRate != null ? baudRate.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "BaudRate")); + parity = Enum.Parse(parity != null ? parity.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "Parity")); + stopBits = Enum.Parse(stopBits != null ? stopBits.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "StopBits")); + dataBits = Enum.Parse(dataBits != null ? dataBits.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "DataBits")); + handshake = Enum.Parse(handshake != null ? handshake.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "Handshake")); connectionTimeout = int.Parse(connectionTimeout != null ? connectionTimeout.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "ConnectionTimeout")); isFullDuplex = bool.Parse(isFullDuplex != null ? isFullDuplex.ToString() : null ?? ConfigurationReader.GetValue("COM:" + com, "FullDuplex")); BaseConnector = new ComConnector(com + ":" + slaveAddress, baudRate.Value, parity.Value, stopBits.Value, dataBits.Value, handshake.Value, connectionTimeout.Value, isFullDuplex.Value); diff --git a/Modbus.Net/Modbus.Net/Linker/ProtocolLinker.cs b/Modbus.Net/Modbus.Net/Linker/ProtocolLinker.cs index b9c72a5..4ef3901 100644 --- a/Modbus.Net/Modbus.Net/Linker/ProtocolLinker.cs +++ b/Modbus.Net/Modbus.Net/Linker/ProtocolLinker.cs @@ -7,7 +7,7 @@ namespace Modbus.Net /// /// 基本的协议连接器 /// - public abstract class ProtocolLinker : ProtocolLinker + public abstract class ProtocolLinker : ProtocolLinker, IProtocolLinkerBytesExtend { /// /// 发送并接收数据 @@ -41,7 +41,7 @@ namespace Modbus.Net /// /// 扩展前的基本协议内容 /// 扩展后的协议内容 - public override byte[] BytesExtend(byte[] content) + public virtual byte[] BytesExtend(byte[] content) { //自动查找相应的协议放缩类,命令规则为——当前的实际类名(注意是继承后的)+"BytesExtend"。 var bytesExtend = @@ -56,7 +56,7 @@ namespace Modbus.Net /// /// 缩减前的完整协议内容 /// 缩减后的协议内容 - public override byte[] BytesDecact(byte[] content) + public virtual byte[] BytesDecact(byte[] content) { //自动查找相应的协议放缩类,命令规则为——当前的实际类名(注意是继承后的)+"BytesExtend"。 var bytesExtend = @@ -122,16 +122,6 @@ namespace Modbus.Net /// public bool IsConnected => BaseConnector != null && BaseConnector.IsConnected; - /// - /// 发送并接收数据 - /// - /// 发送协议的内容 - /// 接收协议的内容 - public virtual TParamOut SendReceive(TParamIn content) - { - return AsyncHelper.RunSync(() => SendReceiveAsync(content)); - } - /// /// 发送并接收数据 /// @@ -139,19 +129,8 @@ namespace Modbus.Net /// 接收协议的内容 public virtual async Task SendReceiveAsync(TParamIn content) { - var extBytes = BytesExtend(content); - var receiveBytes = await SendReceiveWithoutExtAndDecAsync(extBytes); - return BytesDecact(receiveBytes); - } - - /// - /// 发送并接收数据,不进行协议扩展和收缩,用于特殊协议 - /// - /// 发送协议的内容 - /// 接收协议的内容 - public virtual TParamOut SendReceiveWithoutExtAndDec(TParamIn content) - { - return AsyncHelper.RunSync(() => SendReceiveWithoutExtAndDecAsync(content)); + var receiveBytes = await SendReceiveWithoutExtAndDecAsync(content); + return receiveBytes; } /// @@ -180,25 +159,5 @@ namespace Modbus.Net Disconnect(); return false; } - - /// - /// 协议内容扩展,发送时根据需要扩展 - /// - /// 扩展前的基本协议内容 - /// 扩展后的协议内容 - public virtual TParamIn BytesExtend(TParamIn content) - { - throw new NotImplementedException(); - } - - /// - /// 协议内容缩减,接收时根据需要缩减 - /// - /// 缩减前的完整协议内容 - /// 缩减后的协议内容 - public virtual TParamOut BytesDecact(TParamOut content) - { - throw new NotImplementedException(); - } } } \ No newline at end of file diff --git a/Modbus.Net/Modbus.Net/Machine/AddressCombiner.cs b/Modbus.Net/Modbus.Net/Machine/AddressCombiner.cs index 2c25604..a42888d 100644 --- a/Modbus.Net/Modbus.Net/Machine/AddressCombiner.cs +++ b/Modbus.Net/Modbus.Net/Machine/AddressCombiner.cs @@ -7,20 +7,20 @@ namespace Modbus.Net /// /// 地址组合器,组合后的每一组地址将只需一次向设备进行通讯 /// - public abstract class AddressCombiner where TKey : IEquatable + public abstract class AddressCombiner where TKey : IEquatable where TAddressKey : IEquatable where TSubAddressKey : IEquatable { /// /// 组合地址 /// /// 需要进行组合的地址 /// 组合完成后与设备通讯的地址 - public abstract IEnumerable> Combine(IEnumerable> addresses); + public abstract IEnumerable> Combine(IEnumerable> addresses); } /// /// 连续的地址将组合成一组,向设备进行通讯 /// - public class AddressCombinerContinus : AddressCombiner where TKey : IEquatable + public class AddressCombinerContinus : AddressCombiner where TKey : IEquatable { /// /// 构造函数 @@ -48,7 +48,7 @@ namespace Modbus.Net /// /// 需要组合的地址 /// 组合后的地址 - public override IEnumerable> Combine(IEnumerable> addresses) + public override IEnumerable> Combine(IEnumerable> addresses) { //按从小到大的顺序对地址进行排序 var groupedAddresses = from address in addresses @@ -58,7 +58,7 @@ namespace Modbus.Net group address by address.Area into grouped select grouped; - var ans = new List>(); + var ans = new List>(); foreach (var groupedAddress in groupedAddresses) { var area = groupedAddress.Key; @@ -69,7 +69,7 @@ namespace Modbus.Net //上一个地址类型 Type preType = null; //记录一个地址组合当中的所有原始地址 - var originalAddresses = new List>(); + var originalAddresses = new List>(); //对组合内地址从小到大进行排序 var orderedAddresses = groupedAddress.OrderBy( @@ -114,7 +114,7 @@ namespace Modbus.Net AddressTranslator.GetAreaByteLength(address.Area))) { //上一个地址域压入返回结果,并把当前记录的结果清空。 - ans.Add(new CommunicationUnit + ans.Add(new CommunicationUnit { Area = area, Address = (int)Math.Floor(initNum), @@ -144,7 +144,7 @@ namespace Modbus.Net preType = address.DataType; } //最后一个地址域压入返回结果 - ans.Add(new CommunicationUnit + ans.Add(new CommunicationUnit { Area = area, Address = (int)Math.Floor(initNum), @@ -167,9 +167,9 @@ namespace Modbus.Net /// /// 拆分前的连续地址池 /// 拆分后的连续地址池 - protected List> MaxExclude(List> ans) + protected List> MaxExclude(List> ans) { - var newAns = new List>(); + var newAns = new List>(); foreach (var communicationUnit in ans) { var oldByteCount = communicationUnit.GetCount * @@ -178,7 +178,7 @@ namespace Modbus.Net while (oldByteCount * ValueHelper.ByteLength[communicationUnit.DataType.FullName] > MaxLength) { - var newOriginalAddresses = new List>(); + var newOriginalAddresses = new List>(); var newByteCount = 0.0; var newAddressUnitStart = oldOriginalAddresses.First(); do @@ -190,7 +190,7 @@ namespace Modbus.Net newOriginalAddresses.Add(currentAddressUnit); oldOriginalAddresses.RemoveAt(0); } while (newByteCount < MaxLength); - var newCommunicationUnit = new CommunicationUnit + var newCommunicationUnit = new CommunicationUnit { Area = newAddressUnitStart.Area, Address = newAddressUnitStart.Address, @@ -225,26 +225,26 @@ namespace Modbus.Net /// /// 单个地址变为一组,每一个地址都进行一次查询 /// - public class AddressCombinerSingle : AddressCombiner where TKey : IEquatable + public class AddressCombinerSingle : AddressCombiner where TKey : IEquatable where TAddressKey : IEquatable where TSubAddressKey : IEquatable { /// /// 组合地址 /// /// 需要组合的地址 /// 组合后的地址 - public override IEnumerable> Combine(IEnumerable> addresses) + public override IEnumerable> Combine(IEnumerable> addresses) { return addresses.Select( address => - new CommunicationUnit + new CommunicationUnit { Area = address.Area, Address = address.Address, SubAddress = address.SubAddress, DataType = address.DataType, GetCount = 1, - OriginalAddresses = new List> { address } + OriginalAddresses = new List> { address } }).ToList(); } } @@ -254,7 +254,7 @@ namespace Modbus.Net /// internal class CommunicationUnitGap where TKey : IEquatable { - public CommunicationUnit EndUnit { get; set; } + public CommunicationUnit EndUnit { get; set; } public int GapNumber { get; set; } } @@ -285,11 +285,11 @@ namespace Modbus.Net /// /// 需要组合的地址 /// 组合后的地址 - public override IEnumerable> Combine(IEnumerable> addresses) + public override IEnumerable> Combine(IEnumerable> addresses) { var continusAddresses = base.Combine(addresses).ToList(); var addressesGaps = new List>(); - CommunicationUnit preCommunicationUnit = null; + CommunicationUnit preCommunicationUnit = null; foreach (var continusAddress in continusAddresses) { if (preCommunicationUnit == null) @@ -335,7 +335,7 @@ namespace Modbus.Net continusAddresses.RemoveAt(index); continusAddresses.RemoveAt(index); //合并两个已有的地址段,变为一个新的地址段 - var newAddress = new CommunicationUnit + var newAddress = new CommunicationUnit { Area = nowAddress.Area, Address = preAddress.Address, @@ -382,9 +382,9 @@ namespace Modbus.Net /// /// 需要组合的地址 /// 组合后的地址 - public override IEnumerable> Combine(IEnumerable> addresses) + public override IEnumerable> Combine(IEnumerable> addresses) { - var addressUnits = addresses as IList> ?? addresses.ToList(); + var addressUnits = addresses as IList> ?? addresses.ToList(); var count = addressUnits.Sum(address => ValueHelper.ByteLength[address.DataType.FullName]); return new AddressCombinerNumericJump((int)(count * Percentage / 100.0), MaxLength, AddressTranslator) diff --git a/Modbus.Net/Modbus.Net/Machine/AddressFormater.cs b/Modbus.Net/Modbus.Net/Machine/AddressFormater.cs index 111b7c8..44b85d3 100644 --- a/Modbus.Net/Modbus.Net/Machine/AddressFormater.cs +++ b/Modbus.Net/Modbus.Net/Machine/AddressFormater.cs @@ -1,9 +1,11 @@ -namespace Modbus.Net +using System; + +namespace Modbus.Net { /// /// 地址编码器 /// - public abstract class AddressFormater + public abstract class AddressFormater where TAddressKey : IEquatable where TSubAddressKey : IEquatable { /// /// 编码地址 @@ -11,7 +13,7 @@ /// 地址所在的数据区域 /// 地址 /// 编码后的地址 - public abstract string FormatAddress(string area, int address); + public abstract string FormatAddress(string area, TAddressKey address); /// /// 编码地址 @@ -20,13 +22,13 @@ /// 地址 /// 子地址 /// 编码后的地址 - public abstract string FormatAddress(string area, int address, int subAddress); + public abstract string FormatAddress(string area, TAddressKey address, TSubAddressKey subAddress); } /// /// 基本的地址编码器 /// - public class AddressFormaterBase : AddressFormater + public class AddressFormaterBase : AddressFormater { /// /// 编码地址 diff --git a/Modbus.Net/Modbus.Net/Machine/BaseMachine.cs b/Modbus.Net/Modbus.Net/Machine/BaseMachine.cs index 725450f..c296c7e 100644 --- a/Modbus.Net/Modbus.Net/Machine/BaseMachine.cs +++ b/Modbus.Net/Modbus.Net/Machine/BaseMachine.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Quartz.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -11,37 +12,12 @@ namespace Modbus.Net /// /// 设备的Id类型 /// 设备中使用的AddressUnit的Id类型 - public abstract class BaseMachine : IMachine + public abstract class BaseMachine : BaseMachine, IMachine where TKey : IEquatable where TUnitKey : IEquatable { private static readonly ILogger> logger = LogProvider.CreateLogger>(); - private readonly int _maxErrorCount = 3; - - /// - /// 构造器 - /// - /// 设备的ID号 - /// 需要与设备通讯的地址 - protected BaseMachine(TKey id, IEnumerable> getAddresses) - : this(id, getAddresses, false) - { - } - - /// - /// 构造器 - /// - /// 设备的ID号 - /// 需要与设备通讯的地址 - /// 是否保持连接 - protected BaseMachine(TKey id, IEnumerable> getAddresses, bool keepConnect) - { - Id = id; - GetAddresses = getAddresses; - KeepConnect = keepConnect; - } - /// /// 构造器 /// @@ -50,70 +26,18 @@ namespace Modbus.Net /// 是否保持连接 /// 从站地址 /// 主站地址 - protected BaseMachine(TKey id, IEnumerable> getAddresses, bool keepConnect, byte slaveAddress, - byte masterAddress) : this(id, getAddresses, keepConnect) + protected BaseMachine(TKey id, IEnumerable> getAddresses, bool keepConnect, byte slaveAddress, + byte masterAddress) : base(id, getAddresses, keepConnect) { SlaveAddress = slaveAddress; MasterAddress = masterAddress; } + + private readonly int _maxErrorCount = 3; + private int ErrorCount { get; set; } - /// - /// 地址编码器 - /// - public AddressFormater AddressFormater { get; set; } - - /// - /// 获取地址组合器 - /// - public AddressCombiner AddressCombiner { get; set; } - - /// - /// 写入地址组合器 - /// - public AddressCombiner AddressCombinerSet { get; set; } - - /// - /// 地址转换器 - /// - public AddressTranslator AddressTranslator - { - get => BaseUtility.AddressTranslator; - set => BaseUtility.AddressTranslator = value; - } - - /// - /// 与设备实际通讯的连续地址 - /// - protected IEnumerable> CommunicateAddresses - => GetAddresses != null ? AddressCombiner.Combine(GetAddresses) : null; - - /// - /// 描述需要与设备通讯的地址 - /// - private IEnumerable> getAddresses; - - private object getAddressesLock = new object(); - - /// - /// 描述需要与设备通讯的地址 - /// - public IEnumerable> GetAddresses - { - get - { - return getAddresses; - } - set - { - lock (getAddressesLock) - { - getAddresses = value; - } - } - } - /// /// 从站号 /// @@ -125,20 +49,26 @@ namespace Modbus.Net public byte MasterAddress { get; set; } /// - /// 读取数据 + /// 与设备实际通讯的连续地址 /// - /// 从设备读取的数据 - public ReturnStruct>> GetDatas(MachineDataType getDataType) - { - return AsyncHelper.RunSync(() => GetDatasAsync(getDataType)); - } + protected IEnumerable> CommunicateAddresses + => GetAddresses != null ? AddressCombiner.Combine(GetAddresses) : null; + /// + /// 获取地址组合器 + /// + public AddressCombiner AddressCombiner { get; set; } + + /// + /// 写入地址组合器 + /// + public AddressCombiner AddressCombinerSet { get; set; } /// /// 读取数据 /// /// 从设备读取的数据 - public async Task>>> GetDatasAsync(MachineDataType getDataType) + public async override Task>>> GetDatasAsync(MachineDataType getDataType) { try { @@ -316,18 +246,7 @@ namespace Modbus.Net /// 写入类型 /// 需要写入的数据字典,当写入类型为Address时,键为需要写入的地址,当写入类型为CommunicationTag时,键为需要写入的单元的描述 /// 是否写入成功 - public ReturnStruct SetDatas(MachineDataType setDataType, Dictionary values) - { - return AsyncHelper.RunSync(() => SetDatasAsync(setDataType, values)); - } - - /// - /// 写入数据 - /// - /// 写入类型 - /// 需要写入的数据字典,当写入类型为Address时,键为需要写入的地址,当写入类型为CommunicationTag时,键为需要写入的单元的描述 - /// 是否写入成功 - public async Task> SetDatasAsync(MachineDataType setDataType, Dictionary values) + public async override Task> SetDatasAsync(MachineDataType setDataType, Dictionary values) { try { @@ -342,12 +261,12 @@ namespace Modbus.Net ErrorCode = -1, ErrorMsg = "Connection Error" }; - var addresses = new List>(); + var addresses = new List>(); //遍历每个要设置的值 foreach (var value in values) { //根据设置类型找到对应的地址描述 - AddressUnit address = null; + AddressUnit address = null; switch (setDataType) { case MachineDataType.Address: @@ -550,6 +469,49 @@ namespace Modbus.Net ErrorMsg = "" }; } + } + + /// + /// 设备 + /// + /// 设备的Id类型 + /// 设备中使用的AddressUnit的Id类型 + /// 设备中使用的AddressUnit的Address类型 + /// 设备中使用的AddressUnit的SubAddress类型 + public abstract class BaseMachine : IMachine + where TKey : IEquatable + where TUnitKey : IEquatable + where TAddressKey : IEquatable + where TSubAddressKey : IEquatable + { + private static readonly ILogger> logger = LogProvider.CreateLogger>(); + + /// + /// 构造器 + /// + /// 设备的ID号 + /// 需要与设备通讯的地址 + protected BaseMachine(TKey id, IEnumerable> getAddresses) + : this(id, getAddresses, false) + { + } + + /// + /// 构造器 + /// + /// 设备的ID号 + /// 需要与设备通讯的地址 + /// 是否保持连接 + protected BaseMachine(TKey id, IEnumerable> getAddresses, bool keepConnect) + { + Id = id; + GetAddresses = getAddresses; + KeepConnect = keepConnect; + } + + private readonly int _maxErrorCount = 3; + + private int ErrorCount { get; set; } /// /// 是否处于连接状态 @@ -586,6 +548,383 @@ namespace Modbus.Net /// public string ConnectionToken => BaseUtility.ConnectionToken; + /// + /// 描述需要与设备通讯的地址 + /// + private IEnumerable> getAddresses; + + private object getAddressesLock = new object(); + + /// + /// 描述需要与设备通讯的地址 + /// + public IEnumerable> GetAddresses + { + get + { + return getAddresses; + } + set + { + lock (getAddressesLock) + { + getAddresses = value; + } + } + } + + /// + /// 地址编码器 + /// + public AddressFormater AddressFormater { get; set; } + + /// + /// 地址转换器 + /// + public AddressTranslator AddressTranslator + { + get => BaseUtility.AddressTranslator; + set => BaseUtility.AddressTranslator = value; + } + + /// + /// 读取数据 + /// + /// 从设备读取的数据 + public async virtual Task>>> GetDatasAsync(MachineDataType getDataType) + { + try + { + var ans = new Dictionary>(); + //检测并连接设备 + if (!BaseUtility.IsConnected) + await BaseUtility.ConnectAsync(); + //如果无法连接,终止 + if (!BaseUtility.IsConnected) return + new ReturnStruct>>() + { + Datas = null, + IsSuccess = false, + ErrorCode = -1, + ErrorMsg = "Connection Error" + }; + //遍历每一个实际向设备获取数据的连续地址 + foreach (var address in GetAddresses) + { + //获取数据 + var datas = + await + BaseUtility.GetUtilityMethods().GetDatasAsync( + AddressFormater.FormatAddress(address.Area, address.Address, + address.SubAddress), + (int) + Math.Ceiling(ValueHelper.ByteLength[ + address.DataType.FullName])); + + + //如果没有数据,终止 + if (datas.IsSuccess == false || datas.Datas == null) + { + return new ReturnStruct>>() + { + Datas = null, + IsSuccess = false, + ErrorCode = datas.ErrorCode, + ErrorMsg = datas.ErrorMsg + }; + } + else if (datas.Datas.Length != 0 && datas.Datas.Length < + (int) + Math.Ceiling(ValueHelper.ByteLength[ + address.DataType.FullName])) + { + return new ReturnStruct>>() + { + Datas = null, + IsSuccess = false, + ErrorCode = -2, + ErrorMsg = "Data length mismatch" + }; + } + + //字节坐标的主地址位置 + var localMainPos = 0; + //字节坐标的子地址位置 + var localSubPos = 0; + + //根据类型选择返回结果的键是通讯标识还是地址 + string key; + switch (getDataType) + { + case MachineDataType.CommunicationTag: + { + key = address.CommunicationTag; + break; + } + case MachineDataType.Address: + { + key = AddressFormater.FormatAddress(address.Area, address.Address, address.SubAddress); + break; + } + case MachineDataType.Name: + { + key = address.Name; + break; + } + case MachineDataType.Id: + { + key = address.Id.ToString(); + break; + } + default: + { + key = address.CommunicationTag; + break; + } + } + + try + { + //如果没有数据返回空 + if (datas.Datas.Length == 0) + ans.Add(key, new ReturnUnit + { + DeviceValue = null, + AddressUnit = address.MapAddressUnitTUnitKeyToAddressUnit(), + }); + else + ans.Add(key, + new ReturnUnit + { + DeviceValue = + Math.Round(Convert.ToDouble( + ValueHelper.GetInstance(BaseUtility.Endian) + .GetValue(datas.Datas, ref localMainPos, ref localSubPos, + address.DataType)) * address.Zoom, address.DecimalPos), + AddressUnit = address.MapAddressUnitTUnitKeyToAddressUnit(), + }); + } + catch (Exception e) + { + ErrorCount++; + logger.LogError(e, $"BaseMachine -> GetDatas, Id:{Id} Connection:{ConnectionToken} key {key} existing. ErrorCount {ErrorCount}."); + + if (ErrorCount >= _maxErrorCount) + Disconnect(); + return new ReturnStruct>>() + { + Datas = null, + IsSuccess = false, + ErrorCode = -3, + ErrorMsg = "Data translation mismatch" + }; + } + } + + //如果不保持连接,断开连接 + if (!KeepConnect) + BaseUtility.Disconnect(); + //返回数据 + if (ans.All(p => p.Value.DeviceValue == null)) ans = null; + ErrorCount = 0; + return new ReturnStruct>> + { + Datas = ans, + IsSuccess = true, + ErrorCode = 0, + ErrorMsg = "" + }; + } + catch (Exception e) + { + ErrorCount++; + logger.LogError(e, $"BaseMachine -> GetDatas, Id:{Id} Connection:{ConnectionToken} error. ErrorCount {ErrorCount}."); + + if (ErrorCount >= _maxErrorCount) + Disconnect(); + return new ReturnStruct>>() + { + Datas = null, + IsSuccess = false, + ErrorCode = -100, + ErrorMsg = "Unknown Exception" + }; + } + } + + /// + /// 写入数据 + /// + /// 写入类型 + /// 需要写入的数据字典,当写入类型为Address时,键为需要写入的地址,当写入类型为CommunicationTag时,键为需要写入的单元的描述 + /// 是否写入成功 + public async virtual Task> SetDatasAsync(MachineDataType setDataType, Dictionary values) + { + try + { + //检测并连接设备 + if (!BaseUtility.IsConnected) + await BaseUtility.ConnectAsync(); + //如果设备无法连接,终止 + if (!BaseUtility.IsConnected) return new ReturnStruct() + { + Datas = false, + IsSuccess = false, + ErrorCode = -1, + ErrorMsg = "Connection Error" + }; + var addresses = new List>(); + //遍历每个要设置的值 + foreach (var value in values) + { + //根据设置类型找到对应的地址描述 + AddressUnit address = null; + switch (setDataType) + { + case MachineDataType.Address: + { + address = + GetAddresses.SingleOrDefault( + p => + AddressFormater.FormatAddress(p.Area, p.Address, p.SubAddress) == value.Key || + p.DataType != typeof(bool) && + AddressFormater.FormatAddress(p.Area, p.Address) == value.Key); + break; + } + case MachineDataType.CommunicationTag: + { + address = + GetAddresses.SingleOrDefault(p => p.CommunicationTag == value.Key); + break; + } + case MachineDataType.Name: + { + address = GetAddresses.SingleOrDefault(p => p.Name == value.Key); + break; + } + case MachineDataType.Id: + { + address = GetAddresses.SingleOrDefault(p => p.Id.ToString() == value.Key); + break; + } + default: + { + address = + GetAddresses.SingleOrDefault(p => p.CommunicationTag == value.Key); + break; + } + } + //地址为空报错 + if (address == null) + { + logger.LogError($"Machine {ConnectionToken} Address {value.Key} doesn't exist."); + continue; + } + //不能写报错 + if (!address.CanWrite) + { + logger.LogError($"Machine {ConnectionToken} Address {value.Key} cannot write."); + continue; + } + addresses.Add(address); + } + + //遍历每条通讯的地址 + + + var valueHelper = ValueHelper.GetInstance(BaseUtility.Endian); + + foreach (var addressUnit in addresses) + { + //协议主地址字符串 + var address = AddressFormater.FormatAddress(addressUnit.Area, + addressUnit.Address, addressUnit.SubAddress); + //获取写入类型 + var dataType = addressUnit.DataType; + KeyValuePair value; + switch (setDataType) + { + case MachineDataType.Address: + { + //获取要写入的值 + value = values.SingleOrDefault(p => p.Key == address); + break; + } + case MachineDataType.CommunicationTag: + { + value = values.SingleOrDefault(p => p.Key == addressUnit.CommunicationTag); + break; + } + case MachineDataType.Name: + { + value = values.SingleOrDefault(p => p.Key == addressUnit.Name); + break; + } + case MachineDataType.Id: + { + value = values.SingleOrDefault(p => p.Key == addressUnit.Id.ToString()); + break; + } + default: + { + value = values.SingleOrDefault(p => p.Key == addressUnit.CommunicationTag); + break; + } + } + //将要写入的值加入队列 + var data = Convert.ChangeType(value.Value / addressUnit.Zoom, dataType); + + //写入数据 + await + BaseUtility.GetUtilityMethods().SetDatasAsync(address, + new object[] { data }); + } + //如果不保持连接,断开连接 + if (!KeepConnect) + BaseUtility.Disconnect(); + } + catch (Exception e) + { + ErrorCount++; + logger.LogError(e, $"BaseMachine -> SetDatas, Id:{Id} Connection:{ConnectionToken} error. ErrorCount {ErrorCount}."); + + if (ErrorCount >= _maxErrorCount) + Disconnect(); + return new ReturnStruct() + { + Datas = false, + IsSuccess = false, + ErrorCode = -100, + ErrorMsg = "Unknown Exception" + }; + } + return new ReturnStruct() + { + Datas = true, + IsSuccess = true, + ErrorCode = 0, + ErrorMsg = "" + }; + } + + /// + /// 通过Id获取数据字段定义 + /// + /// 数据字段Id + /// 数据字段 + public AddressUnit GetAddressUnitById(TUnitKey addressUnitId) + { + try + { + return GetAddresses.SingleOrDefault(p => p.Id.Equals(addressUnitId)); + } + catch (Exception e) + { + logger.LogError(e, $"BaseMachine -> GetAddressUnitById Id:{Id} ConnectionToken:{ConnectionToken} addressUnitId:{addressUnitId} Repeated"); + return null; + } + } + /// /// 获取设备的方法集合 /// @@ -627,24 +966,6 @@ namespace Modbus.Net { return BaseUtility.Disconnect(); } - - /// - /// 通过Id获取数据字段定义 - /// - /// 数据字段Id - /// 数据字段 - public AddressUnit GetAddressUnitById(TUnitKey addressUnitId) - { - try - { - return GetAddresses.SingleOrDefault(p => p.Id.Equals(addressUnitId)); - } - catch (Exception e) - { - logger.LogError(e, $"BaseMachine -> GetAddressUnitById Id:{Id} ConnectionToken:{ConnectionToken} addressUnitId:{addressUnitId} Repeated"); - return null; - } - } } internal class BaseMachineEqualityComparer : IEqualityComparer> @@ -664,7 +985,7 @@ namespace Modbus.Net /// /// 通讯单元 /// - public class CommunicationUnit where TKey : IEquatable + public class CommunicationUnit where TKey : IEquatable where TAddressKey : IEquatable where TSubAddressKey : IEquatable { /// /// 区域 @@ -674,12 +995,12 @@ namespace Modbus.Net /// /// 地址 /// - public int Address { get; set; } + public TAddressKey Address { get; set; } /// /// 子地址 /// - public int SubAddress { get; set; } = 0; + public TSubAddressKey SubAddress { get; set; } /// /// 获取个数 @@ -694,7 +1015,7 @@ namespace Modbus.Net /// /// 原始的地址 /// - public IEnumerable> OriginalAddresses { get; set; } + public IEnumerable> OriginalAddresses { get; set; } } /// @@ -710,20 +1031,13 @@ namespace Modbus.Net /// /// 数据定义 /// - public AddressUnit AddressUnit { get; set; } + public AddressUnit AddressUnit { get; set; } } /// /// 地址单元 /// - public class AddressUnit : AddressUnit - { - } - - /// - /// 地址单元 - /// - public class AddressUnit : IEquatable> where TKey : IEquatable + public class AddressUnit : IEquatable> where TKey : IEquatable where TAddressKey : IEquatable where TSubAddressKey : IEquatable { /// /// 数据单元Id @@ -738,12 +1052,12 @@ namespace Modbus.Net /// /// 地址 /// - public int Address { get; set; } + public TAddressKey Address { get; set; } /// /// bit位地址 /// - public int SubAddress { get; set; } = 0; + public TSubAddressKey SubAddress { get; set; } /// /// 数据类型 @@ -785,9 +1099,9 @@ namespace Modbus.Net /// /// 另一个地址 /// 是否一致 - public bool Equals(AddressUnit other) + public bool Equals(AddressUnit other) { - return Area.ToUpper() == other.Area.ToUpper() && Address == other.Address || Id.Equals(other.Id); + return Area.ToUpper() == other.Area.ToUpper() && Address.Equals(other.Address) || Id.Equals(other.Id); } } } \ No newline at end of file diff --git a/Modbus.Net/Modbus.Net/Machine/BaseMachineExtend.cs b/Modbus.Net/Modbus.Net/Machine/BaseMachineExtend.cs index 22e2eb9..94f9d9d 100644 --- a/Modbus.Net/Modbus.Net/Machine/BaseMachineExtend.cs +++ b/Modbus.Net/Modbus.Net/Machine/BaseMachineExtend.cs @@ -35,14 +35,14 @@ namespace Modbus.Net /// /// /// - public static AddressUnit MapAddressUnitTUnitKeyToAddressUnit(this AddressUnit addressUnit) where TUnitKey : IEquatable + public static AddressUnit MapAddressUnitTUnitKeyToAddressUnit(this AddressUnit addressUnit) where TUnitKey : IEquatable where TAddressKey : IEquatable where TSubAddressKey : IEquatable { - return new AddressUnit() + return new AddressUnit() { Id = addressUnit.Id.ToString(), Area = addressUnit.Area, - Address = addressUnit.Address, - SubAddress = addressUnit.SubAddress, + Address = addressUnit.Address?.ToString(), + SubAddress = addressUnit.SubAddress?.ToString(), DataType = addressUnit.DataType, Zoom = addressUnit.Zoom, DecimalPos = addressUnit.DecimalPos, diff --git a/Modbus.Net/Modbus.Net/Modbus.Net.csproj b/Modbus.Net/Modbus.Net/Modbus.Net.csproj index e448f08..8b62c00 100644 --- a/Modbus.Net/Modbus.Net/Modbus.Net.csproj +++ b/Modbus.Net/Modbus.Net/Modbus.Net.csproj @@ -1,7 +1,7 @@  - net6.0;net462 + net6.0 10.0 Modbus.Net Modbus.Net @@ -37,7 +37,6 @@ - diff --git a/Modbus.Net/Modbus.Net/Protocol/BaseProtocol.cs b/Modbus.Net/Modbus.Net/Protocol/BaseProtocol.cs index ad6682e..6036fd8 100644 --- a/Modbus.Net/Modbus.Net/Protocol/BaseProtocol.cs +++ b/Modbus.Net/Modbus.Net/Protocol/BaseProtocol.cs @@ -146,18 +146,6 @@ namespace Modbus.Net return false; } - /// - /// 发送协议,通过传入需要使用的协议内容和输入结构 - /// - /// 协议的实例 - /// 输入信息的结构化描述 - /// 输出信息的结构化描述 - public virtual TPipeUnit SendReceive( - TProtocolUnit unit, IInputStruct content) - { - return AsyncHelper.RunSync(() => SendReceiveAsync(unit, content)); - } - /// /// 发送协议,通过传入需要使用的协议内容和输入结构 /// @@ -177,16 +165,6 @@ namespace Modbus.Net return null; } - /// - /// 发送协议内容并接收,一般方法 - /// - /// 写入的内容,使用对象数组描述 - /// 从设备获取的字节流 - public virtual TPipeUnit SendReceive(params object[] content) - { - return AsyncHelper.RunSync(() => SendReceiveAsync(content)); - } - /// /// 发送协议内容并接收,一般方法(不能使用,如需使用请继承) /// @@ -197,18 +175,6 @@ namespace Modbus.Net throw new NotImplementedException(); } - /// - /// 发送协议,通过传入需要使用的协议内容和输入结构 - /// - /// 协议的实例 - /// 输入信息的结构化描述 - /// 输出信息的结构化描述 - /// IOutputStruct的具体类型 - public virtual T SendReceive(TProtocolUnit unit, IInputStruct content) where T : class, IOutputStruct - { - return AsyncHelper.RunSync(() => SendReceiveAsync(unit, content)); - } - /// /// 发送协议,通过传入需要使用的协议内容和输入结构 /// diff --git a/Modbus.Net/Modbus.Net/Utility/BaseUtility.cs b/Modbus.Net/Modbus.Net/Utility/BaseUtility.cs index 89fc1d2..76640af 100644 --- a/Modbus.Net/Modbus.Net/Utility/BaseUtility.cs +++ b/Modbus.Net/Modbus.Net/Utility/BaseUtility.cs @@ -45,17 +45,6 @@ namespace Modbus.Net /// public byte MasterAddress { get; set; } - /// - /// 获取数据 - /// - /// 开始地址 - /// 获取字节数个数 - /// 接收到的byte数据 - public virtual ReturnStruct GetDatas(string startAddress, int getByteCount) - { - return AsyncHelper.RunSync(() => GetDatasAsync(startAddress, getByteCount)); - } - /// /// 获取数据 /// @@ -64,18 +53,6 @@ namespace Modbus.Net /// 接收到的byte数据 public abstract Task> GetDatasAsync(string startAddress, int getByteCount); - /// - /// 获取数据 - /// - /// 开始地址 - /// 获取类型和个数 - /// 接收到的对应的类型和数据 - public virtual ReturnStruct GetDatas(string startAddress, - KeyValuePair getTypeAndCount) - { - return AsyncHelper.RunSync(() => GetDatasAsync(startAddress, getTypeAndCount)); - } - /// /// 获取数据 /// @@ -123,19 +100,6 @@ namespace Modbus.Net } } - /// - /// 获取数据 - /// - /// 需要接收的类型 - /// 开始地址 - /// 获取字节数个数 - /// 接收到的对应的类型和数据 - public virtual ReturnStruct GetDatas(string startAddress, - int getByteCount) - { - return AsyncHelper.RunSync(() => GetDatasAsync(startAddress, getByteCount)); - } - /// /// 获取数据 /// @@ -181,19 +145,6 @@ namespace Modbus.Net } } - /// - /// 获取数据 - /// - /// 开始地址 - /// 获取类型和个数的队列 - /// 获取数据的对象数组,请强制转换成相应类型 - public virtual ReturnStruct GetDatas(string startAddress, - IEnumerable> getTypeAndCountList) - { - return - AsyncHelper.RunSync(() => GetDatasAsync(startAddress, getTypeAndCountList)); - } - /// /// 获取数据 /// @@ -244,17 +195,6 @@ namespace Modbus.Net } } - /// - /// 设置数据 - /// - /// 开始地址 - /// 设置数据 - /// 是否设置成功 - public virtual ReturnStruct SetDatas(string startAddress, object[] setContents) - { - return AsyncHelper.RunSync(() => SetDatasAsync(startAddress, setContents)); - } - /// /// 设置数据 /// diff --git a/README.md b/README.md index 873be19..84706e6 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,4 @@ Thanks * Quartz - Job Scheduler * Serilog - Logging * DotNetty - Network Transporting +* h-opc & Technosoftware.DaAeHdaSolution & OPCFoundation.NetStandard - OPC Trasporting diff --git a/Samples/AnyType/Controllers/HomeController.cs b/Samples/AnyType/Controllers/HomeController.cs index eff41f5..64c4111 100644 --- a/Samples/AnyType/Controllers/HomeController.cs +++ b/Samples/AnyType/Controllers/HomeController.cs @@ -5,6 +5,7 @@ using Modbus.Net.Modbus; using System.Diagnostics; using MachineJobSchedulerCreator = Modbus.Net.MachineJobSchedulerCreator; using ModbusMachine = Modbus.Net.Modbus.ModbusMachine; +using AddressUnit = Modbus.Net.AddressUnit; namespace AnyType.Controllers { diff --git a/Samples/MachineJob/MachineJob.csproj b/Samples/MachineJob/MachineJob.csproj index 0afecc4..2b490b0 100644 --- a/Samples/MachineJob/MachineJob.csproj +++ b/Samples/MachineJob/MachineJob.csproj @@ -23,6 +23,7 @@ + diff --git a/Samples/MachineJob/appsettings.json b/Samples/MachineJob/appsettings.json index 62dd01d..fffe11b 100644 --- a/Samples/MachineJob/appsettings.json +++ b/Samples/MachineJob/appsettings.json @@ -9,7 +9,7 @@ } }, "ConnectionStrings": { - "DatabaseWriteConnectionString": "Server=10.10.18.245; User ID=root; Password=123456; Database=modbusnettest;" + "DatabaseWriteConnectionString": "Server=127.0.0.1; User ID=root; Password=123456; Database=modbusnettest;" }, "Modbus.Net": { @@ -18,7 +18,7 @@ "a:id": "ModbusMachine1", "b:protocol": "Modbus", "c:type": "Tcp", - "d:connectionString": "10.10.18.251", + "d:connectionString": "127.0.0.1", "e:addressMap": "AddressMapModbus", "f:keepConnect": true, "g:slaveAddress": 1, @@ -29,7 +29,7 @@ "a:id": "SiemensMachine1", "b:protocol": "Siemens", "c:type": "Tcp", - "d:connectionString": "10.10.18.251", + "d:connectionString": "127.0.0.1", "e:model": "S7_1200", "f:addressMap": "AddressMapSiemens", "g:keepConnect": true, @@ -53,7 +53,7 @@ "a:id": "SiemensMachine2", "b:protocol": "Siemens", "c:type": "Ppi", - "d:connectionString": "COM11", + "d:connectionString": "COM2", "e:model": "S7_200", "f:addressMap": "AddressMapSiemens", "g:keepConnect": true, @@ -61,6 +61,14 @@ "i:masterAddress": 0, "j:src": 1, "k:dst": 0 + }, + { + "a:id": "OpcMachine1", + "b:protocol": "Opc", + "c:type": "Da", + "d:connectionString": "opcda://localhost/Matrikon.OPC.Simulation.1", + "e:addressMap": "AddressMapOpc", + "f:tagSpliter": "." } ], "addressMap": { @@ -207,6 +215,25 @@ "Id": "10", "Name": "Test10" } + ], + "AddressMapOpc": + [ + { + "Area": "Random", + "Address": "Real4", + "DataType": "Single", + "Id": "1", + "Name": "Test1", + "DecimalPos": 2 + }, + { + "Area": "Random", + "Address": "Real8", + "DataType": "Double", + "Id": "2", + "Name": "Test2", + "DecimalPos": 4 + } ] } } diff --git a/Samples/TripleAdd/Controllers/HomeController.cs b/Samples/TripleAdd/Controllers/HomeController.cs index 1a930c1..35f7d4f 100644 --- a/Samples/TripleAdd/Controllers/HomeController.cs +++ b/Samples/TripleAdd/Controllers/HomeController.cs @@ -3,6 +3,7 @@ using Modbus.Net; using Modbus.Net.Modbus; using System.Diagnostics; using TripleAdd.Models; +using AddressUnit = Modbus.Net.AddressUnit; namespace TripleAdd.Controllers { diff --git a/Tests/Modbus.Net.Tests/BaseTest.cs b/Tests/Modbus.Net.Tests/BaseTest.cs index 9331c3a..951babc 100644 --- a/Tests/Modbus.Net.Tests/BaseTest.cs +++ b/Tests/Modbus.Net.Tests/BaseTest.cs @@ -1,13 +1,14 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Modbus.Net.Modbus; using Modbus.Net.Siemens; +using AddressUnit = Modbus.Net.AddressUnit; namespace Modbus.Net.Tests { [TestClass] public class BaseTest { - private List>? _addressUnits; + private List? _addressUnits; private BaseMachine? _baseMachine; @@ -16,9 +17,9 @@ namespace Modbus.Net.Tests [TestInitialize] public void Init() { - _addressUnits = new List> + _addressUnits = new List { - new AddressUnit + new AddressUnit { Id = 1, Area = "3X", @@ -26,7 +27,7 @@ namespace Modbus.Net.Tests SubAddress = 0, DataType = typeof(bool) }, - new AddressUnit + new AddressUnit { Id = 2, Area = "3X", @@ -34,7 +35,7 @@ namespace Modbus.Net.Tests SubAddress = 1, DataType = typeof(bool) }, - new AddressUnit + new AddressUnit { Id = 3, Area = "3X", @@ -42,7 +43,7 @@ namespace Modbus.Net.Tests SubAddress = 2, DataType = typeof(bool) }, - new AddressUnit + new AddressUnit { Id = 4, Area = "3X", @@ -50,7 +51,7 @@ namespace Modbus.Net.Tests SubAddress = 0, DataType = typeof(byte) }, - new AddressUnit + new AddressUnit { Id = 5, Area = "3X", @@ -58,7 +59,7 @@ namespace Modbus.Net.Tests SubAddress = 8, DataType = typeof(byte) }, - new AddressUnit + new AddressUnit { Id = 6, Area = "3X", @@ -66,7 +67,7 @@ namespace Modbus.Net.Tests SubAddress = 0, DataType = typeof(ushort) }, - new AddressUnit + new AddressUnit { Id = 7, Area = "3X", @@ -74,7 +75,7 @@ namespace Modbus.Net.Tests SubAddress = 0, DataType = typeof(ushort) }, - new AddressUnit + new AddressUnit { Id = 8, Area = "3X", @@ -82,7 +83,7 @@ namespace Modbus.Net.Tests SubAddress = 0, DataType = typeof(ushort) }, - new AddressUnit + new AddressUnit { Id = 9, Area = "3X", @@ -90,7 +91,7 @@ namespace Modbus.Net.Tests SubAddress = 0, DataType = typeof(ushort) }, - new AddressUnit + new AddressUnit { Id = 10, Area = "3X", @@ -98,7 +99,7 @@ namespace Modbus.Net.Tests SubAddress = 0, DataType = typeof(ushort) }, - new AddressUnit + new AddressUnit { Id = 11, Area = "3X", @@ -106,7 +107,7 @@ namespace Modbus.Net.Tests SubAddress = 0, DataType = typeof(ushort) }, - new AddressUnit + new AddressUnit { Id = 12, Area = "4X", @@ -114,7 +115,7 @@ namespace Modbus.Net.Tests SubAddress = 0, DataType = typeof(uint) }, - new AddressUnit + new AddressUnit { Id = 13, Area = "4X", @@ -195,7 +196,7 @@ namespace Modbus.Net.Tests [TestMethod] public void AddressCombinerSingleTest() { - var addressCombiner = new AddressCombinerSingle(); + var addressCombiner = new AddressCombinerSingle(); var combinedAddresses = addressCombiner.Combine(_addressUnits!).ToArray(); Assert.AreEqual(combinedAddresses[0].Area, "3X"); Assert.AreEqual(combinedAddresses[0].Address, 1); diff --git a/Tests/Modbus.Net.Tests/EndianTest.cs b/Tests/Modbus.Net.Tests/EndianTest.cs index 280544f..907f71a 100644 --- a/Tests/Modbus.Net.Tests/EndianTest.cs +++ b/Tests/Modbus.Net.Tests/EndianTest.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Modbus.Net.Modbus; +using AddressUnit = Modbus.Net.AddressUnit; namespace Modbus.Net.Tests { diff --git a/Tests/Modbus.Net.Tests/MachineMethodTest.cs b/Tests/Modbus.Net.Tests/MachineMethodTest.cs index 5289da5..59a0ccf 100644 --- a/Tests/Modbus.Net.Tests/MachineMethodTest.cs +++ b/Tests/Modbus.Net.Tests/MachineMethodTest.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Modbus.Net.Modbus; using System.Reflection; +using AddressUnit = Modbus.Net.AddressUnit; namespace Modbus.Net.Tests { @@ -35,9 +36,9 @@ namespace Modbus.Net.Tests [TestMethod] public async Task InvokeMachine() { - BaseMachine baseMachine = new ModbusMachine(1, ModbusType.Tcp, _machineIp, new List> + BaseMachine baseMachine = new ModbusMachine(1, ModbusType.Tcp, _machineIp, new List { - new AddressUnit + new AddressUnit { Id = 0, Area = "0X", diff --git a/Tests/Modbus.Net.Tests/Modbus.Net.Tests.csproj b/Tests/Modbus.Net.Tests/Modbus.Net.Tests.csproj index 0fe727b..2c3705c 100644 --- a/Tests/Modbus.Net.Tests/Modbus.Net.Tests.csproj +++ b/Tests/Modbus.Net.Tests/Modbus.Net.Tests.csproj @@ -30,6 +30,7 @@ + diff --git a/Tests/Modbus.Net.Tests/ModbusMultiStationTest.cs b/Tests/Modbus.Net.Tests/ModbusMultiStationTest.cs index 8dfe01e..0300503 100644 --- a/Tests/Modbus.Net.Tests/ModbusMultiStationTest.cs +++ b/Tests/Modbus.Net.Tests/ModbusMultiStationTest.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Modbus.Net.Modbus; +using AddressUnit = Modbus.Net.AddressUnit; namespace Modbus.Net.Tests { diff --git a/Tests/Modbus.Net.Tests/ModbusTest.cs b/Tests/Modbus.Net.Tests/ModbusTest.cs index 332516f..0a288e2 100644 --- a/Tests/Modbus.Net.Tests/ModbusTest.cs +++ b/Tests/Modbus.Net.Tests/ModbusTest.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Modbus.Net.Modbus; +using AddressUnit = Modbus.Net.AddressUnit; namespace Modbus.Net.Tests { diff --git a/Tests/Modbus.Net.Tests/SiemensTest.cs b/Tests/Modbus.Net.Tests/SiemensTest.cs index adf60ce..83c9722 100644 --- a/Tests/Modbus.Net.Tests/SiemensTest.cs +++ b/Tests/Modbus.Net.Tests/SiemensTest.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Modbus.Net.Siemens; +using AddressUnit = Modbus.Net.AddressUnit; namespace Modbus.Net.Tests { diff --git a/h-opc/h-opc/Common/ClientExtensions.cs b/h-opc/h-opc/Common/ClientExtensions.cs new file mode 100644 index 0000000..3116830 --- /dev/null +++ b/h-opc/h-opc/Common/ClientExtensions.cs @@ -0,0 +1,32 @@ +namespace Hylasoft.Opc.Common +{ + /// + /// Useful extension methods for OPC Clients + /// + public static class ClientExtensions + { + /// + /// Reads a tag from the OPC. If for whatever reason the read fails (Tag doesn't exist, server not available) returns a default value + /// + /// the opc client to use for the read + /// The fully qualified identifier of the tag + /// the default value to read if the read fails + /// + public static ReadEvent ReadOrdefault(this IClient client, string tag, T defaultValue = default(T)) + { + try + { + return client.Read(tag); + } + catch (OpcException) + { + var readEvent = new ReadEvent(); + readEvent.Quality = Quality.Good; + readEvent.Value = defaultValue; + readEvent.SourceTimestamp = DateTime.Now; + readEvent.ServerTimestamp = DateTime.Now; + return readEvent; + } + } + } +} \ No newline at end of file diff --git a/h-opc/h-opc/Common/IClient.cs b/h-opc/h-opc/Common/IClient.cs new file mode 100644 index 0000000..dbfa34d --- /dev/null +++ b/h-opc/h-opc/Common/IClient.cs @@ -0,0 +1,99 @@ +namespace Hylasoft.Opc.Common +{ + /// + /// Client interface to perform basic Opc tasks, like discovery, monitoring, reading/writing tags, + /// + public interface IClient : IDisposable + where TNode : Node + { + /// + /// Connect the client to the OPC Server + /// + void Connect(); + + /// + /// Gets the current status of the OPC Client + /// + OpcStatus Status { get; } + + /// + /// Gets the datatype of an OPC tag + /// + /// Tag to get datatype of + /// System Type + System.Type GetDataType(string tag); + + /// + /// Read a tag + /// + /// The type of tag to read + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo` + /// The value retrieved from the OPC + ReadEvent Read(string tag); + + /// + /// Write a value on the specified opc tag + /// + /// The type of tag to write on + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` writes on the tag `bar` on the folder `foo` + /// + void Write(string tag, T item); + + /// + /// Monitor the specified tag for changes + /// + /// the type of tag to monitor + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` monitors the tag `bar` on the folder `foo` + /// the callback to execute when the value is changed. + /// The first parameter is the new value of the node, the second is an `unsubscribe` function to unsubscribe the callback + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an async method.")] + void Monitor(string tag, Action, Action> callback); + + /// + /// Finds a node on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the tag `bar` on the folder `foo` + /// If there is a tag, it returns it, otherwise it throws an + TNode FindNode(string tag); + + /// + /// Gets the root node of the server + /// + TNode RootNode { get; } + + /// + /// Explore a folder on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the sub nodes of `bar` on the folder `foo` + /// The list of sub-nodes + IEnumerable ExploreFolder(string tag); + + /// + /// Read a tag asynchronusly + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an async method.")] + Task> ReadAsync(string tag); + + /// + /// Write a value on the specified opc tag asynchronously + /// + Task WriteAsync(string tag, T item); + + /// + /// Finds a node on the Opc Server asynchronously + /// + Task FindNodeAsync(string tag); + + /// + /// Explore a folder on the Opc Server asynchronously + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", + Justification = "Task")] + Task> ExploreFolderAsync(string tag); + } +} \ No newline at end of file diff --git a/h-opc/h-opc/Common/Node.cs b/h-opc/h-opc/Common/Node.cs new file mode 100644 index 0000000..ae6750f --- /dev/null +++ b/h-opc/h-opc/Common/Node.cs @@ -0,0 +1,46 @@ +namespace Hylasoft.Opc.Common +{ + /// + /// Base class representing a node on the OPC server + /// + public abstract class Node + { + /// + /// Gets the displayed name of the node + /// + public string Name { get; protected set; } + + /// + /// Gets the dot-separated fully qualified tag of the node + /// + public string Tag { get; protected set; } + + /// + /// Gets the parent node. If the node is root, returns null + /// + public Node Parent { get; private set; } + + /// + /// Creates a new node + /// + /// the name of the node + /// The parent node + protected Node(string name, Node parent = null) + { + Name = name; + Parent = parent; + if (parent != null && !string.IsNullOrEmpty(parent.Tag)) + Tag = parent.Tag + '.' + name; + else + Tag = name; + } + + /// + /// Overrides ToString() + /// + public override string ToString() + { + return Tag; + } + } +} diff --git a/h-opc/h-opc/Common/OpcException.cs b/h-opc/h-opc/Common/OpcException.cs new file mode 100644 index 0000000..adfead0 --- /dev/null +++ b/h-opc/h-opc/Common/OpcException.cs @@ -0,0 +1,69 @@ +using Opc.Ua; +using System.Runtime.Serialization; + +namespace Hylasoft.Opc.Common +{ + /// + /// Identifies an exception occurred during OPC Communication + /// + [Serializable] + public class OpcException : Exception + { + /// + /// Initialize a new instance of the OpcException class + /// + public OpcException() + { + } + + /// + /// Initialize a new instance of the OpcException class + /// + public OpcException(string message) + : base(message) + { + } + + /// + /// Returns an (optional) associated OPC UA StatusCode for the exception. + /// + public StatusCode? Status { get; private set; } + + /// + /// Initialize a new instance of the OpcException class + /// + public OpcException(string message, StatusCode status) + : base(message) + { + Status = status; + } + + /// + /// Initialize a new instance of the OpcException class + /// + public OpcException(string message, Exception inner) + : base(message, inner) + { + } + + /// + /// Initialize a new instance of the OpcException class + /// + protected OpcException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// Sets the System.Runtime.Serialization.SerializationInfo with information about the exception. + /// + /// The System.Runtime.Serialization.SerializationInfo that holds the serialized object data about the exception being thrown. + /// The System.Runtime.Serialization.StreamingContext that contains contextual information about the source or destination. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + } + + } + +} \ No newline at end of file diff --git a/h-opc/h-opc/Common/OpcStatus.cs b/h-opc/h-opc/Common/OpcStatus.cs new file mode 100644 index 0000000..3534d4a --- /dev/null +++ b/h-opc/h-opc/Common/OpcStatus.cs @@ -0,0 +1,18 @@ +namespace Hylasoft.Opc.Common +{ + /// + /// Identifies the status of an OPC connector + /// + public enum OpcStatus + { + /// + /// The client is not connected + /// + NotConnected, + + /// + /// The client is connected + /// + Connected + } +} \ No newline at end of file diff --git a/h-opc/h-opc/Common/Quality.cs b/h-opc/h-opc/Common/Quality.cs new file mode 100644 index 0000000..b9bfcc9 --- /dev/null +++ b/h-opc/h-opc/Common/Quality.cs @@ -0,0 +1,28 @@ +using System.ComponentModel; + +namespace Hylasoft.Opc.Common +{ + /// + /// Represents the quality of the value captured + /// + public enum Quality + { + /// + /// Quality: Unknown, the value of the quality could not be inferred by the library + /// + [Description("Unknown")] + Unknown, + + /// + /// Quality: Good + /// + [Description("Good")] + Good, + + /// + /// Quality: Bad + /// + [Description("Bad")] + Bad + } +} \ No newline at end of file diff --git a/h-opc/h-opc/Common/ReadEvent.cs b/h-opc/h-opc/Common/ReadEvent.cs new file mode 100644 index 0000000..6bc48d2 --- /dev/null +++ b/h-opc/h-opc/Common/ReadEvent.cs @@ -0,0 +1,32 @@ +using System.ComponentModel; + +namespace Hylasoft.Opc.Common +{ + /// + /// Base class representing a monitor event on the OPC server + /// + /// + public class ReadEvent + { + /// + /// Gets the value that was read from the server + /// + public T Value { get; set; } + + /// + /// Gets the quality of the signal from the server + /// + [DefaultValue(Common.Quality.Unknown)] + public Quality Quality { get; set; } + + /// + /// Gets the source timestamp on when the event ocurred + /// + public DateTime SourceTimestamp { get; set; } + + /// + /// Gets the server timestamp on when the event ocurred + /// + public DateTime ServerTimestamp { get; set; } + } +} diff --git a/h-opc/h-opc/Da/DaClient.cs b/h-opc/h-opc/Da/DaClient.cs new file mode 100644 index 0000000..456fe48 --- /dev/null +++ b/h-opc/h-opc/Da/DaClient.cs @@ -0,0 +1,298 @@ +using Hylasoft.Opc.Common; +using System.Globalization; +using Technosoftware.DaAeHdaClient; +using Factory = Technosoftware.DaAeHdaClient.Com.Factory; +using OpcDa = Technosoftware.DaAeHdaClient.Da; + +namespace Hylasoft.Opc.Da +{ + /// + /// Client Implementation for DA + /// + public partial class DaClient : IClient + { + private readonly OpcUrl _url; + private OpcDa.TsCDaServer _server; + private long _sub; + private readonly IDictionary _nodesCache = new Dictionary(); + + // default monitor interval in Milliseconds + private const int DefaultMonitorInterval = 100; + + /// + /// Initialize a new Data Access Client + /// + /// The url of the server to connect to. WARNING: If server URL includes + /// spaces (ex. "RSLinx OPC Server") then pass the server URL in to the constructor as an Opc.URL object + /// directly instead. + public DaClient(Uri serverUrl) + { + _url = new OpcUrl(serverUrl.OriginalString) + { + Scheme = serverUrl.Scheme, + HostName = serverUrl.Host + }; + } + + /// + /// Initialize a new Data Access Client + /// + /// The url of the server to connect to + public DaClient(OpcUrl serverUrl) + { + _url = serverUrl; + } + + /// + /// Gets the datatype of an OPC tag + /// + /// Tag to get datatype of + /// System Type + public System.Type GetDataType(string tag) + { + var item = new OpcDa.TsCDaItem { ItemName = tag }; + OpcDa.TsCDaItemProperty result; + try + { + var propertyCollection = _server.GetProperties(new[] { item }, new[] { new OpcDa.TsDaPropertyID(1) }, false)[0]; + result = propertyCollection[0]; + } + catch (NullReferenceException) + { + throw new OpcException("Could not find node because server not connected."); + } + return result.DataType; + } + + /// + /// OpcDa underlying server object. + /// + protected OpcDa.TsCDaServer Server + { + get + { + return _server; + } + } + + #region interface methods + + /// + /// Connect the client to the OPC Server + /// + public void Connect() + { + if (Status == OpcStatus.Connected) + return; + _server = new OpcDa.TsCDaServer(new Factory(), _url); + _server.Connect(); + var root = new DaNode(string.Empty, string.Empty); + RootNode = root; + AddNodeToCache(root); + } + + /// + /// Gets the current status of the OPC Client + /// + public OpcStatus Status + { + get + { + if (_server == null || _server.GetServerStatus().ServerState != OpcServerState.Operational) + return OpcStatus.NotConnected; + return OpcStatus.Connected; + } + } + + /// + /// Read a tag + /// + /// The type of tag to read + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo` + /// The value retrieved from the OPC + public ReadEvent Read(string tag) + { + var item = new OpcDa.TsCDaItem { ItemName = tag }; + if (Status == OpcStatus.NotConnected) + { + throw new OpcException("Server not connected. Cannot read tag."); + } + var result = _server.Read(new[] { item })[0]; + T casted; + TryCastResult(result.Value, out casted); + + var readEvent = new ReadEvent(); + readEvent.Value = casted; + readEvent.SourceTimestamp = result.Timestamp; + readEvent.ServerTimestamp = result.Timestamp; + if (result.Quality == OpcDa.TsCDaQuality.Good) readEvent.Quality = Quality.Good; + if (result.Quality == OpcDa.TsCDaQuality.Bad) readEvent.Quality = Quality.Bad; + + return readEvent; + } + + /// + /// Write a value on the specified opc tag + /// + /// The type of tag to write on + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` writes on the tag `bar` on the folder `foo` + /// + public void Write(string tag, T item) + { + var itmVal = new OpcDa.TsCDaItemValue + { + ItemName = tag, + Value = item + }; + var result = _server.Write(new[] { itmVal })[0]; + CheckResult(result, tag); + } + + /// + /// Casts result of monitoring and reading values + /// + /// Value to convert + /// The casted result + /// Type of object to try to cast + public void TryCastResult(object value, out T casted) + { + try + { + casted = (T)value; + } + catch (InvalidCastException) + { + throw new InvalidCastException( + string.Format( + "Could not monitor tag. Cast failed for type \"{0}\" on the new value \"{1}\" with type \"{2}\". Make sure tag data type matches.", + typeof(T), value, value.GetType())); + } + } + + /// + /// Monitor the specified tag for changes + /// + /// the type of tag to monitor + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` monitors the tag `bar` on the folder `foo` + /// the callback to execute when the value is changed. + /// The first parameter is a MonitorEvent object which represents the data point, the second is an `unsubscribe` function to unsubscribe the callback + public void Monitor(string tag, Action, Action> callback) + { + var subItem = new OpcDa.TsCDaSubscriptionState + { + Name = (++_sub).ToString(CultureInfo.InvariantCulture), + Active = true, + UpdateRate = DefaultMonitorInterval + }; + var sub = _server.CreateSubscription(subItem); + + // I have to start a new thread here because unsubscribing + // the subscription during a datachanged event causes a deadlock + Action unsubscribe = () => new Thread(o => + _server.CancelSubscription(sub)).Start(); + + sub.DataChangedEvent += (handle, requestHandle, values) => + { + T casted; + TryCastResult(values[0].Value, out casted); + var monitorEvent = new ReadEvent(); + monitorEvent.Value = casted; + monitorEvent.SourceTimestamp = values[0].Timestamp; + monitorEvent.ServerTimestamp = values[0].Timestamp; + if (values[0].Quality == OpcDa.TsCDaQuality.Good) monitorEvent.Quality = Quality.Good; + if (values[0].Quality == OpcDa.TsCDaQuality.Bad) monitorEvent.Quality = Quality.Bad; + callback(monitorEvent, unsubscribe); + }; + sub.AddItems(new[] { new OpcDa.TsCDaItem { ItemName = tag } }); + sub.SetEnabled(true); + } + + /// + /// Finds a node on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the tag `bar` on the folder `foo` + /// If there is a tag, it returns it, otherwise it throws an + public DaNode FindNode(string tag) + { + // if the tag already exists in cache, return it + if (_nodesCache.ContainsKey(tag)) + return _nodesCache[tag]; + + // try to find the tag otherwise + var item = new OpcDa.TsCDaItem { ItemName = tag }; + OpcDa.TsCDaItemValueResult result; + try + { + result = _server.Read(new[] { item })[0]; + } + catch (NullReferenceException) + { + throw new OpcException("Could not find node because server not connected."); + } + CheckResult(result, tag); + var node = new DaNode(item.ItemName, item.ItemName, RootNode); + AddNodeToCache(node); + return node; + } + + /// + /// Gets the root node of the server + /// + public DaNode RootNode { get; private set; } + + /// + /// Explore a folder on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the sub nodes of `bar` on the folder `foo` + /// The list of sub-nodes + public IEnumerable ExploreFolder(string tag) + { + var parent = FindNode(tag); + OpcDa.TsCDaBrowsePosition p; + var nodes = _server.Browse(new OpcItem(parent.Tag), new OpcDa.TsCDaBrowseFilters(), out p) + .Select(t => new DaNode(t.Name, t.ItemName, parent)) + .ToList(); + //add nodes to cache + foreach (var node in nodes) + AddNodeToCache(node); + + return nodes; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + if (_server != null) + _server.Dispose(); + GC.SuppressFinalize(this); + } + + #endregion + + /// + /// Adds a node to the cache using the tag as its key + /// + /// the node to add + private void AddNodeToCache(DaNode node) + { + if (!_nodesCache.ContainsKey(node.Tag)) + _nodesCache.Add(node.Tag, node); + } + + private static void CheckResult(IOpcResult result, string tag) + { + if (result == null) + throw new OpcException("The server replied with an empty response"); + if (result.Result.ToString() != "S_OK" && result.Result.ToString() != "E_READONLY") + throw new OpcException(string.Format("Invalid response from the server. (Response Status: {0}, Opc Tag: {1})", result.Result, tag)); + } + } +} + diff --git a/h-opc/h-opc/Da/DaClient_async.cs b/h-opc/h-opc/Da/DaClient_async.cs new file mode 100644 index 0000000..d974466 --- /dev/null +++ b/h-opc/h-opc/Da/DaClient_async.cs @@ -0,0 +1,42 @@ +using Hylasoft.Opc.Common; + +namespace Hylasoft.Opc.Da +{ + /// + /// Client Implementation for DA + /// + public partial class DaClient + { + /// + /// Read a tag asynchronusly + /// + public async Task> ReadAsync(string tag) + { + return await Task.Run(() => Read(tag)); + } + + /// + /// Write a value on the specified opc tag asynchronously + /// + public async Task WriteAsync(string tag, T item) + { + await Task.Run(() => Write(tag, item)); + } + + /// + /// Finds a node on the Opc Server asynchronously + /// + public async Task FindNodeAsync(string tag) + { + return await Task.Run(() => FindNode(tag)); + } + + /// + /// Explore a folder on the Opc Server asynchronously + /// + public async Task> ExploreFolderAsync(string tag) + { + return await Task.Run(() => ExploreFolder(tag)); + } + } +} diff --git a/h-opc/h-opc/Da/DaNode.cs b/h-opc/h-opc/Da/DaNode.cs new file mode 100644 index 0000000..9cc1edd --- /dev/null +++ b/h-opc/h-opc/Da/DaNode.cs @@ -0,0 +1,22 @@ +using Hylasoft.Opc.Common; + +namespace Hylasoft.Opc.Da +{ + /// + /// Represents a node to be used specifically for OPC DA + /// + public class DaNode : Node + { + /// + /// Instantiates a DaNode class + /// + /// the name of the node + /// + /// The parent node + public DaNode(string name, string tag, Node parent = null) + : base(name, parent) + { + Tag = tag; + } + } +} diff --git a/h-opc/h-opc/Ua/ClientUtils.cs b/h-opc/h-opc/Ua/ClientUtils.cs new file mode 100644 index 0000000..5e43f78 --- /dev/null +++ b/h-opc/h-opc/Ua/ClientUtils.cs @@ -0,0 +1,98 @@ +using Opc.Ua; +using Opc.Ua.Client; + +namespace Hylasoft.Opc.Ua +{ + /// + /// List of static utility methods + /// + internal static class ClientUtils + { + // TODO I didn't write these methods. I should rewrite it once I understand whtat it does, beacuse it looks crazy + + public static EndpointDescription SelectEndpoint(Uri discoveryUrl, bool useSecurity) + { + var configuration = EndpointConfiguration.Create(); + configuration.OperationTimeout = 5000; + EndpointDescription endpointDescription1 = null; + using (var discoveryClient = DiscoveryClient.Create(discoveryUrl, configuration)) + { + var endpoints = discoveryClient.GetEndpoints(null); + foreach (var endpointDescription2 in endpoints.Where(endpointDescription2 => endpointDescription2.EndpointUrl.StartsWith(discoveryUrl.Scheme))) + { + if (useSecurity) + { + if (endpointDescription2.SecurityMode == MessageSecurityMode.None) + continue; + } + else if (endpointDescription2.SecurityMode != MessageSecurityMode.None) + continue; + if (endpointDescription1 == null) + endpointDescription1 = endpointDescription2; + if (endpointDescription2.SecurityLevel > endpointDescription1.SecurityLevel) + endpointDescription1 = endpointDescription2; + } + if (endpointDescription1 == null) + { + if (endpoints.Count > 0) + endpointDescription1 = endpoints[0]; + } + } + var uri = Utils.ParseUri(endpointDescription1.EndpointUrl); + if (uri != null && uri.Scheme == discoveryUrl.Scheme) + endpointDescription1.EndpointUrl = new UriBuilder(uri) + { + Host = discoveryUrl.DnsSafeHost, + Port = discoveryUrl.Port + }.ToString(); + return endpointDescription1; + } + + public static ReferenceDescriptionCollection Browse(Session session, NodeId nodeId) + { + var desc = new BrowseDescription + { + NodeId = nodeId, + BrowseDirection = BrowseDirection.Forward, + IncludeSubtypes = true, + NodeClassMask = 0U, + ResultMask = 63U, + }; + return Browse(session, desc, true); + } + + public static ReferenceDescriptionCollection Browse(Session session, BrowseDescription nodeToBrowse, bool throwOnError) + { + try + { + var descriptionCollection = new ReferenceDescriptionCollection(); + var nodesToBrowse = new BrowseDescriptionCollection { nodeToBrowse }; + BrowseResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + session.Browse(null, null, 0U, nodesToBrowse, out results, out diagnosticInfos); + ClientBase.ValidateResponse(results, nodesToBrowse); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse); + while (!StatusCode.IsBad(results[0].StatusCode)) + { + for (var index = 0; index < results[0].References.Count; ++index) + descriptionCollection.Add(results[0].References[index]); + if (results[0].References.Count == 0 || results[0].ContinuationPoint == null) + return descriptionCollection; + var continuationPoints = new ByteStringCollection(); + continuationPoints.Add(results[0].ContinuationPoint); + session.BrowseNext(null, false, continuationPoints, out results, out diagnosticInfos); + ClientBase.ValidateResponse(results, continuationPoints); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, continuationPoints); + } + throw new ServiceResultException(results[0].StatusCode); + } + catch (Exception ex) + { + if (throwOnError) + throw new ServiceResultException(ex, 2147549184U); + return null; + } + } + } +} + diff --git a/h-opc/h-opc/Ua/NodeExtensions.cs b/h-opc/h-opc/Ua/NodeExtensions.cs new file mode 100644 index 0000000..8913f4b --- /dev/null +++ b/h-opc/h-opc/Ua/NodeExtensions.cs @@ -0,0 +1,24 @@ +using Hylasoft.Opc.Common; +using OpcF = Opc.Ua; + +namespace Hylasoft.Opc.Ua +{ + /// + /// Class with extension methods for OPC UA + /// + public static class NodeExtensions + { + /// + /// Converts an OPC Foundation node to an Hylasoft OPC UA Node + /// + /// The node to convert + /// the parent node (optional) + /// + internal static UaNode ToHylaNode(this OpcF.ReferenceDescription node, Node parent = null) + { + var name = node.DisplayName.ToString(); + var nodeId = node.NodeId.ToString(); + return new UaNode(name, nodeId, parent); + } + } +} \ No newline at end of file diff --git a/h-opc/h-opc/Ua/UaClient.cs b/h-opc/h-opc/Ua/UaClient.cs new file mode 100644 index 0000000..f9452e1 --- /dev/null +++ b/h-opc/h-opc/Ua/UaClient.cs @@ -0,0 +1,642 @@ +using Hylasoft.Opc.Common; +using Opc.Ua; +using Opc.Ua.Client; +using Opc.Ua.Configuration; + +namespace Hylasoft.Opc.Ua +{ + /// + /// Client Implementation for UA + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", + Justification = "Doesn't make sense to split this class")] + public class UaClient : IClient + { + private readonly UaClientOptions _options = new UaClientOptions(); + private readonly Uri _serverUrl; + private Session _session; + + private readonly IDictionary _nodesCache = new Dictionary(); + private readonly IDictionary> _folderCache = new Dictionary>(); + + /// + /// Creates a server object + /// + /// the url of the server to connect to + public UaClient(Uri serverUrl) + { + _serverUrl = serverUrl; + Status = OpcStatus.NotConnected; + } + + /// + /// Creates a server object + /// + /// the url of the server to connect to + /// custom options to use with ua client + public UaClient(Uri serverUrl, UaClientOptions options) + { + _serverUrl = serverUrl; + _options = options; + Status = OpcStatus.NotConnected; + } + + /// + /// Options to configure the UA client session + /// + public UaClientOptions Options + { + get { return _options; } + } + + /// + /// OPC Foundation underlying session object + /// + protected Session Session + { + get + { + return _session; + } + } + + private void PostInitializeSession() + { + var node = _session.NodeCache.Find(ObjectIds.ObjectsFolder); + RootNode = new UaNode(string.Empty, node.NodeId.ToString()); + AddNodeToCache(RootNode); + Status = OpcStatus.Connected; + } + + /// + /// Connect the client to the OPC Server + /// + public void Connect() + { + if (Status == OpcStatus.Connected) + return; + _session = InitializeSession(_serverUrl).Result; + _session.KeepAlive += SessionKeepAlive; + _session.SessionClosing += SessionClosing; + PostInitializeSession(); + } + + /// + /// Gets the datatype of an OPC tag + /// + /// Tag to get datatype of + /// System Type + public System.Type GetDataType(string tag) + { + var nodesToRead = BuildReadValueIdCollection(tag, Attributes.Value); + DataValueCollection results; + DiagnosticInfoCollection diag; + _session.Read( + requestHeader: null, + maxAge: 0, + timestampsToReturn: TimestampsToReturn.Neither, + nodesToRead: nodesToRead, + results: out results, + diagnosticInfos: out diag); + var type = results[0].WrappedValue.TypeInfo.BuiltInType; + return System.Type.GetType("System." + type.ToString()); + } + + private void SessionKeepAlive(ISession session, KeepAliveEventArgs e) + { + if (e.CurrentState != ServerState.Running) + { + if (Status == OpcStatus.Connected) + { + Status = OpcStatus.NotConnected; + NotifyServerConnectionLost(); + } + } + else if (e.CurrentState == ServerState.Running) + { + if (Status == OpcStatus.NotConnected) + { + Status = OpcStatus.Connected; + NotifyServerConnectionRestored(); + } + } + } + + private void SessionClosing(object sender, EventArgs e) + { + Status = OpcStatus.NotConnected; + NotifyServerConnectionLost(); + } + + + /// + /// Reconnect the OPC session + /// + public void ReConnect() + { + Status = OpcStatus.NotConnected; + _session.Reconnect(); + Status = OpcStatus.Connected; + } + + /// + /// Create a new OPC session, based on the current session parameters. + /// + public void RecreateSession() + { + Status = OpcStatus.NotConnected; + _session = Session.Recreate(_session); + PostInitializeSession(); + } + + + /// + /// Gets the current status of the OPC Client + /// + public OpcStatus Status { get; private set; } + + + private ReadValueIdCollection BuildReadValueIdCollection(string tag, uint attributeId) + { + var n = FindNode(tag, RootNode); + var readValue = new ReadValueId + { + NodeId = n.NodeId, + AttributeId = attributeId + }; + return new ReadValueIdCollection { readValue }; + } + + /// + /// Read a tag + /// + /// The type of tag to read + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo` + /// The value retrieved from the OPC + public ReadEvent Read(string tag) + { + var nodesToRead = BuildReadValueIdCollection(tag, Attributes.Value); + DataValueCollection results; + DiagnosticInfoCollection diag; + _session.Read( + requestHeader: null, + maxAge: 0, + timestampsToReturn: TimestampsToReturn.Neither, + nodesToRead: nodesToRead, + results: out results, + diagnosticInfos: out diag); + var val = results[0]; + + var readEvent = new ReadEvent(); + readEvent.Value = (T)val.Value; + readEvent.SourceTimestamp = val.SourceTimestamp; + readEvent.ServerTimestamp = val.ServerTimestamp; + if (StatusCode.IsGood(val.StatusCode)) readEvent.Quality = Quality.Good; + if (StatusCode.IsBad(val.StatusCode)) readEvent.Quality = Quality.Bad; + return readEvent; + } + + + /// + /// Read a tag asynchronously + /// + /// The type of tag to read + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo` + /// The value retrieved from the OPC + public Task> ReadAsync(string tag) + { + var nodesToRead = BuildReadValueIdCollection(tag, Attributes.Value); + + // Wrap the ReadAsync logic in a TaskCompletionSource, so we can use C# async/await syntax to call it: + var taskCompletionSource = new TaskCompletionSource>(); + _session.BeginRead( + requestHeader: null, + maxAge: 0, + timestampsToReturn: TimestampsToReturn.Neither, + nodesToRead: nodesToRead, + callback: ar => + { + DataValueCollection results; + DiagnosticInfoCollection diag; + var response = _session.EndRead( + result: ar, + results: out results, + diagnosticInfos: out diag); + + try + { + CheckReturnValue(response.ServiceResult); + var val = results[0]; + var readEvent = new ReadEvent(); + readEvent.Value = (T)val.Value; + readEvent.SourceTimestamp = val.SourceTimestamp; + readEvent.ServerTimestamp = val.ServerTimestamp; + if (StatusCode.IsGood(val.StatusCode)) readEvent.Quality = Quality.Good; + if (StatusCode.IsBad(val.StatusCode)) readEvent.Quality = Quality.Bad; + taskCompletionSource.TrySetResult(readEvent); + } + catch (Exception ex) + { + taskCompletionSource.TrySetException(ex); + } + }, + asyncState: null); + + return taskCompletionSource.Task; + } + + + private WriteValueCollection BuildWriteValueCollection(string tag, uint attributeId, object dataValue) + { + var n = FindNode(tag, RootNode); + var writeValue = new WriteValue + { + NodeId = n.NodeId, + AttributeId = attributeId, + Value = { Value = dataValue } + }; + return new WriteValueCollection { writeValue }; + } + + /// + /// Write a value on the specified opc tag + /// + /// The type of tag to write on + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` writes on the tag `bar` on the folder `foo` + /// The value for the item to write + public void Write(string tag, T item) + { + var nodesToWrite = BuildWriteValueCollection(tag, Attributes.Value, item); + + StatusCodeCollection results; + DiagnosticInfoCollection diag; + _session.Write( + requestHeader: null, + nodesToWrite: nodesToWrite, + results: out results, + diagnosticInfos: out diag); + + CheckReturnValue(results[0]); + } + + /// + /// Write a value on the specified opc tag asynchronously + /// + /// The type of tag to write on + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` writes on the tag `bar` on the folder `foo` + /// The value for the item to write + public Task WriteAsync(string tag, T item) + { + var nodesToWrite = BuildWriteValueCollection(tag, Attributes.Value, item); + + // Wrap the WriteAsync logic in a TaskCompletionSource, so we can use C# async/await syntax to call it: + var taskCompletionSource = new TaskCompletionSource(); + _session.BeginWrite( + requestHeader: null, + nodesToWrite: nodesToWrite, + callback: ar => + { + StatusCodeCollection results; + DiagnosticInfoCollection diag; + var response = _session.EndWrite( + result: ar, + results: out results, + diagnosticInfos: out diag); + try + { + CheckReturnValue(response.ServiceResult); + CheckReturnValue(results[0]); + taskCompletionSource.SetResult(response.ServiceResult); + } + catch (Exception ex) + { + taskCompletionSource.TrySetException(ex); + } + }, + asyncState: null); + return taskCompletionSource.Task; + } + + + /// + /// Monitor the specified tag for changes + /// + /// the type of tag to monitor + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` monitors the tag `bar` on the folder `foo` + /// the callback to execute when the value is changed. + /// The first parameter is a MonitorEvent object which represents the data point, the second is an `unsubscribe` function to unsubscribe the callback + public void Monitor(string tag, Action, Action> callback) + { + var node = FindNode(tag); + + var sub = new Subscription + { + PublishingInterval = _options.DefaultMonitorInterval, + PublishingEnabled = true, + LifetimeCount = _options.SubscriptionLifetimeCount, + KeepAliveCount = _options.SubscriptionKeepAliveCount, + DisplayName = tag, + Priority = byte.MaxValue + }; + + var item = new MonitoredItem + { + StartNodeId = node.NodeId, + AttributeId = Attributes.Value, + DisplayName = tag, + SamplingInterval = _options.DefaultMonitorInterval + }; + sub.AddItem(item); + _session.AddSubscription(sub); + sub.Create(); + sub.ApplyChanges(); + + item.Notification += (monitoredItem, args) => + { + var p = (MonitoredItemNotification)args.NotificationValue; + var t = p.Value.WrappedValue.Value; + Action unsubscribe = () => + { + sub.RemoveItems(sub.MonitoredItems); + sub.Delete(true); + _session.RemoveSubscription(sub); + sub.Dispose(); + }; + + var monitorEvent = new ReadEvent(); + monitorEvent.Value = (T)t; + monitorEvent.SourceTimestamp = p.Value.SourceTimestamp; + monitorEvent.ServerTimestamp = p.Value.ServerTimestamp; + if (StatusCode.IsGood(p.Value.StatusCode)) monitorEvent.Quality = Quality.Good; + if (StatusCode.IsBad(p.Value.StatusCode)) monitorEvent.Quality = Quality.Bad; + callback(monitorEvent, unsubscribe); + }; + } + + /// + /// Explore a folder on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the sub nodes of `bar` on the folder `foo` + /// The list of sub-nodes + public IEnumerable ExploreFolder(string tag) + { + IList nodes; + _folderCache.TryGetValue(tag, out nodes); + if (nodes != null) + return nodes; + + var folder = FindNode(tag); + nodes = ClientUtils.Browse(_session, folder.NodeId) + .GroupBy(n => n.NodeId) //this is to select distinct + .Select(n => n.First()) + .Where(n => n.NodeClass == NodeClass.Variable || n.NodeClass == NodeClass.Object) + .Select(n => n.ToHylaNode(folder)) + .ToList(); + + //add nodes to cache + if (!_folderCache.ContainsKey(tag)) + _folderCache.Add(tag, nodes); + foreach (var node in nodes) + AddNodeToCache(node); + + return nodes; + } + + /// + /// Explores a folder asynchronously + /// + public async Task> ExploreFolderAsync(string tag) + { + return await Task.Run(() => ExploreFolder(tag)); + } + + /// + /// Finds a node on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the tag `bar` on the folder `foo` + /// If there is a tag, it returns it, otherwise it throws an + public UaNode FindNode(string tag) + { + // if the tag already exists in cache, return it + if (_nodesCache.ContainsKey(tag)) + return _nodesCache[tag]; + + // try to find the tag otherwise + var found = FindNode(tag, RootNode); + if (found != null) + { + AddNodeToCache(found); + return found; + } + + // throws an exception if not found + throw new OpcException(string.Format("The tag \"{0}\" doesn't exist on the Server", tag)); + } + + /// + /// Find node asynchronously + /// + public async Task FindNodeAsync(string tag) + { + return await Task.Run(() => FindNode(tag)); + } + + /// + /// Gets the root node of the server + /// + public UaNode RootNode { get; private set; } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + if (_session != null) + { + _session.RemoveSubscriptions(_session.Subscriptions.ToList()); + _session.Close(); + _session.Dispose(); + } + GC.SuppressFinalize(this); + } + + private void CheckReturnValue(StatusCode status) + { + if (!StatusCode.IsGood(status)) + throw new OpcException(string.Format("Invalid response from the server. (Response Status: {0})", status), status); + } + + /// + /// Adds a node to the cache using the tag as its key + /// + /// the node to add + private void AddNodeToCache(UaNode node) + { + if (!_nodesCache.ContainsKey(node.Tag)) + _nodesCache.Add(node.Tag, node); + } + + /// + /// Return identity login object for a given URI. + /// + /// Login URI + /// AnonUser or User with name and password + private UserIdentity GetIdentity(Uri url) + { + if (_options.UserIdentity != null) + { + return _options.UserIdentity; + } + var uriLogin = new UserIdentity(); + if (!string.IsNullOrEmpty(url.UserInfo)) + { + var uis = url.UserInfo.Split(':'); + uriLogin = new UserIdentity(uis[0], uis[1]); + } + return uriLogin; + } + + /// + /// Crappy method to initialize the session. I don't know what many of these things do, sincerely. + /// + private async Task InitializeSession(Uri url) + { + var certificateValidator = new CertificateValidator(); + certificateValidator.CertificateValidation += (sender, eventArgs) => + { + if (ServiceResult.IsGood(eventArgs.Error)) + eventArgs.Accept = true; + else if ((eventArgs.Error.StatusCode.Code == StatusCodes.BadCertificateUntrusted) && _options.AutoAcceptUntrustedCertificates) + eventArgs.Accept = true; + else + throw new OpcException(string.Format("Failed to validate certificate with error code {0}: {1}", eventArgs.Error.Code, eventArgs.Error.AdditionalInfo), eventArgs.Error.StatusCode); + }; + // Build the application configuration + var appInstance = new ApplicationInstance + { + ApplicationType = ApplicationType.Client, + ConfigSectionName = _options.ConfigSectionName, + ApplicationConfiguration = new ApplicationConfiguration + { + ApplicationUri = url.ToString(), + ApplicationName = _options.ApplicationName, + ApplicationType = ApplicationType.Client, + CertificateValidator = certificateValidator, + ServerConfiguration = new ServerConfiguration + { + MaxSubscriptionCount = _options.MaxSubscriptionCount, + MaxMessageQueueSize = _options.MaxMessageQueueSize, + MaxNotificationQueueSize = _options.MaxNotificationQueueSize, + MaxPublishRequestCount = _options.MaxPublishRequestCount + }, + SecurityConfiguration = new SecurityConfiguration + { + AutoAcceptUntrustedCertificates = _options.AutoAcceptUntrustedCertificates + }, + TransportQuotas = new TransportQuotas + { + OperationTimeout = 600000, + MaxStringLength = 1048576, + MaxByteStringLength = 1048576, + MaxArrayLength = 65535, + MaxMessageSize = 4194304, + MaxBufferSize = 65535, + ChannelLifetime = 600000, + SecurityTokenLifetime = 3600000 + }, + ClientConfiguration = new ClientConfiguration + { + DefaultSessionTimeout = 60000, + MinSubscriptionLifetime = 10000 + }, + DisableHiResClock = true + } + }; + + // Assign a application certificate (when specified) + if (_options.ApplicationCertificate != null) + appInstance.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier(_options.ApplicationCertificate); + + // Find the endpoint to be used + var endpoints = ClientUtils.SelectEndpoint(url, _options.UseMessageSecurity); + + // Create the OPC session: + var session = await Session.Create( + configuration: appInstance.ApplicationConfiguration, + endpoint: new ConfiguredEndpoint( + collection: null, + description: endpoints, + configuration: EndpointConfiguration.Create(applicationConfiguration: appInstance.ApplicationConfiguration)), + updateBeforeConnect: false, + checkDomain: false, + sessionName: _options.SessionName, + sessionTimeout: _options.SessionTimeout, + identity: GetIdentity(url), + preferredLocales: new string[] { }); + + return session; + } + + /// + /// Finds a node starting from the specified node as the root folder + /// + /// the tag to find + /// the root node + /// + private UaNode FindNode(string tag, UaNode node) + { + var folders = tag.Split('.'); + var head = folders.FirstOrDefault(); + UaNode found; + try + { + var subNodes = ExploreFolder(node.Tag); + found = subNodes.Single(n => n.Name == head); + } + catch (Exception ex) + { + throw new OpcException(string.Format("The tag \"{0}\" doesn't exist on folder \"{1}\"", head, node.Tag), ex); + } + + // remove an array element by converting it to a list + var folderList = folders.ToList(); + folderList.RemoveAt(0); // remove the first node + folders = folderList.ToArray(); + return folders.Length == 0 + ? found // last node, return it + : FindNode(string.Join(".", folders), found); // find sub nodes + } + + + private void NotifyServerConnectionLost() + { + if (ServerConnectionLost != null) + ServerConnectionLost(this, EventArgs.Empty); + } + + private void NotifyServerConnectionRestored() + { + if (ServerConnectionRestored != null) + ServerConnectionRestored(this, EventArgs.Empty); + } + + /// + /// This event is raised when the connection to the OPC server is lost. + /// + public event EventHandler ServerConnectionLost; + + /// + /// This event is raised when the connection to the OPC server is restored. + /// + public event EventHandler ServerConnectionRestored; + + } + +} diff --git a/h-opc/h-opc/Ua/UaClientOptions.cs b/h-opc/h-opc/Ua/UaClientOptions.cs new file mode 100644 index 0000000..5bb82a4 --- /dev/null +++ b/h-opc/h-opc/Ua/UaClientOptions.cs @@ -0,0 +1,120 @@ +using System.Security.Cryptography.X509Certificates; +using OpcUa = Opc.Ua; + +namespace Hylasoft.Opc.Ua +{ + /// + /// This class defines the configuration options for the setup of the UA client session + /// + public class UaClientOptions + { + /// + /// Specifies the (optional) certificate for the application to connect to the server + /// + public X509Certificate2 ApplicationCertificate { get; set; } + + /// + /// Specifies the ApplicationName for the client application. + /// + public string ApplicationName { get; set; } + + /// + /// Should untrusted certificates be silently accepted by the client? + /// + public bool AutoAcceptUntrustedCertificates { get; set; } + + /// + /// Specifies the ConfigSectionName for the client configuration. + /// + public string ConfigSectionName { get; set; } + + /// + /// default monitor interval in Milliseconds. + /// + public int DefaultMonitorInterval { get; set; } + + /// + /// Specifies a name to be associated with the created sessions. + /// + public string SessionName { get; set; } + + /// + /// Specifies the timeout for the sessions. + /// + public uint SessionTimeout { get; set; } + + /// + /// Specify whether message exchange should be secured. + /// + public bool UseMessageSecurity { get; set; } + + /// + /// The maximum number of notifications per publish request. + /// The client’s responsibility is to send PublishRequests to the server, + /// in order to enable the server to send PublishResponses back. + /// The PublishResponses are used to deliver the notifications: but if there + /// are no PublishRequests, the server cannot send a notification to the client. + /// The server will also verify that the client is alive by checking that + /// new PublishRequests are received – LifeTimeCount defines the number of + /// PublishingIntervals to wait for a new PublishRequest, before realizing + /// that the client is no longer active.The Subscription is then removed from + /// the server. + /// + public uint SubscriptionLifetimeCount { get; set; } + + /// + /// If there is no data to send after the next PublishingInterval, + /// the server will skip it. But KeepAlive defines how many intervals may be skipped, + /// before an empty notification is sent anyway: to give the client a hint that + /// the subscription is still alive in the server and that there just has not been + /// any data arriving to the client. + /// + public uint SubscriptionKeepAliveCount { get; set; } + + /// + /// Gets or sets the max subscription count. + /// + public int MaxSubscriptionCount { get; set; } + + /// + /// The maximum number of messages saved in the queue for each subscription. + /// + public int MaxMessageQueueSize { get; set; } + + /// + /// The maximum number of notificates saved in the queue for each monitored item. + /// + public int MaxNotificationQueueSize { get; set; } + + /// + /// Gets or sets the max publish request count. + /// + public int MaxPublishRequestCount { get; set; } + + /// + /// The identity to connect to the OPC server as + /// + public OpcUa.UserIdentity UserIdentity { get; set; } + + /// + /// Creates a client options object + /// + public UaClientOptions() + { + // Initialize default values: + ApplicationName = "h-opc-client"; + AutoAcceptUntrustedCertificates = true; + ConfigSectionName = "h-opc-client"; + DefaultMonitorInterval = 100; + SessionName = "h-opc-client"; + SessionTimeout = 60000U; + UseMessageSecurity = false; + SubscriptionLifetimeCount = 0; + SubscriptionKeepAliveCount = 0; + MaxSubscriptionCount = 100; + MaxMessageQueueSize = 10; + MaxNotificationQueueSize = 100; + MaxPublishRequestCount = 20; + } + } +} diff --git a/h-opc/h-opc/Ua/UaNode.cs b/h-opc/h-opc/Ua/UaNode.cs new file mode 100644 index 0000000..edaa021 --- /dev/null +++ b/h-opc/h-opc/Ua/UaNode.cs @@ -0,0 +1,29 @@ +using Hylasoft.Opc.Common; + +namespace Hylasoft.Opc.Ua +{ + /// + /// Represents a node to be used specifically for OPC UA + /// + public class UaNode : Node + { + /// + /// The UA Id of the node + /// + public string NodeId { get; private set; } + + /// + /// Instantiates a UaNode class + /// + /// the name of the node + /// The UA Id of the node + /// The parent node + internal UaNode(string name, string nodeId, Node parent = null) + : base(name, parent) + { + NodeId = nodeId; + } + + } + +} diff --git a/h-opc/h-opc/h-opc.csproj b/h-opc/h-opc/h-opc.csproj new file mode 100644 index 0000000..15f0fda --- /dev/null +++ b/h-opc/h-opc/h-opc.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + h_opc + enable + disable + + + + + + + +