2016-01-28 update 1

This commit is contained in:
parallelbgls@outlook.com
2016-01-28 17:31:25 +08:00
parent fcb4432a98
commit 72c1948bbb
46 changed files with 2860 additions and 814 deletions

View File

@@ -1,4 +1,4 @@
<?xml version="1.0"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
For more information on how to configure your ASP.NET application, please visit For more information on how to configure your ASP.NET application, please visit
http://go.microsoft.com/fwlink/?LinkId=301879 http://go.microsoft.com/fwlink/?LinkId=301879
@@ -6,12 +6,12 @@
<configuration> <configuration>
<configSections> <configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false"/> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections> </configSections>
<connectionStrings> <connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\aspnet-CrossLampControl.WebApi-20140912112502.mdf;Initial Catalog=aspnet-CrossLampControl.WebApi-20140912112502;Integrated Security=True" providerName="System.Data.SqlClient"/> <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\aspnet-CrossLampControl.WebApi-20140912112502.mdf;Initial Catalog=aspnet-CrossLampControl.WebApi-20140912112502;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings> </connectionStrings>
<appSettings/> <appSettings />
<!-- <!--
有关 web.config 更改的说明,请参见 http://go.microsoft.com/fwlink/?LinkId=235367。 有关 web.config 更改的说明,请参见 http://go.microsoft.com/fwlink/?LinkId=235367。
@@ -21,61 +21,61 @@
</system.Web> </system.Web>
--> -->
<system.web> <system.web>
<authentication mode="None"/> <authentication mode="None" />
<compilation debug="true" targetFramework="4.6"/> <compilation debug="true" targetFramework="4.6" />
<httpRuntime targetFramework="4.5"/> <httpRuntime targetFramework="4.5" />
</system.web> </system.web>
<system.webServer> <system.webServer>
<modules> <modules>
<remove name="FormsAuthentication"/> <remove name="FormsAuthentication" />
</modules> </modules>
<handlers> <handlers>
<remove name="ExtensionlessUrlHandler-Integrated-4.0"/> <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<remove name="OPTIONSVerbHandler"/> <remove name="OPTIONSVerbHandler" />
<remove name="TRACEVerbHandler"/> <remove name="TRACEVerbHandler" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0"/> <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers> </handlers>
</system.webServer> </system.webServer>
<runtime> <runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35"/> <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0"/> <bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0" />
</dependentAssembly> </dependentAssembly>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" culture="neutral" publicKeyToken="30ad4fe6b2a6aeed"/> <assemblyIdentity name="Newtonsoft.Json" culture="neutral" publicKeyToken="30ad4fe6b2a6aeed" />
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/> <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
</dependentAssembly> </dependentAssembly>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35"/> <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0"/> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
</dependentAssembly> </dependentAssembly>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35"/> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="1.0.0.0-5.2.0.0" newVersion="5.2.0.0"/> <bindingRedirect oldVersion="1.0.0.0-5.2.0.0" newVersion="5.2.0.0" />
</dependentAssembly> </dependentAssembly>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="System.Web.Optimization" publicKeyToken="31bf3856ad364e35"/> <assemblyIdentity name="System.Web.Optimization" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="1.1.0.0"/> <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="1.1.0.0" />
</dependentAssembly> </dependentAssembly>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35"/> <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0"/> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
</dependentAssembly> </dependentAssembly>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35"/> <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="0.0.0.0-1.5.2.14234" newVersion="1.5.2.14234"/> <bindingRedirect oldVersion="0.0.0.0-1.5.2.14234" newVersion="1.5.2.14234" />
</dependentAssembly> </dependentAssembly>
</assemblyBinding> </assemblyBinding>
</runtime> </runtime>
<entityFramework> <entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
<parameters> <parameters>
<parameter value="mssqllocaldb"/> <parameter value="mssqllocaldb" />
</parameters> </parameters>
</defaultConnectionFactory> </defaultConnectionFactory>
<providers> <providers>
<provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer"/> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
</providers> </providers>
</entityFramework> </entityFramework>
</configuration> </configuration>

View File

@@ -36,32 +36,61 @@ namespace ModBus.Net
int preNum = -1; int preNum = -1;
Type preType = null; Type preType = null;
int getCount = 0; int getCount = 0;
foreach (var address in groupedAddress.OrderBy(address=>address.Address)) foreach (var address in groupedAddress.OrderBy(address => address.Address))
{ {
if (initNum < 0) if (initNum < 0)
{ {
initNum = address.Address; initNum = address.Address;
getCount = (int)BigEndianValueHelper.Instance.ByteLength[address.DataType.FullName]; getCount = (int) BigEndianValueHelper.Instance.ByteLength[address.DataType.FullName];
} }
else else
{ {
if (address.Address > preNum + BigEndianValueHelper.Instance.ByteLength[preType.FullName]) if (address.Address > preNum + BigEndianValueHelper.Instance.ByteLength[preType.FullName])
{ {
ans.Add(new CommunicationUnit(){Area = area, Address = initNum, GetCount = getCount, DataType = typeof(byte)}); ans.Add(new CommunicationUnit()
{
Area = area,
Address = initNum,
GetCount = getCount,
DataType = typeof (byte)
});
initNum = address.Address; initNum = address.Address;
getCount = (int)BigEndianValueHelper.Instance.ByteLength[address.DataType.FullName]; getCount = (int) BigEndianValueHelper.Instance.ByteLength[address.DataType.FullName];
} }
else else
{ {
getCount += (int)BigEndianValueHelper.Instance.ByteLength[address.DataType.FullName]; getCount += (int) BigEndianValueHelper.Instance.ByteLength[address.DataType.FullName];
} }
} }
preNum = address.Address; preNum = address.Address;
preType = address.DataType; preType = address.DataType;
} }
ans.Add(new CommunicationUnit() { Area = area, Address = initNum, GetCount = getCount, DataType = typeof(byte) }); ans.Add(new CommunicationUnit()
{
Area = area,
Address = initNum,
GetCount = getCount,
DataType = typeof (byte)
});
} }
return ans; return ans;
} }
} }
public class AddressCombinerSingle : AddressCombiner
{
public override IEnumerable<CommunicationUnit> Combine(IEnumerable<AddressUnit> addresses)
{
return
addresses.Select(
address =>
new CommunicationUnit()
{
Area = address.Area,
Address = address.Address,
DataType = address.DataType,
GetCount = 1
}).ToList();
}
}
} }

View File

@@ -144,7 +144,11 @@ namespace ModBus.Net
BigEndianValueHelper.Instance.ByteLength[ BigEndianValueHelper.Instance.ByteLength[
communicateAddress.DataType.FullName])); communicateAddress.DataType.FullName]));
//如果没有数据,终止 //如果没有数据,终止
if (datas == null || datas.Length == 0) return null; if (datas == null || datas.Length == 0 || datas.Length !=
(int)
Math.Ceiling(communicateAddress.GetCount *
BigEndianValueHelper.Instance.ByteLength[
communicateAddress.DataType.FullName])) return null;
int pos = 0; int pos = 0;
//解码数据 //解码数据
while (pos < communicateAddress.GetCount) while (pos < communicateAddress.GetCount)

View File

