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
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
+
+
+
+
+
+
+
+