@@ -69,6 +69,15 @@ namespace ModBus.Net {
} }
} }
/// <summary>
/// 查找类似 opcda://localhost/FBoxOpcServer 的本地化字符串。
/// </summary>
internal static string FBoxOpcDaHost {
get {
return ResourceManager.GetString("FBoxOpcDaHost", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 192.168.1.1 的本地化字符串。 /// 查找类似 192.168.1.1 的本地化字符串。
/// </summary> /// </summary>
@@ -96,6 +105,15 @@ namespace ModBus.Net {
} }
} }
/// <summary>
/// 查找类似 opcda://localhost/... 的本地化字符串。
/// </summary>
internal static string OpcDaHost {
get {
return ResourceManager.GetString("OpcDaHost", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 102 的本地化字符串。 /// 查找类似 102 的本地化字符串。
/// </summary> /// </summary>

View File

@@ -120,6 +120,9 @@
<data name="COM" xml:space="preserve"> <data name="COM" xml:space="preserve">
<value>COM1</value> <value>COM1</value>
</data> </data>
<data name="FBoxOpcDaHost" xml:space="preserve">
<value>opcda://localhost/FBoxOpcServer</value>
</data>
<data name="IP" xml:space="preserve"> <data name="IP" xml:space="preserve">
<value>192.168.1.1</value> <value>192.168.1.1</value>
</data> </data>
@@ -129,6 +132,9 @@
<data name="ModbusPort" xml:space="preserve"> <data name="ModbusPort" xml:space="preserve">
<value>502</value> <value>502</value>
</data> </data>
<data name="OpcDaHost" xml:space="preserve">
<value>opcda://localhost/...</value>
</data>
<data name="SiemensPort" xml:space="preserve"> <data name="SiemensPort" xml:space="preserve">
<value>102</value> <value>102</value>
</data> </data>

View File

@@ -0,0 +1,621 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Client;
using Newtonsoft.Json;
using Thinktecture.IdentityModel.Client;
namespace ModBus.Net.FBox
{
public struct SignalRSigninMsg
{
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string UserId { get; set; }
public string Password { get; set; }
public string SigninAdditionalValues { get; set; }
public SignalRServer SignalRServer { get; set; }
}
public class FBoxConnector : BaseConnector
{
private OAuth2Client _oauth2;
private string _refreshToken;
private HttpClient _httpClient;
private HttpClient _httpClient2;
private HubConnection _hubConnection;
private SignalRSigninMsg _msg;
protected SignalRSigninMsg Msg => _msg;
private DMonGroup _dataGroup;
private int _state;
private string _groupUid;
private string _boxUid;
private string _boxNo;
private int _boxSessionId;
private int _connectionState;
private Dictionary<string, double> _data;
private Dictionary<string, Type> _dataType;
private DateTime _timeStamp = DateTime.MinValue;
public override string ConnectionToken { get; }
private AsyncLock _lock = new AsyncLock();
private Timer _timer;
private string MachineId => ConnectionToken.Split(',')[0];
private string LocalSequence => ConnectionToken.Split(',')[1];
private bool _connected;
public override bool IsConnected => _connected;
private bool Connected
{
get { return _connected; }
set
{
if (value == false)
{
Disconnect();
}
_connected = value;
}
}
private Constants _constants;
private Constants Constants => _constants ?? (_constants = new Constants());
public FBoxConnector(string machineId, string localSequence, SignalRSigninMsg msg)
{
Constants.SignalRServer = msg.SignalRServer;
System.Net.ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
ConnectionToken = machineId + "," + localSequence;
_msg = msg;
_data = new Dictionary<string, double>();
_dataType = new Dictionary<string, Type>();
}
private async void ChangeToken(object sender)
{
try
{
var tokenResponse = await _oauth2.RequestRefreshTokenAsync(_refreshToken);
_refreshToken = tokenResponse.RefreshToken;
_httpClient.SetBearerToken(tokenResponse.AccessToken);
_httpClient2.SetBearerToken(tokenResponse.AccessToken);
_hubConnection.Stop();
_hubConnection.Headers["Authorization"] = "Bearer " + tokenResponse.AccessToken;
await _hubConnection.Start();
await
_httpClient2.PostAsync(
"dmon/group/" + _dataGroup.Uid + "/start", null);
}
catch (Exception e)
{
Console.WriteLine("Retoken failed." + e.Message);
}
Console.WriteLine("Retoken success.");
}
public override bool Connect()
{
return AsyncHelper.RunSync(ConnectAsync);
}
public override async Task<bool> ConnectAsync()
{
try
{
_oauth2 = new OAuth2Client(
new Uri(Constants.TokenEndpoint),
Msg.ClientId,
Msg.ClientSecret
);
var tokenResponse = await _oauth2.RequestResourceOwnerPasswordAsync
(
Msg.UserId,
Msg.Password,
Msg.SigninAdditionalValues
);
if (tokenResponse != null)
{
_refreshToken = tokenResponse.RefreshToken;
await CallService(Msg, tokenResponse.AccessToken);
}
await
_httpClient2.PostAsync(
"dmon/group/" + _dataGroup.Uid + "/start", null);
Connected = true;
_timer = new Timer(ChangeToken, null, 3600*1000*4, 3600*1000*4);
Console.WriteLine("SignalR Connected success");
return true;
}
catch (Exception e)
{
_oauth2 = null;
Console.WriteLine("SignalR Connected failed");
return false;
}
}
private async Task CallService(SignalRSigninMsg msg, string token)
{
var guid = Guid.NewGuid().ToString();
var baseAddress = Constants.AspNetWebApiSampleApi;
_httpClient = new HttpClient
{
BaseAddress = new Uri(baseAddress)
};
_httpClient.SetBearerToken(token);
//var response = await _httpClient.GetStringAsync("device/spec");
//List<DeviceSpecSource> deviceSpecs = JsonConvert.DeserializeObject<List<DeviceSpecSource>>(response);
//deviceSpecs = deviceSpecs.OrderBy(p => p.Id).ToList();
var response = await _httpClient.GetStringAsync("boxgroup");
List<BoxGroup> boxGroups = JsonConvert.DeserializeObject<List<BoxGroup>>(response);
foreach (var boxGroup in boxGroups)
{
var boxes = boxGroup.BoxRegs;
foreach (var box in boxes)
{
var sessionId = box.Box.CurrentSessionId;
var baseUrl = box.Box.CommServer.ApiBaseUrl;
var signalrUrl = box.Box.CommServer.SignalRUrl;
var boxUid = box.Box.Uid;
var boxNo = box.Box.BoxNo;
var connectionState = box.Box.ConnectionState;
if (boxNo != MachineId) continue;
_httpClient2 = new HttpClient
{
BaseAddress = new Uri(baseUrl)
};
_httpClient2.SetBearerToken(token);
_httpClient2.DefaultRequestHeaders.Add("X-FBox-ClientId", guid);
response = await _httpClient2.GetStringAsync("box/" + box.Box.Uid + "/dmon/def/grouped");
List<DMonGroup> dataGroups = JsonConvert.DeserializeObject<List<DMonGroup>>(response);
foreach (var dataGroup in dataGroups)
{
if (dataGroup.Name == LocalSequence)
{
_dataGroup = dataGroup;
break;
}
}
_boxSessionId = sessionId;
_boxNo = boxNo;
_connectionState = connectionState;
_hubConnection = new HubConnection(signalrUrl);
_hubConnection.Headers.Add("Authorization", "Bearer " + token);
_hubConnection.Headers.Add("X-FBox-ClientId", guid);
_hubConnection.Headers.Add("X-FBox-Session", sessionId.ToString());
IHubProxy dataHubProxy = _hubConnection.CreateHubProxy("clientHub");
dataHubProxy.On<int, List<GetValue>>("dMonUpdateValue",
(boxSessionId, values) =>
{
//#if DEBUG
//Console.WriteLine($"Box session {boxSessionId} return at {DateTime.Now}");
//#endif
_timeStamp = DateTime.Now;
foreach (var value in values)
{
if (value.Status != 0)
{
lock (_data)
{
var dMonEntry =
_dataGroup.DMonEntries.FirstOrDefault(
p => p.Uid == value.Id);
if (dMonEntry != null)
{
if (_data.ContainsKey(dMonEntry.Desc))
{
_data.Remove(dMonEntry.Desc);
}
}
}
return;
}
lock (_data)
{
if (_dataGroup.DMonEntries.Any(p => p.Uid == value.Id))
{
if (_data == null)
{
_data = new Dictionary<string, double>();
}
var dMonEntry = _dataGroup.DMonEntries.FirstOrDefault(
p => p.Uid == value.Id);
if (value.Value.HasValue && dMonEntry != null)
{
if (_data.ContainsKey(dMonEntry.Desc))
{
_data[dMonEntry.Desc] = value.Value.Value;
}
else
{
_data.Add(dMonEntry.Desc, value.Value.Value);
}
}
}
}
}
});
dataHubProxy.On<int, string, int, int>("boxConnectionStateChanged",
async (newConnectionToken, getBoxUid, oldStatus, newStatus) =>
{
//#if DEBUG
//Console.WriteLine(
//$"Box uid {getBoxUid} change state at {DateTime.Now} new connectionToken {newConnectionToken} newStatus {newStatus}");
//#endif
sessionId = newConnectionToken;
_boxSessionId = sessionId;
_connectionState = newStatus;
_httpClient2.DefaultRequestHeaders.Remove("X-FBox-Session");
_httpClient2.DefaultRequestHeaders.Add("X-FBox-Session",
sessionId.ToString());
_hubConnection.Headers["X-FBox-Session"] = sessionId.ToString();
if (_hubConnection.State == ConnectionState.Disconnected)
{
await _hubConnection.Start();
}
if (newStatus == 1)
{
if (Connected)
{
await
_httpClient2.PostAsync(
"dmon/group/" + _dataGroup.Uid + "/start", null);
}
}
else
{
lock (_data)
{
_data.Clear();
}
Connected = false;
}
});
_hubConnection.Error += async ex =>
{
Console.WriteLine(@"SignalR error: {0}", ex.Message);
await ConnectRecovery(_hubConnection);
};
_hubConnection.Closed += async () =>
{
await ConnectRecovery(_hubConnection);
};
ServicePointManager.DefaultConnectionLimit = 10;
if (_dataGroup == null) return;
var groupUid = _dataGroup.Uid;
var groupName = _dataGroup.Name;
if (groupName != "(Default)" && groupName != "默认组" && _connectionState == 1)
{
_groupUid = groupUid;
}
if (groupName != "(Default)" && groupName != "默认组" && _connectionState == 1)
{
_boxUid = boxUid;
}
_dataType = new Dictionary<string, Type>();
foreach (var dMonEntry in _dataGroup.DMonEntries)
{
Type type;
switch (dMonEntry.DataType)
{
//位
case 0:
{
type = typeof (bool);
break;
}
//16位无符号
case 1:
{
type = typeof (ushort);
break;
}
//16位有符号
case 2:
{
type = typeof (short);
break;
}
//32位无符号
case 11:
{
type = typeof (uint);
break;
}
//32位有符号
case 12:
{
type = typeof (int);
break;
}
//16位BCD
case 3:
{
type = typeof (short);
break;
}
//32位BCD
case 13:
{
type = typeof (int);
break;
}
//浮点数
case 16:
{
type = typeof (float);
break;
}
//16位16进制
case 4:
{
type = typeof (short);
break;
}
//32位16进制
case 14:
{
type = typeof (int);
break;
}
//16位2进制
case 5:
{
type = typeof (short);
break;
}
//32位2进制
case 15:
{
type = typeof (int);
break;
}
default:
{
type = typeof (short);
break;
}
}
if (!_dataType.ContainsKey(dMonEntry.Desc))
{
_dataType.Add(dMonEntry.Desc, type);
}
else
{
_dataType[dMonEntry.Desc] = type;
}
}
await _hubConnection.Start();
await dataHubProxy.Invoke("updateClientId", guid);
}
}
}
private async Task ConnectRecovery(HubConnection hubConnection)
{
try
{
if (hubConnection.State != ConnectionState.Connected)
{
try
{
hubConnection.Stop();
}
catch
{
// ignored
}
await hubConnection.Start();
if (Connected)
{
await
_httpClient2.PostAsync(
"dmon/group/" + _dataGroup.Uid + "/start", null);
}
}
}
catch
{
lock (_data)
{
_data.Clear();
}
Connected = false;
}
}
public override bool Disconnect()
{
return AsyncHelper.RunSync(DisconnectAsync);
}
public async Task<bool> DisconnectAsync()
{
try
{
await
_httpClient2.PostAsync(
"dmon/group/" + _groupUid + "/stop",
null);
_connected = false;
_timer.Dispose();
_timer = null;
Console.WriteLine("SignalR Disconnect success");
return true;
}
catch
{
Console.WriteLine("SignalR Disconnect failed");
return false;
}
}
public override bool SendMsgWithoutReturn(byte[] message)
{
throw new NotImplementedException();
}
public override Task<bool> SendMsgWithoutReturnAsync(byte[] message)
{
throw new NotImplementedException();
}
public override byte[] SendMsg(byte[] message)
{
if (_httpClient == null)
{
Connected = false;
return null;
}
if (_hubConnection.State == ConnectionState.Disconnected)
{
_hubConnection.Start();
}
var formater = new AddressFormaterFBox();
var translator = new AddressTranslatorFBox();
byte[] ans;
if (_connectionState != 1)
{
Connected = false;
Console.WriteLine($"Return Value Rejected with connectionToken {ConnectionToken}");
return null;
}
if (_timeStamp == DateTime.MinValue)
{
return Encoding.ASCII.GetBytes("NoData");
}
if (DateTime.Now - _timeStamp > TimeSpan.FromMinutes(1))
{
Connected = false;
return null;
}
Dictionary<string, double> machineDataValue;
lock (_data)
{
machineDataValue = _data.ToDictionary(pair => pair.Key, pair => pair.Value);
}
var machineDataType = _dataType;
if (machineDataType == null || machineDataType.Count == 0)
{
Connected = false;
Console.WriteLine($"Return Value Rejected with connectionToken {ConnectionToken}");
return null;
}
if (machineDataValue == null || machineDataValue.Count == 0)
{
return Encoding.ASCII.GetBytes("NoData");
}
int pos = 0;
int area = BigEndianValueHelper.Instance.GetInt(message, ref pos);
int address = BigEndianValueHelper.Instance.GetInt(message, ref pos);
//short count = BigEndianValueHelper.Instance.GetShort(message, ref pos);
object[] dataAns = new object[1];
try
{
dataAns[0] =
Convert.ChangeType(
machineDataValue[formater.FormatAddress(translator.GetAreaName(area), address)],
machineDataType[formater.FormatAddress(translator.GetAreaName(area), address)]);
}
catch (Exception e)
{
return Encoding.ASCII.GetBytes("NoData");
//dataAns[0] =
//Convert.ChangeType(
//0,
//machineDataType[formater.FormatAddress(translator.GetAreaName(area), address)]);
}
finally
{
ans = BigEndianValueHelper.Instance.ObjectArrayToByteArray(dataAns);
}
return ans;
}
public override Task<byte[]> SendMsgAsync(byte[] message)
{
return Task.Factory.StartNew(() => SendMsg(message));
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModBus.Net.FBox
{
public class FBoxProtocalLinker : ProtocalLinker
{
protected FBoxProtocalLinker(string machineId, string localSequence, SignalRSigninMsg msg)
{
_baseConnector = new FBoxConnector(machineId, localSequence, msg);
}
public override bool CheckRight(byte[] content)
{
if (content != null && content.Length == 6 && Encoding.ASCII.GetString(content) == "NoData")
{
return false;
}
return base.CheckRight(content);
}
}
}

View File

@@ -6,15 +6,10 @@ using System.Threading.Tasks;
namespace ModBus.Net.FBox namespace ModBus.Net.FBox
{ {
public class FBoxSignalRProtocalLinker : SignalRProtocalLinker public class FBoxSignalRProtocalLinker : FBoxProtocalLinker
{ {
public FBoxSignalRProtocalLinker(string machineId, string localSequence, SignalRSigninMsg msg) : base(machineId, localSequence, msg) public FBoxSignalRProtocalLinker(string machineId, string localSequence, SignalRSigninMsg msg) : base(machineId, localSequence, msg)
{ {
} }
public override bool CheckRight(byte[] content)
{
return true;
}
} }
} }

View File

@@ -1,747 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Client;
using Newtonsoft.Json;
using Thinktecture.IdentityModel.Client;
namespace ModBus.Net.FBox
{
public struct SignalRSigninMsg
{
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string UserId { get; set; }
public string Password { get; set; }
public string SigninAdditionalValues { get; set; }
public SignalRServer SignalRServer { get; set; }
}
public class SignalRConnector : BaseConnector
{
private static Dictionary<SignalRSigninMsg, OAuth2Client> _oauth2;
private static Dictionary<SignalRSigninMsg, string> _refreshToken;
private static Dictionary<SignalRSigninMsg, HttpClient> _httpClient;
private static Dictionary<string, HttpClient> _httpClient2;
private static Dictionary<string, HubConnection> _hubConnections;
private static Dictionary<string, SignalRSigninMsg> _boxUidMsg;
private static Dictionary<string, int> _boxUidSessionId;
private static Dictionary<string, List<DMonGroup>> _boxUidDataGroups;
private static Dictionary<string, int> _connectionTokenState;
private static Dictionary<string, string> _groupNameUid;
private static Dictionary<string, string> _groupNameBoxUid;
private static Dictionary<string, Dictionary<string, double>> _machineData;
private static Dictionary<string, Dictionary<string, Type>> _machineDataType;
private static Dictionary<string, string> _boxUidBoxNo;
private static HashSet<string> _connectedDataGroupUid;
public override string ConnectionToken { get; }
private Timer _timer;
private string MachineId => ConnectionToken.Split(',')[0];
private string LocalSequence => ConnectionToken.Split(',')[1];
private static readonly AsyncLock _lock = new AsyncLock();
private bool _connected;
public override bool IsConnected => _connected;
private SignalRSigninMsg Msg { get; set;}
private Constants _constants;
private Constants Constants => _constants ?? (_constants = new Constants());
public SignalRConnector(string machineId, string localSequence, SignalRSigninMsg msg)
{
Constants.SignalRServer = msg.SignalRServer;
System.Net.ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
ConnectionToken = machineId + "," + localSequence;
if (_oauth2 == null)
{
_httpClient = new Dictionary<SignalRSigninMsg, HttpClient>();
_oauth2 = new Dictionary<SignalRSigninMsg, OAuth2Client>();
_refreshToken = new Dictionary<SignalRSigninMsg, string>();
_boxUidMsg = new Dictionary<string, SignalRSigninMsg>();
_hubConnections = new Dictionary<string, HubConnection>();
_httpClient2 = new Dictionary<string, HttpClient>();
_machineData = new Dictionary<string, Dictionary<string, double>>();
_machineDataType = new Dictionary<string, Dictionary<string, Type>>();
_boxUidSessionId = new Dictionary<string, int>();
_connectionTokenState = new Dictionary<string, int>();
_groupNameUid = new Dictionary<string, string>();
_groupNameBoxUid = new Dictionary<string, string>();
_boxUidDataGroups = new Dictionary<string, List<DMonGroup>>();
_boxUidBoxNo = new Dictionary<string, string>();
_connectedDataGroupUid = new HashSet<string>();
_timer = new Timer(ChangeToken, null, 3600 * 1000 * 4, 3600 * 1000 * 4);
}
Msg = msg;
}
private async void ChangeToken(object sender)
{
try
{
using (await _lock.LockAsync())
{
var tokenResponse = await _oauth2[Msg].RequestRefreshTokenAsync(_refreshToken[Msg]);
_refreshToken[Msg] = tokenResponse.RefreshToken;
_httpClient[Msg].SetBearerToken(tokenResponse.AccessToken);
foreach (var boxUidMsg in _boxUidMsg)
{
if (boxUidMsg.Value.Equals(Msg))
{
if (_httpClient2.ContainsKey(boxUidMsg.Key) && _hubConnections.ContainsKey(boxUidMsg.Key))
_httpClient2[boxUidMsg.Key].SetBearerToken(tokenResponse.AccessToken);
_hubConnections[boxUidMsg.Key].Stop();
_hubConnections[boxUidMsg.Key].Headers["Authorization"] = "Bearer " +
tokenResponse.AccessToken;
await _hubConnections[boxUidMsg.Key].Start();
var localDataGroups = _boxUidDataGroups[boxUidMsg.Key];
foreach (var localDataGroup in localDataGroups)
{
if (_connectedDataGroupUid.Contains(localDataGroup.Uid))
{
await
_httpClient2[boxUidMsg.Key].PostAsync(
"dmon/group/" + localDataGroup.Uid + "/start", null);
}
}
}
}
}
}
catch (Exception e)
{
Console.WriteLine("Retoken failed." + e.Message);
}
Console.WriteLine("Retoken success.");
}
public override bool Connect()
{
return AsyncHelper.RunSync(ConnectAsync);
}
public override async Task<bool> ConnectAsync()
{
try
{
using (await _lock.LockAsync())
{
if (!_oauth2.ContainsKey(Msg))
{
_oauth2.Add(Msg, new OAuth2Client(
new Uri(Constants.TokenEndpoint),
Msg.ClientId,
Msg.ClientSecret
));
var tokenResponse = await _oauth2[Msg].RequestResourceOwnerPasswordAsync
(
Msg.UserId,
Msg.Password,
Msg.SigninAdditionalValues
);
if (tokenResponse != null)
{
_refreshToken.Add(Msg, tokenResponse.RefreshToken);
AsyncHelper.RunSync(()=> CallService(Msg, tokenResponse.AccessToken));
}
}
if (_groupNameBoxUid.ContainsKey(ConnectionToken) && _hubConnections.ContainsKey(_groupNameBoxUid[ConnectionToken]) &&
_httpClient2.ContainsKey(_groupNameBoxUid[ConnectionToken]) &&
_groupNameUid.ContainsKey(ConnectionToken))
{
await
_httpClient2[_groupNameBoxUid[ConnectionToken]].PostAsync(
"dmon/group/" + _groupNameUid[ConnectionToken] + "/start",
null);
_connectedDataGroupUid.Add(_groupNameUid[ConnectionToken]);
_connected = true;
Console.WriteLine("SignalR Connected success");
return true;
}
else
{
_connected = false;
Console.WriteLine("SignalR Connected failed");
return false;
}
}
}
catch (Exception e)
{
if (_oauth2.ContainsKey(Msg))
{
_oauth2.Remove(Msg);
}
Console.WriteLine("SignalR Connected failed");
return false;
}
}
private async Task CallService(SignalRSigninMsg msg, string token)
{
var guid = Guid.NewGuid().ToString();
var baseAddress = Constants.AspNetWebApiSampleApi;
if (!_httpClient.ContainsKey(msg))
{
_httpClient.Add(msg, new HttpClient
{
BaseAddress = new Uri(baseAddress)
});
}
_httpClient[msg].SetBearerToken(token);
//var response = await _httpClient.GetStringAsync("device/spec");
//List<DeviceSpecSource> deviceSpecs = JsonConvert.DeserializeObject<List<DeviceSpecSource>>(response);
//deviceSpecs = deviceSpecs.OrderBy(p => p.Id).ToList();
var response = await _httpClient[msg].GetStringAsync("boxgroup");
List<BoxGroup> boxGroups = JsonConvert.DeserializeObject<List<BoxGroup>>(response);
foreach (var boxGroup in boxGroups)
{
var boxes = boxGroup.BoxRegs;
foreach (var box in boxes)
{
var sessionId = box.Box.CurrentSessionId;
var baseUrl = box.Box.CommServer.ApiBaseUrl;
var signalrUrl = box.Box.CommServer.SignalRUrl;
var boxUid = box.Box.Uid;
var boxNo = box.Box.BoxNo;
//var currentStat = box.Box.ConnectionState;
var client2 = new HttpClient
{
BaseAddress = new Uri(baseUrl)
};
client2.SetBearerToken(token);
client2.DefaultRequestHeaders.Add("X-FBox-ClientId", guid);
response = await client2.GetStringAsync("box/" + box.Box.Uid + "/dmon/def/grouped");
if (_boxUidDataGroups.ContainsKey(boxUid))
{
break;
}
List<DMonGroup> dataGroups = JsonConvert.DeserializeObject<List<DMonGroup>>(response);
_boxUidDataGroups.Add(boxUid, dataGroups);
lock (_boxUidSessionId)
{
_boxUidSessionId.Add(boxUid, sessionId);
}
_boxUidBoxNo.Add(boxUid, boxNo);
_boxUidMsg.Add(boxUid, Msg);
var hubConnection = new HubConnection(signalrUrl);
_hubConnections.Add(boxUid, hubConnection);
hubConnection.Headers.Add("Authorization", "Bearer " + token);
hubConnection.Headers.Add("X-FBox-ClientId", guid);
hubConnection.Headers.Add("X-FBox-Session", sessionId.ToString());
IHubProxy dataHubProxy = hubConnection.CreateHubProxy("clientHub");
dataHubProxy.On<int, List<GetValue>>("dMonUpdateValue",
(boxSessionId, values) =>
{
lock (_boxUidSessionId)
{
if (_boxUidSessionId.ContainsValue(boxSessionId))
{
Console.WriteLine($"Box session {boxSessionId} return at {DateTime.Now}");
var localBoxUid =
_boxUidSessionId.FirstOrDefault(p => p.Value == boxSessionId).Key;
var localBoxNo = _boxUidBoxNo[localBoxUid];
foreach (var value in values)
{
if (value.Status != 0)
{
lock (_machineData)
{
if (_boxUidDataGroups.ContainsKey(localBoxUid))
{
foreach (var dataGroupInner in _boxUidDataGroups[localBoxUid])
{
var dMonEntry =
dataGroupInner.DMonEntries.FirstOrDefault(
p => p.Uid == value.Id);
if (dMonEntry != null &&
_machineData.ContainsKey(localBoxNo + "," +
dataGroupInner.Name))
{
if (_machineData[localBoxNo + "," + dataGroupInner.Name]
.ContainsKey(dMonEntry.Desc))
{
_machineData[localBoxNo + "," + dataGroupInner.Name]
.Remove(dMonEntry.Desc);
}
}
}
}
}
return;
}
lock (_machineData)
{
if (_boxUidDataGroups.ContainsKey(localBoxUid))
{
foreach (var dataGroupInner in _boxUidDataGroups[localBoxUid])
{
if (dataGroupInner.DMonEntries.Any(p => p.Uid == value.Id))
{
if (
!_machineData.ContainsKey(localBoxNo + "," +
dataGroupInner.Name))
{
_machineData.Add(localBoxNo + "," + dataGroupInner.Name,
new Dictionary<string, double>());
}
if (_machineData[localBoxNo + "," + dataGroupInner.Name] ==
null)
{
_machineData[localBoxNo + "," + dataGroupInner.Name] =
new Dictionary<string, double>();
}
var dMonEntry =
dataGroupInner.DMonEntries.FirstOrDefault(
p => p.Uid == value.Id);
if (value.Value.HasValue && dMonEntry != null)
{
if (
_machineData[localBoxNo + "," + dataGroupInner.Name]
.ContainsKey(
dMonEntry.Desc))
{
_machineData[localBoxNo + "," + dataGroupInner.Name]
[dMonEntry.Desc] =
value.Value.Value;
}
else
{
_machineData[localBoxNo + "," + dataGroupInner.Name]
.Add(dMonEntry.Desc,
value.Value.Value);
}
}
break;
}
}
}
}
}
}
}
});
dataHubProxy.On<int, string, int, int>("boxConnectionStateChanged",
async (newConnectionToken, getBoxUid, oldStatus, newStatus) =>
{
using (await _lock.LockAsync())
{
if (_httpClient2.ContainsKey(getBoxUid))
{
Console.WriteLine(
$"Box uid {getBoxUid} change state at {DateTime.Now} new connectionToken {newConnectionToken} newStatus {newStatus}");
sessionId = newConnectionToken;
lock (_boxUidSessionId)
{
_boxUidSessionId[getBoxUid] = sessionId;
}
var localBoxNo = _boxUidBoxNo[getBoxUid];
var localDataGroups = _boxUidDataGroups[getBoxUid];
lock (_connectionTokenState)
{
foreach (var localDataGroup in localDataGroups)
{
if (
!_connectionTokenState.ContainsKey(localBoxNo + "," +
localDataGroup.Name))
{
_connectionTokenState.Add(localBoxNo + "," + localDataGroup.Name,
newStatus);
}
else
{
_connectionTokenState[localBoxNo + "," + localDataGroup.Name] =
newStatus;
}
}
}
_httpClient2[getBoxUid].DefaultRequestHeaders.Remove("X-FBox-Session");
_httpClient2[getBoxUid].DefaultRequestHeaders.Add("X-FBox-Session",
sessionId.ToString());
_hubConnections[getBoxUid].Headers["X-FBox-Session"] = sessionId.ToString();
if (_hubConnections[getBoxUid].State == ConnectionState.Disconnected)
{
await _hubConnections[getBoxUid].Start();
}
if (newStatus == 1)
{
foreach (var localDataGroup in localDataGroups)
{
if (_connectedDataGroupUid.Contains(localDataGroup.Uid))
{
await
_httpClient2[getBoxUid].PostAsync(
"dmon/group/" + localDataGroup.Uid + "/start", null);
}
}
}
}
}
});
hubConnection.Error += async ex =>
{
Console.WriteLine(@"SignalR error: {0}", ex.Message);
await ConnectRecovery(hubConnection);
};
hubConnection.Closed += async () =>
{
await ConnectRecovery(hubConnection);
};
ServicePointManager.DefaultConnectionLimit = 10;
foreach (var dataGroup in dataGroups)
{
if (dataGroup == null) return;
var groupUid = dataGroup.Uid;
var groupName = dataGroup.Name;
if (groupName != "(Default)" && groupName != "默认组" &&
!_connectionTokenState.ContainsKey(boxNo + "," + groupName))
{
_connectionTokenState.Add(boxNo + "," + groupName, 1);
}
if (groupName != "(Default)" && groupName != "默认组" &&
!_groupNameUid.ContainsKey(boxNo + "," + groupName))
{
_groupNameUid.Add(boxNo + "," + groupName, groupUid);
}
if (groupName != "(Default)" && groupName != "默认组" &&
!_groupNameBoxUid.ContainsKey(boxNo + "," + groupName))
{
_groupNameBoxUid.Add(boxNo + "," + groupName, boxUid);
}
if (groupName != "(Default)" && groupName != "默认组" && !_httpClient2.ContainsKey(boxUid))
{
_httpClient2.Add(boxUid, client2);
}
if (!_machineDataType.ContainsKey(boxNo + "," + groupName))
{
_machineDataType.Add(boxNo + "," + groupName, new Dictionary<string, Type>());
}
foreach (var dMonEntry in dataGroup.DMonEntries)
{
Type type;
switch (dMonEntry.DataType)
{
//位
case 0:
{
type = typeof (bool);
break;
}
//16位无符号
case 1:
{
type = typeof (ushort);
break;
}
//16位有符号
case 2:
{
type = typeof (short);
break;
}
//32位无符号
case 11:
{
type = typeof (uint);
break;
}
//32位有符号
case 12:
{
type = typeof (int);
break;
}
//16位BCD
case 3:
{
type = typeof (short);
break;
}
//32位BCD
case 13:
{
type = typeof (int);
break;
}
//浮点数
case 16:
{
type = typeof (float);
break;
}
//16位16进制
case 4:
{
type = typeof (short);
break;
}
//32位16进制
case 14:
{
type = typeof (int);
break;
}
//16位2进制
case 5:
{
type = typeof (short);
break;
}
//32位2进制
case 15:
{
type = typeof (int);
break;
}
default:
{
type = typeof (short);
break;
}
}
if (!_machineDataType[boxNo + "," + groupName].ContainsKey(dMonEntry.Desc))
{
_machineDataType[boxNo + "," + groupName].Add(dMonEntry.Desc, type);
}
else
{
_machineDataType[boxNo + "," + groupName][dMonEntry.Desc] = type;
}
}
}
await hubConnection.Start();
await dataHubProxy.Invoke("updateClientId", guid);
}
}
}
private async Task ConnectRecovery(HubConnection hubConnection)
{
using (await _lock.LockAsync())
{
string getBoxUid;
lock (_boxUidSessionId)
{
getBoxUid =
_boxUidSessionId.FirstOrDefault(
p => p.Value == int.Parse(hubConnection.Headers["X-FBox-Session"])).Key;
}
try
{
if (hubConnection.State != ConnectionState.Connected)
{
try
{
hubConnection.Stop();
}
catch
{
// ignored
}
await hubConnection.Start();
var localDataGroups = _boxUidDataGroups[getBoxUid];
foreach (var localDataGroup in localDataGroups)
{
if (_connectedDataGroupUid.Contains(localDataGroup.Uid))
{
await
_httpClient2[getBoxUid].PostAsync(
"dmon/group/" + localDataGroup.Uid + "/start", null);
}
}
}
}
catch
{
if (_boxUidBoxNo.ContainsKey(getBoxUid))
{
var localBoxNo = _boxUidBoxNo[getBoxUid];
lock (_machineData)
{
foreach (var machineDataUnit in _machineData)
{
if (machineDataUnit.Key.Contains(localBoxNo))
{
_machineData.Remove(machineDataUnit.Key);
}
}
}
}
var localDataGroups = _boxUidDataGroups[getBoxUid];
foreach (var localDataGroup in localDataGroups)
{
_connectedDataGroupUid.RemoveWhere(p => p == localDataGroup.Uid);
}
_connected = false;
}
}
}
public override bool Disconnect()
{
return AsyncHelper.RunSync(DisconnectAsync);
}
public async Task<bool> DisconnectAsync()
{
try
{
using (await _lock.LockAsync())
{
if (_groupNameBoxUid.ContainsKey(ConnectionToken) && _hubConnections.ContainsKey(_groupNameBoxUid[ConnectionToken]) &&
_httpClient2.ContainsKey(_groupNameBoxUid[ConnectionToken]) &&
_groupNameUid.ContainsKey(ConnectionToken))
{
await
_httpClient2[_groupNameBoxUid[ConnectionToken]].PostAsync(
"dmon/group/" + _groupNameUid[ConnectionToken] + "/stop",
null);
_connectedDataGroupUid.RemoveWhere(p => p == _groupNameUid[ConnectionToken]);
_connected = false;
Console.WriteLine("SignalR Disconnect success");
return true;
}
else
{
Console.WriteLine("SignalR Disconnect failed");
return false;
}
}
}
catch
{
Console.WriteLine("SignalR Disconnect failed");
return false;
}
}
public override bool SendMsgWithoutReturn(byte[] message)
{
throw new NotImplementedException();
}
public override Task<bool> SendMsgWithoutReturnAsync(byte[] message)
{
throw new NotImplementedException();
}
public override byte[] SendMsg(byte[] message)
{
if (!_httpClient.ContainsKey(Msg))
{
_connected = false;
return null;
}
var formater = new AddressFormaterFBox();
var translator = new AddressTranslatorFBox();
byte[] ans;
lock (_connectionTokenState)
{
if (_connectionTokenState.ContainsKey(ConnectionToken) && _connectionTokenState[ConnectionToken] != 1)
{
_connected = false;
Console.WriteLine($"Return Value Rejected with connectionToken {ConnectionToken}");
return null;
}
}
lock (_machineData)
{
if (!_machineData.ContainsKey(ConnectionToken) || !_machineDataType.ContainsKey(ConnectionToken))
{
_connected = false;
Console.WriteLine($"Return Value Rejected with connectionToken {ConnectionToken}");
return null;
}
var machineDataValue = _machineData[ConnectionToken];
var machineDataType = _machineDataType[ConnectionToken];
if (machineDataValue != null && machineDataType.Count == 0)
{
_connected = false;
Console.WriteLine($"Return Value Rejected with connectionToken {ConnectionToken}");
return null;
}
int pos = 0;
int area = BigEndianValueHelper.Instance.GetInt(message, ref pos);
int address = BigEndianValueHelper.Instance.GetInt(message, ref pos);
//short count = BigEndianValueHelper.Instance.GetShort(message, ref pos);
object[] dataAns = new object[1];
try
{
dataAns[0] =
Convert.ChangeType(
machineDataValue[formater.FormatAddress(translator.GetAreaName(area), address)],
machineDataType[formater.FormatAddress(translator.GetAreaName(area), address)]);
}
catch (Exception)
{
dataAns[0] =
Convert.ChangeType(
0,
machineDataType[formater.FormatAddress(translator.GetAreaName(area), address)]);
}
finally
{
ans = BigEndianValueHelper.Instance.ObjectArrayToByteArray(dataAns);
}
}
return ans;
}
public override Task<byte[]> SendMsgAsync(byte[] message)
{
return Task.Factory.StartNew(() => SendMsg(message));
}
}
}

View File

@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModBus.Net.FBox
{
public class SignalRProtocalLinker : ProtocalLinker
{
protected SignalRProtocalLinker(string machineId, string localSequence, SignalRSigninMsg msg)
{
_baseConnector = new SignalRConnector(machineId, localSequence, msg);
}
}
}

View File

@@ -47,6 +47,30 @@
<HintPath>..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> <HintPath>..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="Opc.Ua.Client, Version=1.2.334.4, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages_local\Opc.Ua.Client.dll</HintPath>
</Reference>
<Reference Include="Opc.Ua.Configuration, Version=1.2.334.4, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages_local\Opc.Ua.Configuration.dll</HintPath>
</Reference>
<Reference Include="Opc.Ua.Core, Version=1.2.334.4, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages_local\Opc.Ua.Core.dll</HintPath>
</Reference>
<Reference Include="OpcComRcw, Version=2.0.105.1, Culture=neutral, PublicKeyToken=9a40e993cbface53, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages_local\OpcComRcw.dll</HintPath>
</Reference>
<Reference Include="OpcNetApi, Version=2.1.105.1, Culture=neutral, PublicKeyToken=9a40e993cbface53, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages_local\OpcNetApi.dll</HintPath>
</Reference>
<Reference Include="OpcNetApi.Com, Version=2.1.105.1, Culture=neutral, PublicKeyToken=9a40e993cbface53, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages_local\OpcNetApi.Com.dll</HintPath>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
@@ -97,14 +121,23 @@
<Compile Include="Modbus\ModbusRtuProtocal.cs" /> <Compile Include="Modbus\ModbusRtuProtocal.cs" />
<Compile Include="Modbus\ModbusRtuProtocalLinker.cs" /> <Compile Include="Modbus\ModbusRtuProtocalLinker.cs" />
<Compile Include="Modbus\ModbusTcpProtocalLinker.cs" /> <Compile Include="Modbus\ModbusTcpProtocalLinker.cs" />
<Compile Include="OPC\AddressFormaterOpc.cs" />
<Compile Include="OPC\DaClientExtend.cs" />
<Compile Include="OPC\FBox\FBoxOpcDaManchine.cs" />
<Compile Include="OPC\OpcDaConnector.cs" />
<Compile Include="OPC\OpcDaMachine.cs" />
<Compile Include="OPC\OpcDaProtocal.cs" />
<Compile Include="OPC\OpcDaProtocalLinker.cs" />
<Compile Include="OPC\OpcDaUtility.cs" />
<Compile Include="OPC\OpcProtocal.cs" />
<Compile Include="ProtocalLinker.cs" /> <Compile Include="ProtocalLinker.cs" />
<Compile Include="ProtocalLinkerBytesExtend.cs" /> <Compile Include="ProtocalLinkerBytesExtend.cs" />
<Compile Include="ProtocalUnit.cs" /> <Compile Include="ProtocalUnit.cs" />
<Compile Include="Modbus\ModbusProtocal.cs" /> <Compile Include="Modbus\ModbusProtocal.cs" />
<Compile Include="Modbus\ModbusTcpProtocal.cs" /> <Compile Include="Modbus\ModbusTcpProtocal.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="FBox\SignalRConnector.cs" /> <Compile Include="FBox\FBoxConnector.cs" />
<Compile Include="FBox\SignalRProtocalLinker.cs" /> <Compile Include="FBox\FBoxProtocalLinker.cs" />
<Compile Include="Siemens\AddressFormaterSiemens.cs" /> <Compile Include="Siemens\AddressFormaterSiemens.cs" />
<Compile Include="Siemens\AddressTranslatorSiemens.cs" /> <Compile Include="Siemens\AddressTranslatorSiemens.cs" />
<Compile Include="Siemens\SiemensMachine.cs" /> <Compile Include="Siemens\SiemensMachine.cs" />
@@ -131,6 +164,12 @@
<None Include="LICENSE.md" /> <None Include="LICENSE.md" />
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\h-opc\h-opc.csproj">
<Project>{4f43b6f0-0c32-4c34-978e-9b8b5b0b6e80}</Project>
<Name>h-opc</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModBus.Net.OPC
{
public class AddressFormaterOpc : AddressFormater
{
public BaseMachine Machine { get; set; }
protected Func<BaseMachine, AddressUnit, string[]> TagGeter { get; set; }
public AddressFormaterOpc(Func<BaseMachine, AddressUnit, string[]> tagGeter)
{
TagGeter = tagGeter;
}
public override string FormatAddress(string area, int address)
{
var findAddress = Machine?.GetAddresses.FirstOrDefault(p => p.Area == area && p.Address == address);
if (findAddress == null) return null;
var strings = TagGeter(Machine, findAddress);
var ans = "";
for (int i = 0; i < strings.Length; i++)
{
ans += strings[i].Trim().Replace(" ", "") + ".";
}
ans = ans.Substring(0, ans.Length - 1);
return ans;
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Threading.Tasks;
using Opc.Da;
namespace Hylasoft.Opc.Da
{
/// <summary>
/// Read value full result
/// </summary>
public class OpcValueResult
{
public object Value { get; set; }
public DateTime Timestamp { get; set; }
public bool QualityGood { get; set; }
}
public class MyDaClient : DaClient
{
/// <summary>
/// Write a value on the specified opc tag
/// </summary>
/// <param name="tag">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`</param>
public OpcValueResult Read(string tag)
{
var item = new Item { ItemName = tag };
var result = _server.Read(new[] { item })[0];
CheckResult(result, tag);
return new OpcValueResult()
{
Value = result.Value,
Timestamp = result.Timestamp,
QualityGood = result.Quality == Quality.Good
};
}
/// <summary>
/// Read a tag asynchronusly
/// </summary>
public Task<OpcValueResult> ReadAsync(string tag)
{
return Task.Run(() => Read(tag));
}
public MyDaClient(Uri serverUrl) : base(serverUrl)
{
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModBus.Net.OPC.FBox
{
public class FBoxOpcDaMachine : OpcDaMachine
{
public string LocalSequence { get; set; }
public string LinkerName { get; set; }
public FBoxOpcDaMachine(string localSequence, string linkerName,
IEnumerable<AddressUnit> getAddresses, bool keepConnect) : base(ConfigurationManager.FBoxOpcDaHost, getAddresses, keepConnect)
{
LocalSequence = localSequence;
LinkerName = linkerName;
AddressFormater =
new AddressFormaterOpc(
((machine, unit) =>
new string[]
{
"他人分享", ((FBoxOpcDaMachine) machine).LinkerName, ((FBoxOpcDaMachine) machine).LocalSequence,
unit.Name
}));
((AddressFormaterOpc)AddressFormater).Machine = this;
}
public FBoxOpcDaMachine(string localSequence, string linkerName,
IEnumerable<AddressUnit> getAddresses)
: this(localSequence, linkerName, getAddresses, false)
{
}
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Hylasoft.Opc.Common;
using Hylasoft.Opc.Da;
namespace ModBus.Net.OPC
{
public class OpcDaConnector : BaseConnector
{
public override string ConnectionToken { get; }
protected bool _connect;
public override bool IsConnected => _connect;
protected static Dictionary<string, OpcDaConnector> _instances = new Dictionary<string, OpcDaConnector>();
protected MyDaClient _daClient;
protected OpcDaConnector(string host)
{
ConnectionToken = host;
}
public static OpcDaConnector Instance(string host)
{
if (!_instances.ContainsKey(host))
{
var connector = new OpcDaConnector(host);
_instances.Add(host, connector);
}
return _instances[host];
}
public override bool Connect()
{
try
{
_daClient = new MyDaClient(new Uri(ConnectionToken));
_daClient.Connect();
_connect = true;
AddInfo("client connected.");
return true;
}
catch (Exception ex)
{
AddInfo("client connected exception: " + ex.Message);
AddInfo("connect failed.");
_connect = false;
return false;
}
}
public override Task<bool> ConnectAsync()
{
return Task.FromResult(Connect());
}
public override bool Disconnect()
{
try
{
_daClient?.Dispose();
_daClient = null;
_connect = false;
AddInfo("client disconnected successfully.");
return true;
}
catch (Exception ex)
{
AddInfo("client disconnected exception: " + ex.Message);
_connect = false;
return false;
}
}
public override bool SendMsgWithoutReturn(byte[] message)
{
throw new NotImplementedException();
}
public override Task<bool> SendMsgWithoutReturnAsync(byte[] message)
{
throw new NotImplementedException();
}
public override byte[] SendMsg(byte[] message)
{
return AsyncHelper.RunSync(() => SendMsgAsync(message));
}
public override async Task<byte[]> SendMsgAsync(byte[] message)
{
try
{
string tag = Encoding.UTF8.GetString(message);
var result = await _daClient.ReadAsync(tag);
if (result.QualityGood)
{
return BigEndianValueHelper.Instance.GetBytes(result.Value, result.Value.GetType());
}
else
{
return Encoding.ASCII.GetBytes("NoData");
}
}
catch (Exception e)
{
//AddInfo("opc client exception:" + e);
return Encoding.ASCII.GetBytes("NoData");
//return null;
}
}
private void AddInfo(string message)
{
Console.WriteLine(message);
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModBus.Net.OPC
{
public class OpcDaMachine : BaseMachine
{
public OpcDaMachine(string connectionString, IEnumerable<AddressUnit> getAddresses, bool keepConnect)
: base(getAddresses, keepConnect)
{
BaseUtility = new OpcDaUtility(connectionString);
AddressCombiner = new AddressCombinerSingle();
}
public OpcDaMachine(string connectionString, IEnumerable<AddressUnit> getAddresses)
: this(connectionString, getAddresses, false)
{
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModBus.Net.OPC
{
public class OpcDaProtocal : OpcProtocal
{
private int _connectTryCount;
private readonly string _host;
public OpcDaProtocal(string host)
{
_host = host;
}
public override bool Connect()
{
return AsyncHelper.RunSync(ConnectAsync);
}
public override async Task<bool> ConnectAsync()
{
_connectTryCount++;
ProtocalLinker = new OpcDaProtocalLinker(_host);
if (!await ProtocalLinker.ConnectAsync()) return false;
_connectTryCount = 0;
return true;
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModBus.Net.OPC
{
public class OpcDaProtocalLinker : ProtocalLinker
{
public OpcDaProtocalLinker() : this(ConfigurationManager.OpcDaHost)
{
}
public OpcDaProtocalLinker(string host)
{
_baseConnector = OpcDaConnector.Instance(host);
}
public override bool CheckRight(byte[] content)
{
if (content != null && content.Length == 6 && Encoding.ASCII.GetString(content) == "NoData")
{
return false;
}
return base.CheckRight(content);
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModBus.Net.OPC
{
public class OpcDaUtility : BaseUtility
{
public OpcDaUtility(string connectionString)
{
ConnectionString = connectionString;
AddressTranslator = null;
Wrapper = new OpcDaProtocal(ConnectionString);
}
public override void SetConnectionType(int connectionType)
{
}
protected override async Task<byte[]> GetDatasAsync(byte belongAddress, byte masterAddress, string startAddress, int getByteCount)
{
try
{
var readRequestOpcInputStruct = new ReadRequestOpcInputStruct(startAddress, getByteCount);
var readRequestOpcOutputStruct =
await
Wrapper.SendReceiveAsync(Wrapper[typeof(ReadRequestOpcProtocal)], readRequestOpcInputStruct) as ReadRequestOpcOutputStruct;
return readRequestOpcOutputStruct?.GetValue;
}
catch (Exception)
{
return null;
}
}
public override Task<bool> SetDatasAsync(byte belongAddress, byte masterAddress, string startAddress, object[] setContents)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModBus.Net.OPC
{
public abstract class OpcProtocal : BaseProtocal
{
}
public class ReadRequestOpcInputStruct : InputStruct
{
public ReadRequestOpcInputStruct(string tag, int getCount)
{
Tag = tag;
GetCount = getCount;
}
public string Tag { get; private set; }
public int GetCount { get; private set; }
}
public class ReadRequestOpcOutputStruct : OutputStruct
{
public ReadRequestOpcOutputStruct(byte[] value)
{
GetValue = value;
}
public byte[] GetValue { get; private set; }
}
public class ReadRequestOpcProtocal : SpecialProtocalUnit
{
public override byte[] Format(InputStruct message)
{
var r_message = (ReadRequestOpcInputStruct) message;
return Format(Encoding.UTF8.GetBytes(r_message.Tag));
}
public override OutputStruct Unformat(byte[] messageBytes, ref int pos)
{
return new ReadRequestOpcOutputStruct(messageBytes);
}
}
/*public class WriteRequestSiemensInputStruct : InputStruct
{
public WriteRequestSiemensInputStruct(ushort pduRef, string startAddress, object[] writeValue, AddressTranslator addressTranslator)
{
PduRef = pduRef;
var address = addressTranslator.AddressTranslate(startAddress, true);
Offset = address.Key;
int area = address.Value;
Area = (byte)(area % 256);
DbBlock = Area == 0x84 ? (ushort)(area / 256) : (ushort)0;
WriteValue = writeValue;
}
public ushort PduRef { get; private set; }
public ushort DbBlock { get; private set; }
public byte Area { get; private set; }
public int Offset { get; private set; }
public object[] WriteValue { get; private set; }
}
public class WriteRequestSiemensOutputStruct : OutputStruct
{
public WriteRequestSiemensOutputStruct(ushort pduRef, SiemensAccessResult accessResult)
{
PduRef = pduRef;
AccessResult = accessResult;
}
public ushort PduRef { get; private set; }
public SiemensAccessResult AccessResult { get; private set; }
}
public class WriteRequestSiemensProtocal : ProtocalUnit
{
public override byte[] Format(InputStruct message)
{
var r_message = (WriteRequestSiemensInputStruct)message;
byte[] valueBytes = BigEndianValueHelper.Instance.ObjectArrayToByteArray(r_message.WriteValue);
const byte protoId = 0x32;
const byte rosctr = 0x01;
const ushort redId = 0x0000;
ushort pduRef = r_message.PduRef;
const ushort parLg = 14; // 参数字节数2+12的倍数目前仅为14
ushort datLg = (ushort)(4 + valueBytes.Length); // 数据字节数
const byte serviceId = 0x05;
const byte numberOfVariables = 1;
const byte variableSpec = 0x12;
const byte vAddrLg = 0x0A;
const byte syntaxId = 0x10;
const byte typeR = (byte)SiemensTypeCode.Byte;
ushort numberOfElements = (ushort)valueBytes.Length;
ushort dbBlock = r_message.DbBlock;
byte area = r_message.Area;
int offsetBit = r_message.Offset * 8;
byte[] offsetBitBytes = BigEndianValueHelper.Instance.GetBytes(offsetBit);
const byte reserved = 0x00;
const byte type = (byte)SiemensDataType.OtherAccess;
ushort numberOfWriteBits = (ushort)(valueBytes.Length * 8);
return Format(new byte[7], protoId, rosctr, redId, pduRef, parLg, datLg, serviceId, numberOfVariables
, variableSpec, vAddrLg, syntaxId, typeR, numberOfElements, dbBlock, area,
offsetBitBytes.Skip(1).ToArray(), reserved, type, numberOfWriteBits, valueBytes);
}
public override OutputStruct Unformat(byte[] messageBytes, ref int pos)
{
pos = 4;
ushort pduRef = BigEndianValueHelper.Instance.GetUShort(messageBytes, ref pos);
pos = 14;
byte accessResult = BigEndianValueHelper.Instance.GetByte(messageBytes, ref pos);
return new WriteRequestSiemensOutputStruct(pduRef, (SiemensAccessResult)accessResult);
}
}*/
}

View File

@@ -35,5 +35,5 @@ using System.Runtime.InteropServices;
// 方法是按如下所示使用“*”: // 方法是按如下所示使用“*”:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.4.0.0119")] [assembly: AssemblyVersion("0.4.5.0128")]
[assembly: AssemblyFileVersion("0.4.0.0119")] [assembly: AssemblyFileVersion("0.4.5.0128")]

View File

@@ -1,4 +1,5 @@
using System.Reflection; using System.Reflection;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace ModBus.Net namespace ModBus.Net

View File

@@ -136,6 +136,78 @@ namespace ModBus.Net
return BitConverter.GetBytes(value); return BitConverter.GetBytes(value);
} }
/// <summary>
/// 将object数字转换为byte数组
/// </summary>
/// <param name="value"></param>
/// <param name="type"></param>
/// <returns></returns>
public virtual Byte[] GetBytes(object value, Type type)
{
ValueHelper Instance;
if (this is BigEndianValueHelper)
{
Instance = BigEndianValueHelper.Instance;
}
else
{
Instance = ValueHelper.Instance;
}
switch (type.FullName)
{
case "System.Int16":
{
byte[] bytes = Instance.GetBytes((short) value);
return bytes;
}
case "System.Int32":
{
byte[] bytes = Instance.GetBytes((int) value);
return bytes;
}
case "System.Int64":
{
byte[] bytes = Instance.GetBytes((long) value);
return bytes;
}
case "System.UInt16":
{
byte[] bytes = Instance.GetBytes((ushort) value);
return bytes;
}
case "System.UInt32":
{
byte[] bytes = Instance.GetBytes((uint) value);
return bytes;
}
case "System.UInt64":
{
byte[] bytes = Instance.GetBytes((ulong) value);
return bytes;
}
case "System.Single":
{
byte[] bytes = Instance.GetBytes((float) value);
return bytes;
}
case "System.Double":
{
byte[] bytes = Instance.GetBytes((double) value);
return bytes;
}
case "System.Byte":
{
byte[] bytes = Instance.GetBytes((byte) value);
return bytes;
}
default:
{
throw new NotImplementedException("没有实现除整数以外的其它转换方式");
}
}
}
/// <summary> /// <summary>
/// 将byte数组中相应的位置转换为对应类型的数字 /// 将byte数组中相应的位置转换为对应类型的数字
/// </summary> /// </summary>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
..\README.md = ..\README.md ..\README.md = ..\README.md
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "h-opc", "h-opc\h-opc.csproj", "{4F43B6F0-0C32-4C34-978E-9B8B5B0B6E80}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -44,6 +46,10 @@ Global
{02CE1787-291A-47CB-A7F7-929DA1D920A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {02CE1787-291A-47CB-A7F7-929DA1D920A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02CE1787-291A-47CB-A7F7-929DA1D920A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {02CE1787-291A-47CB-A7F7-929DA1D920A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02CE1787-291A-47CB-A7F7-929DA1D920A2}.Release|Any CPU.Build.0 = Release|Any CPU {02CE1787-291A-47CB-A7F7-929DA1D920A2}.Release|Any CPU.Build.0 = Release|Any CPU
{4F43B6F0-0C32-4C34-978E-9B8B5B0B6E80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F43B6F0-0C32-4C34-978E-9B8B5B0B6E80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F43B6F0-0C32-4C34-978E-9B8B5B0B6E80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F43B6F0-0C32-4C34-978E-9B8B5B0B6E80}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<startup> <startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
</startup> </startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration> </configuration>

View File

@@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<startup> <startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
</startup> </startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration> </configuration>

View File

@@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<startup> <startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
</startup> </startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration> </configuration>

View File

@@ -0,0 +1,27 @@
namespace Hylasoft.Opc.Common
{
/// <summary>
/// Useful extension methods for OPC Clients
/// </summary>
public static class ClientExtensions
{
/// <summary>
/// Reads a tag from the OPC. If for whatever reason the read fails (Tag doesn't exist, server not available) returns a default value
/// </summary>
/// <param name="client">the opc client to use for the read</param>
/// <param name="tag">The fully qualified identifier of the tag</param>
/// <param name="defaultValue">the default value to read if the read fails</param>
/// <returns></returns>
public static T ReadOrdefault<T>(this IClient<Node> client, string tag, T defaultValue = default(T))
{
try
{
return client.Read<T>(tag);
}
catch (OpcException)
{
return defaultValue;
}
}
}
}

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Hylasoft.Opc.Common
{
/// <summary>
/// Client interface to perform basic Opc tasks, like discovery, monitoring, reading/writing tags,
/// </summary>
public interface IClient<out TNode> : IDisposable
where TNode : Node
{
/// <summary>
/// Connect the client to the OPC Server
/// </summary>
void Connect();
/// <summary>
/// Gets the current status of the OPC Client
/// </summary>
OpcStatus Status { get; }
/// <summary>
/// Read a tag
/// </summary>
/// <typeparam name="T">The type of tag to read</typeparam>
/// <param name="tag">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`</param>
/// <returns>The value retrieved from the OPC</returns>
T Read<T>(string tag);
/// <summary>
/// Write a value on the specified opc tag
/// </summary>
/// <typeparam name="T">The type of tag to write on</typeparam>
/// <param name="tag">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`</param>
/// <param name="item"></param>
void Write<T>(string tag, T item);
/// <summary>
/// Monitor the specified tag for changes
/// </summary>
/// <typeparam name="T">the type of tag to monitor</typeparam>
/// <param name="tag">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`</param>
/// <param name="callback">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</param>
void Monitor<T>(string tag, Action<T, Action> callback);
/// <summary>
/// Finds a node on the Opc Server
/// </summary>
/// <param name="tag">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`</param>
/// <returns>If there is a tag, it returns it, otherwise it throws an </returns>
TNode FindNode(string tag);
/// <summary>
/// Gets the root node of the server
/// </summary>
TNode RootNode { get; }
/// <summary>
/// Explore a folder on the Opc Server
/// </summary>
/// <param name="tag">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`</param>
/// <returns>The list of sub-nodes</returns>
IEnumerable<TNode> ExploreFolder(string tag);
/// <summary>
/// Read a tag asynchronusly
/// </summary>
Task<T> ReadAsync<T>(string tag);
/// <summary>
/// Write a value on the specified opc tag asynchronously
/// </summary>
Task WriteAsync<T>(string tag, T item);
/// <summary>
/// Finds a node on the Opc Server asynchronously
/// </summary>
Task<Node> FindNodeAsync(string tag);
/// <summary>
/// Explore a folder on the Opc Server asynchronously
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures",
Justification = "Task")]
Task<IEnumerable<Node>> ExploreFolderAsync(string tag);
}
}

View File

@@ -0,0 +1,48 @@
using System.Collections.Generic;
namespace Hylasoft.Opc.Common
{
/// <summary>
/// Base class representing a node on the OPC server
/// </summary>
public abstract class Node
{
/// <summary>
/// Gets the displayed name of the node
/// </summary>
public string Name { get; protected set; }
/// <summary>
/// Gets the dot-separated fully qualified tag of the node
/// </summary>
public string Tag { get; protected set; }
/// <summary>
/// Gets the parent node. If the node is root, returns null
/// </summary>
public Node Parent { get; private set; }
/// <summary>
/// Creates a new node
/// </summary>
/// <param name="name">the name of the node</param>
/// <param name="parent">The parent node</param>
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;
}
/// <summary>
/// Overrides ToString()
/// </summary>
public override string ToString()
{
return Tag;
}
}
}

View File

@@ -0,0 +1,70 @@
using Opc.Ua;
using System;
using System.Runtime.Serialization;
namespace Hylasoft.Opc.Common
{
/// <summary>
/// Identifies an exception occurred during OPC Communication
/// </summary>
[Serializable]
public class OpcException : Exception
{
/// <summary>
/// Initialize a new instance of the OpcException class
/// </summary>
public OpcException()
{
}
/// <summary>
/// Initialize a new instance of the OpcException class
/// </summary>
public OpcException(string message)
: base(message)
{
}
/// <summary>
/// Returns an (optional) associated OPC UA StatusCode for the exception.
/// </summary>
public StatusCode? Status { get; private set; }
/// <summary>
/// Initialize a new instance of the OpcException class
/// </summary>
public OpcException(string message, StatusCode status)
: base(message)
{
Status = status;
}
/// <summary>
/// Initialize a new instance of the OpcException class
/// </summary>
public OpcException(string message, Exception inner)
: base(message, inner)
{
}
/// <summary>
/// Initialize a new instance of the OpcException class
/// </summary>
protected OpcException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
/// <summary>
/// Sets the System.Runtime.Serialization.SerializationInfo with information about the exception.
/// </summary>
/// <param name="info">The System.Runtime.Serialization.SerializationInfo that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The System.Runtime.Serialization.StreamingContext that contains contextual information about the source or destination.</param>
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
}
}
}

View File

@@ -0,0 +1,18 @@
namespace Hylasoft.Opc.Common
{
/// <summary>
/// Identifies the status of an OPC connector
/// </summary>
public enum OpcStatus
{
/// <summary>
/// The client is not connected
/// </summary>
NotConnected,
/// <summary>
/// The client is connected
/// </summary>
Connected
}
}

View File

@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using Hylasoft.Opc.Common;
using Opc;
using Factory = OpcCom.Factory;
using OpcDa = Opc.Da;
namespace Hylasoft.Opc.Da
{
/// <summary>
/// Client Implementation for DA
/// </summary>
public partial class DaClient : IClient<DaNode>
{
private readonly URL _url;
protected OpcDa.Server _server;
private long _sub;
private readonly IDictionary<string, DaNode> _nodesCache = new Dictionary<string, DaNode>();
// default monitor interval in Milliseconds
private const int DefaultMonitorInterval = 100;
/// <summary>
/// Initialize a new Data Access Client
/// </summary>
/// <param name="serverUrl">the url of the server to connect to</param>
public DaClient(Uri serverUrl)
{
_url = new URL(serverUrl.AbsolutePath)
{
Scheme = serverUrl.Scheme,
HostName = serverUrl.Host
};
}
#region interface methods
/// <summary>
/// Connect the client to the OPC Server
/// </summary>
public void Connect()
{
if (Status == OpcStatus.Connected)
return;
_server = new OpcDa.Server(new Factory(), _url);
_server.Connect();
var root = new DaNode(string.Empty, string.Empty);
RootNode = root;
AddNodeToCache(root);
}
/// <summary>
/// Gets the current status of the OPC Client
/// </summary>
public OpcStatus Status
{
get
{
if (_server == null || _server.GetStatus().ServerState != OpcDa.serverState.running)
return OpcStatus.NotConnected;
return OpcStatus.Connected;
}
}
/// <summary>
/// Read a tag
/// </summary>
/// <typeparam name="T">The type of tag to read</typeparam>
/// <param name="tag">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`</param>
/// <returns>The value retrieved from the OPC</returns>
public T Read<T>(string tag)
{
var item = new OpcDa.Item { ItemName = tag };
var result = _server.Read(new[] { item })[0];
CheckResult(result, tag);
return (T)result.Value;
}
/// <summary>
/// Write a value on the specified opc tag
/// </summary>
/// <typeparam name="T">The type of tag to write on</typeparam>
/// <param name="tag">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`</param>
/// <param name="item"></param>
public void Write<T>(string tag, T item)
{
var itmVal = new OpcDa.ItemValue
{
ItemName = tag,
Value = item
};
var result = _server.Write(new[] { itmVal })[0];
CheckResult(result, tag);
}
/// <summary>
/// Monitor the specified tag for changes
/// </summary>
/// <typeparam name="T">the type of tag to monitor</typeparam>
/// <param name="tag">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`</param>
/// <param name="callback">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</param>
public void Monitor<T>(string tag, Action<T, Action> callback)
{
var subItem = new OpcDa.SubscriptionState
{
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.DataChanged += (handle, requestHandle, values) =>
callback((T)values[0].Value, unsubscribe);
sub.AddItems(new[] { new OpcDa.Item { ItemName = tag } });
sub.SetEnabled(true);
}
/// <summary>
/// Finds a node on the Opc Server
/// </summary>
/// <param name="tag">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`</param>
/// <returns>If there is a tag, it returns it, otherwise it throws an </returns>
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.Item { ItemName = tag };
var result = _server.Read(new[] { item })[0];
CheckResult(result, tag);
var node = new DaNode(item.ItemName, item.ItemName, RootNode);
AddNodeToCache(node);
return node;
}
/// <summary>
/// Gets the root node of the server
/// </summary>
public DaNode RootNode { get; private set; }
/// <summary>
/// Explore a folder on the Opc Server
/// </summary>
/// <param name="tag">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`</param>
/// <returns>The list of sub-nodes</returns>
public IEnumerable<DaNode> ExploreFolder(string tag)
{
var parent = FindNode(tag);
OpcDa.BrowsePosition p;
var nodes = _server.Browse(new ItemIdentifier(parent.Tag), new OpcDa.BrowseFilters(), 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;
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
if (_server != null)
_server.Dispose();
GC.SuppressFinalize(this);
}
#endregion
/// <summary>
/// Adds a node to the cache using the tag as its key
/// </summary>
/// <param name="node">the node to add</param>
protected void AddNodeToCache(DaNode node)
{
if (!_nodesCache.ContainsKey(node.Tag))
_nodesCache.Add(node.Tag, node);
}
protected static void CheckResult(IResult result, string tag)
{
if (result == null)
throw new OpcException("The server replied with an empty response");
if (result.ResultID.ToString() != "S_OK")
throw new OpcException(string.Format("Invalid response from the server. (Response Status: {0}, Opc Tag: {1})", result.ResultID, tag));
}
}
}

View File

@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Hylasoft.Opc.Common;
using OpcDa = Opc.Da;
namespace Hylasoft.Opc.Da
{
/// <summary>
/// Client Implementation for DA
/// </summary>
public partial class DaClient
{
/// <summary>
/// Read a tag asynchronusly
/// </summary>
public async Task<T> ReadAsync<T>(string tag)
{
return await Task.Run(() => Read<T>(tag));
}
/// <summary>
/// Write a value on the specified opc tag asynchronously
/// </summary>
public async Task WriteAsync<T>(string tag, T item)
{
await Task.Run(() => Write(tag, item));
}
/// <summary>
/// Finds a node on the Opc Server asynchronously
/// </summary>
public async Task<Node> FindNodeAsync(string tag)
{
return await Task.Run(() => FindNode(tag));
}
/// <summary>
/// Explore a folder on the Opc Server asynchronously
/// </summary>
public async Task<IEnumerable<Node>> ExploreFolderAsync(string tag)
{
return await Task.Run(() => ExploreFolder(tag));
}
}
}

View File

@@ -0,0 +1,22 @@
using Hylasoft.Opc.Common;
namespace Hylasoft.Opc.Da
{
/// <summary>
/// Represents a node to be used specifically for OPC DA
/// </summary>
public class DaNode : Node
{
/// <summary>
/// Instantiates a DaNode class
/// </summary>
/// <param name="name">the name of the node</param>
/// <param name="tag"></param>
/// <param name="parent">The parent node</param>
public DaNode(string name, string tag, Node parent = null)
: base(name, parent)
{
Tag = tag;
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("h-opc")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("hylasoft")]
[assembly: AssemblyProduct("h-opc")]
[assembly: AssemblyCopyright(" ")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("cec4db47-9316-4591-a7e7-35118fb1e375")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("0.4.0")]
[assembly: AssemblyVersion("0.4.0")]
[assembly: AssemblyFileVersion("0.4.0")]

View File

@@ -0,0 +1,5 @@
<StyleCopSettings Version="105" >
<GlobalSettings>
<BooleanProperty Name="RulesEnabledByDefault">False</BooleanProperty>
</GlobalSettings>
</StyleCopSettings>

View File

@@ -0,0 +1,100 @@
using System.Linq;
using Opc.Ua;
using System;
using Opc.Ua.Client;
namespace Hylasoft.Opc.Ua
{
/// <summary>
/// List of static utility methods
/// </summary>
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;
}
}
}
}

View File

@@ -0,0 +1,24 @@
using Hylasoft.Opc.Common;
using OpcF = Opc.Ua;
namespace Hylasoft.Opc.Ua
{
/// <summary>
/// Class with extension methods for OPC UA
/// </summary>
public static class NodeExtensions
{
/// <summary>
/// Converts an OPC Foundation node to an Hylasoft OPC UA Node
/// </summary>
/// <param name="node">The node to convert</param>
/// <param name="parent">the parent node (optional)</param>
/// <returns></returns>
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);
}
}
}

View File

@@ -0,0 +1,558 @@
using Hylasoft.Opc.Common;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Hylasoft.Opc.Ua
{
/// <summary>
/// Client Implementation for UA
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling",
Justification = "Doesn't make sense to split this class")]
public class UaClient : IClient<UaNode>
{
private readonly UaClientOptions _options = new UaClientOptions();
private readonly Uri _serverUrl;
protected Session _session;
private readonly IDictionary<string, UaNode> _nodesCache = new Dictionary<string, UaNode>();
private readonly IDictionary<string, IList<UaNode>> _folderCache = new Dictionary<string, IList<UaNode>>();
/// <summary>
/// Creates a server object
/// </summary>
/// <param name="serverUrl">the url of the server to connect to</param>
public UaClient(Uri serverUrl)
{
_serverUrl = serverUrl;
Status = OpcStatus.NotConnected;
}
/// <summary>
/// Options to configure the UA client session
/// </summary>
public UaClientOptions Options
{
get { return _options; }
}
private void PostInitializeSession()
{
var node = _session.NodeCache.Find(ObjectIds.ObjectsFolder);
RootNode = new UaNode(string.Empty, node.NodeId.ToString());
AddNodeToCache(RootNode);
Status = OpcStatus.Connected;
}
/// <summary>
/// Connect the client to the OPC Server
/// </summary>
public void Connect()
{
if (Status == OpcStatus.Connected)
return;
_session = InitializeSession(_serverUrl);
_session.KeepAlive += SessionKeepAlive;
_session.SessionClosing += SessionClosing;
PostInitializeSession();
}
private void SessionKeepAlive(Session 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();
}
/// <summary>
/// Reconnect the OPC session
/// </summary>
public void ReConnect()
{
if (Status != OpcStatus.Connected)
return;
Status = OpcStatus.NotConnected;
_session.Reconnect();
Status = OpcStatus.Connected;
}
/// <summary>
/// Create a new OPC session, based on the current session parameters.
/// </summary>
public void RecreateSession()
{
if (Status != OpcStatus.Connected)
return;
Status = OpcStatus.NotConnected;
_session = Session.Recreate(_session);
PostInitializeSession();
}
/// <summary>
/// Gets the current status of the OPC Client
/// </summary>
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 };
}
/// <summary>
/// Read a tag
/// </summary>
/// <typeparam name="T">The type of tag to read</typeparam>
/// <param name="tag">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`</param>
/// <returns>The value retrieved from the OPC</returns>
public T Read<T>(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];
CheckReturnValue(val.StatusCode);
return (T)val.Value;
}
/// <summary>
/// Read a tag asynchronously
/// </summary>
/// <typeparam name="T">The type of tag to read</typeparam>
/// <param name="tag">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`</param>
/// <returns>The value retrieved from the OPC</returns>
public Task<T> ReadAsync<T>(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<T>();
_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);
CheckReturnValue(results[0].StatusCode);
var val = results[0];
taskCompletionSource.TrySetResult((T)val.Value);
}
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 };
}
/// <summary>
/// Write a value on the specified opc tag
/// </summary>
/// <typeparam name="T">The type of tag to write on</typeparam>
/// <param name="tag">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`</param>
/// <param name="item">The value for the item to write</param>
public void Write<T>(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]);
}
/// <summary>
/// Write a value on the specified opc tag asynchronously
/// </summary>
/// <typeparam name="T">The type of tag to write on</typeparam>
/// <param name="tag">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`</param>
/// <param name="item">The value for the item to write</param>
public Task WriteAsync<T>(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<T>();
_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]);
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
},
asyncState: null);
return taskCompletionSource.Task;
}
/// <summary>
/// Monitor the specified tag for changes
/// </summary>
/// <typeparam name="T">the type of tag to monitor</typeparam>
/// <param name="tag">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`</param>
/// <param name="callback">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</param>
public void Monitor<T>(string tag, Action<T, Action> callback)
{
var node = FindNode(tag);
var sub = new Subscription
{
PublishingInterval = _options.DefaultMonitorInterval,
PublishingEnabled = true,
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();
};
callback((T)t, unsubscribe);
};
}
/// <summary>
/// Explore a folder on the Opc Server
/// </summary>
/// <param name="tag">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`</param>
/// <returns>The list of sub-nodes</returns>
public IEnumerable<UaNode> ExploreFolder(string tag)
{
IList<UaNode> 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
_folderCache.Add(tag, nodes);
foreach (var node in nodes)
AddNodeToCache(node);
return nodes;
}
/// <summary>
/// Explores a folder asynchronously
/// </summary>
public async Task<IEnumerable<Common.Node>> ExploreFolderAsync(string tag)
{
return await Task.Run(() => ExploreFolder(tag));
}
/// <summary>
/// Finds a node on the Opc Server
/// </summary>
/// <param name="tag">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`</param>
/// <returns>If there is a tag, it returns it, otherwise it throws an </returns>
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));
}
/// <summary>
/// Find node asynchronously
/// </summary>
public async Task<Common.Node> FindNodeAsync(string tag)
{
return await Task.Run(() => FindNode(tag));
}
/// <summary>
/// Gets the root node of the server
/// </summary>
public UaNode RootNode { get; private set; }
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
if (_session != null)
{
_session.RemoveSubscriptions(_session.Subscriptions.ToList());
_session.Close();
_session.Dispose();
}
GC.SuppressFinalize(this);
}
protected void CheckReturnValue(StatusCode status)
{
if (!StatusCode.IsGood(status))
throw new OpcException(string.Format("Invalid response from the server. (Response Status: {0})", status), status);
}
/// <summary>
/// Adds a node to the cache using the tag as its key
/// </summary>
/// <param name="node">the node to add</param>
protected void AddNodeToCache(UaNode node)
{
if (!_nodesCache.ContainsKey(node.Tag))
_nodesCache.Add(node.Tag, node);
}
/// <summary>
/// Crappy method to initialize the session. I don't know what many of these things do, sincerely.
/// </summary>
private Session 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,
SecurityConfiguration = new SecurityConfiguration
{
AutoAcceptUntrustedCertificates = _options.AutoAcceptUntrustedCertificates
},
TransportQuotas = new TransportQuotas
{
OperationTimeout = 600000,
MaxStringLength = 1048576,
MaxByteStringLength = 1048576,
MaxArrayLength = 65535,
MaxMessageSize = 4194304,
MaxBufferSize = 65535,
ChannelLifetime = 300000,
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 = 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: null,
preferredLocales: new string[] { });
return session;
}
/// <summary>
/// Finds a node starting from the specified node as the root folder
/// </summary>
/// <param name="tag">the tag to find</param>
/// <param name="node">the root node</param>
/// <returns></returns>
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);
}
return folders.Length == 1
? found // last node, return it
: FindNode(string.Join(".", folders.Except(new[] { head })), found); // find sub nodes
}
private void NotifyServerConnectionLost()
{
if (ServerConnectionLost != null)
ServerConnectionLost(this, EventArgs.Empty);
}
private void NotifyServerConnectionRestored()
{
if (ServerConnectionRestored != null)
ServerConnectionRestored(this, EventArgs.Empty);
}
/// <summary>
/// This event is raised when the connection to the OPC server is lost.
/// </summary>
public event EventHandler ServerConnectionLost;
/// <summary>
/// This event is raised when the connection to the OPC server is restored.
/// </summary>
public event EventHandler ServerConnectionRestored;
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Security.Cryptography.X509Certificates;
namespace Hylasoft.Opc.Ua
{
/// <summary>
/// This class defines the configuration options for the setup of the UA client session
/// </summary>
public class UaClientOptions
{
/// <summary>
/// Specifies the (optional) certificate for the applicaiton to connect to the server
/// </summary>
public X509Certificate2 ApplicationCertificate { get; set; }
/// <summary>
/// Specifies the ApplicationName for the client application.
/// </summary>
public string ApplicationName { get; set; }
/// <summary>
/// Should untrusted certificates be silently accepted by the client?
/// </summary>
public bool AutoAcceptUntrustedCertificates { get; set; }
/// <summary>
/// Specifies the ConfigSectionName for the client configuration.
/// </summary>
public string ConfigSectionName { get; set; }
/// <summary>
/// default monitor interval in Milliseconds.
/// </summary>
public int DefaultMonitorInterval { get; set; }
/// <summary>
/// Specifies a name to be associated with the created sessions.
/// </summary>
public string SessionName { get; set; }
/// <summary>
/// Specifies the timeout for the sessions.
/// </summary>
public uint SessionTimeout { get; set; }
/// <summary>
/// Specify whether message exchange should be secured.
/// </summary>
public bool UseMessageSecurity { get; set; }
internal UaClientOptions()
{
// Initialize default values:
ApplicationName = "h-opc-client";
AutoAcceptUntrustedCertificates = true;
ConfigSectionName = "h-opc-client";
DefaultMonitorInterval = 100;
SessionName = "h-opc-client";
SessionTimeout = 60000U;
UseMessageSecurity = false;
}
}
}

View File

@@ -0,0 +1,29 @@
using Hylasoft.Opc.Common;
namespace Hylasoft.Opc.Ua
{
/// <summary>
/// Represents a node to be used specifically for OPC UA
/// </summary>
public class UaNode : Node
{
/// <summary>
/// The UA Id of the node
/// </summary>
public string NodeId { get; private set; }
/// <summary>
/// Instantiates a UaNode class
/// </summary>
/// <param name="name">the name of the node</param>
/// <param name="nodeId">The UA Id of the node</param>
/// <param name="parent">The parent node</param>
internal UaNode(string name, string nodeId, Node parent = null)
: base(name, parent)
{
NodeId = nodeId;
}
}
}

View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{4F43B6F0-0C32-4C34-978E-9B8B5B0B6E80}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Hylasoft.Opc</RootNamespace>
<AssemblyName>h-opc</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<StyleCopOverrideSettingsFile>Settings.StyleCop</StyleCopOverrideSettingsFile>
<BuildToolsStyleCopVersion>4.7.44.0</BuildToolsStyleCopVersion>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<StyleCopEnabled>False</StyleCopEnabled>
<DocumentationFile>bin\Debug\h-opc.XML</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE;CODE_ANALYSIS</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<StyleCopEnabled>True</StyleCopEnabled>
<StyleCopTreatErrorsAsWarnings>False</StyleCopTreatErrorsAsWarnings>
<DocumentationFile>bin\Release\h-opc.XML</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="Opc.Ua.Client">
<HintPath>..\ext_packages\Opc.Ua.Client.dll</HintPath>
</Reference>
<Reference Include="Opc.Ua.Configuration">
<HintPath>..\ext_packages\Opc.Ua.Configuration.dll</HintPath>
</Reference>
<Reference Include="Opc.Ua.Core">
<HintPath>..\ext_packages\Opc.Ua.Core.dll</HintPath>
</Reference>
<Reference Include="OpcComRcw">
<HintPath>..\ext_packages\OpcComRcw.dll</HintPath>
</Reference>
<Reference Include="OpcNetApi">
<HintPath>..\ext_packages\OpcNetApi.dll</HintPath>
</Reference>
<Reference Include="OpcNetApi.Com">
<HintPath>..\ext_packages\OpcNetApi.Com.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Common\ClientExtensions.cs" />
<Compile Include="Common\Node.cs" />
<Compile Include="Common\IClient.cs" />
<Compile Include="Common\OpcException.cs" />
<Compile Include="Common\OpcStatus.cs" />
<Compile Include="Da\DaClient_async.cs" />
<Compile Include="Da\DaClient.cs" />
<Compile Include="Da\DaNode.cs" />
<Compile Include="Ua\ClientUtils.cs" />
<Compile Include="Ua\NodeExtensions.cs" />
<Compile Include="Ua\UaClientOptions.cs" />
<Compile Include="Ua\UaNode.cs" />
<Compile Include="Ua\UaClient.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="Settings.StyleCop">
<SubType>Designer</SubType>
</None>
</ItemGroup>
<ItemGroup Label="StyleCopAdditionalAddinPaths">
<StyleCopAdditionalAddinPaths Include="..\packages\BuildTools.StyleCopPlus.4.7.49.4\tools">
<Visible>false</Visible>
</StyleCopAdditionalAddinPaths>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\packages\BuildTools.StyleCop.4.7.49.0\tools\StyleCop.targets" Condition="Exists('..\packages\BuildTools.StyleCop.4.7.49.0\tools\StyleCop.targets')" />
<Target Name="packages_BuildTools_StyleCop_4_7_49_0_tools_StyleCop_targets" Condition="$(StyleCopOutputFile)==''" BeforeTargets="BeforeBuild">
<Error Text="BuildTools_StyleCop - the BuildTools_StyleCop package has not been restored.&#xD;&#xA;If you are running this from an IDE, make sure NuGet Package Restore has been enabled, then reload the solution and re-run the build.&#xD;&#xA;If you are running this from the command line, run the build again.&#xD;&#xA;If this is a CI server, you may want to make sure NuGet Package Restore runs before your build with:&#xD;&#xA; msbuild solution.sln /t:restorepackages" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="BuildTools.StyleCop" version="4.7.49.0" targetFramework="net45" />
<package id="BuildTools.StyleCopPlus" version="4.7.49.4" targetFramework="net45" />
</packages>

View File

@@ -7,6 +7,8 @@ Caution: I really want to implement the COM communication system, but nowaday Us
Caution2: In the current version, you can't get bit or set bit in this library. Please get a byte, change the bit value in the byte, and set to PLC. I will fix this bug in future. Caution2: In the current version, you can't get bit or set bit in this library. Please get a byte, change the bit value in the byte, and set to PLC. I will fix this bug in future.
Reference: <b>"h-opc"</b> is linked by [https://github.com/hylasoft-usa/h-opc](https://github.com/hylasoft-usa/h-opc)
Table of Content: Table of Content:
* [Features](#features) * [Features](#features)
* [Usage](#usage) * [Usage](#usage)
@@ -17,6 +19,7 @@ Table of Content:
* A open platform that you can easy to extend a industrial communication protocal. * A open platform that you can easy to extend a industrial communication protocal.
* Modbus Tcp protocal. * Modbus Tcp protocal.
* Siemens Tcp protocal (acturally it is the same as Profinet) * Siemens Tcp protocal (acturally it is the same as Profinet)
* OPC DA protocal.
* All communications can be asyncronized. * All communications can be asyncronized.
* A task manager that you can easily manage multiple connections. * A task manager that you can easily manage multiple connections.
* .net framework 4.6 and Visual Studio 2015 support. * .net framework 4.6 and Visual Studio 2015 support.