diff --git a/.vs/IoTGateway/v17/.suo b/.vs/IoTGateway/v17/.suo index 28a75b3..f4aa5d9 100644 Binary files a/.vs/IoTGateway/v17/.suo and b/.vs/IoTGateway/v17/.suo differ diff --git a/Dockerfile b/Dockerfile index 189a6cc..413ba56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base WORKDIR /app EXPOSE 518 +EXPOSE 1888 RUN apt-get update RUN apt-get install libgdiplus -y @@ -10,10 +11,19 @@ RUN apt-get install nano -y FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build WORKDIR /src + COPY ["IoTGateway/IoTGateway.csproj", "IoTGateway/"] COPY ["IoTGateway.ViewModel/IoTGateway.ViewModel.csproj", "IoTGateway.ViewModel/"] +COPY ["Plugins/Plugin/Plugin.csproj", "Plugins/Plugin/"] COPY ["IoTGateway.Model/IoTGateway.Model.csproj", "IoTGateway.Model/"] +COPY ["WalkingTec.Mvvm/WalkingTec.Mvvm.Core/WalkingTec.Mvvm.Core.csproj", "WalkingTec.Mvvm/WalkingTec.Mvvm.Core/"] +COPY ["Plugins/PluginInterface/PluginInterface.csproj", "Plugins/PluginInterface/"] COPY ["IoTGateway.DataAccess/IoTGateway.DataAccess.csproj", "IoTGateway.DataAccess/"] +COPY ["Plugins/Drivers/DriverSiemensS7/DriverSiemensS7.csproj", "Plugins/Drivers/DriverSiemensS7/"] +COPY ["Plugins/Drivers/DriverModbusMaster/DriverModbusMaster.csproj", "Plugins/Drivers/DriverModbusMaster/"] +COPY ["WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/WalkingTec.Mvvm.TagHelpers.LayUI.csproj", "WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/"] +COPY ["WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/WalkingTec.Mvvm.Mvc.csproj", "WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/"] + RUN dotnet restore "IoTGateway/IoTGateway.csproj" COPY . . WORKDIR "/src/IoTGateway" diff --git a/IoTGateway.sln b/IoTGateway.sln index 9f03442..9d1e5df 100644 --- a/IoTGateway.sln +++ b/IoTGateway.sln @@ -19,8 +19,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PluginInterface", "Plugins\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Drivers", "Drivers", "{52D96C24-2F2F-49B5-9F29-00414DEA41D8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DriverModbusTCP", "Plugins\Drivers\DriverModbusTCP\DriverModbusTCP.csproj", "{7B432FC9-57E6-44BF-B8A7-2A1FB31D6ADD}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WalkingTec.Mvvm", "WalkingTec.Mvvm", "{98B1C9F0-028C-48D8-8148-54B69CCA4590}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WalkingTec.Mvvm.Core", "WalkingTec.Mvvm\WalkingTec.Mvvm.Core\WalkingTec.Mvvm.Core.csproj", "{C2672620-8E65-486C-B967-C4C673F8DA0F}" @@ -29,6 +27,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WalkingTec.Mvvm.Mvc", "Walk EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WalkingTec.Mvvm.TagHelpers.LayUI", "WalkingTec.Mvvm\WalkingTec.Mvvm.TagHelpers.LayUI\WalkingTec.Mvvm.TagHelpers.LayUI.csproj", "{81CBFD0E-1D89-440A-8CC3-E32672504FF4}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DriverModbusMaster", "Plugins\Drivers\DriverModbusMaster\DriverModbusMaster.csproj", "{4FC43620-00B1-48C1-A5A0-02FCC038FB08}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DriverSiemensS7", "Plugins\Drivers\DriverSiemensS7\DriverSiemensS7.csproj", "{B884FBE3-C8C5-471E-B629-12ECA0FC5DAC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,10 +61,6 @@ Global {E5F79995-AB61-41F4-820D-BA39967B406B}.Debug|Any CPU.Build.0 = Debug|Any CPU {E5F79995-AB61-41F4-820D-BA39967B406B}.Release|Any CPU.ActiveCfg = Release|Any CPU {E5F79995-AB61-41F4-820D-BA39967B406B}.Release|Any CPU.Build.0 = Release|Any CPU - {7B432FC9-57E6-44BF-B8A7-2A1FB31D6ADD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B432FC9-57E6-44BF-B8A7-2A1FB31D6ADD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B432FC9-57E6-44BF-B8A7-2A1FB31D6ADD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B432FC9-57E6-44BF-B8A7-2A1FB31D6ADD}.Release|Any CPU.Build.0 = Release|Any CPU {C2672620-8E65-486C-B967-C4C673F8DA0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C2672620-8E65-486C-B967-C4C673F8DA0F}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2672620-8E65-486C-B967-C4C673F8DA0F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -75,6 +73,14 @@ Global {81CBFD0E-1D89-440A-8CC3-E32672504FF4}.Debug|Any CPU.Build.0 = Debug|Any CPU {81CBFD0E-1D89-440A-8CC3-E32672504FF4}.Release|Any CPU.ActiveCfg = Release|Any CPU {81CBFD0E-1D89-440A-8CC3-E32672504FF4}.Release|Any CPU.Build.0 = Release|Any CPU + {4FC43620-00B1-48C1-A5A0-02FCC038FB08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FC43620-00B1-48C1-A5A0-02FCC038FB08}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FC43620-00B1-48C1-A5A0-02FCC038FB08}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FC43620-00B1-48C1-A5A0-02FCC038FB08}.Release|Any CPU.Build.0 = Release|Any CPU + {B884FBE3-C8C5-471E-B629-12ECA0FC5DAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B884FBE3-C8C5-471E-B629-12ECA0FC5DAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B884FBE3-C8C5-471E-B629-12ECA0FC5DAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B884FBE3-C8C5-471E-B629-12ECA0FC5DAC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -83,10 +89,11 @@ Global {61D79F77-09EF-4A98-A50B-043B1D72C111} = {FBED048F-7AB9-4348-AD56-F9BF4D9E3A55} {E5F79995-AB61-41F4-820D-BA39967B406B} = {FBED048F-7AB9-4348-AD56-F9BF4D9E3A55} {52D96C24-2F2F-49B5-9F29-00414DEA41D8} = {FBED048F-7AB9-4348-AD56-F9BF4D9E3A55} - {7B432FC9-57E6-44BF-B8A7-2A1FB31D6ADD} = {52D96C24-2F2F-49B5-9F29-00414DEA41D8} {C2672620-8E65-486C-B967-C4C673F8DA0F} = {98B1C9F0-028C-48D8-8148-54B69CCA4590} {B370F699-965B-4D86-93B1-0F022C95B5C9} = {98B1C9F0-028C-48D8-8148-54B69CCA4590} {81CBFD0E-1D89-440A-8CC3-E32672504FF4} = {98B1C9F0-028C-48D8-8148-54B69CCA4590} + {4FC43620-00B1-48C1-A5A0-02FCC038FB08} = {52D96C24-2F2F-49B5-9F29-00414DEA41D8} + {B884FBE3-C8C5-471E-B629-12ECA0FC5DAC} = {52D96C24-2F2F-49B5-9F29-00414DEA41D8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1F219808-E6E8-4C1D-B846-41F2F7CF20FA} diff --git a/IoTGateway/Areas/BasicData/Views/DeviceConfig/Edit.cshtml b/IoTGateway/Areas/BasicData/Views/DeviceConfig/Edit.cshtml index 0f8d016..02ff285 100644 --- a/IoTGateway/Areas/BasicData/Views/DeviceConfig/Edit.cshtml +++ b/IoTGateway/Areas/BasicData/Views/DeviceConfig/Edit.cshtml @@ -3,7 +3,7 @@ - + @**@ @{ if (Model.Entity.EnumInfo != null) { @@ -15,8 +15,8 @@ } } - - + @**@ + @**@ diff --git a/IoTGateway/Dockerfile b/IoTGateway/Dockerfile deleted file mode 100644 index 189a6cc..0000000 --- a/IoTGateway/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base -WORKDIR /app -EXPOSE 518 - -RUN apt-get update -RUN apt-get install libgdiplus -y -RUN apt-get install nano -y - -FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build -WORKDIR /src -COPY ["IoTGateway/IoTGateway.csproj", "IoTGateway/"] -COPY ["IoTGateway.ViewModel/IoTGateway.ViewModel.csproj", "IoTGateway.ViewModel/"] -COPY ["IoTGateway.Model/IoTGateway.Model.csproj", "IoTGateway.Model/"] -COPY ["IoTGateway.DataAccess/IoTGateway.DataAccess.csproj", "IoTGateway.DataAccess/"] -RUN dotnet restore "IoTGateway/IoTGateway.csproj" -COPY . . -WORKDIR "/src/IoTGateway" -RUN dotnet build "IoTGateway.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "IoTGateway.csproj" -c Release -o /app/publish - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . - -ENV TZ=Asia/Shanghai -ENTRYPOINT ["dotnet", "IoTGateway.dll"] \ No newline at end of file diff --git a/IoTGateway/IoTGateway.csproj b/IoTGateway/IoTGateway.csproj index 740db28..9c1f322 100644 --- a/IoTGateway/IoTGateway.csproj +++ b/IoTGateway/IoTGateway.csproj @@ -16,12 +16,18 @@ + + + + + + diff --git a/IoTGateway/Plugin.dll b/IoTGateway/Plugin.dll deleted file mode 100644 index 2378f81..0000000 Binary files a/IoTGateway/Plugin.dll and /dev/null differ diff --git a/IoTGateway/Program.cs b/IoTGateway/Program.cs index 43a9ca3..0d89d8a 100644 --- a/IoTGateway/Program.cs +++ b/IoTGateway/Program.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using MQTTnet.AspNetCore.Extensions; using WalkingTec.Mvvm.Core; namespace IoTGateway @@ -35,10 +36,10 @@ namespace IoTGateway webBuilder.UseStartup(); webBuilder.UseKestrel(option => { + option.ListenAnyIP(1888, l => l.UseMqtt()); option.ListenAnyIP(518); }); }); - //.UseServiceProviderFactory(new AutofacServiceProviderFactory()); } } } diff --git a/IoTGateway/Startup.cs b/IoTGateway/Startup.cs index 737f3c6..012052d 100644 --- a/IoTGateway/Startup.cs +++ b/IoTGateway/Startup.cs @@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using MQTTnet.AspNetCore; +using MQTTnet.AspNetCore.Extensions; using Plugin; using WalkingTec.Mvvm.Core; using WalkingTec.Mvvm.Core.Extensions; @@ -30,7 +32,7 @@ namespace IoTGateway public void ConfigureServices(IServiceCollection services) { services.AddDistributedMemoryCache(); - services.AddWtmSession(3600, ConfigRoot); + services.AddWtmSession(36000, ConfigRoot); services.AddWtmCrossDomain(ConfigRoot); services.AddWtmAuthentication(ConfigRoot); services.AddWtmHttpClient(ConfigRoot); @@ -41,7 +43,8 @@ namespace IoTGateway { options.UseWtmMvcOptions(); }) - .AddJsonOptions(options => { + .AddJsonOptions(options => + { options.UseWtmJsonOptions(); }) .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) @@ -51,23 +54,30 @@ namespace IoTGateway }) .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix) .AddWtmDataAnnotationsLocalization(typeof(Program)); - - services.AddWtmContext(ConfigRoot, (options)=> { + + services.AddWtmContext(ConfigRoot, (options) => + { options.DataPrivileges = DataPrivilegeSettings(); options.CsSelector = CSSelector; options.FileSubDirSelector = SubDirSelector; options.ReloadUserFunc = ReloadUser; }); + + //MQTTServer + services.AddHostedMqttServer(mqttServer => + { + mqttServer.WithoutDefaultEndpoint(); + }) + .AddMqttConnectionHandler() + .AddConnections(); + + services.AddHostedService(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); } - //public void ConfigureContainer(ContainerBuilder containerBuilder) - //{ - // containerBuilder.RegisterModule(); - //} // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IOptionsMonitor configs, DeviceService deviceService) { @@ -87,6 +97,12 @@ namespace IoTGateway app.UseEndpoints(endpoints => { + //MqttServerWebSocket + endpoints.MapConnectionHandler("/mqtt", options => + { + options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol; + }); + endpoints.MapControllerRoute( name: "areaRoute", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); diff --git a/IoTGateway/appsettings.json b/IoTGateway/appsettings.json index d12bf79..c10185a 100644 --- a/IoTGateway/appsettings.json +++ b/IoTGateway/appsettings.json @@ -83,19 +83,19 @@ "JwtOptions": { "Issuer": "http://localhost", "Audience": "http://localhost", - "Expires": 3600, + "Expires": 36000, "SecurityKey": "superSecretKey@345", - "RefreshTokenExpires": 86400, + "RefreshTokenExpires": 864000, "LoginPath": "/_Framework/Redirect401" }, "CookieOptions": { "Issuer": "http://localhost", "Audience": "http://localhost", "Domain": "", - "Expires": 3600, + "Expires": 36000, "SlidingExpiration": true, "SecurityKey": "superSecretKey@345", - "RefreshTokenExpires": 86400, + "RefreshTokenExpires": 864000, "LoginPath": "/Login/Login" }, "Domains": { diff --git a/IoTGateway/iotgateway.db b/IoTGateway/iotgateway.db index 142e91f..5335f6b 100644 Binary files a/IoTGateway/iotgateway.db and b/IoTGateway/iotgateway.db differ diff --git a/Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/DesignTimeBuild/.dtbcache.v2 b/Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000..9e32959 Binary files /dev/null and b/Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/DesignTimeBuild/.dtbcache.v2 differ diff --git a/Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/v17/.futdcache.v1 b/Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/v17/.futdcache.v1 new file mode 100644 index 0000000..03725c5 Binary files /dev/null and b/Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/v17/.futdcache.v1 differ diff --git a/Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/v17/.suo b/Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/v17/.suo new file mode 100644 index 0000000..ca1688a Binary files /dev/null and b/Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/v17/.suo differ diff --git a/Plugins/Drivers/DriverModbusTCP/DriverModbusTCP.csproj b/Plugins/Drivers/DriverModbusMaster/DriverModbusMaster.csproj similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/DriverModbusTCP.csproj rename to Plugins/Drivers/DriverModbusMaster/DriverModbusMaster.csproj diff --git a/Plugins/Drivers/DriverModbusTCP/ModbusTCP.cs b/Plugins/Drivers/DriverModbusMaster/ModbusMaster.cs similarity index 98% rename from Plugins/Drivers/DriverModbusTCP/ModbusTCP.cs rename to Plugins/Drivers/DriverModbusMaster/ModbusMaster.cs index 629d452..fb1b49d 100644 --- a/Plugins/Drivers/DriverModbusTCP/ModbusTCP.cs +++ b/Plugins/Drivers/DriverModbusMaster/ModbusMaster.cs @@ -7,19 +7,19 @@ using System.IO.Ports; using System.Net; using System.Net.Sockets; -namespace DriverModbusTCP +namespace DriverModbusMaster { [DriverSupported("ModbusTCP")] [DriverSupported("ModbusUDP")] [DriverSupported("ModbusRtu")] [DriverSupported("ModbusAscii")] [DriverInfoAttribute("ModbusMaster", "V1.0.0", "Copyright WHD© 2021-12-19")] - public class ModbusTCP : IDriver + public class ModbusMaster : IDriver { private TcpClient clientTcp = null; private UdpClient clientUdp = null; private SerialPort port = null; - private ModbusMaster master = null; + private Modbus.Device.ModbusMaster master = null; private SerialPortAdapter adapter = null; #region 配置参数 @@ -60,11 +60,11 @@ namespace DriverModbusTCP public uint Timeout { get; set; } = 3000; [ConfigParameter("最小通讯周期ms")] - public uint MinPeriod { get; set; } = 30000; + public uint MinPeriod { get; set; } = 3000; #endregion - public ModbusTCP(Guid deviceId) + public ModbusMaster(Guid deviceId) { DeviceId = deviceId; } diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStore.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Data/DataStore.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStore.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Data/DataStore.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreEventArgs.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Data/DataStoreEventArgs.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreEventArgs.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Data/DataStoreEventArgs.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreFactory.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Data/DataStoreFactory.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreFactory.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Data/DataStoreFactory.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DiscreteCollection.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Data/DiscreteCollection.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DiscreteCollection.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Data/DiscreteCollection.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/IModbusMessageDataCollection.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Data/IModbusMessageDataCollection.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Data/IModbusMessageDataCollection.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Data/IModbusMessageDataCollection.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataCollection.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Data/ModbusDataCollection.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataCollection.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Data/ModbusDataCollection.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataType.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Data/ModbusDataType.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataType.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Data/ModbusDataType.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/RegisterCollection.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Data/RegisterCollection.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Data/RegisterCollection.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Data/RegisterCollection.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusMaster.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/IModbusMaster.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusMaster.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/IModbusMaster.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusSerialMaster.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/IModbusSerialMaster.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusSerialMaster.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/IModbusSerialMaster.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusDevice.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusDevice.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusDevice.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusDevice.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusIpMaster.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusIpMaster.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusIpMaster.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusIpMaster.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMaster.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusMaster.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMaster.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusMaster.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMasterTcpConnection.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusMasterTcpConnection.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMasterTcpConnection.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusMasterTcpConnection.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialMaster.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusSerialMaster.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialMaster.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusSerialMaster.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialSlave.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusSerialSlave.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialSlave.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusSerialSlave.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlave.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusSlave.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlave.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusSlave.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlaveRequestEventArgs.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusSlaveRequestEventArgs.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlaveRequestEventArgs.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusSlaveRequestEventArgs.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusTcpSlave.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusTcpSlave.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusTcpSlave.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusTcpSlave.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusUdpSlave.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusUdpSlave.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusUdpSlave.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/ModbusUdpSlave.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/TcpConnectionEventArgs.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Device/TcpConnectionEventArgs.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Device/TcpConnectionEventArgs.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Device/TcpConnectionEventArgs.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Extensions/Enron/EnronModbus.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Extensions/Enron/EnronModbus.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Extensions/Enron/EnronModbus.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Extensions/Enron/EnronModbus.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/GlobalSuppressions.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/GlobalSuppressions.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/GlobalSuppressions.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/GlobalSuppressions.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/EmptyTransport.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/IO/EmptyTransport.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/IO/EmptyTransport.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/IO/EmptyTransport.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/IStreamResource.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/IO/IStreamResource.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/IO/IStreamResource.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/IO/IStreamResource.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusAsciiTransport.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/IO/ModbusAsciiTransport.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusAsciiTransport.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/IO/ModbusAsciiTransport.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusIpTransport.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/IO/ModbusIpTransport.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusIpTransport.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/IO/ModbusIpTransport.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusRtuTransport.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/IO/ModbusRtuTransport.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusRtuTransport.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/IO/ModbusRtuTransport.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusSerialTransport.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/IO/ModbusSerialTransport.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusSerialTransport.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/IO/ModbusSerialTransport.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusTransport.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/IO/ModbusTransport.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusTransport.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/IO/ModbusTransport.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/StreamResourceUtility.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/IO/StreamResourceUtility.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/IO/StreamResourceUtility.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/IO/StreamResourceUtility.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/TcpClientAdapter.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/IO/TcpClientAdapter.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/IO/TcpClientAdapter.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/IO/TcpClientAdapter.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/UdpClientAdapter.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/IO/UdpClientAdapter.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/IO/UdpClientAdapter.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/IO/UdpClientAdapter.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/InvalidModbusRequestException.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/InvalidModbusRequestException.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/InvalidModbusRequestException.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/InvalidModbusRequestException.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessage.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/AbstractModbusMessage.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessage.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/AbstractModbusMessage.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessageWithData.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/AbstractModbusMessageWithData.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessageWithData.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/AbstractModbusMessageWithData.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/DiagnosticsRequestResponse.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/DiagnosticsRequestResponse.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/DiagnosticsRequestResponse.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/DiagnosticsRequestResponse.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusMessage.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/IModbusMessage.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusMessage.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/IModbusMessage.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusRequest.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/IModbusRequest.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusRequest.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/IModbusRequest.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageFactory.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ModbusMessageFactory.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageFactory.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ModbusMessageFactory.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageImpl.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ModbusMessageImpl.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageImpl.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ModbusMessageImpl.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsRequest.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ReadCoilsInputsRequest.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsRequest.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ReadCoilsInputsRequest.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsResponse.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ReadCoilsInputsResponse.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsResponse.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ReadCoilsInputsResponse.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersRequest.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ReadHoldingInputRegistersRequest.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersRequest.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ReadHoldingInputRegistersRequest.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersResponse.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ReadHoldingInputRegistersResponse.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersResponse.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ReadHoldingInputRegistersResponse.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadWriteMultipleRegistersRequest.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ReadWriteMultipleRegistersRequest.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadWriteMultipleRegistersRequest.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/ReadWriteMultipleRegistersRequest.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/SlaveExceptionResponse.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/SlaveExceptionResponse.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/SlaveExceptionResponse.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/SlaveExceptionResponse.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsRequest.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteMultipleCoilsRequest.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsRequest.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteMultipleCoilsRequest.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsResponse.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteMultipleCoilsResponse.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsResponse.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteMultipleCoilsResponse.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersRequest.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteMultipleRegistersRequest.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersRequest.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteMultipleRegistersRequest.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersResponse.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteMultipleRegistersResponse.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersResponse.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteMultipleRegistersResponse.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleCoilRequestResponse.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteSingleCoilRequestResponse.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleCoilRequestResponse.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteSingleCoilRequestResponse.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleRegisterRequestResponse.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteSingleRegisterRequestResponse.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleRegisterRequestResponse.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Message/WriteSingleRegisterRequestResponse.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Modbus.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Modbus.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Modbus.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Modbus.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Properties/AssemblyInfo.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Properties/AssemblyInfo.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Properties/AssemblyInfo.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Properties/AssemblyInfo.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Resources.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Resources.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Resources.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Resources.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/SerialPortAdapter.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/SerialPortAdapter.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/SerialPortAdapter.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/SerialPortAdapter.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/SlaveException.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/SlaveException.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/SlaveException.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/SlaveException.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/DisposableUtility.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Unme.Common/DisposableUtility.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/DisposableUtility.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Unme.Common/DisposableUtility.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/SequenceUtility.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Unme.Common/SequenceUtility.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/SequenceUtility.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Unme.Common/SequenceUtility.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/DiscriminatedUnion.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Utility/DiscriminatedUnion.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/DiscriminatedUnion.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Utility/DiscriminatedUnion.cs diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/ModbusUtility.cs b/Plugins/Drivers/DriverModbusMaster/NModbus4/Utility/ModbusUtility.cs similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/ModbusUtility.cs rename to Plugins/Drivers/DriverModbusMaster/NModbus4/Utility/ModbusUtility.cs diff --git a/Plugins/Drivers/DriverModbusTCP/System.IO.Ports.dll b/Plugins/Drivers/DriverModbusMaster/System.IO.Ports.dll similarity index 100% rename from Plugins/Drivers/DriverModbusTCP/System.IO.Ports.dll rename to Plugins/Drivers/DriverModbusMaster/System.IO.Ports.dll diff --git a/Plugins/Drivers/DriverSiemensS7/DriverSiemensS7.csproj b/Plugins/Drivers/DriverSiemensS7/DriverSiemensS7.csproj new file mode 100644 index 0000000..d9e44ca --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/DriverSiemensS7.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + ../../../IoTGateway/bin/Debug/net5.0/drivers + + + + + + + diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/COTP.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/COTP.cs new file mode 100644 index 0000000..3e5abbb --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/COTP.cs @@ -0,0 +1,118 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace S7.Net +{ + + /// + /// COTP Protocol functions and types + /// + internal class COTP + { + public enum PduType : byte + { + Data = 0xf0, + ConnectionConfirmed = 0xd0 + } + /// + /// Describes a COTP TPDU (Transport protocol data unit) + /// + public class TPDU + { + public TPKT TPkt { get; } + public byte HeaderLength; + public PduType PDUType; + public int TPDUNumber; + public byte[] Data; + public bool LastDataUnit; + + public TPDU(TPKT tPKT) + { + TPkt = tPKT; + + HeaderLength = tPKT.Data[0]; // Header length excluding this length byte + if (HeaderLength >= 2) + { + PDUType = (PduType)tPKT.Data[1]; + if (PDUType == PduType.Data) //DT Data + { + var flags = tPKT.Data[2]; + TPDUNumber = flags & 0x7F; + LastDataUnit = (flags & 0x80) > 0; + Data = new byte[tPKT.Data.Length - HeaderLength - 1]; // substract header length byte + header length. + Array.Copy(tPKT.Data, HeaderLength + 1, Data, 0, Data.Length); + return; + } + //TODO: Handle other PDUTypes + } + Data = new byte[0]; + } + + /// + /// Reads COTP TPDU (Transport protocol data unit) from the network stream + /// See: https://tools.ietf.org/html/rfc905 + /// + /// The socket to read from + /// COTP DPDU instance + public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken) + { + var tpkt = await TPKT.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + if (tpkt.Length == 0) + { + throw new TPDUInvalidException("No protocol data received"); + } + return new TPDU(tpkt); + } + + public override string ToString() + { + return string.Format("Length: {0} PDUType: {1} TPDUNumber: {2} Last: {3} Segment Data: {4}", + HeaderLength, + PDUType, + TPDUNumber, + LastDataUnit, + BitConverter.ToString(Data) + ); + } + + } + + /// + /// Describes a COTP TSDU (Transport service data unit). One TSDU consist of 1 ore more TPDUs + /// + public class TSDU + { + /// + /// Reads the full COTP TSDU (Transport service data unit) + /// See: https://tools.ietf.org/html/rfc905 + /// + /// The stream to read from + /// Data in TSDU + public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken) + { + var segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + + if (segment.LastDataUnit) + { + return segment.Data; + } + + // More segments are expected, prepare a buffer to store all data + var buffer = new byte[segment.Data.Length]; + Array.Copy(segment.Data, buffer, segment.Data.Length); + + while (!segment.LastDataUnit) + { + segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + var previousLength = buffer.Length; + Array.Resize(ref buffer, buffer.Length + segment.Data.Length); + Array.Copy(segment.Data, 0, buffer, previousLength, segment.Data.Length); + } + + return buffer; + } + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Compat/TcpClientMixins.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Compat/TcpClientMixins.cs new file mode 100644 index 0000000..0b449e1 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Compat/TcpClientMixins.cs @@ -0,0 +1,19 @@ +using System.Net.Sockets; + +namespace S7.Net +{ + public static class TcpClientMixins + { + #if NETSTANDARD1_3 + public static void Close(this TcpClient tcpClient) + { + tcpClient.Dispose(); + } + + public static void Connect(this TcpClient tcpClient, string host, int port) + { + tcpClient.ConnectAsync(host, port).GetAwaiter().GetResult(); + } + #endif + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Conversion.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Conversion.cs new file mode 100644 index 0000000..44f9e25 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Conversion.cs @@ -0,0 +1,226 @@ +using System; +using System.Globalization; + +namespace S7.Net +{ + /// + /// Conversion methods to convert from Siemens numeric format to C# and back + /// + public static class Conversion + { + /// + /// Converts a binary string to Int32 value + /// + /// + /// + public static int BinStringToInt32(this string txt) + { + int ret = 0; + + for (int i = 0; i < txt.Length; i++) + { + ret = (ret << 1) | ((txt[i] == '1') ? 1 : 0); + } + return ret; + } + + /// + /// Converts a binary string to a byte. Can return null. + /// + /// + /// + public static byte? BinStringToByte(this string txt) + { + if (txt.Length == 8) return (byte)BinStringToInt32(txt); + return null; + } + + /// + /// Converts the value to a binary string + /// + /// + /// + public static string ValToBinString(this object value) + { + int cnt = 0; + int cnt2 = 0; + int x = 0; + string txt = ""; + long longValue = 0; + + try + { + if (value.GetType().Name.IndexOf("[]") < 0) + { + // ist nur ein Wert + switch (value.GetType().Name) + { + case "Byte": + x = 7; + longValue = (long)((byte)value); + break; + case "Int16": + x = 15; + longValue = (long)((Int16)value); + break; + case "Int32": + x = 31; + longValue = (long)((Int32)value); + break; + case "Int64": + x = 63; + longValue = (long)((Int64)value); + break; + default: + throw new Exception(); + } + + for (cnt = x; cnt >= 0; cnt += -1) + { + if (((Int64)longValue & (Int64)Math.Pow(2, cnt)) > 0) + txt += "1"; + else + txt += "0"; + } + } + else + { + // ist ein Array + switch (value.GetType().Name) + { + case "Byte[]": + x = 7; + byte[] ByteArr = (byte[])value; + for (cnt2 = 0; cnt2 <= ByteArr.Length - 1; cnt2++) + { + for (cnt = x; cnt >= 0; cnt += -1) + if ((ByteArr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0"; + } + break; + case "Int16[]": + x = 15; + Int16[] Int16Arr = (Int16[])value; + for (cnt2 = 0; cnt2 <= Int16Arr.Length - 1; cnt2++) + { + for (cnt = x; cnt >= 0; cnt += -1) + if ((Int16Arr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0"; + } + break; + case "Int32[]": + x = 31; + Int32[] Int32Arr = (Int32[])value; + for (cnt2 = 0; cnt2 <= Int32Arr.Length - 1; cnt2++) + { + for (cnt = x; cnt >= 0; cnt += -1) + if ((Int32Arr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0"; + } + break; + case "Int64[]": + x = 63; + byte[] Int64Arr = (byte[])value; + for (cnt2 = 0; cnt2 <= Int64Arr.Length - 1; cnt2++) + { + for (cnt = x; cnt >= 0; cnt += -1) + if ((Int64Arr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0"; + } + break; + default: + throw new Exception(); + } + } + return txt; + } + catch + { + return ""; + } + } + + /// + /// Helper to get a bit value given a byte and the bit index. + /// Example: DB1.DBX0.5 -> var bytes = ReadBytes(DB1.DBW0); bool bit = bytes[0].SelectBit(5); + /// + /// + /// + /// + public static bool SelectBit(this byte data, int bitPosition) + { + int mask = 1 << bitPosition; + int result = data & mask; + + return (result != 0); + } + + /// + /// Converts from ushort value to short value; it's used to retrieve negative values from words + /// + /// + /// + public static short ConvertToShort(this ushort input) + { + short output; + output = short.Parse(input.ToString("X"), NumberStyles.HexNumber); + return output; + } + + /// + /// Converts from short value to ushort value; it's used to pass negative values to DWs + /// + /// + /// + public static ushort ConvertToUshort(this short input) + { + ushort output; + output = ushort.Parse(input.ToString("X"), NumberStyles.HexNumber); + return output; + } + + /// + /// Converts from UInt32 value to Int32 value; it's used to retrieve negative values from DBDs + /// + /// + /// + public static Int32 ConvertToInt(this uint input) + { + int output; + output = int.Parse(input.ToString("X"), NumberStyles.HexNumber); + return output; + } + + /// + /// Converts from Int32 value to UInt32 value; it's used to pass negative values to DBDs + /// + /// + /// + public static UInt32 ConvertToUInt(this int input) + { + uint output; + output = uint.Parse(input.ToString("X"), NumberStyles.HexNumber); + return output; + } + + /// + /// Converts from float to DWord (DBD) + /// + /// + /// + public static UInt32 ConvertToUInt(this float input) + { + uint output; + output = S7.Net.Types.DWord.FromByteArray(S7.Net.Types.Real.ToByteArray(input)); + return output; + } + + /// + /// Converts from DWord (DBD) to float + /// + /// + /// + public static float ConvertToFloat(this uint input) + { + float output; + output = S7.Net.Types.Real.FromByteArray(S7.Net.Types.DWord.ToByteArray(input)); + return output; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Enums.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Enums.cs new file mode 100644 index 0000000..fc412d3 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Enums.cs @@ -0,0 +1,211 @@ +namespace S7.Net +{ + /// + /// Types of S7 cpu supported by the library + /// + public enum CpuType + { + /// + /// S7 200 cpu type + /// + S7200 = 0, + + /// + /// Siemens Logo 0BA8 + /// + Logo0BA8 = 1, + + /// + /// S7 200 Smart + /// + S7200Smart = 2, + + /// + /// S7 300 cpu type + /// + S7300 = 10, + + /// + /// S7 400 cpu type + /// + S7400 = 20, + + /// + /// S7 1200 cpu type + /// + S71200 = 30, + + /// + /// S7 1500 cpu type + /// + S71500 = 40, + } + + /// + /// Types of error code that can be set after a function is called + /// + public enum ErrorCode + { + /// + /// The function has been executed correctly + /// + NoError = 0, + + /// + /// Wrong type of CPU error + /// + WrongCPU_Type = 1, + + /// + /// Connection error + /// + ConnectionError = 2, + + /// + /// Ip address not available + /// + IPAddressNotAvailable, + + /// + /// Wrong format of the variable + /// + WrongVarFormat = 10, + + /// + /// Wrong number of received bytes + /// + WrongNumberReceivedBytes = 11, + + /// + /// Error on send data + /// + SendData = 20, + + /// + /// Error on read data + /// + ReadData = 30, + + /// + /// Error on write data + /// + WriteData = 50 + } + + /// + /// Types of memory area that can be read + /// + public enum DataType + { + /// + /// Input area memory + /// + Input = 129, + + /// + /// Output area memory + /// + Output = 130, + + /// + /// Merkers area memory (M0, M0.0, ...) + /// + Memory = 131, + + /// + /// DB area memory (DB1, DB2, ...) + /// + DataBlock = 132, + + /// + /// Timer area memory(T1, T2, ...) + /// + Timer = 29, + + /// + /// Counter area memory (C1, C2, ...) + /// + Counter = 28 + } + + /// + /// Types + /// + public enum VarType + { + /// + /// S7 Bit variable type (bool) + /// + Bit, + + /// + /// S7 Byte variable type (8 bits) + /// + Byte, + + /// + /// S7 Word variable type (16 bits, 2 bytes) + /// + Word, + + /// + /// S7 DWord variable type (32 bits, 4 bytes) + /// + DWord, + + /// + /// S7 Int variable type (16 bits, 2 bytes) + /// + Int, + + /// + /// DInt variable type (32 bits, 4 bytes) + /// + DInt, + + /// + /// Real variable type (32 bits, 4 bytes) + /// + Real, + + /// + /// LReal variable type (64 bits, 8 bytes) + /// + LReal, + + /// + /// Char Array / C-String variable type (variable) + /// + String, + + /// + /// S7 String variable type (variable) + /// + S7String, + + /// + /// S7 WString variable type (variable) + /// + S7WString, + + /// + /// Timer variable type + /// + Timer, + + /// + /// Counter variable type + /// + Counter, + + /// + /// DateTIme variable type + /// + DateTime, + + /// + /// DateTimeLong variable type + /// + DateTimeLong + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Helper/MemoryStreamExtension.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Helper/MemoryStreamExtension.cs new file mode 100644 index 0000000..dd10fbc --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Helper/MemoryStreamExtension.cs @@ -0,0 +1,18 @@ + +namespace S7.Net.Helper +{ + internal static class MemoryStreamExtension + { + /// + /// Helper function to write to whole content of the given byte array to a memory stream. + /// + /// Writes all bytes in value from 0 to value.Length to the memory stream. + /// + /// + /// + public static void WriteByteArray(this System.IO.MemoryStream stream, byte[] value) + { + stream.Write(value, 0, value.Length); + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Internal/TaskQueue.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Internal/TaskQueue.cs new file mode 100644 index 0000000..bd0cd6b --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Internal/TaskQueue.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace S7.Net.Internal +{ + internal class TaskQueue + { + private static readonly object Sentinel = new object(); + + private Task prev = Task.FromResult(Sentinel); + + public async Task Enqueue(Func> action) + { + var tcs = new TaskCompletionSource(); + await Interlocked.Exchange(ref prev, tcs.Task).ConfigureAwait(false); + + try + { + return await action.Invoke().ConfigureAwait(false); + } + finally + { + tcs.SetResult(Sentinel); + } + } + } +} \ No newline at end of file diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/InvalidDataException.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/InvalidDataException.cs new file mode 100644 index 0000000..560d764 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/InvalidDataException.cs @@ -0,0 +1,43 @@ +using System; + +namespace S7.Net +{ + #if NET_FULL + [Serializable] + #endif + public class InvalidDataException : Exception + { + public byte[] ReceivedData { get; } + public int ErrorIndex { get; } + public byte ExpectedValue { get; } + + public InvalidDataException(string message, byte[] receivedData, int errorIndex, byte expectedValue) + : base(FormatMessage(message, receivedData, errorIndex, expectedValue)) + { + ReceivedData = receivedData; + ErrorIndex = errorIndex; + ExpectedValue = expectedValue; + } + + #if NET_FULL + protected InvalidDataException(System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) + { + ReceivedData = (byte[]) info.GetValue(nameof(ReceivedData), typeof(byte[])); + ErrorIndex = info.GetInt32(nameof(ErrorIndex)); + ExpectedValue = info.GetByte(nameof(ExpectedValue)); + } + #endif + + private static string FormatMessage(string message, byte[] receivedData, int errorIndex, byte expectedValue) + { + if (errorIndex >= receivedData.Length) + throw new ArgumentOutOfRangeException(nameof(errorIndex), + $"{nameof(errorIndex)} {errorIndex} is outside the bounds of {nameof(receivedData)} with length {receivedData.Length}."); + + return $"{message} Invalid data received. Expected '{expectedValue}' at index {errorIndex}, " + + $"but received {receivedData[errorIndex]}. See the {nameof(ReceivedData)} property " + + "for the full message received."; + } + } +} \ No newline at end of file diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/PLC.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/PLC.cs new file mode 100644 index 0000000..35d038e --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/PLC.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using S7.Net.Internal; +using S7.Net.Protocol; +using S7.Net.Types; + + +namespace S7.Net +{ + /// + /// Creates an instance of S7.Net driver + /// + public partial class Plc : IDisposable + { + /// + /// The default port for the S7 protocol. + /// + public const int DefaultPort = 102; + + /// + /// The default timeout (in milliseconds) used for and . + /// + public const int DefaultTimeout = 10_000; + + private readonly TaskQueue queue = new TaskQueue(); + + //TCP connection to device + private TcpClient? tcpClient; + private NetworkStream? _stream; + + private int readTimeout = DefaultTimeout; // default no timeout + private int writeTimeout = DefaultTimeout; // default no timeout + + /// + /// IP address of the PLC + /// + public string IP { get; } + + /// + /// PORT Number of the PLC, default is 102 + /// + public int Port { get; } + + /// + /// The TSAP addresses used during the connection request. + /// + public TsapPair TsapPair { get; set; } + + /// + /// CPU type of the PLC + /// + public CpuType CPU { get; } + + /// + /// Rack of the PLC + /// + public Int16 Rack { get; } + + /// + /// Slot of the CPU of the PLC + /// + public Int16 Slot { get; } + + /// + /// Max PDU size this cpu supports + /// + public int MaxPDUSize { get; private set; } + + /// Gets or sets the amount of time that a read operation blocks waiting for data from PLC. + /// A that specifies the amount of time, in milliseconds, that will elapse before a read operation fails. The default value, , specifies that the read operation does not time out. + public int ReadTimeout + { + get => readTimeout; + set + { + readTimeout = value; + if (tcpClient != null) tcpClient.ReceiveTimeout = readTimeout; + } + } + + /// Gets or sets the amount of time that a write operation blocks waiting for data to PLC. + /// A that specifies the amount of time, in milliseconds, that will elapse before a write operation fails. The default value, , specifies that the write operation does not time out. + public int WriteTimeout + { + get => writeTimeout; + set + { + writeTimeout = value; + if (tcpClient != null) tcpClient.SendTimeout = writeTimeout; + } + } + + /// + /// Gets a value indicating whether a connection to the PLC has been established. + /// + /// + /// The property gets the connection state of the Client socket as + /// of the last I/O operation. When it returns false, the Client socket was either + /// never connected, or is no longer connected. + /// + /// + /// Because the property only reflects the state of the connection + /// as of the most recent operation, you should attempt to send or receive a message to + /// determine the current state. After the message send fails, this property no longer + /// returns true. Note that this behavior is by design. You cannot reliably test the + /// state of the connection because, in the time between the test and a send/receive, the + /// connection could have been lost. Your code should assume the socket is connected, and + /// gracefully handle failed transmissions. + /// + /// + public bool IsConnected => tcpClient?.Connected ?? false; + + /// + /// Creates a PLC object with all the parameters needed for connections. + /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0. + /// You need slot > 0 if you are connecting to external ethernet card (CP). + /// For S7-300 and S7-400 the default is rack = 0 and slot = 2. + /// + /// CpuType of the PLC (select from the enum) + /// Ip address of the PLC + /// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal + /// slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500. + /// If you use an external ethernet card, this must be set accordingly. + public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot) + : this(cpu, ip, DefaultPort, rack, slot) + { + } + + /// + /// Creates a PLC object with all the parameters needed for connections. + /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0. + /// You need slot > 0 if you are connecting to external ethernet card (CP). + /// For S7-300 and S7-400 the default is rack = 0 and slot = 2. + /// + /// CpuType of the PLC (select from the enum) + /// Ip address of the PLC + /// Port number used for the connection, default 102. + /// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal + /// slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500. + /// If you use an external ethernet card, this must be set accordingly. + public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot) + : this(ip, port, TsapPair.GetDefaultTsapPair(cpu, rack, slot)) + { + if (!Enum.IsDefined(typeof(CpuType), cpu)) + throw new ArgumentException( + $"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", + nameof(cpu)); + + CPU = cpu; + Rack = rack; + Slot = slot; + } + + /// + /// Creates a PLC object with all the parameters needed for connections. + /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0. + /// You need slot > 0 if you are connecting to external ethernet card (CP). + /// For S7-300 and S7-400 the default is rack = 0 and slot = 2. + /// + /// Ip address of the PLC + /// The TSAP addresses used for the connection request. + public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair) + { + } + + /// + /// Creates a PLC object with all the parameters needed for connections. Use this constructor + /// if you want to manually override the TSAP addresses used during the connection request. + /// + /// Ip address of the PLC + /// Port number used for the connection, default 102. + /// The TSAP addresses used for the connection request. + public Plc(string ip, int port, TsapPair tsapPair) + { + if (string.IsNullOrEmpty(ip)) + throw new ArgumentException("IP address must valid.", nameof(ip)); + + IP = ip; + Port = port; + MaxPDUSize = 240; + TsapPair = tsapPair; + } + + /// + /// Close connection to PLC + /// + public void Close() + { + if (tcpClient != null) + { + if (tcpClient.Connected) tcpClient.Close(); + tcpClient = null; // Can not reuse TcpClient once connection gets closed. + } + } + + private void AssertPduSizeForRead(ICollection dataItems) + { + // send request limit: 19 bytes of header data, 12 bytes of parameter data for each dataItem + var requiredRequestSize = 19 + dataItems.Count * 12; + if (requiredRequestSize > MaxPDUSize) throw new Exception($"Too many vars requested for read. Request size ({requiredRequestSize}) is larger than protocol limit ({MaxPDUSize})."); + + // response limit: 14 bytes of header data, 4 bytes of result data for each dataItem and the actual data + var requiredResponseSize = GetDataLength(dataItems) + dataItems.Count * 4 + 14; + if (requiredResponseSize > MaxPDUSize) throw new Exception($"Too much data requested for read. Response size ({requiredResponseSize}) is larger than protocol limit ({MaxPDUSize})."); + } + + private void AssertPduSizeForWrite(ICollection dataItems) + { + // 12 bytes of header data, 18 bytes of parameter data for each dataItem + if (dataItems.Count * 18 + 12 > MaxPDUSize) throw new Exception("Too many vars supplied for write"); + + // 12 bytes of header data, 16 bytes of data for each dataItem and the actual data + if (GetDataLength(dataItems) + dataItems.Count * 16 + 12 > MaxPDUSize) + throw new Exception("Too much data supplied for write"); + } + + private void ConfigureConnection() + { + if (tcpClient == null) + { + return; + } + + tcpClient.ReceiveTimeout = ReadTimeout; + tcpClient.SendTimeout = WriteTimeout; + } + + private int GetDataLength(IEnumerable dataItems) + { + // Odd length variables are 0-padded + return dataItems.Select(di => VarTypeToByteLength(di.VarType, di.Count)) + .Sum(len => (len & 1) == 1 ? len + 1 : len); + } + + private static void AssertReadResponse(byte[] s7Data, int dataLength) + { + var expectedLength = dataLength + 18; + + PlcException NotEnoughBytes() => + new PlcException(ErrorCode.WrongNumberReceivedBytes, + $"Received {s7Data.Length} bytes: '{BitConverter.ToString(s7Data)}', expected {expectedLength} bytes.") + ; + + if (s7Data == null) + throw new PlcException(ErrorCode.WrongNumberReceivedBytes, "No s7Data received."); + + if (s7Data.Length < 15) throw NotEnoughBytes(); + + ValidateResponseCode((ReadWriteErrorCode)s7Data[14]); + + if (s7Data.Length < expectedLength) throw NotEnoughBytes(); + } + + internal static void ValidateResponseCode(ReadWriteErrorCode statusCode) + { + switch (statusCode) + { + case ReadWriteErrorCode.ObjectDoesNotExist: + throw new Exception("Received error from PLC: Object does not exist."); + case ReadWriteErrorCode.DataTypeInconsistent: + throw new Exception("Received error from PLC: Data type inconsistent."); + case ReadWriteErrorCode.DataTypeNotSupported: + throw new Exception("Received error from PLC: Data type not supported."); + case ReadWriteErrorCode.AccessingObjectNotAllowed: + throw new Exception("Received error from PLC: Accessing object not allowed."); + case ReadWriteErrorCode.AddressOutOfRange: + throw new Exception("Received error from PLC: Address out of range."); + case ReadWriteErrorCode.HardwareFault: + throw new Exception("Received error from PLC: Hardware fault."); + case ReadWriteErrorCode.Success: + break; + default: + throw new Exception( $"Invalid response from PLC: statusCode={(byte)statusCode}."); + } + } + + private Stream GetStreamIfAvailable() + { + if (_stream == null) + { + throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected"); + } + + return _stream; + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + /// + /// Dispose Plc Object + /// + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + Close(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + disposedValue = true; + } + } + + // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~Plc() { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + // This code added to correctly implement the disposable pattern. + void IDisposable.Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // TODO: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + #endregion + + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/PLCAddress.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/PLCAddress.cs new file mode 100644 index 0000000..9aea1b0 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/PLCAddress.cs @@ -0,0 +1,207 @@ +namespace S7.Net +{ + internal class PLCAddress + { + private DataType dataType; + private int dbNumber; + private int startByte; + private int bitNumber; + private VarType varType; + + public DataType DataType + { + get => dataType; + set => dataType = value; + } + + public int DbNumber + { + get => dbNumber; + set => dbNumber = value; + } + + public int StartByte + { + get => startByte; + set => startByte = value; + } + + public int BitNumber + { + get => bitNumber; + set => bitNumber = value; + } + + public VarType VarType + { + get => varType; + set => varType = value; + } + + public PLCAddress(string address) + { + Parse(address, out dataType, out dbNumber, out varType, out startByte, out bitNumber); + } + + public static void Parse(string input, out DataType dataType, out int dbNumber, out VarType varType, out int address, out int bitNumber) + { + bitNumber = -1; + dbNumber = 0; + + switch (input.Substring(0, 2)) + { + case "DB": + string[] strings = input.Split(new char[] { '.' }); + if (strings.Length < 2) + throw new InvalidAddressException("To few periods for DB address"); + + dataType = DataType.DataBlock; + dbNumber = int.Parse(strings[0].Substring(2)); + address = int.Parse(strings[1].Substring(3)); + + string dbType = strings[1].Substring(0, 3); + switch (dbType) + { + case "DBB": + varType = VarType.Byte; + return; + case "DBW": + varType = VarType.Word; + return; + case "DBD": + varType = VarType.DWord; + return; + case "DBX": + bitNumber = int.Parse(strings[2]); + if (bitNumber > 7) + throw new InvalidAddressException("Bit can only be 0-7"); + varType = VarType.Bit; + return; + default: + throw new InvalidAddressException(); + } + case "IB": + case "EB": + // Input byte + dataType = DataType.Input; + dbNumber = 0; + address = int.Parse(input.Substring(2)); + varType = VarType.Byte; + return; + case "IW": + case "EW": + // Input word + dataType = DataType.Input; + dbNumber = 0; + address = int.Parse(input.Substring(2)); + varType = VarType.Word; + return; + case "ID": + case "ED": + // Input double-word + dataType = DataType.Input; + dbNumber = 0; + address = int.Parse(input.Substring(2)); + varType = VarType.DWord; + return; + case "QB": + case "AB": + case "OB": + // Output byte + dataType = DataType.Output; + dbNumber = 0; + address = int.Parse(input.Substring(2)); + varType = VarType.Byte; + return; + case "QW": + case "AW": + case "OW": + // Output word + dataType = DataType.Output; + dbNumber = 0; + address = int.Parse(input.Substring(2)); + varType = VarType.Word; + return; + case "QD": + case "AD": + case "OD": + // Output double-word + dataType = DataType.Output; + dbNumber = 0; + address = int.Parse(input.Substring(2)); + varType = VarType.DWord; + return; + case "MB": + // Memory byte + dataType = DataType.Memory; + dbNumber = 0; + address = int.Parse(input.Substring(2)); + varType = VarType.Byte; + return; + case "MW": + // Memory word + dataType = DataType.Memory; + dbNumber = 0; + address = int.Parse(input.Substring(2)); + varType = VarType.Word; + return; + case "MD": + // Memory double-word + dataType = DataType.Memory; + dbNumber = 0; + address = int.Parse(input.Substring(2)); + varType = VarType.DWord; + return; + default: + switch (input.Substring(0, 1)) + { + case "E": + case "I": + // Input + dataType = DataType.Input; + varType = VarType.Bit; + break; + case "Q": + case "A": + case "O": + // Output + dataType = DataType.Output; + varType = VarType.Bit; + break; + case "M": + // Memory + dataType = DataType.Memory; + varType = VarType.Bit; + break; + case "T": + // Timer + dataType = DataType.Timer; + dbNumber = 0; + address = int.Parse(input.Substring(1)); + varType = VarType.Timer; + return; + case "Z": + case "C": + // Counter + dataType = DataType.Counter; + dbNumber = 0; + address = int.Parse(input.Substring(1)); + varType = VarType.Counter; + return; + default: + throw new InvalidAddressException(string.Format("{0} is not a valid address", input.Substring(0, 1))); + } + + string txt2 = input.Substring(1); + if (txt2.IndexOf(".") == -1) + throw new InvalidAddressException("To few periods for DB address"); + + address = int.Parse(txt2.Substring(0, txt2.IndexOf("."))); + bitNumber = int.Parse(txt2.Substring(txt2.IndexOf(".") + 1)); + if (bitNumber > 7) + throw new InvalidAddressException("Bit can only be 0-7"); + return; + } + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/PLCExceptions.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/PLCExceptions.cs new file mode 100644 index 0000000..2b1034d --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/PLCExceptions.cs @@ -0,0 +1,113 @@ +using System; +#if NET_FULL +using System.Runtime.Serialization; +#endif + + +namespace S7.Net +{ + internal class WrongNumberOfBytesException : Exception + { + public WrongNumberOfBytesException() : base() + { + } + + public WrongNumberOfBytesException(string message) : base(message) + { + } + + public WrongNumberOfBytesException(string message, Exception innerException) : base(message, innerException) + { + } + + #if NET_FULL + protected WrongNumberOfBytesException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + #endif + } + + internal class InvalidAddressException : Exception + { + public InvalidAddressException() : base () + { + } + + public InvalidAddressException(string message) : base(message) + { + } + + public InvalidAddressException(string message, Exception innerException) : base(message, innerException) + { + } + + #if NET_FULL + protected InvalidAddressException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + #endif + } + + internal class InvalidVariableTypeException : Exception + { + public InvalidVariableTypeException() : base() + { + } + + public InvalidVariableTypeException(string message) : base(message) + { + } + + public InvalidVariableTypeException(string message, Exception innerException) : base(message, innerException) + { + } + + #if NET_FULL + protected InvalidVariableTypeException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + #endif + } + + internal class TPKTInvalidException : Exception + { + public TPKTInvalidException() : base() + { + } + + public TPKTInvalidException(string message) : base(message) + { + } + + public TPKTInvalidException(string message, Exception innerException) : base(message, innerException) + { + } + + #if NET_FULL + protected TPKTInvalidException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + #endif + } + + internal class TPDUInvalidException : Exception + { + public TPDUInvalidException() : base() + { + } + + public TPDUInvalidException(string message) : base(message) + { + } + + public TPDUInvalidException(string message, Exception innerException) : base(message, innerException) + { + } + +#if NET_FULL + protected TPDUInvalidException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } +#endif + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/PLCHelpers.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/PLCHelpers.cs new file mode 100644 index 0000000..7715922 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/PLCHelpers.cs @@ -0,0 +1,266 @@ +using S7.Net.Helper; +using S7.Net.Protocol.S7; +using S7.Net.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using DateTime = S7.Net.Types.DateTime; + +namespace S7.Net +{ + public partial class Plc + { + /// + /// Creates the header to read bytes from the PLC + /// + /// + /// + private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1) + { + //header size = 19 bytes + stream.WriteByteArray(new byte[] { 0x03, 0x00 }); + //complete package size + stream.WriteByteArray(Types.Int.ToByteArray((short)(19 + (12 * amount)))); + stream.WriteByteArray(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 }); + //data part size + stream.WriteByteArray(Types.Word.ToByteArray((ushort)(2 + (amount * 12)))); + stream.WriteByteArray(new byte[] { 0x00, 0x00, 0x04 }); + //amount of requests + stream.WriteByte((byte)amount); + } + + /// + /// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType), + /// the address of the memory, the address of the byte and the bytes count. + /// + /// MemoryType (DB, Timer, Counter, etc.) + /// Address of the memory to be read + /// Start address of the byte + /// Number of bytes to be read + /// + private static void BuildReadDataRequestPackage(System.IO.MemoryStream stream, DataType dataType, int db, int startByteAdr, int count = 1) + { + //single data req = 12 + stream.WriteByteArray(new byte[] { 0x12, 0x0a, 0x10 }); + switch (dataType) + { + case DataType.Timer: + case DataType.Counter: + stream.WriteByte((byte)dataType); + break; + default: + stream.WriteByte(0x02); + break; + } + + stream.WriteByteArray(Word.ToByteArray((ushort)(count))); + stream.WriteByteArray(Word.ToByteArray((ushort)(db))); + stream.WriteByte((byte)dataType); + var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191 + stream.WriteByte((byte)overflow); + switch (dataType) + { + case DataType.Timer: + case DataType.Counter: + stream.WriteByteArray(Types.Word.ToByteArray((ushort)(startByteAdr))); + break; + default: + stream.WriteByteArray(Types.Word.ToByteArray((ushort)((startByteAdr) * 8))); + break; + } + } + + /// + /// Given a S7 variable type (Bool, Word, DWord, etc.), it converts the bytes in the appropriate C# format. + /// + /// + /// + /// + /// + /// + private object? ParseBytes(VarType varType, byte[] bytes, int varCount, byte bitAdr = 0) + { + if (bytes == null || bytes.Length == 0) + return null; + + switch (varType) + { + case VarType.Byte: + if (varCount == 1) + return bytes[0]; + else + return bytes; + case VarType.Word: + if (varCount == 1) + return Word.FromByteArray(bytes); + else + return Word.ToArray(bytes); + case VarType.Int: + if (varCount == 1) + return Int.FromByteArray(bytes); + else + return Int.ToArray(bytes); + case VarType.DWord: + if (varCount == 1) + return DWord.FromByteArray(bytes); + else + return DWord.ToArray(bytes); + case VarType.DInt: + if (varCount == 1) + return DInt.FromByteArray(bytes); + else + return DInt.ToArray(bytes); + case VarType.Real: + if (varCount == 1) + return Types.Real.FromByteArray(bytes); + else + return Types.Real.ToArray(bytes); + case VarType.LReal: + if (varCount == 1) + return Types.LReal.FromByteArray(bytes); + else + return Types.LReal.ToArray(bytes); + + case VarType.String: + return Types.String.FromByteArray(bytes); + case VarType.S7String: + return S7String.FromByteArray(bytes); + case VarType.S7WString: + return S7WString.FromByteArray(bytes); + + case VarType.Timer: + if (varCount == 1) + return Timer.FromByteArray(bytes); + else + return Timer.ToArray(bytes); + case VarType.Counter: + if (varCount == 1) + return Counter.FromByteArray(bytes); + else + return Counter.ToArray(bytes); + case VarType.Bit: + if (varCount == 1) + { + if (bitAdr > 7) + return null; + else + return Bit.FromByte(bytes[0], bitAdr); + } + else + { + return Bit.ToBitArray(bytes, varCount); + } + case VarType.DateTime: + if (varCount == 1) + { + return DateTime.FromByteArray(bytes); + } + else + { + return DateTime.ToArray(bytes); + } + case VarType.DateTimeLong: + if (varCount == 1) + { + return DateTimeLong.FromByteArray(bytes); + } + else + { + return DateTimeLong.ToArray(bytes); + } + default: + return null; + } + } + + /// + /// Given a S7 (Bool, Word, DWord, etc.), it returns how many bytes to read. + /// + /// + /// + /// Byte lenght of variable + internal static int VarTypeToByteLength(VarType varType, int varCount = 1) + { + switch (varType) + { + case VarType.Bit: + return (varCount + 7) / 8; + case VarType.Byte: + return (varCount < 1) ? 1 : varCount; + case VarType.String: + return varCount; + case VarType.S7String: + return ((varCount + 2) & 1) == 1 ? (varCount + 3) : (varCount + 2); + case VarType.S7WString: + return (varCount * 2) + 4; + case VarType.Word: + case VarType.Timer: + case VarType.Int: + case VarType.Counter: + return varCount * 2; + case VarType.DWord: + case VarType.DInt: + case VarType.Real: + return varCount * 4; + case VarType.LReal: + case VarType.DateTime: + return varCount * 8; + case VarType.DateTimeLong: + return varCount * 12; + default: + return 0; + } + } + + private byte[] GetS7ConnectionSetup() + { + return new byte[] { 3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3, + 3, 192 // Use 960 PDU size + }; + } + + private void ParseDataIntoDataItems(byte[] s7data, List dataItems) + { + int offset = 14; + foreach (var dataItem in dataItems) + { + // check for Return Code = Success + if (s7data[offset] != 0xff) + throw new PlcException(ErrorCode.WrongNumberReceivedBytes); + + // to Data bytes + offset += 4; + + int byteCnt = VarTypeToByteLength(dataItem.VarType, dataItem.Count); + dataItem.Value = ParseBytes( + dataItem.VarType, + s7data.Skip(offset).Take(byteCnt).ToArray(), + dataItem.Count, + dataItem.BitAdr + ); + + // next Item + offset += byteCnt; + + // Always align to even offset + if (offset % 2 != 0) + offset++; + } + } + + private static byte[] BuildReadRequestPackage(IList dataItems) + { + int packageSize = 19 + (dataItems.Count * 12); + var package = new System.IO.MemoryStream(packageSize); + + BuildHeaderPackage(package, dataItems.Count); + + foreach (var dataItem in dataItems) + { + BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAddress, dataItem.ByteLength); + } + + return package.ToArray(); + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/PlcAsynchronous.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/PlcAsynchronous.cs new file mode 100644 index 0000000..a58f629 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/PlcAsynchronous.cs @@ -0,0 +1,566 @@ +using S7.Net.Types; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Threading.Tasks; +using S7.Net.Protocol; +using System.Threading; +using S7.Net.Protocol.S7; + +namespace S7.Net +{ + /// + /// Creates an instance of S7.Net driver + /// + public partial class Plc + { + /// + /// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup. + /// + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that the cancellation will not affect opening the socket in any way and only affects data transfers for configuring the connection after the socket connection is successfully established. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// A task that represents the asynchronous open operation. + public async Task OpenAsync(CancellationToken cancellationToken = default) + { + var stream = await ConnectAsync().ConfigureAwait(false); + try + { + await queue.Enqueue(async () => + { + cancellationToken.ThrowIfCancellationRequested(); + await EstablishConnection(stream, cancellationToken).ConfigureAwait(false); + _stream = stream; + + return default(object); + }).ConfigureAwait(false); + } + catch (Exception) + { + stream.Dispose(); + throw; + } + } + + private async Task ConnectAsync() + { + tcpClient = new TcpClient(); + ConfigureConnection(); + await tcpClient.ConnectAsync(IP, Port).ConfigureAwait(false); + return tcpClient.GetStream(); + } + + private async Task EstablishConnection(Stream stream, CancellationToken cancellationToken) + { + await RequestConnection(stream, cancellationToken).ConfigureAwait(false); + await SetupConnection(stream, cancellationToken).ConfigureAwait(false); + } + + private async Task RequestConnection(Stream stream, CancellationToken cancellationToken) + { + var requestData = ConnectionRequest.GetCOTPConnectionRequest(TsapPair); + var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false); + + if (response.PDUType != COTP.PduType.ConnectionConfirmed) + { + throw new InvalidDataException("Connection request was denied", response.TPkt.Data, 1, 0x0d); + } + } + + private async Task SetupConnection(Stream stream, CancellationToken cancellationToken) + { + var setupData = GetS7ConnectionSetup(); + + var s7data = await NoLockRequestTsduAsync(stream, setupData, 0, setupData.Length, cancellationToken) + .ConfigureAwait(false); + + if (s7data.Length < 2) + throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup"); + + //Check for S7 Ack Data + if (s7data[1] != 0x03) + throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03); + + if (s7data.Length < 20) + throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup"); + + // TODO: check if this should not rather be UInt16. + MaxPDUSize = s7data[18] * 256 + s7data[19]; + } + + + /// + /// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests. + /// If the read was not successful, check LastErrorCode or LastErrorString. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// Byte count, if you want to read 120 bytes, set this to 120. + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// Returns the bytes in an array + public async Task ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken cancellationToken = default) + { + var resultBytes = new byte[count]; + int index = 0; + while (count > 0) + { + //This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0. + var maxToRead = Math.Min(count, MaxPDUSize - 18); + await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, resultBytes, index, maxToRead, cancellationToken).ConfigureAwait(false); + count -= maxToRead; + index += maxToRead; + } + return resultBytes; + } + + /// + /// Read and decode a certain number of bytes of the "VarType" provided. + /// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc). + /// If the read was not successful, check LastErrorCode or LastErrorString. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// Type of the variable/s that you are reading + /// Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter. + /// + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + public async Task ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0, CancellationToken cancellationToken = default) + { + int cntBytes = VarTypeToByteLength(varType, varCount); + byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes, cancellationToken).ConfigureAwait(false); + return ParseBytes(varType, bytes, varCount, bitAdr); + } + + /// + /// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. + /// If the read was not successful, check LastErrorCode or LastErrorString. + /// + /// Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// Returns an object that contains the value. This object must be cast accordingly. + public async Task ReadAsync(string variable, CancellationToken cancellationToken = default) + { + var adr = new PLCAddress(variable); + return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber, cancellationToken).ConfigureAwait(false); + } + + /// + /// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and return an object that can be casted to the struct. + /// + /// Type of the struct to be readed (es.: TypeOf(MyStruct)). + /// Address of the DB. + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// Returns a struct that must be cast. + public async Task ReadStructAsync(Type structType, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) + { + int numBytes = Types.Struct.GetStructSize(structType); + // now read the package + var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false); + + // and decode it + return Types.Struct.FromBytes(structType, resultBytes); + } + + /// + /// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and returns the struct or null if nothing was read. + /// + /// The struct type + /// Address of the DB. + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// Returns a nulable struct. If nothing was read null will be returned. + public async Task ReadStructAsync(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : struct + { + return await ReadStructAsync(typeof(T), db, startByteAdr, cancellationToken).ConfigureAwait(false) as T?; + } + + /// + /// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC. + /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. + /// + /// Instance of the class that will store the values + /// Index of the DB; es.: 1 is for DB1 + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// The number of read bytes + public async Task> ReadClassAsync(object sourceClass, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) + { + int numBytes = (int)Class.GetClassSize(sourceClass); + if (numBytes <= 0) + { + throw new Exception("The size of the class is less than 1 byte and therefore cannot be read"); + } + + // now read the package + var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false); + // and decode it + Class.FromBytes(sourceClass, resultBytes); + + return new Tuple(resultBytes.Length, sourceClass); + } + + /// + /// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC. + /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic + /// type, the class needs a default constructor. + /// + /// The class that will be instantiated. Requires a default constructor + /// Index of the DB; es.: 1 is for DB1 + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// An instance of the class with the values read from the PLC. If no data has been read, null will be returned + public async Task ReadClassAsync(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class + { + return await ReadClassAsync(() => Activator.CreateInstance(), db, startByteAdr, cancellationToken).ConfigureAwait(false); + } + + /// + /// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC. + /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. + /// + /// The class that will be instantiated + /// Function to instantiate the class + /// Index of the DB; es.: 1 is for DB1 + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// An instance of the class with the values read from the PLC. If no data has been read, null will be returned + public async Task ReadClassAsync(Func classFactory, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class + { + var instance = classFactory(); + var res = await ReadClassAsync(instance, db, startByteAdr, cancellationToken).ConfigureAwait(false); + int readBytes = res.Item1; + if (readBytes <= 0) + { + return null; + } + + return (T)res.Item2; + } + + /// + /// Reads multiple vars in a single request. + /// You have to create and pass a list of DataItems and you obtain in response the same list with the values. + /// Values are stored in the property "Value" of the dataItem and are already converted. + /// If you don't want the conversion, just create a dataItem of bytes. + /// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction). + /// + /// List of dataitems that contains the list of variables that must be read. + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + public async Task> ReadMultipleVarsAsync(List dataItems, CancellationToken cancellationToken = default) + { + //Snap7 seems to choke on PDU sizes above 256 even if snap7 + //replies with bigger PDU size in connection setup. + AssertPduSizeForRead(dataItems); + + try + { + var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList()); + var s7data = await RequestTsduAsync(dataToSend, cancellationToken); + + ValidateResponseCode((ReadWriteErrorCode)s7data[14]); + + ParseDataIntoDataItems(s7data, dataItems); + } + catch (SocketException socketException) + { + throw new PlcException(ErrorCode.ReadData, socketException); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception exc) + { + throw new PlcException(ErrorCode.ReadData, exc); + } + return dataItems; + } + + + /// + /// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests. + /// If the write was not successful, check LastErrorCode or LastErrorString. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to write DB1.DBW200, this is 200. + /// Bytes to write. If more than 200, multiple requests will be made. + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// A task that represents the asynchronous write operation. + public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken = default) + { + int localIndex = 0; + int count = value.Length; + while (count > 0) + { + var maxToWrite = (int)Math.Min(count, MaxPDUSize - 35); + await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite, cancellationToken).ConfigureAwait(false); + count -= maxToWrite; + localIndex += maxToWrite; + } + } + + /// + /// Write a single bit from a DB with the specified index. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to write DB1.DBW200, this is 200. + /// The address of the bit. (0-7) + /// Bytes to write. If more than 200, multiple requests will be made. + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// A task that represents the asynchronous write operation. + public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool value, CancellationToken cancellationToken = default) + { + if (bitAdr < 0 || bitAdr > 7) + throw new InvalidAddressException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr)); + + await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value, cancellationToken).ConfigureAwait(false); + } + + /// + /// Write a single bit from a DB with the specified index. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to write DB1.DBW200, this is 200. + /// The address of the bit. (0-7) + /// Bytes to write. If more than 200, multiple requests will be made. + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// A task that represents the asynchronous write operation. + public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, int value, CancellationToken cancellationToken = default) + { + if (value < 0 || value > 1) + throw new ArgumentException("Value must be 0 or 1", nameof(value)); + + await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1, cancellationToken).ConfigureAwait(false); + } + + /// + /// Takes in input an object and tries to parse it to an array of values. This can be used to write many data, all of the same type. + /// You must specify the memory area type, memory are address, byte start address and bytes count. + /// If the read was not successful, check LastErrorCode or LastErrorString. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion. + /// The address of the bit. (0-7) + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// A task that represents the asynchronous write operation. + public async Task WriteAsync(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1, CancellationToken cancellationToken = default) + { + if (bitAdr != -1) + { + //Must be writing a bit value as bitAdr is specified + if (value is bool boolean) + { + await WriteBitAsync(dataType, db, startByteAdr, bitAdr, boolean, cancellationToken).ConfigureAwait(false); + } + else if (value is int intValue) + { + if (intValue < 0 || intValue > 7) + throw new ArgumentOutOfRangeException( + string.Format( + "Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", + bitAdr), nameof(bitAdr)); + + await WriteBitAsync(dataType, db, startByteAdr, bitAdr, intValue == 1, cancellationToken).ConfigureAwait(false); + } + else throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value)); + } + else await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value), cancellationToken).ConfigureAwait(false); + } + + /// + /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. + /// If the write was not successful, check or . + /// + /// Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. + /// Value to be written to the PLC + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// A task that represents the asynchronous write operation. + public async Task WriteAsync(string variable, object value, CancellationToken cancellationToken = default) + { + var adr = new PLCAddress(variable); + await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber, cancellationToken).ConfigureAwait(false); + } + + /// + /// Writes a C# struct to a DB in the PLC + /// + /// The struct to be written + /// Db address + /// Start bytes on the PLC + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// A task that represents the asynchronous write operation. + public async Task WriteStructAsync(object structValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) + { + var bytes = Struct.ToBytes(structValue).ToList(); + await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray(), cancellationToken).ConfigureAwait(false); + } + + /// + /// Writes a C# class to a DB in the PLC + /// + /// The class to be written + /// Db address + /// Start bytes on the PLC + /// The token to monitor for cancellation requests. The default value is None. + /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases. + /// A task that represents the asynchronous write operation. + public async Task WriteClassAsync(object classValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) + { + byte[] bytes = new byte[(int)Class.GetClassSize(classValue)]; + Types.Class.ToBytes(classValue, bytes); + await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken).ConfigureAwait(false); + } + + private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, count) }); + + var s7data = await RequestTsduAsync(dataToSend, cancellationToken); + AssertReadResponse(s7data, count); + + Array.Copy(s7data, 18, buffer, offset, count); + } + + /// + /// Write DataItem(s) to the PLC. Throws an exception if the response is invalid + /// or when the PLC reports errors for item(s) written. + /// + /// The DataItem(s) to write to the PLC. + /// Task that completes when response from PLC is parsed. + public async Task WriteAsync(params DataItem[] dataItems) + { + AssertPduSizeForWrite(dataItems); + + var message = new ByteArray(); + var length = S7WriteMultiple.CreateRequest(message, dataItems); + + var response = await RequestTsduAsync(message.Array, 0, length).ConfigureAwait(false); + + S7WriteMultiple.ParseResponse(response, response.Length, dataItems); + } + + /// + /// Writes up to 200 bytes to the PLC. You must specify the memory area type, memory are address, byte start address and bytes count. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion. + /// A task that represents the asynchronous write operation. + private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count, CancellationToken cancellationToken) + { + try + { + var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count); + var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false); + + ValidateResponseCode((ReadWriteErrorCode)s7data[14]); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception exc) + { + throw new PlcException(ErrorCode.WriteData, exc); + } + } + + private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken) + { + try + { + var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr); + var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false); + + ValidateResponseCode((ReadWriteErrorCode)s7data[14]); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception exc) + { + throw new PlcException(ErrorCode.WriteData, exc); + } + } + + private Task RequestTsduAsync(byte[] requestData, CancellationToken cancellationToken = default) => + RequestTsduAsync(requestData, 0, requestData.Length, cancellationToken); + + private Task RequestTsduAsync(byte[] requestData, int offset, int length, CancellationToken cancellationToken = default) + { + var stream = GetStreamIfAvailable(); + + return queue.Enqueue(() => + NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken)); + } + + private async Task NoLockRequestTpduAsync(Stream stream, byte[] requestData, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + using var closeOnCancellation = cancellationToken.Register(Close); + await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false); + return await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + } + catch (Exception exc) + { + if (exc is TPDUInvalidException || exc is TPKTInvalidException) + { + Close(); + } + + throw; + } + } + + private async Task NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + using var closeOnCancellation = cancellationToken.Register(Close); + await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false); + return await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + } + catch (Exception exc) + { + if (exc is TPDUInvalidException || exc is TPKTInvalidException) + { + Close(); + } + + throw; + } + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/PlcException.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/PlcException.cs new file mode 100644 index 0000000..772060b --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/PlcException.cs @@ -0,0 +1,39 @@ +using System; + +namespace S7.Net +{ + #if NET_FULL + [Serializable] + #endif + public class PlcException : Exception + { + public ErrorCode ErrorCode { get; } + + public PlcException(ErrorCode errorCode) : this(errorCode, $"PLC communication failed with error '{errorCode}'.") + { + } + + public PlcException(ErrorCode errorCode, Exception innerException) : this(errorCode, innerException.Message, + innerException) + { + } + + public PlcException(ErrorCode errorCode, string message) : base(message) + { + ErrorCode = errorCode; + } + + public PlcException(ErrorCode errorCode, string message, Exception inner) : base(message, inner) + { + ErrorCode = errorCode; + } + + #if NET_FULL + protected PlcException(System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) + { + ErrorCode = (ErrorCode) info.GetInt32(nameof(ErrorCode)); + } + #endif + } +} \ No newline at end of file diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/PlcSynchronous.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/PlcSynchronous.cs new file mode 100644 index 0000000..80e4738 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/PlcSynchronous.cs @@ -0,0 +1,475 @@ +using S7.Net.Types; +using System; +using System.IO; +using System.Collections.Generic; +using S7.Net.Protocol; +using S7.Net.Helper; + +//Implement synchronous methods here +namespace S7.Net +{ + public partial class Plc + { + /// + /// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup. + /// + public void Open() + { + try + { + OpenAsync().GetAwaiter().GetResult(); + } + catch (Exception exc) + { + throw new PlcException(ErrorCode.ConnectionError, + $"Couldn't establish the connection to {IP}.\nMessage: {exc.Message}", exc); + } + } + + + /// + /// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests. + /// If the read was not successful, check LastErrorCode or LastErrorString. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// Byte count, if you want to read 120 bytes, set this to 120. + /// Returns the bytes in an array + public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count) + { + var result = new byte[count]; + int index = 0; + while (count > 0) + { + //This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0. + var maxToRead = Math.Min(count, MaxPDUSize - 18); + ReadBytesWithSingleRequest(dataType, db, startByteAdr + index, result, index, maxToRead); + count -= maxToRead; + index += maxToRead; + } + return result; + } + + /// + /// Read and decode a certain number of bytes of the "VarType" provided. + /// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc). + /// If the read was not successful, check LastErrorCode or LastErrorString. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// Type of the variable/s that you are reading + /// Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter. + /// + public object? Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0) + { + int cntBytes = VarTypeToByteLength(varType, varCount); + byte[] bytes = ReadBytes(dataType, db, startByteAdr, cntBytes); + + return ParseBytes(varType, bytes, varCount, bitAdr); + } + + /// + /// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. + /// If the read was not successful, check LastErrorCode or LastErrorString. + /// + /// Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. + /// Returns an object that contains the value. This object must be cast accordingly. If no data has been read, null will be returned + public object? Read(string variable) + { + var adr = new PLCAddress(variable); + return Read(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber); + } + + /// + /// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and return an object that can be casted to the struct. + /// + /// Type of the struct to be readed (es.: TypeOf(MyStruct)). + /// Address of the DB. + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// Returns a struct that must be cast. If no data has been read, null will be returned + public object? ReadStruct(Type structType, int db, int startByteAdr = 0) + { + int numBytes = Struct.GetStructSize(structType); + // now read the package + var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes); + + // and decode it + return Struct.FromBytes(structType, resultBytes); + } + + /// + /// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and returns the struct or null if nothing was read. + /// + /// The struct type + /// Address of the DB. + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// Returns a nullable struct. If nothing was read null will be returned. + public T? ReadStruct(int db, int startByteAdr = 0) where T : struct + { + return ReadStruct(typeof(T), db, startByteAdr) as T?; + } + + + /// + /// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC. + /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. + /// + /// Instance of the class that will store the values + /// Index of the DB; es.: 1 is for DB1 + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// The number of read bytes + public int ReadClass(object sourceClass, int db, int startByteAdr = 0) + { + int numBytes = (int)Class.GetClassSize(sourceClass); + if (numBytes <= 0) + { + throw new Exception("The size of the class is less than 1 byte and therefore cannot be read"); + } + + // now read the package + var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes); + // and decode it + Class.FromBytes(sourceClass, resultBytes); + return resultBytes.Length; + } + + /// + /// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC. + /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic + /// type, the class needs a default constructor. + /// + /// The class that will be instantiated. Requires a default constructor + /// Index of the DB; es.: 1 is for DB1 + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// An instance of the class with the values read from the PLC. If no data has been read, null will be returned + public T? ReadClass(int db, int startByteAdr = 0) where T : class + { + return ReadClass(() => Activator.CreateInstance(), db, startByteAdr); + } + + /// + /// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC. + /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. + /// + /// The class that will be instantiated + /// Function to instantiate the class + /// Index of the DB; es.: 1 is for DB1 + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// An instance of the class with the values read from the PLC. If no data has been read, null will be returned + public T? ReadClass(Func classFactory, int db, int startByteAdr = 0) where T : class + { + var instance = classFactory(); + int readBytes = ReadClass(instance, db, startByteAdr); + if (readBytes <= 0) + { + return null; + } + return instance; + } + + /// + /// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests. + /// If the write was not successful, check LastErrorCode or LastErrorString. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to write DB1.DBW200, this is 200. + /// Bytes to write. If more than 200, multiple requests will be made. + public void WriteBytes(DataType dataType, int db, int startByteAdr, byte[] value) + { + int localIndex = 0; + int count = value.Length; + while (count > 0) + { + //TODO: Figure out how to use MaxPDUSize here + //Snap7 seems to choke on PDU sizes above 256 even if snap7 + //replies with bigger PDU size in connection setup. + var maxToWrite = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480 + WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite); + count -= maxToWrite; + localIndex += maxToWrite; + } + } + + /// + /// Write a single bit from a DB with the specified index. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to write DB1.DBW200, this is 200. + /// The address of the bit. (0-7) + /// Bytes to write. If more than 200, multiple requests will be made. + public void WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, bool value) + { + if (bitAdr < 0 || bitAdr > 7) + throw new InvalidAddressException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr)); + + WriteBitWithASingleRequest(dataType, db, startByteAdr, bitAdr, value); + } + + /// + /// Write a single bit to a DB with the specified index. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to write DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to write DB1.DBW200, this is 200. + /// The address of the bit. (0-7) + /// Value to write (0 or 1). + public void WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, int value) + { + if (value < 0 || value > 1) + throw new ArgumentException("Value must be 0 or 1", nameof(value)); + + WriteBit(dataType, db, startByteAdr, bitAdr, value == 1); + } + + /// + /// Takes in input an object and tries to parse it to an array of values. This can be used to write many data, all of the same type. + /// You must specify the memory area type, memory are address, byte start address and bytes count. + /// If the read was not successful, check LastErrorCode or LastErrorString. + /// + /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. + /// Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc. + /// Start byte address. If you want to read DB1.DBW200, this is 200. + /// Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion. + /// The address of the bit. (0-7) + public void Write(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1) + { + if (bitAdr != -1) + { + //Must be writing a bit value as bitAdr is specified + if (value is bool boolean) + { + WriteBit(dataType, db, startByteAdr, bitAdr, boolean); + } + else if (value is int intValue) + { + if (intValue < 0 || intValue > 7) + throw new ArgumentOutOfRangeException( + string.Format( + "Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", + bitAdr), nameof(bitAdr)); + + WriteBit(dataType, db, startByteAdr, bitAdr, intValue == 1); + } + else + throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value)); + } + else WriteBytes(dataType, db, startByteAdr, Serialization.SerializeValue(value)); + } + + /// + /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. + /// If the write was not successful, check or . + /// + /// Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. + /// Value to be written to the PLC + public void Write(string variable, object value) + { + var adr = new PLCAddress(variable); + Write(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber); + } + + /// + /// Writes a C# struct to a DB in the PLC + /// + /// The struct to be written + /// Db address + /// Start bytes on the PLC + public void WriteStruct(object structValue, int db, int startByteAdr = 0) + { + WriteStructAsync(structValue, db, startByteAdr).GetAwaiter().GetResult(); + } + + /// + /// Writes a C# class to a DB in the PLC + /// + /// The class to be written + /// Db address + /// Start bytes on the PLC + public void WriteClass(object classValue, int db, int startByteAdr = 0) + { + WriteClassAsync(classValue, db, startByteAdr).GetAwaiter().GetResult(); + } + + private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count) + { + try + { + // first create the header + int packageSize = 19 + 12; // 19 header + 12 for 1 request + var package = new System.IO.MemoryStream(packageSize); + BuildHeaderPackage(package); + // package.Add(0x02); // datenart + BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count); + + var dataToSend = package.ToArray(); + var s7data = RequestTsdu(dataToSend); + AssertReadResponse(s7data, count); + + Array.Copy(s7data, 18, buffer, offset, count); + } + catch (Exception exc) + { + throw new PlcException(ErrorCode.ReadData, exc); + } + } + + /// + /// Write DataItem(s) to the PLC. Throws an exception if the response is invalid + /// or when the PLC reports errors for item(s) written. + /// + /// The DataItem(s) to write to the PLC. + public void Write(params DataItem[] dataItems) + { + AssertPduSizeForWrite(dataItems); + + + var message = new ByteArray(); + var length = S7WriteMultiple.CreateRequest(message, dataItems); + var response = RequestTsdu(message.Array, 0, length); + + S7WriteMultiple.ParseResponse(response, response.Length, dataItems); + } + + private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count) + { + try + { + var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count); + var s7data = RequestTsdu(dataToSend); + + ValidateResponseCode((ReadWriteErrorCode)s7data[14]); + } + catch (Exception exc) + { + throw new PlcException(ErrorCode.WriteData, exc); + } + } + + private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count) + { + int varCount = count; + // first create the header + int packageSize = 35 + varCount; + var package = new MemoryStream(new byte[packageSize]); + + package.WriteByte(3); + package.WriteByte(0); + //complete package size + package.WriteByteArray(Int.ToByteArray((short)packageSize)); + package.WriteByteArray(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 }); + package.WriteByteArray(Word.ToByteArray((ushort)(varCount - 1))); + package.WriteByteArray(new byte[] { 0, 0x0e }); + package.WriteByteArray(Word.ToByteArray((ushort)(varCount + 4))); + package.WriteByteArray(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 }); + package.WriteByteArray(Word.ToByteArray((ushort)varCount)); + package.WriteByteArray(Word.ToByteArray((ushort)(db))); + package.WriteByte((byte)dataType); + var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191 + package.WriteByte((byte)overflow); + package.WriteByteArray(Word.ToByteArray((ushort)(startByteAdr * 8))); + package.WriteByteArray(new byte[] { 0, 4 }); + package.WriteByteArray(Word.ToByteArray((ushort)(varCount * 8))); + + // now join the header and the data + package.Write(value, dataOffset, count); + + return package.ToArray(); + } + + private byte[] BuildWriteBitPackage(DataType dataType, int db, int startByteAdr, bool bitValue, int bitAdr) + { + var value = new[] { bitValue ? (byte)1 : (byte)0 }; + int varCount = 1; + // first create the header + int packageSize = 35 + varCount; + var package = new MemoryStream(new byte[packageSize]); + + package.WriteByte(3); + package.WriteByte(0); + //complete package size + package.WriteByteArray(Int.ToByteArray((short)packageSize)); + package.WriteByteArray(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 }); + package.WriteByteArray(Word.ToByteArray((ushort)(varCount - 1))); + package.WriteByteArray(new byte[] { 0, 0x0e }); + package.WriteByteArray(Word.ToByteArray((ushort)(varCount + 4))); + package.WriteByteArray(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit + package.WriteByteArray(Word.ToByteArray((ushort)varCount)); + package.WriteByteArray(Word.ToByteArray((ushort)(db))); + package.WriteByte((byte)dataType); + var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191 + package.WriteByte((byte)overflow); + package.WriteByteArray(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr))); + package.WriteByteArray(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit + package.WriteByteArray(Word.ToByteArray((ushort)(varCount))); + + // now join the header and the data + package.WriteByteArray(value); + + return package.ToArray(); + } + + + private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue) + { + try + { + var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr); + var s7data = RequestTsdu(dataToSend); + + ValidateResponseCode((ReadWriteErrorCode)s7data[14]); + } + catch (Exception exc) + { + throw new PlcException(ErrorCode.WriteData, exc); + } + } + + /// + /// Reads multiple vars in a single request. + /// You have to create and pass a list of DataItems and you obtain in response the same list with the values. + /// Values are stored in the property "Value" of the dataItem and are already converted. + /// If you don't want the conversion, just create a dataItem of bytes. + /// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction). + /// + /// List of dataitems that contains the list of variables that must be read. + public void ReadMultipleVars(List dataItems) + { + AssertPduSizeForRead(dataItems); + + try + { + // first create the header + int packageSize = 19 + (dataItems.Count * 12); + var package = new System.IO.MemoryStream(packageSize); + BuildHeaderPackage(package, dataItems.Count); + // package.Add(0x02); // datenart + foreach (var dataItem in dataItems) + { + BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count)); + } + + var dataToSend = package.ToArray(); + var s7data = RequestTsdu(dataToSend); + + ValidateResponseCode((ReadWriteErrorCode)s7data[14]); + + ParseDataIntoDataItems(s7data, dataItems); + } + catch (Exception exc) + { + throw new PlcException(ErrorCode.ReadData, exc); + } + } + + private byte[] RequestTsdu(byte[] requestData) => RequestTsdu(requestData, 0, requestData.Length); + + private byte[] RequestTsdu(byte[] requestData, int offset, int length) + { + return RequestTsduAsync(requestData, offset, length).GetAwaiter().GetResult(); + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Properties/AssemblyInfo.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e8835a5 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("S7.Net.UnitTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d1032db55f60d64bf90ea1cc2247b5a8b9b6168a07bcd464a07ce2e425d027ff9409a64ba0e3f37718e14c50cf964d0d921e5ae8b8d74bd8a82431794f897cebf0ee668feb2ccd030153611b2808fcb7785c5e5136a98e0ec23de3c1ed385d2026c26e4bed5805ff9db7e0544f59b1f19d369d43403a624586795926e38c48d")] diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Properties/S7.Net.snk b/Plugins/Drivers/DriverSiemensS7/S7.Net/Properties/S7.Net.snk new file mode 100644 index 0000000..191a65d Binary files /dev/null and b/Plugins/Drivers/DriverSiemensS7/S7.Net/Properties/S7.Net.snk differ diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/ConnectionRequest.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/ConnectionRequest.cs new file mode 100644 index 0000000..9dbd396 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/ConnectionRequest.cs @@ -0,0 +1,28 @@ +namespace S7.Net.Protocol +{ + internal static class ConnectionRequest + { + public static byte[] GetCOTPConnectionRequest(TsapPair tsapPair) + { + byte[] bSend1 = { + 3, 0, 0, 22, //TPKT + 17, //COTP Header Length + 224, //Connect Request + 0, 0, //Destination Reference + 0, 46, //Source Reference + 0, //Flags + 193, //Parameter Code (src-tasp) + 2, //Parameter Length + tsapPair.Local.FirstByte, tsapPair.Local.SecondByte, //Source TASP + 194, //Parameter Code (dst-tasp) + 2, //Parameter Length + tsapPair.Remote.FirstByte, tsapPair.Remote.SecondByte, //Destination TASP + 192, //Parameter Code (tpdu-size) + 1, //Parameter Length + 10 //TPDU Size (2^10 = 1024) + }; + + return bSend1; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/ReadWriteErrorCode.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/ReadWriteErrorCode.cs new file mode 100644 index 0000000..9d6b943 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/ReadWriteErrorCode.cs @@ -0,0 +1,15 @@ + +namespace S7.Net.Protocol +{ + internal enum ReadWriteErrorCode : byte + { + Reserved = 0x00, + HardwareFault = 0x01, + AccessingObjectNotAllowed = 0x03, + AddressOutOfRange = 0x05, + DataTypeNotSupported = 0x06, + DataTypeInconsistent = 0x07, + ObjectDoesNotExist = 0x0a, + Success = 0xff + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/S7/DataItemAddress.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/S7/DataItemAddress.cs new file mode 100644 index 0000000..cf37382 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/S7/DataItemAddress.cs @@ -0,0 +1,37 @@ +namespace S7.Net.Protocol.S7 +{ + /// + /// Represents an area of memory in the PLC + /// + internal class DataItemAddress + { + public DataItemAddress(DataType dataType, int db, int startByteAddress, int byteLength) + { + DataType = dataType; + DB = db; + StartByteAddress = startByteAddress; + ByteLength = byteLength; + } + + + /// + /// Memory area to read + /// + public DataType DataType { get; } + + /// + /// Address of memory area to read (example: for DB1 this value is 1, for T45 this value is 45) + /// + public int DB { get; } + + /// + /// Address of the first byte to read + /// + public int StartByteAddress { get; } + + /// + /// Length of data to read + /// + public int ByteLength { get; } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/S7WriteMultiple.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/S7WriteMultiple.cs new file mode 100644 index 0000000..9f0257f --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/S7WriteMultiple.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using S7.Net.Types; + +namespace S7.Net.Protocol +{ + internal static class S7WriteMultiple + { + public static int CreateRequest(ByteArray message, DataItem[] dataItems) + { + message.Add(Header.Template); + + message[Header.Offsets.ParameterCount] = (byte) dataItems.Length; + var paramSize = dataItems.Length * Parameter.Template.Length; + + Serialization.SetWordAt(message, Header.Offsets.ParameterSize, + (ushort) (2 + paramSize)); + + var paramOffset = Header.Template.Length; + var data = new ByteArray(); + + var itemCount = 0; + + foreach (var item in dataItems) + { + itemCount++; + message.Add(Parameter.Template); + var value = Serialization.SerializeDataItem(item); + var wordLen = item.Value is bool ? 1 : 2; + + message[paramOffset + Parameter.Offsets.WordLength] = (byte) wordLen; + Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.Amount, (ushort) value.Length); + Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.DbNumber, (ushort) item.DB); + message[paramOffset + Parameter.Offsets.Area] = (byte) item.DataType; + + data.Add(0x00); + if (item.Value is bool b) + { + if (item.BitAdr > 7) + throw new ArgumentException( + $"Cannot read bit with invalid {nameof(item.BitAdr)} '{item.BitAdr}'.", nameof(dataItems)); + + Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr, + item.BitAdr); + + data.Add(0x03); + data.AddWord(1); + + data.Add(b ? (byte)1 : (byte)0); + if (itemCount != dataItems.Length) { + data.Add(0); + } + } + else + { + Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr, 0); + + var len = value.Length; + data.Add(0x04); + data.AddWord((ushort) (len << 3)); + data.Add(value); + + if ((len & 0b1) == 1 && itemCount != dataItems.Length) + { + data.Add(0); + } + } + + paramOffset += Parameter.Template.Length; + } + + message.Add(data.Array); + + Serialization.SetWordAt(message, Header.Offsets.MessageLength, (ushort) message.Length); + Serialization.SetWordAt(message, Header.Offsets.DataLength, (ushort) (message.Length - paramOffset)); + + return message.Length; + } + + public static void ParseResponse(byte[] message, int length, DataItem[] dataItems) + { + if (length < 12) throw new Exception("Not enough data received to parse write response."); + + var messageError = Serialization.GetWordAt(message, 10); + if (messageError != 0) + throw new Exception($"Write failed with error {messageError}."); + + if (length < 14 + dataItems.Length) + throw new Exception("Not enough data received to parse individual item responses."); + + IList itemResults = new ArraySegment(message, 14, dataItems.Length); + + List? errors = null; + + for (int i = 0; i < dataItems.Length; i++) + { + try + { + Plc.ValidateResponseCode((ReadWriteErrorCode)itemResults[i]); + } + catch(Exception e) + { + if (errors == null) errors = new List(); + errors.Add(new Exception($"Write of dataItem {dataItems[i]} failed: {e.Message}.")); + } + + } + + if (errors != null) + throw new AggregateException( + $"Write failed for {errors.Count} items. See the innerExceptions for details.", errors); + } + + private static class Header + { + public static byte[] Template { get; } = + { + 0x03, 0x00, 0x00, 0x00, // TPKT + 0x02, 0xf0, 0x80, // ISO DT + 0x32, // S7 protocol ID + 0x01, // JobRequest + 0x00, 0x00, // Reserved + 0x05, 0x00, // PDU reference + 0x00, 0x0e, // Parameters length + 0x00, 0x00, // Data length + 0x05, // Function: Write var + 0x00, // Number of items to write + }; + + public static class Offsets + { + public const int MessageLength = 2; + public const int ParameterSize = 13; + public const int DataLength = 15; + public const int ParameterCount = 18; + } + } + + private static class Parameter + { + public static byte[] Template { get; } = + { + 0x12, // Spec + 0x0a, // Length of remaining bytes + 0x10, // Addressing mode + 0x02, // Transport size + 0x00, 0x00, // Number of elements + 0x00, 0x00, // DB number + 0x84, // Area type + 0x00, 0x00, 0x00 // Area offset + }; + + public static class Offsets + { + public const int WordLength = 3; + public const int Amount = 4; + public const int DbNumber = 6; + public const int Area = 8; + public const int Address = 9; + } + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/Serialization.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/Serialization.cs new file mode 100644 index 0000000..3d114b6 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/Serialization.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using S7.Net.Types; + +namespace S7.Net.Protocol +{ + internal static class Serialization + { + public static ushort GetWordAt(IList buf, int index) + { + return (ushort)((buf[index] << 8) + buf[index]); + } + + public static byte[] SerializeDataItem(DataItem dataItem) + { + if (dataItem.Value == null) + { + throw new Exception($"DataItem.Value is null, cannot serialize. StartAddr={dataItem.StartByteAdr} VarType={dataItem.VarType}"); + } + + if (dataItem.Value is string s) + return dataItem.VarType switch + { + VarType.S7String => S7String.ToByteArray(s, dataItem.Count), + VarType.S7WString => S7WString.ToByteArray(s, dataItem.Count), + _ => Types.String.ToByteArray(s, dataItem.Count) + }; + + return SerializeValue(dataItem.Value); + } + + public static byte[] SerializeValue(object value) + { + switch (value.GetType().Name) + { + case "Boolean": + return new[] { (byte)((bool)value ? 1 : 0) }; + case "Byte": + return Types.Byte.ToByteArray((byte)value); + case "Int16": + return Types.Int.ToByteArray((Int16)value); + case "UInt16": + return Types.Word.ToByteArray((UInt16)value); + case "Int32": + return Types.DInt.ToByteArray((Int32)value); + case "UInt32": + return Types.DWord.ToByteArray((UInt32)value); + case "Single": + return Types.Real.ToByteArray((float)value); + case "Double": + return Types.LReal.ToByteArray((double)value); + case "DateTime": + return Types.DateTime.ToByteArray((System.DateTime)value); + case "Byte[]": + return (byte[])value; + case "Int16[]": + return Types.Int.ToByteArray((Int16[])value); + case "UInt16[]": + return Types.Word.ToByteArray((UInt16[])value); + case "Int32[]": + return Types.DInt.ToByteArray((Int32[])value); + case "UInt32[]": + return Types.DWord.ToByteArray((UInt32[])value); + case "Single[]": + return Types.Real.ToByteArray((float[])value); + case "Double[]": + return Types.LReal.ToByteArray((double[])value); + case "String": + // Hack: This is backwards compatible with the old code, but functionally it's broken + // if the consumer does not pay attention to string length. + var stringVal = (string)value; + return Types.String.ToByteArray(stringVal, stringVal.Length); + case "DateTime[]": + return Types.DateTime.ToByteArray((System.DateTime[])value); + case "DateTimeLong[]": + return Types.DateTimeLong.ToByteArray((System.DateTime[])value); + default: + throw new InvalidVariableTypeException(); + } + } + + public static void SetAddressAt(ByteArray buffer, int index, int startByte, byte bitNumber) + { + var start = startByte * 8 + bitNumber; + buffer[index + 2] = (byte)start; + start >>= 8; + buffer[index + 1] = (byte)start; + start >>= 8; + buffer[index] = (byte)start; + } + + public static void SetWordAt(ByteArray buffer, int index, ushort value) + { + buffer[index] = (byte)(value >> 8); + buffer[index + 1] = (byte)value; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/Tsap.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/Tsap.cs new file mode 100644 index 0000000..dc9d46c --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/Tsap.cs @@ -0,0 +1,31 @@ +namespace S7.Net.Protocol +{ + /// + /// Provides a representation of the Transport Service Access Point, or TSAP in short. TSAP's are used + /// to specify a client and server address. For most PLC types a default TSAP is available that allows + /// connection from any IP and can be calculated using the rack and slot numbers. + /// + public struct Tsap + { + /// + /// First byte of the TSAP. + /// + public byte FirstByte { get; set; } + + /// + /// Second byte of the TSAP. + /// + public byte SecondByte { get; set; } + + /// + /// Initializes a new instance of the class using the specified values. + /// + /// The first byte of the TSAP. + /// The second byte of the TSAP. + public Tsap(byte firstByte, byte secondByte) + { + FirstByte = firstByte; + SecondByte = secondByte; + } + } +} \ No newline at end of file diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/TsapPair.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/TsapPair.cs new file mode 100644 index 0000000..e54fc32 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/TsapPair.cs @@ -0,0 +1,96 @@ +using System; + +namespace S7.Net.Protocol +{ + /// + /// Implements a pair of TSAP addresses used to connect to a PLC. + /// + public class TsapPair + { + /// + /// The local . + /// + public Tsap Local { get; set; } + + /// + /// The remote + /// + public Tsap Remote { get; set; } + + /// + /// Initializes a new instance of the class using the specified local and + /// remote TSAP. + /// + /// The local TSAP. + /// The remote TSAP. + public TsapPair(Tsap local, Tsap remote) + { + Local = local; + Remote = remote; + } + + /// + /// Builds a that can be used to connect to a PLC using the default connection + /// addresses. + /// + /// + /// The remote TSAP is constructed using new Tsap(0x03, (byte) ((rack << 5) | slot)). + /// + /// The CPU type of the PLC. + /// The rack of the PLC's network card. + /// The slot of the PLC's network card. + /// A TSAP pair that matches the given parameters. + /// The is invalid. + /// + /// -or- + /// + /// The parameter is less than 0. + /// + /// -or- + /// + /// The parameter is greater than 15. + /// + /// -or- + /// + /// The parameter is less than 0. + /// + /// -or- + /// + /// The parameter is greater than 15. + public static TsapPair GetDefaultTsapPair(CpuType cpuType, int rack, int slot) + { + if (rack < 0) throw InvalidRackOrSlot(rack, nameof(rack), "minimum", 0); + if (rack > 0x0F) throw InvalidRackOrSlot(rack, nameof(rack), "maximum", 0x0F); + + if (slot < 0) throw InvalidRackOrSlot(slot, nameof(slot), "minimum", 0); + if (slot > 0x0F) throw InvalidRackOrSlot(slot, nameof(slot), "maximum", 0x0F); + + switch (cpuType) + { + case CpuType.S7200: + return new TsapPair(new Tsap(0x10, 0x00), new Tsap(0x10, 0x01)); + case CpuType.Logo0BA8: + // The actual values are probably on a per-project basis + return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x01, 0x02)); + case CpuType.S7200Smart: + case CpuType.S71200: + case CpuType.S71500: + case CpuType.S7300: + case CpuType.S7400: + // Testing with S7 1500 shows only the remote TSAP needs to match. This might differ for other + // PLC types. + return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x03, (byte) ((rack << 5) | slot))); + default: + throw new ArgumentOutOfRangeException(nameof(cpuType), "Invalid CPU Type specified"); + } + } + + private static ArgumentOutOfRangeException InvalidRackOrSlot(int value, string name, string extrema, + int extremaValue) + { + return new ArgumentOutOfRangeException(name, + $"Invalid {name} value specified (decimal: {value}, hexadecimal: {value:X}), {extrema} value " + + $"is {extremaValue} (decimal) or {extremaValue:X} (hexadecimal)."); + } + } +} \ No newline at end of file diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/StreamExtensions.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/StreamExtensions.cs new file mode 100644 index 0000000..749b915 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/StreamExtensions.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace S7.Net +{ + /// + /// Extensions for Streams + /// + public static class StreamExtensions + { + /// + /// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read + /// + /// the Stream to read from + /// the buffer to read into + /// the offset in the buffer to read into + /// the amount of bytes to read into the buffer + /// returns the amount of read bytes + public static int ReadExact(this Stream stream, byte[] buffer, int offset, int count) + { + int read = 0; + int received; + do + { + received = stream.Read(buffer, offset + read, count - read); + read += received; + } + while (read < count && received > 0); + + return read; + } + + /// + /// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read + /// + /// the Stream to read from + /// the buffer to read into + /// the offset in the buffer to read into + /// the amount of bytes to read into the buffer + /// returns the amount of read bytes + public static async Task ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int read = 0; + int received; + do + { + received = await stream.ReadAsync(buffer, offset + read, count - read, cancellationToken).ConfigureAwait(false); + read += received; + } + while (read < count && received > 0); + + return read; + } + } +} \ No newline at end of file diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/TPKT.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/TPKT.cs new file mode 100644 index 0000000..a311dce --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/TPKT.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace S7.Net +{ + + /// + /// Describes a TPKT Packet + /// + internal class TPKT + { + + + public byte Version; + public byte Reserved1; + public int Length; + public byte[] Data; + private TPKT(byte version, byte reserved1, int length, byte[] data) + { + Version = version; + Reserved1 = reserved1; + Length = length; + Data = data; + } + + /// + /// Reads a TPKT from the socket Async + /// + /// The stream to read from + /// Task TPKT Instace + public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken) + { + var buf = new byte[4]; + int len = await stream.ReadExactAsync(buf, 0, 4, cancellationToken).ConfigureAwait(false); + if (len < 4) throw new TPKTInvalidException("TPKT is incomplete / invalid"); + + var version = buf[0]; + var reserved1 = buf[1]; + var length = buf[2] * 256 + buf[3]; //BigEndian + + var data = new byte[length - 4]; + len = await stream.ReadExactAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false); + if (len < data.Length) + throw new TPKTInvalidException("TPKT payload incomplete / invalid"); + + return new TPKT + ( + version: version, + reserved1: reserved1, + length: length, + data: data + ); + } + + public override string ToString() + { + return string.Format("Version: {0} Length: {1} Data: {2}", + Version, + Length, + BitConverter.ToString(Data) + ); + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Bit.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Bit.cs new file mode 100644 index 0000000..5214fd9 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Bit.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections; + +namespace S7.Net.Types +{ + /// + /// Contains the conversion methods to convert Bit from S7 plc to C#. + /// + public static class Bit + { + /// + /// Converts a Bit to bool + /// + public static bool FromByte(byte v, byte bitAdr) + { + return (((int)v & (1 << bitAdr)) != 0); + } + + /// + /// Converts an array of bytes to a BitArray. + /// + /// The bytes to convert. + /// A BitArray with the same number of bits and equal values as . + public static BitArray ToBitArray(byte[] bytes) => ToBitArray(bytes, bytes.Length * 8); + + /// + /// Converts an array of bytes to a BitArray. + /// + /// The bytes to convert. + /// The number of bits to return. + /// A BitArray with bits. + public static BitArray ToBitArray(byte[] bytes, int length) + { + if (length > bytes.Length * 8) throw new ArgumentException($"Not enough data in bytes to return {length} bits.", nameof(bytes)); + + var bitArr = new BitArray(bytes); + var bools = new bool[length]; + for (var i = 0; i < length; i++) bools[i] = bitArr[i]; + + return new BitArray(bools); + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Boolean.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Boolean.cs new file mode 100644 index 0000000..f7bc83e --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Boolean.cs @@ -0,0 +1,64 @@ +namespace S7.Net.Types +{ + /// + /// Contains the methods to read, set and reset bits inside bytes + /// + public static class Boolean + { + /// + /// Returns the value of a bit in a bit, given the address of the bit + /// + public static bool GetValue(byte value, int bit) + { + return (((int)value & (1 << bit)) != 0); + } + + /// + /// Sets the value of a bit to 1 (true), given the address of the bit. Returns + /// a copy of the value with the bit set. + /// + /// The input value to modify. + /// The index (zero based) of the bit to set. + /// The modified value with the bit at index set. + public static byte SetBit(byte value, int bit) + { + SetBit(ref value, bit); + + return value; + } + + /// + /// Sets the value of a bit to 1 (true), given the address of the bit. + /// + /// The value to modify. + /// The index (zero based) of the bit to set. + public static void SetBit(ref byte value, int bit) + { + value = (byte) ((value | (1 << bit)) & 0xFF); + } + + /// + /// Resets the value of a bit to 0 (false), given the address of the bit. Returns + /// a copy of the value with the bit cleared. + /// + /// The input value to modify. + /// The index (zero based) of the bit to clear. + /// The modified value with the bit at index cleared. + public static byte ClearBit(byte value, int bit) + { + ClearBit(ref value, bit); + + return value; + } + + /// + /// Resets the value of a bit to 0 (false), given the address of the bit + /// + /// The input value to modify. + /// The index (zero based) of the bit to clear. + public static void ClearBit(ref byte value, int bit) + { + value = (byte) (value & ~(1 << bit) & 0xFF); + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Byte.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Byte.cs new file mode 100644 index 0000000..d52bb45 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Byte.cs @@ -0,0 +1,33 @@ +using System; + +namespace S7.Net.Types +{ + /// + /// Contains the methods to convert from bytes to byte arrays + /// + public static class Byte + { + /// + /// Converts a byte to byte array + /// + public static byte[] ToByteArray(byte value) + { + return new byte[] { value }; ; + } + + /// + /// Converts a byte array to byte + /// + /// + /// + public static byte FromByteArray(byte[] bytes) + { + if (bytes.Length != 1) + { + throw new ArgumentException("Wrong number of bytes. Bytes array must contain 1 bytes."); + } + return bytes[0]; + } + + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/ByteArray.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/ByteArray.cs new file mode 100644 index 0000000..e80e4b5 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/ByteArray.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; + +namespace S7.Net.Types +{ + class ByteArray + { + List list = new List(); + + public byte this[int index] + { + get => list[index]; + set => list[index] = value; + } + + public byte[] Array + { + get { return list.ToArray(); } + } + + public int Length => list.Count; + + public ByteArray() + { + list = new List(); + } + + public ByteArray(int size) + { + list = new List(size); + } + + public void Clear() + { + list = new List(); + } + + public void Add(byte item) + { + list.Add(item); + } + + public void AddWord(ushort value) + { + list.Add((byte) (value >> 8)); + list.Add((byte) value); + } + + public void Add(byte[] items) + { + list.AddRange(items); + } + + public void Add(IEnumerable items) + { + list.AddRange(items); + } + + public void Add(ByteArray byteArray) + { + list.AddRange(byteArray.Array); + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Class.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Class.cs new file mode 100644 index 0000000..0dd78fa --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Class.cs @@ -0,0 +1,340 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace S7.Net.Types +{ + /// + /// Contains the methods to convert a C# class to S7 data types + /// + public static class Class + { + private static IEnumerable GetAccessableProperties(Type classType) + { + return classType +#if NETSTANDARD1_3 + .GetTypeInfo().DeclaredProperties.Where(p => p.SetMethod != null); +#else + .GetProperties( + BindingFlags.SetProperty | + BindingFlags.Public | + BindingFlags.Instance) + .Where(p => p.GetSetMethod() != null); +#endif + + } + + private static double GetIncreasedNumberOfBytes(double numBytes, Type type) + { + switch (type.Name) + { + case "Boolean": + numBytes += 0.125; + break; + case "Byte": + numBytes = Math.Ceiling(numBytes); + numBytes++; + break; + case "Int16": + case "UInt16": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + numBytes += 2; + break; + case "Int32": + case "UInt32": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + numBytes += 4; + break; + case "Single": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + numBytes += 4; + break; + case "Double": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + numBytes += 8; + break; + default: + var propertyClass = Activator.CreateInstance(type); + numBytes = GetClassSize(propertyClass, numBytes, true); + break; + } + + return numBytes; + } + + /// + /// Gets the size of the class in bytes. + /// + /// An instance of the class + /// the number of bytes + public static double GetClassSize(object instance, double numBytes = 0.0, bool isInnerProperty = false) + { + var properties = GetAccessableProperties(instance.GetType()); + foreach (var property in properties) + { + if (property.PropertyType.IsArray) + { + Type elementType = property.PropertyType.GetElementType(); + Array array = (Array)property.GetValue(instance, null); + if (array.Length <= 0) + { + throw new Exception("Cannot determine size of class, because an array is defined which has no fixed size greater than zero."); + } + + IncrementToEven(ref numBytes); + for (int i = 0; i < array.Length; i++) + { + numBytes = GetIncreasedNumberOfBytes(numBytes, elementType); + } + } + else + { + numBytes = GetIncreasedNumberOfBytes(numBytes, property.PropertyType); + } + } + if (false == isInnerProperty) + { + // enlarge numBytes to next even number because S7-Structs in a DB always will be resized to an even byte count + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + } + return numBytes; + } + + private static object? GetPropertyValue(Type propertyType, byte[] bytes, ref double numBytes) + { + object? value = null; + + switch (propertyType.Name) + { + case "Boolean": + // get the value + int bytePos = (int)Math.Floor(numBytes); + int bitPos = (int)((numBytes - (double)bytePos) / 0.125); + if ((bytes[bytePos] & (int)Math.Pow(2, bitPos)) != 0) + value = true; + else + value = false; + numBytes += 0.125; + break; + case "Byte": + numBytes = Math.Ceiling(numBytes); + value = (byte)(bytes[(int)numBytes]); + numBytes++; + break; + case "Int16": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + // hier auswerten + ushort source = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]); + value = source.ConvertToShort(); + numBytes += 2; + break; + case "UInt16": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + // hier auswerten + value = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]); + numBytes += 2; + break; + case "Int32": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + // hier auswerten + uint sourceUInt = DWord.FromBytes(bytes[(int)numBytes + 3], + bytes[(int)numBytes + 2], + bytes[(int)numBytes + 1], + bytes[(int)numBytes + 0]); + value = sourceUInt.ConvertToInt(); + numBytes += 4; + break; + case "UInt32": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + // hier auswerten + value = DWord.FromBytes( + bytes[(int)numBytes], + bytes[(int)numBytes + 1], + bytes[(int)numBytes + 2], + bytes[(int)numBytes + 3]); + numBytes += 4; + break; + case "Single": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + // hier auswerten + value = Real.FromByteArray( + new byte[] { + bytes[(int)numBytes], + bytes[(int)numBytes + 1], + bytes[(int)numBytes + 2], + bytes[(int)numBytes + 3] }); + numBytes += 4; + break; + case "Double": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + var buffer = new byte[8]; + Array.Copy(bytes, (int)numBytes, buffer, 0, 8); + // hier auswerten + value = LReal.FromByteArray(buffer); + numBytes += 8; + break; + default: + var propClass = Activator.CreateInstance(propertyType); + numBytes = FromBytes(propClass, bytes, numBytes); + value = propClass; + break; + } + + return value; + } + + /// + /// Sets the object's values with the given array of bytes + /// + /// The object to fill in the given array of bytes + /// The array of bytes + public static double FromBytes(object sourceClass, byte[] bytes, double numBytes = 0, bool isInnerClass = false) + { + if (bytes == null) + return numBytes; + + var properties = GetAccessableProperties(sourceClass.GetType()); + foreach (var property in properties) + { + if (property.PropertyType.IsArray) + { + Array array = (Array)property.GetValue(sourceClass, null); + IncrementToEven(ref numBytes); + Type elementType = property.PropertyType.GetElementType(); + for (int i = 0; i < array.Length && numBytes < bytes.Length; i++) + { + array.SetValue( + GetPropertyValue(elementType, bytes, ref numBytes), + i); + } + } + else + { + property.SetValue( + sourceClass, + GetPropertyValue(property.PropertyType, bytes, ref numBytes), + null); + } + } + + return numBytes; + } + + private static double SetBytesFromProperty(object propertyValue, byte[] bytes, double numBytes) + { + int bytePos = 0; + int bitPos = 0; + byte[]? bytes2 = null; + + switch (propertyValue.GetType().Name) + { + case "Boolean": + // get the value + bytePos = (int)Math.Floor(numBytes); + bitPos = (int)((numBytes - (double)bytePos) / 0.125); + if ((bool)propertyValue) + bytes[bytePos] |= (byte)Math.Pow(2, bitPos); // is true + else + bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false + numBytes += 0.125; + break; + case "Byte": + numBytes = (int)Math.Ceiling(numBytes); + bytePos = (int)numBytes; + bytes[bytePos] = (byte)propertyValue; + numBytes++; + break; + case "Int16": + bytes2 = Int.ToByteArray((Int16)propertyValue); + break; + case "UInt16": + bytes2 = Word.ToByteArray((UInt16)propertyValue); + break; + case "Int32": + bytes2 = DInt.ToByteArray((Int32)propertyValue); + break; + case "UInt32": + bytes2 = DWord.ToByteArray((UInt32)propertyValue); + break; + case "Single": + bytes2 = Real.ToByteArray((float)propertyValue); + break; + case "Double": + bytes2 = LReal.ToByteArray((double)propertyValue); + break; + default: + numBytes = ToBytes(propertyValue, bytes, numBytes); + break; + } + + if (bytes2 != null) + { + IncrementToEven(ref numBytes); + + bytePos = (int)numBytes; + for (int bCnt = 0; bCnt < bytes2.Length; bCnt++) + bytes[bytePos + bCnt] = bytes2[bCnt]; + numBytes += bytes2.Length; + } + + return numBytes; + } + + /// + /// Creates a byte array depending on the struct type. + /// + /// The struct object + /// A byte array or null if fails. + public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0) + { + var properties = GetAccessableProperties(sourceClass.GetType()); + foreach (var property in properties) + { + if (property.PropertyType.IsArray) + { + Array array = (Array)property.GetValue(sourceClass, null); + IncrementToEven(ref numBytes); + Type elementType = property.PropertyType.GetElementType(); + for (int i = 0; i < array.Length && numBytes < bytes.Length; i++) + { + numBytes = SetBytesFromProperty(array.GetValue(i), bytes, numBytes); + } + } + else + { + numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), bytes, numBytes); + } + } + return numBytes; + } + + private static void IncrementToEven(ref double numBytes) + { + numBytes = Math.Ceiling(numBytes); + if (numBytes % 2 > 0) numBytes++; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Counter.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Counter.cs new file mode 100644 index 0000000..f52f727 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Counter.cs @@ -0,0 +1,63 @@ +using System; + +namespace S7.Net.Types +{ + /// + /// Contains the conversion methods to convert Counter from S7 plc to C# ushort (UInt16). + /// + public static class Counter + { + /// + /// Converts a Counter (2 bytes) to ushort (UInt16) + /// + public static UInt16 FromByteArray(byte[] bytes) + { + if (bytes.Length != 2) + { + throw new ArgumentException("Wrong number of bytes. Bytes array must contain 2 bytes."); + } + // bytes[0] -> HighByte + // bytes[1] -> LowByte + return (UInt16)((bytes[0] << 8) | bytes[1]); + } + + + /// + /// Converts a ushort (UInt16) to word (2 bytes) + /// + public static byte[] ToByteArray(UInt16 value) + { + byte[] bytes = new byte[2]; + + bytes[0] = (byte)((value << 8) & 0xFF); + bytes[1] = (byte)((value) & 0xFF); + + return bytes; + } + + /// + /// Converts an array of ushort (UInt16) to an array of bytes + /// + public static byte[] ToByteArray(UInt16[] value) + { + ByteArray arr = new ByteArray(); + foreach (UInt16 val in value) + arr.Add(ToByteArray(val)); + return arr.Array; + } + + /// + /// Converts an array of bytes to an array of ushort + /// + public static UInt16[] ToArray(byte[] bytes) + { + UInt16[] values = new UInt16[bytes.Length / 2]; + + int counter = 0; + for (int cnt = 0; cnt < bytes.Length / 2; cnt++) + values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++] }); + + return values; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DInt.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DInt.cs new file mode 100644 index 0000000..4d42bde --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DInt.cs @@ -0,0 +1,65 @@ +using System; + +namespace S7.Net.Types +{ + /// + /// Contains the conversion methods to convert DInt from S7 plc to C# int (Int32). + /// + public static class DInt + { + /// + /// Converts a S7 DInt (4 bytes) to int (Int32) + /// + public static Int32 FromByteArray(byte[] bytes) + { + if (bytes.Length != 4) + { + throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes."); + } + return bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]; + } + + + /// + /// Converts a int (Int32) to S7 DInt (4 bytes) + /// + public static byte[] ToByteArray(Int32 value) + { + byte[] bytes = new byte[4]; + + bytes[0] = (byte)((value >> 24) & 0xFF); + bytes[1] = (byte)((value >> 16) & 0xFF); + bytes[2] = (byte)((value >> 8) & 0xFF); + bytes[3] = (byte)((value) & 0xFF); + + return bytes; + } + + /// + /// Converts an array of int (Int32) to an array of bytes + /// + public static byte[] ToByteArray(Int32[] value) + { + ByteArray arr = new ByteArray(); + foreach (Int32 val in value) + arr.Add(ToByteArray(val)); + return arr.Array; + } + + /// + /// Converts an array of S7 DInt to an array of int (Int32) + /// + public static Int32[] ToArray(byte[] bytes) + { + Int32[] values = new Int32[bytes.Length / 4]; + + int counter = 0; + for (int cnt = 0; cnt < bytes.Length / 4; cnt++) + values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] }); + + return values; + } + + + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DWord.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DWord.cs new file mode 100644 index 0000000..87c254f --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DWord.cs @@ -0,0 +1,73 @@ +using System; + +namespace S7.Net.Types +{ + /// + /// Contains the conversion methods to convert DWord from S7 plc to C#. + /// + public static class DWord + { + /// + /// Converts a S7 DWord (4 bytes) to uint (UInt32) + /// + public static UInt32 FromByteArray(byte[] bytes) + { + return (UInt32)(bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]); + } + + + /// + /// Converts 4 bytes to DWord (UInt32) + /// + public static UInt32 FromBytes(byte b1, byte b2, byte b3, byte b4) + { + return (UInt32)((b4 << 24) | (b3 << 16) | (b2 << 8) | b1); + } + + + /// + /// Converts a uint (UInt32) to S7 DWord (4 bytes) + /// + public static byte[] ToByteArray(UInt32 value) + { + byte[] bytes = new byte[4]; + + bytes[0] = (byte)((value >> 24) & 0xFF); + bytes[1] = (byte)((value >> 16) & 0xFF); + bytes[2] = (byte)((value >> 8) & 0xFF); + bytes[3] = (byte)((value) & 0xFF); + + return bytes; + } + + + + + + + /// + /// Converts an array of uint (UInt32) to an array of S7 DWord (4 bytes) + /// + public static byte[] ToByteArray(UInt32[] value) + { + ByteArray arr = new ByteArray(); + foreach (UInt32 val in value) + arr.Add(ToByteArray(val)); + return arr.Array; + } + + /// + /// Converts an array of S7 DWord to an array of uint (UInt32) + /// + public static UInt32[] ToArray(byte[] bytes) + { + UInt32[] values = new UInt32[bytes.Length / 4]; + + int counter = 0; + for (int cnt = 0; cnt < bytes.Length / 4; cnt++) + values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] }); + + return values; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DataItem.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DataItem.cs new file mode 100644 index 0000000..254eab3 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DataItem.cs @@ -0,0 +1,104 @@ +using S7.Net.Protocol.S7; +using System; + +namespace S7.Net.Types +{ + /// + /// Create an instance of a memory block that can be read by using ReadMultipleVars + /// + public class DataItem + { + /// + /// Memory area to read + /// + public DataType DataType { get; set; } + + /// + /// Type of data to be read (default is bytes) + /// + public VarType VarType { get; set; } + + /// + /// Address of memory area to read (example: for DB1 this value is 1, for T45 this value is 45) + /// + public int DB { get; set; } + + /// + /// Address of the first byte to read + /// + public int StartByteAdr { get; set; } + + /// + /// Addess of bit to read from StartByteAdr + /// + public byte BitAdr { get; set; } + + /// + /// Number of variables to read + /// + public int Count { get; set; } + + /// + /// Contains the value of the memory area after the read has been executed + /// + public object? Value { get; set; } + + /// + /// Create an instance of DataItem + /// + public DataItem() + { + VarType = VarType.Byte; + Count = 1; + } + + /// + /// Create an instance of from the supplied address. + /// + /// The address to create the DataItem for. + /// A new instance with properties parsed from . + /// The property is not parsed from the address. + public static DataItem FromAddress(string address) + { + PLCAddress.Parse(address, out var dataType, out var dbNumber, out var varType, out var startByte, + out var bitNumber); + + return new DataItem + { + DataType = dataType, + DB = dbNumber, + VarType = varType, + StartByteAdr = startByte, + BitAdr = (byte) (bitNumber == -1 ? 0 : bitNumber) + }; + } + + /// + /// Create an instance of from the supplied address and value. + /// + /// The address to create the DataItem for. + /// The value to be applied to the DataItem. + /// A new instance with properties parsed from and the supplied value set. + public static DataItem FromAddressAndValue(string address, T value) + { + var dataItem = FromAddress(address); + dataItem.Value = value; + + if (typeof(T).IsArray) + { + var array = ((Array?)dataItem.Value); + if ( array != null) + { + dataItem.Count = array.Length; + } + } + + return dataItem; + } + + internal static DataItemAddress GetDataItemAddress(DataItem dataItem) + { + return new DataItemAddress(dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, Plc.VarTypeToByteLength(dataItem.VarType, dataItem.Count)); + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DateTime.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DateTime.cs new file mode 100644 index 0000000..9cafa67 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DateTime.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; + +namespace S7.Net.Types +{ + /// + /// Contains the methods to convert between and S7 representation of datetime values. + /// + public static class DateTime + { + /// + /// The minimum value supported by the specification. + /// + public static readonly System.DateTime SpecMinimumDateTime = new System.DateTime(1990, 1, 1); + + /// + /// The maximum value supported by the specification. + /// + public static readonly System.DateTime SpecMaximumDateTime = new System.DateTime(2089, 12, 31, 23, 59, 59, 999); + + /// + /// Parses a value from bytes. + /// + /// Input bytes read from PLC. + /// A object representing the value read from PLC. + /// Thrown when the length of + /// is not 8 or any value in + /// is outside the valid range of values. + public static System.DateTime FromByteArray(byte[] bytes) + { + return FromByteArrayImpl(bytes); + } + + /// + /// Parses an array of values from bytes. + /// + /// Input bytes read from PLC. + /// An array of objects representing the values read from PLC. + /// Thrown when the length of + /// is not a multiple of 8 or any value in + /// is outside the valid range of values. + public static System.DateTime[] ToArray(byte[] bytes) + { + if (bytes.Length % 8 != 0) + throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length, + $"Parsing an array of DateTime requires a multiple of 8 bytes of input data, input data is '{bytes.Length}' long."); + + var cnt = bytes.Length / 8; + var result = new System.DateTime[bytes.Length / 8]; + + for (var i = 0; i < cnt; i++) + result[i] = FromByteArrayImpl(new ArraySegment(bytes, i * 8, 8)); + + return result; + } + + private static System.DateTime FromByteArrayImpl(IList bytes) + { + if (bytes.Count != 8) + throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Count, + $"Parsing a DateTime requires exactly 8 bytes of input data, input data is {bytes.Count} bytes long."); + + int DecodeBcd(byte input) => 10 * (input >> 4) + (input & 0b00001111); + + int ByteToYear(byte bcdYear) + { + var input = DecodeBcd(bcdYear); + if (input < 90) return input + 2000; + if (input < 100) return input + 1900; + + throw new ArgumentOutOfRangeException(nameof(bcdYear), bcdYear, + $"Value '{input}' is higher than the maximum '99' of S7 date and time representation."); + } + + int AssertRangeInclusive(int input, byte min, byte max, string field) + { + if (input < min) + throw new ArgumentOutOfRangeException(nameof(input), input, + $"Value '{input}' is lower than the minimum '{min}' allowed for {field}."); + if (input > max) + throw new ArgumentOutOfRangeException(nameof(input), input, + $"Value '{input}' is higher than the maximum '{max}' allowed for {field}."); + + return input; + } + + var year = ByteToYear(bytes[0]); + var month = AssertRangeInclusive(DecodeBcd(bytes[1]), 1, 12, "month"); + var day = AssertRangeInclusive(DecodeBcd(bytes[2]), 1, 31, "day of month"); + var hour = AssertRangeInclusive(DecodeBcd(bytes[3]), 0, 23, "hour"); + var minute = AssertRangeInclusive(DecodeBcd(bytes[4]), 0, 59, "minute"); + var second = AssertRangeInclusive(DecodeBcd(bytes[5]), 0, 59, "second"); + var hsec = AssertRangeInclusive(DecodeBcd(bytes[6]), 0, 99, "first two millisecond digits"); + var msec = AssertRangeInclusive(bytes[7] >> 4, 0, 9, "third millisecond digit"); + var dayOfWeek = AssertRangeInclusive(bytes[7] & 0b00001111, 1, 7, "day of week"); + + return new System.DateTime(year, month, day, hour, minute, second, hsec * 10 + msec); + } + + /// + /// Converts a value to a byte array. + /// + /// The DateTime value to convert. + /// A byte array containing the S7 date time representation of . + /// Thrown when the value of + /// is before + /// or after . + public static byte[] ToByteArray(System.DateTime dateTime) + { + byte EncodeBcd(int value) + { + return (byte) ((value / 10 << 4) | value % 10); + } + + if (dateTime < SpecMinimumDateTime) + throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime, + $"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 date time representation."); + + if (dateTime > SpecMaximumDateTime) + throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime, + $"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 date time representation."); + + byte MapYear(int year) => (byte) (year < 2000 ? year - 1900 : year - 2000); + + int DayOfWeekToInt(DayOfWeek dayOfWeek) => (int) dayOfWeek + 1; + + return new[] + { + EncodeBcd(MapYear(dateTime.Year)), + EncodeBcd(dateTime.Month), + EncodeBcd(dateTime.Day), + EncodeBcd(dateTime.Hour), + EncodeBcd(dateTime.Minute), + EncodeBcd(dateTime.Second), + EncodeBcd(dateTime.Millisecond / 10), + (byte) (dateTime.Millisecond % 10 << 4 | DayOfWeekToInt(dateTime.DayOfWeek)) + }; + } + + /// + /// Converts an array of values to a byte array. + /// + /// The DateTime values to convert. + /// A byte array containing the S7 date time representations of . + /// Thrown when any value of + /// is before + /// or after . + public static byte[] ToByteArray(System.DateTime[] dateTimes) + { + var bytes = new List(dateTimes.Length * 8); + foreach (var dateTime in dateTimes) bytes.AddRange(ToByteArray(dateTime)); + + return bytes.ToArray(); + } + } +} \ No newline at end of file diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DateTimeLong.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DateTimeLong.cs new file mode 100644 index 0000000..6479f6f --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DateTimeLong.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace S7.Net.Types +{ + /// + /// Contains the methods to convert between and S7 representation of DateTimeLong (DTL) values. + /// + public static class DateTimeLong + { + public const int TypeLengthInBytes = 12; + /// + /// The minimum value supported by the specification. + /// + public static readonly System.DateTime SpecMinimumDateTime = new System.DateTime(1970, 1, 1); + + /// + /// The maximum value supported by the specification. + /// + public static readonly System.DateTime SpecMaximumDateTime = new System.DateTime(2262, 4, 11, 23, 47, 16, 854); + + /// + /// Parses a value from bytes. + /// + /// Input bytes read from PLC. + /// A object representing the value read from PLC. + /// + /// Thrown when the length of + /// is not 12 or any value in + /// is outside the valid range of values. + /// + public static System.DateTime FromByteArray(byte[] bytes) + { + return FromByteArrayImpl(bytes); + } + + /// + /// Parses an array of values from bytes. + /// + /// Input bytes read from PLC. + /// An array of objects representing the values read from PLC. + /// + /// Thrown when the length of + /// is not a multiple of 12 or any value in + /// is outside the valid range of values. + /// + public static System.DateTime[] ToArray(byte[] bytes) + { + if (bytes.Length % TypeLengthInBytes != 0) + { + throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length, + $"Parsing an array of DateTimeLong requires a multiple of 12 bytes of input data, input data is '{bytes.Length}' long."); + } + + var cnt = bytes.Length / TypeLengthInBytes; + var result = new System.DateTime[cnt]; + + for (var i = 0; i < cnt; i++) + { + var slice = new byte[TypeLengthInBytes]; + Array.Copy(bytes, i * TypeLengthInBytes, slice, 0, TypeLengthInBytes); + result[i] = FromByteArrayImpl(slice); + } + + return result; + } + + private static System.DateTime FromByteArrayImpl(byte[] bytes) + { + if (bytes.Length != TypeLengthInBytes) + { + throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length, + $"Parsing a DateTimeLong requires exactly 12 bytes of input data, input data is {bytes.Length} bytes long."); + } + + + var year = AssertRangeInclusive(Word.FromBytes(bytes[1], bytes[0]), 1970, 2262, "year"); + var month = AssertRangeInclusive(bytes[2], 1, 12, "month"); + var day = AssertRangeInclusive(bytes[3], 1, 31, "day of month"); + var dayOfWeek = AssertRangeInclusive(bytes[4], 1, 7, "day of week"); + var hour = AssertRangeInclusive(bytes[5], 0, 23, "hour"); + var minute = AssertRangeInclusive(bytes[6], 0, 59, "minute"); + var second = AssertRangeInclusive(bytes[7], 0, 59, "second"); + ; + + var nanoseconds = AssertRangeInclusive(DWord.FromBytes(bytes[11], bytes[10], bytes[9], bytes[8]), 0, + 999999999, "nanoseconds"); + + var time = new System.DateTime(year, month, day, hour, minute, second); + return time.AddTicks(nanoseconds / 100); + } + + /// + /// Converts a value to a byte array. + /// + /// The DateTime value to convert. + /// A byte array containing the S7 DateTimeLong representation of . + /// + /// Thrown when the value of + /// is before + /// or after . + /// + public static byte[] ToByteArray(System.DateTime dateTime) + { + if (dateTime < SpecMinimumDateTime) + { + throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime, + $"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 DateTimeLong representation."); + } + + if (dateTime > SpecMaximumDateTime) + { + throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime, + $"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 DateTimeLong representation."); + } + + var stream = new MemoryStream(TypeLengthInBytes); + // Convert Year + stream.Write(Word.ToByteArray(Convert.ToUInt16(dateTime.Year)), 0, 2); + + // Convert Month + stream.WriteByte(Convert.ToByte(dateTime.Month)); + + // Convert Day + stream.WriteByte(Convert.ToByte(dateTime.Day)); + + // Convert WeekDay. NET DateTime starts with Sunday = 0, while S7DT has Sunday = 1. + stream.WriteByte(Convert.ToByte(dateTime.DayOfWeek + 1)); + + // Convert Hour + stream.WriteByte(Convert.ToByte(dateTime.Hour)); + + // Convert Minutes + stream.WriteByte(Convert.ToByte(dateTime.Minute)); + + // Convert Seconds + stream.WriteByte(Convert.ToByte(dateTime.Second)); + + // Convert Nanoseconds. Net DateTime has a representation of 1 Tick = 100ns. + // Thus First take the ticks Mod 1 Second (1s = 10'000'000 ticks), and then Convert to nanoseconds. + stream.Write(DWord.ToByteArray(Convert.ToUInt32(dateTime.Ticks % 10000000 * 100)), 0, 4); + + return stream.ToArray(); + } + + /// + /// Converts an array of values to a byte array. + /// + /// The DateTime values to convert. + /// A byte array containing the S7 DateTimeLong representations of . + /// + /// Thrown when any value of + /// is before + /// or after . + /// + public static byte[] ToByteArray(System.DateTime[] dateTimes) + { + var bytes = new List(dateTimes.Length * TypeLengthInBytes); + foreach (var dateTime in dateTimes) + { + bytes.AddRange(ToByteArray(dateTime)); + } + + return bytes.ToArray(); + } + + private static T AssertRangeInclusive(T input, T min, T max, string field) where T : IComparable + { + if (input.CompareTo(min) < 0) + { + throw new ArgumentOutOfRangeException(nameof(input), input, + $"Value '{input}' is lower than the minimum '{min}' allowed for {field}."); + } + + if (input.CompareTo(max) > 0) + { + throw new ArgumentOutOfRangeException(nameof(input), input, + $"Value '{input}' is higher than the maximum '{max}' allowed for {field}."); + } + + return input; + } + } +} \ No newline at end of file diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Double.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Double.cs new file mode 100644 index 0000000..c9daf24 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Double.cs @@ -0,0 +1,68 @@ +using System; + +namespace S7.Net.Types +{ + /// + /// Contains the conversion methods to convert Real from S7 plc to C# double. + /// + [Obsolete("Class Double is obsolete. Use Real instead for 32bit floating point, or LReal for 64bit floating point.")] + public static class Double + { + /// + /// Converts a S7 Real (4 bytes) to double + /// + public static double FromByteArray(byte[] bytes) => Real.FromByteArray(bytes); + + /// + /// Converts a S7 DInt to double + /// + public static double FromDWord(Int32 value) + { + byte[] b = DInt.ToByteArray(value); + double d = FromByteArray(b); + return d; + } + + /// + /// Converts a S7 DWord to double + /// + public static double FromDWord(UInt32 value) + { + byte[] b = DWord.ToByteArray(value); + double d = FromByteArray(b); + return d; + } + + + /// + /// Converts a double to S7 Real (4 bytes) + /// + public static byte[] ToByteArray(double value) => Real.ToByteArray((float)value); + + /// + /// Converts an array of double to an array of bytes + /// + public static byte[] ToByteArray(double[] value) + { + ByteArray arr = new ByteArray(); + foreach (double val in value) + arr.Add(ToByteArray(val)); + return arr.Array; + } + + /// + /// Converts an array of S7 Real to an array of double + /// + public static double[] ToArray(byte[] bytes) + { + double[] values = new double[bytes.Length / 4]; + + int counter = 0; + for (int cnt = 0; cnt < bytes.Length / 4; cnt++) + values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] }); + + return values; + } + + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Int.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Int.cs new file mode 100644 index 0000000..5489691 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Int.cs @@ -0,0 +1,87 @@ +using System; + +namespace S7.Net.Types +{ + /// + /// Contains the conversion methods to convert Int from S7 plc to C#. + /// + public static class Int + { + /// + /// Converts a S7 Int (2 bytes) to short (Int16) + /// + public static short FromByteArray(byte[] bytes) + { + if (bytes.Length != 2) + { + throw new ArgumentException("Wrong number of bytes. Bytes array must contain 2 bytes."); + } + // bytes[0] -> HighByte + // bytes[1] -> LowByte + return (short)((int)(bytes[1]) | ((int)(bytes[0]) << 8)); + } + + + /// + /// Converts a short (Int16) to a S7 Int byte array (2 bytes) + /// + public static byte[] ToByteArray(Int16 value) + { + byte[] bytes = new byte[2]; + + bytes[0] = (byte) (value >> 8 & 0xFF); + bytes[1] = (byte)(value & 0xFF); + + return bytes; + } + + /// + /// Converts an array of short (Int16) to a S7 Int byte array (2 bytes) + /// + public static byte[] ToByteArray(Int16[] value) + { + byte[] bytes = new byte[value.Length * 2]; + int bytesPos = 0; + + for(int i=0; i< value.Length; i++) + { + bytes[bytesPos++] = (byte)((value[i] >> 8) & 0xFF); + bytes[bytesPos++] = (byte) (value[i] & 0xFF); + } + return bytes; + } + + /// + /// Converts an array of S7 Int to an array of short (Int16) + /// + public static Int16[] ToArray(byte[] bytes) + { + int shortsCount = bytes.Length / 2; + + Int16[] values = new Int16[shortsCount]; + + int counter = 0; + for (int cnt = 0; cnt < shortsCount; cnt++) + values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++] }); + + return values; + } + + /// + /// Converts a C# int value to a C# short value, to be used as word. + /// + /// + /// + public static Int16 CWord(int value) + { + if (value > 32767) + { + value -= 32768; + value = 32768 - value; + value *= -1; + } + return (short)value; + } + + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/LReal.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/LReal.cs new file mode 100644 index 0000000..c541645 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/LReal.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; + +namespace S7.Net.Types +{ + /// + /// Contains the conversion methods to convert Real from S7 plc to C# double. + /// + public static class LReal + { + /// + /// Converts a S7 LReal (8 bytes) to double + /// + public static double FromByteArray(byte[] bytes) + { + if (bytes.Length != 8) + { + throw new ArgumentException("Wrong number of bytes. Bytes array must contain 8 bytes."); + } + var buffer = bytes; + + // sps uses bigending so we have to reverse if platform needs + if (BitConverter.IsLittleEndian) + { + Array.Reverse(buffer); + } + + return BitConverter.ToDouble(buffer, 0); + } + + /// + /// Converts a double to S7 LReal (8 bytes) + /// + public static byte[] ToByteArray(double value) + { + var bytes = BitConverter.GetBytes(value); + + // sps uses bigending so we have to check if platform is same + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + return bytes; + } + + /// + /// Converts an array of double to an array of bytes + /// + public static byte[] ToByteArray(double[] value) => TypeHelper.ToByteArray(value, ToByteArray); + + /// + /// Converts an array of S7 LReal to an array of double + /// + public static double[] ToArray(byte[] bytes) => TypeHelper.ToArray(bytes, FromByteArray); + + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Real.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Real.cs new file mode 100644 index 0000000..1d28202 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Real.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; + +namespace S7.Net.Types +{ + /// + /// Contains the conversion methods to convert Real from S7 plc to C# double. + /// + public static class Real + { + /// + /// Converts a S7 Real (4 bytes) to float + /// + public static float FromByteArray(byte[] bytes) + { + if (bytes.Length != 4) + { + throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes."); + } + + // sps uses bigending so we have to reverse if platform needs + if (BitConverter.IsLittleEndian) + { + // create deep copy of the array and reverse + bytes = new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] }; + } + + return BitConverter.ToSingle(bytes, 0); + } + + /// + /// Converts a float to S7 Real (4 bytes) + /// + public static byte[] ToByteArray(float value) + { + byte[] bytes = BitConverter.GetBytes(value); + + // sps uses bigending so we have to check if platform is same + if (!BitConverter.IsLittleEndian) return bytes; + + // create deep copy of the array and reverse + return new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] }; + } + + /// + /// Converts an array of float to an array of bytes + /// + public static byte[] ToByteArray(float[] value) + { + var buffer = new byte[4 * value.Length]; + var stream = new MemoryStream(buffer); + foreach (var val in value) + { + stream.Write(ToByteArray(val), 0, 4); + } + + return buffer; + } + + /// + /// Converts an array of S7 Real to an array of float + /// + public static float[] ToArray(byte[] bytes) + { + var values = new float[bytes.Length / 4]; + + int counter = 0; + for (int cnt = 0; cnt < bytes.Length / 4; cnt++) + values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] }); + + return values; + } + + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7String.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7String.cs new file mode 100644 index 0000000..5bc383e --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7String.cs @@ -0,0 +1,69 @@ +using System; +using System.Text; + +namespace S7.Net.Types +{ + /// + /// Contains the methods to convert from S7 strings to C# strings + /// An S7 String has a preceeding 2 byte header containing its capacity and length + /// + public static class S7String + { + /// + /// Converts S7 bytes to a string + /// + /// + /// + public static string FromByteArray(byte[] bytes) + { + if (bytes.Length < 2) + { + throw new PlcException(ErrorCode.ReadData, "Malformed S7 String / too short"); + } + + int size = bytes[0]; + int length = bytes[1]; + if (length > size) + { + throw new PlcException(ErrorCode.ReadData, "Malformed S7 String / length larger than capacity"); + } + + try + { + return Encoding.ASCII.GetString(bytes, 2, length); + } + catch (Exception e) + { + throw new PlcException(ErrorCode.ReadData, + $"Failed to parse {VarType.S7String} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.", + e); + } + + } + + /// + /// Converts a to S7 string with 2-byte header. + /// + /// The string to convert to byte array. + /// The length (in characters) allocated in PLC for the string. + /// A containing the string header and string value with a maximum length of + 2. + public static byte[] ToByteArray(string value, int reservedLength) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (reservedLength > 254) throw new ArgumentException($"The maximum string length supported is 254."); + + var bytes = Encoding.ASCII.GetBytes(value); + if (bytes.Length > reservedLength) throw new ArgumentException($"The provided string length ({bytes.Length} is larger than the specified reserved length ({reservedLength})."); + + var buffer = new byte[2 + reservedLength]; + Array.Copy(bytes, 0, buffer, 2, bytes.Length); + buffer[0] = (byte)reservedLength; + buffer[1] = (byte)bytes.Length; + return buffer; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7StringAttribute.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7StringAttribute.cs new file mode 100644 index 0000000..4d6e107 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7StringAttribute.cs @@ -0,0 +1,67 @@ +using System; + +namespace S7.Net.Types +{ + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public sealed class S7StringAttribute : Attribute + { + private readonly S7StringType type; + private readonly int reservedLength; + + /// + /// Initializes a new instance of the class. + /// + /// The string type. + /// Reserved length of the string in characters. + /// Please use a valid value for the string type + public S7StringAttribute(S7StringType type, int reservedLength) + { + if (!Enum.IsDefined(typeof(S7StringType), type)) + throw new ArgumentException("Please use a valid value for the string type"); + + this.type = type; + this.reservedLength = reservedLength; + } + + /// + /// Gets the type of the string. + /// + /// + /// The string type. + /// + public S7StringType Type => type; + + /// + /// Gets the reserved length of the string in characters. + /// + /// + /// The reserved length of the string in characters. + /// + public int ReservedLength => reservedLength; + + /// + /// Gets the reserved length in bytes. + /// + /// + /// The reserved length in bytes. + /// + public int ReservedLengthInBytes => type == S7StringType.S7String ? reservedLength + 2 : (reservedLength * 2) + 4; + } + + + /// + /// String type. + /// + public enum S7StringType + { + /// + /// ASCII string. + /// + S7String = VarType.S7String, + + /// + /// Unicode string. + /// + S7WString = VarType.S7WString + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7WString.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7WString.cs new file mode 100644 index 0000000..8d8aabf --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7WString.cs @@ -0,0 +1,72 @@ +using System; +using System.Text; + +namespace S7.Net.Types +{ + /// + /// Contains the methods to convert from S7 wstrings to C# strings + /// An S7 WString has a preceding 4 byte header containing its capacity and length + /// + public static class S7WString + { + /// + /// Converts S7 bytes to a string + /// + /// + /// + public static string FromByteArray(byte[] bytes) + { + if (bytes.Length < 4) + { + throw new PlcException(ErrorCode.ReadData, "Malformed S7 WString / too short"); + } + + int size = (bytes[0] << 8) | bytes[1]; + int length = (bytes[2] << 8) | bytes[3]; + + if (length > size) + { + throw new PlcException(ErrorCode.ReadData, "Malformed S7 WString / length larger than capacity"); + } + + try + { + return Encoding.BigEndianUnicode.GetString(bytes, 4, length * 2); + } + catch (Exception e) + { + throw new PlcException(ErrorCode.ReadData, + $"Failed to parse {VarType.S7WString} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.", + e); + } + + } + + /// + /// Converts a to S7 wstring with 4-byte header. + /// + /// The string to convert to byte array. + /// The length (in characters) allocated in PLC for the string. + /// A containing the string header and string value with a maximum length of + 4. + public static byte[] ToByteArray(string value, int reservedLength) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (reservedLength > 16382) throw new ArgumentException("The maximum string length supported is 16382."); + + var buffer = new byte[4 + reservedLength * 2]; + buffer[0] = (byte)((reservedLength >> 8) & 0xFF); + buffer[1] = (byte)(reservedLength & 0xFF); + buffer[2] = (byte)((value.Length >> 8) & 0xFF); + buffer[3] = (byte)(value.Length & 0xFF); + + var stringLength = Encoding.BigEndianUnicode.GetBytes(value, 0, value.Length, buffer, 4) / 2; + if (stringLength > reservedLength) throw new ArgumentException($"The provided string length ({stringLength} is larger than the specified reserved length ({reservedLength})."); + + return buffer; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Single.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Single.cs new file mode 100644 index 0000000..4f27553 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Single.cs @@ -0,0 +1,68 @@ +using System; + +namespace S7.Net.Types +{ + /// + /// Contains the conversion methods to convert Real from S7 plc to C# float. + /// + [Obsolete("Class Single is obsolete. Use Real instead.")] + public static class Single + { + /// + /// Converts a S7 Real (4 bytes) to float + /// + public static float FromByteArray(byte[] bytes) => Real.FromByteArray(bytes); + + /// + /// Converts a S7 DInt to float + /// + public static float FromDWord(Int32 value) + { + byte[] b = DInt.ToByteArray(value); + float d = FromByteArray(b); + return d; + } + + /// + /// Converts a S7 DWord to float + /// + public static float FromDWord(UInt32 value) + { + byte[] b = DWord.ToByteArray(value); + float d = FromByteArray(b); + return d; + } + + + /// + /// Converts a double to S7 Real (4 bytes) + /// + public static byte[] ToByteArray(float value) => Real.ToByteArray(value); + + /// + /// Converts an array of float to an array of bytes + /// + public static byte[] ToByteArray(float[] value) + { + ByteArray arr = new ByteArray(); + foreach (float val in value) + arr.Add(ToByteArray(val)); + return arr.Array; + } + + /// + /// Converts an array of S7 Real to an array of float + /// + public static float[] ToArray(byte[] bytes) + { + float[] values = new float[bytes.Length / 4]; + + int counter = 0; + for (int cnt = 0; cnt < bytes.Length / 4; cnt++) + values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] }); + + return values; + } + + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/String.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/String.cs new file mode 100644 index 0000000..3917635 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/String.cs @@ -0,0 +1,37 @@ +namespace S7.Net.Types +{ + /// + /// Contains the methods to convert from S7 Array of Chars (like a const char[N] C-String) to C# strings + /// + public class String + { + /// + /// Converts a string to of bytes, padded with 0-bytes if required. + /// + /// The string to write to the PLC. + /// The amount of bytes reserved for the in the PLC. + public static byte[] ToByteArray(string value, int reservedLength) + { + var length = value?.Length; + if (length > reservedLength) length = reservedLength; + var bytes = new byte[reservedLength]; + + if (length == null || length == 0) return bytes; + + System.Text.Encoding.ASCII.GetBytes(value, 0, length.Value, bytes, 0); + + return bytes; + } + + /// + /// Converts S7 bytes to a string + /// + /// + /// + public static string FromByteArray(byte[] bytes) + { + return System.Text.Encoding.ASCII.GetString(bytes); + } + + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/StringEx.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/StringEx.cs new file mode 100644 index 0000000..6c0381a --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/StringEx.cs @@ -0,0 +1,15 @@ +using System; + +namespace S7.Net.Types +{ + /// + [Obsolete("Please use S7String class")] + public static class StringEx + { + /// + public static string FromByteArray(byte[] bytes) => S7String.FromByteArray(bytes); + + /// + public static byte[] ToByteArray(string value, int reservedLength) => S7String.ToByteArray(value, reservedLength); + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Struct.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Struct.cs new file mode 100644 index 0000000..1e95508 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Struct.cs @@ -0,0 +1,322 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace S7.Net.Types +{ + /// + /// Contains the method to convert a C# struct to S7 data types + /// + public static class Struct + { + /// + /// Gets the size of the struct in bytes. + /// + /// the type of the struct + /// the number of bytes + public static int GetStructSize(Type structType) + { + double numBytes = 0.0; + + var infos = structType +#if NETSTANDARD1_3 + .GetTypeInfo().DeclaredFields; +#else + .GetFields(); +#endif + + foreach (var info in infos) + { + switch (info.FieldType.Name) + { + case "Boolean": + numBytes += 0.125; + break; + case "Byte": + numBytes = Math.Ceiling(numBytes); + numBytes++; + break; + case "Int16": + case "UInt16": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + numBytes += 2; + break; + case "Int32": + case "UInt32": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + numBytes += 4; + break; + case "Single": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + numBytes += 4; + break; + case "Double": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + numBytes += 8; + break; + case "String": + S7StringAttribute? attribute = info.GetCustomAttributes().SingleOrDefault(); + if (attribute == default(S7StringAttribute)) + throw new ArgumentException("Please add S7StringAttribute to the string field"); + + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + numBytes += attribute.ReservedLengthInBytes; + break; + default: + numBytes += GetStructSize(info.FieldType); + break; + } + } + return (int)numBytes; + } + + /// + /// Creates a struct of a specified type by an array of bytes. + /// + /// The struct type + /// The array of bytes + /// The object depending on the struct type or null if fails(array-length != struct-length + public static object? FromBytes(Type structType, byte[] bytes) + { + if (bytes == null) + return null; + + if (bytes.Length != GetStructSize(structType)) + return null; + + // and decode it + int bytePos = 0; + int bitPos = 0; + double numBytes = 0.0; + object structValue = Activator.CreateInstance(structType); + + + var infos = structValue.GetType() +#if NETSTANDARD1_3 + .GetTypeInfo().DeclaredFields; +#else + .GetFields(); +#endif + + foreach (var info in infos) + { + switch (info.FieldType.Name) + { + case "Boolean": + // get the value + bytePos = (int)Math.Floor(numBytes); + bitPos = (int)((numBytes - (double)bytePos) / 0.125); + if ((bytes[bytePos] & (int)Math.Pow(2, bitPos)) != 0) + info.SetValue(structValue, true); + else + info.SetValue(structValue, false); + numBytes += 0.125; + break; + case "Byte": + numBytes = Math.Ceiling(numBytes); + info.SetValue(structValue, (byte)(bytes[(int)numBytes])); + numBytes++; + break; + case "Int16": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + // get the value + ushort source = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]); + info.SetValue(structValue, source.ConvertToShort()); + numBytes += 2; + break; + case "UInt16": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + // get the value + info.SetValue(structValue, Word.FromBytes(bytes[(int)numBytes + 1], + bytes[(int)numBytes])); + numBytes += 2; + break; + case "Int32": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + // get the value + uint sourceUInt = DWord.FromBytes(bytes[(int)numBytes + 3], + bytes[(int)numBytes + 2], + bytes[(int)numBytes + 1], + bytes[(int)numBytes + 0]); + info.SetValue(structValue, sourceUInt.ConvertToInt()); + numBytes += 4; + break; + case "UInt32": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + // get the value + info.SetValue(structValue, DWord.FromBytes(bytes[(int)numBytes], + bytes[(int)numBytes + 1], + bytes[(int)numBytes + 2], + bytes[(int)numBytes + 3])); + numBytes += 4; + break; + case "Single": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + // get the value + info.SetValue(structValue, Real.FromByteArray(new byte[] { bytes[(int)numBytes], + bytes[(int)numBytes + 1], + bytes[(int)numBytes + 2], + bytes[(int)numBytes + 3] })); + numBytes += 4; + break; + case "Double": + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + // get the value + var data = new byte[8]; + Array.Copy(bytes, (int)numBytes, data, 0, 8); + info.SetValue(structValue, LReal.FromByteArray(data)); + numBytes += 8; + break; + case "String": + S7StringAttribute? attribute = info.GetCustomAttributes().SingleOrDefault(); + if (attribute == default(S7StringAttribute)) + throw new ArgumentException("Please add S7StringAttribute to the string field"); + + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + + // get the value + var sData = new byte[attribute.ReservedLengthInBytes]; + Array.Copy(bytes, (int)numBytes, sData, 0, sData.Length); + switch (attribute.Type) + { + case S7StringType.S7String: + info.SetValue(structValue, S7String.FromByteArray(sData)); + break; + case S7StringType.S7WString: + info.SetValue(structValue, S7WString.FromByteArray(sData)); + break; + default: + throw new ArgumentException("Please use a valid string type for the S7StringAttribute"); + } + + numBytes += sData.Length; + break; + default: + var buffer = new byte[GetStructSize(info.FieldType)]; + if (buffer.Length == 0) + continue; + Buffer.BlockCopy(bytes, (int)Math.Ceiling(numBytes), buffer, 0, buffer.Length); + info.SetValue(structValue, FromBytes(info.FieldType, buffer)); + numBytes += buffer.Length; + break; + } + } + return structValue; + } + + /// + /// Creates a byte array depending on the struct type. + /// + /// The struct object + /// A byte array or null if fails. + public static byte[] ToBytes(object structValue) + { + Type type = structValue.GetType(); + + int size = Struct.GetStructSize(type); + byte[] bytes = new byte[size]; + byte[]? bytes2 = null; + + int bytePos = 0; + int bitPos = 0; + double numBytes = 0.0; + + var infos = type +#if NETSTANDARD1_3 + .GetTypeInfo().DeclaredFields; +#else + .GetFields(); +#endif + + foreach (var info in infos) + { + bytes2 = null; + switch (info.FieldType.Name) + { + case "Boolean": + // get the value + bytePos = (int)Math.Floor(numBytes); + bitPos = (int)((numBytes - (double)bytePos) / 0.125); + if ((bool)info.GetValue(structValue)) + bytes[bytePos] |= (byte)Math.Pow(2, bitPos); // is true + else + bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false + numBytes += 0.125; + break; + case "Byte": + numBytes = (int)Math.Ceiling(numBytes); + bytePos = (int)numBytes; + bytes[bytePos] = (byte)info.GetValue(structValue); + numBytes++; + break; + case "Int16": + bytes2 = Int.ToByteArray((Int16)info.GetValue(structValue)); + break; + case "UInt16": + bytes2 = Word.ToByteArray((UInt16)info.GetValue(structValue)); + break; + case "Int32": + bytes2 = DInt.ToByteArray((Int32)info.GetValue(structValue)); + break; + case "UInt32": + bytes2 = DWord.ToByteArray((UInt32)info.GetValue(structValue)); + break; + case "Single": + bytes2 = Real.ToByteArray((float)info.GetValue(structValue)); + break; + case "Double": + bytes2 = LReal.ToByteArray((double)info.GetValue(structValue)); + break; + case "String": + S7StringAttribute? attribute = info.GetCustomAttributes().SingleOrDefault(); + if (attribute == default(S7StringAttribute)) + throw new ArgumentException("Please add S7StringAttribute to the string field"); + + bytes2 = attribute.Type switch + { + S7StringType.S7String => S7String.ToByteArray((string)info.GetValue(structValue), attribute.ReservedLength), + S7StringType.S7WString => S7WString.ToByteArray((string)info.GetValue(structValue), attribute.ReservedLength), + _ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute") + }; + break; + } + if (bytes2 != null) + { + // add them + numBytes = Math.Ceiling(numBytes); + if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) + numBytes++; + bytePos = (int)numBytes; + for (int bCnt = 0; bCnt < bytes2.Length; bCnt++) + bytes[bytePos + bCnt] = bytes2[bCnt]; + numBytes += bytes2.Length; + } + } + return bytes; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Timer.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Timer.cs new file mode 100644 index 0000000..b5ecd45 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Timer.cs @@ -0,0 +1,82 @@ +using System; + +namespace S7.Net.Types +{ + /// + /// Converts the Timer data type to C# data type + /// + public static class Timer + { + /// + /// Converts the timer bytes to a double + /// + public static double FromByteArray(byte[] bytes) + { + double wert = 0; + + wert = ((bytes[0]) & 0x0F) * 100.0; + wert += ((bytes[1] >> 4) & 0x0F) * 10.0; + wert += ((bytes[1]) & 0x0F) * 1.0; + + // this value is not used... may for a nother exponation + //int unknown = (bytes[0] >> 6) & 0x03; + + switch ((bytes[0] >> 4) & 0x03) + { + case 0: + wert *= 0.01; + break; + case 1: + wert *= 0.1; + break; + case 2: + wert *= 1.0; + break; + case 3: + wert *= 10.0; + break; + } + + return wert; + } + + /// + /// Converts a ushort (UInt16) to an array of bytes formatted as time + /// + public static byte[] ToByteArray(UInt16 value) + { + byte[] bytes = new byte[2]; + bytes[1] = (byte)((int)value & 0xFF); + bytes[0] = (byte)((int)value >> 8 & 0xFF); + + return bytes; + } + + /// + /// Converts an array of ushorts (Uint16) to an array of bytes formatted as time + /// + public static byte[] ToByteArray(UInt16[] value) + { + ByteArray arr = new ByteArray(); + foreach (UInt16 val in value) + arr.Add(ToByteArray(val)); + return arr.Array; + } + + /// + /// Converts an array of bytes formatted as time to an array of doubles + /// + /// + /// + public static double[] ToArray(byte[] bytes) + { + double[] values = new double[bytes.Length / 2]; + + int counter = 0; + for (int cnt = 0; cnt < bytes.Length / 2; cnt++) + values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++] }); + + return values; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/TypeHelper.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/TypeHelper.cs new file mode 100644 index 0000000..af5441b --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/TypeHelper.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace S7.Net.Types +{ + internal static class TypeHelper + { + /// + /// Converts an array of T to an array of bytes + /// + public static byte[] ToByteArray(T[] value, Func converter) where T : struct + { + var buffer = new byte[Marshal.SizeOf(default(T)) * value.Length]; + var stream = new MemoryStream(buffer); + foreach (var val in value) + { + stream.Write(converter(val), 0, 4); + } + + return buffer; + } + + /// + /// Converts an array of T repesented as S7 binary data to an array of T + /// + public static T[] ToArray(byte[] bytes, Func converter) where T : struct + { + var typeSize = Marshal.SizeOf(default(T)); + var entries = bytes.Length / typeSize; + var values = new T[entries]; + + for(int i = 0; i < entries; ++i) + { + var buffer = new byte[typeSize]; + Array.Copy(bytes, i * typeSize, buffer, 0, typeSize); + values[i] = converter(buffer); + } + + return values; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Word.cs b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Word.cs new file mode 100644 index 0000000..8004992 --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Word.cs @@ -0,0 +1,71 @@ +using System; + +namespace S7.Net.Types +{ + /// + /// Contains the conversion methods to convert Words from S7 plc to C#. + /// + public static class Word + { + /// + /// Converts a word (2 bytes) to ushort (UInt16) + /// + public static UInt16 FromByteArray(byte[] bytes) + { + if (bytes.Length != 2) + { + throw new ArgumentException("Wrong number of bytes. Bytes array must contain 2 bytes."); + } + + return (UInt16)((bytes[0] << 8) | bytes[1]); + } + + + /// + /// Converts 2 bytes to ushort (UInt16) + /// + public static UInt16 FromBytes(byte b1, byte b2) + { + return (UInt16)((b2 << 8) | b1); + } + + + /// + /// Converts a ushort (UInt16) to word (2 bytes) + /// + public static byte[] ToByteArray(UInt16 value) + { + byte[] bytes = new byte[2]; + + bytes[1] = (byte)(value & 0xFF); + bytes[0] = (byte)((value>>8) & 0xFF); + + return bytes; + } + + /// + /// Converts an array of ushort (UInt16) to an array of bytes + /// + public static byte[] ToByteArray(UInt16[] value) + { + ByteArray arr = new ByteArray(); + foreach (UInt16 val in value) + arr.Add(ToByteArray(val)); + return arr.Array; + } + + /// + /// Converts an array of bytes to an array of ushort + /// + public static UInt16[] ToArray(byte[] bytes) + { + UInt16[] values = new UInt16[bytes.Length/2]; + + int counter = 0; + for (int cnt = 0; cnt < bytes.Length/2; cnt++) + values[cnt] = FromByteArray(new byte[] {bytes[counter++], bytes[counter++]}); + + return values; + } + } +} diff --git a/Plugins/Drivers/DriverSiemensS7/SiemensS7.cs b/Plugins/Drivers/DriverSiemensS7/SiemensS7.cs new file mode 100644 index 0000000..dab9c6e --- /dev/null +++ b/Plugins/Drivers/DriverSiemensS7/SiemensS7.cs @@ -0,0 +1,184 @@ +using PluginInterface; +using S7.Net; +using System; + +namespace DriverSiemensS7 +{ + [DriverSupported("1500")] + [DriverSupported("1200")] + [DriverSupported("400")] + [DriverSupported("300")] + [DriverSupported("200")] + [DriverSupported("200Smart")] + [DriverInfoAttribute("SiemensS7", "V1.0.0", "Copyright WHD© 2021-12-19")] + public class SiemensS7 : IDriver + { + private Plc plc = null; + #region 配置参数 + + [ConfigParameter("设备Id")] + public Guid DeviceId { get; set; } + + [ConfigParameter("PLC类型")] + public CpuType CpuType { get; set; } = CpuType.S71200; + + [ConfigParameter("IP地址")] + public string IpAddress { get; set; } = "127.0.0.1"; + + [ConfigParameter("端口号")] + public int Port { get; set; } = 102; + + [ConfigParameter("Rack")] + public short Rack { get; set; } = 0; + + [ConfigParameter("Slot")] + public short Slot { get; set; } = 0; + + [ConfigParameter("超时时间ms")] + public uint Timeout { get; set; } = 3000; + + [ConfigParameter("最小通讯周期ms")] + public uint MinPeriod { get; set; } = 3000; + + #endregion + + public SiemensS7(Guid deviceId) + { + DeviceId = deviceId; + plc = new Plc(CpuType, IpAddress, Port, Rack, Slot); + } + + + public bool IsConnected + { + get + { + return plc != null && plc.IsConnected; + } + } + + public bool Connect() + { + try + { + plc.Open(); + } + catch (Exception) + { + return false; + } + return IsConnected; + } + + public bool Close() + { + try + { + plc?.Close(); + return !IsConnected; + } + catch (Exception) + { + + return false; + } + } + + public void Dispose() + { + try + { + plc = null; + } + catch (Exception) + { + + } + } + + [Method("读西门子PLC", description: "读西门子PLC")] + public DriverReturnValueModel Read(DriverAddressIoArgModel ioarg) + { + var ret = new DriverReturnValueModel { StatusType = VaribaleStatusTypeEnum.Good }; + + if (plc != null && plc.IsConnected) + { + try + { + ret.Value = plc.Read(ioarg.Address); + } + catch (Exception ex) + { + + ret.StatusType = VaribaleStatusTypeEnum.Bad; + ret.Message = $"读取失败,{ex.Message}"; + } + } + else + { + ret.StatusType = VaribaleStatusTypeEnum.Bad; + ret.Message = "连接失败"; + } + return ret; + } + + //预留了大小端转换的 + private ushort[] ChangeBuffersOrder(ushort[] buffers, DataTypeEnum dataType) + { + var newBuffers = new ushort[buffers.Length]; + if (dataType.ToString().Contains("32") || dataType.ToString().Contains("Float")) + { + var A = buffers[0] & 0xff00;//A + var B = buffers[0] & 0x00ff;//B + var C = buffers[1] & 0xff00;//C + var D = buffers[1] & 0x00ff;//D + if (dataType.ToString().Contains("_1")) + { + newBuffers[0] = (ushort)(A + B);//AB + newBuffers[1] = (ushort)(C + D);//CD + } + else if (dataType.ToString().Contains("_2")) + { + newBuffers[0] = (ushort)((A >> 8) + (B << 8));//BA + newBuffers[1] = (ushort)((C >> 8) + (D << 8));//DC + } + else if (dataType.ToString().Contains("_3")) + { + newBuffers[0] = (ushort)((C >> 8) + (D << 8));//DC + newBuffers[1] = (ushort)((A >> 8) + (B << 8));//BA + } + else + { + newBuffers[0] = (ushort)(C + D);//CD + newBuffers[1] = (ushort)(A + B);//AB + } + } + else if (dataType.ToString().Contains("64") || dataType.ToString().Contains("Double")) + { + if (dataType.ToString().Contains("_1")) + { + + } + else + { + newBuffers[0] = buffers[3]; + newBuffers[1] = buffers[2]; + newBuffers[2] = buffers[1]; + newBuffers[3] = buffers[0]; + } + } + else + { + if (dataType.ToString().Contains("_1")) + { + var h8 = buffers[0] & 0xf0; + var l8 = buffers[0] & 0x0f; + newBuffers[0] = (ushort)(h8 >> 8 + l8 << 8); + } + else + newBuffers[0] = buffers[0]; + } + return newBuffers; + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index deb6211..7d41ddb 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ * 抛砖引玉,共同进步 * 可视化的配置方式实现数据采集(使用wtm开发) * 基于.net5的开源物联网网关 -* 内置ModbusTcp驱动(使用nmodbus4) +* 内置Mqtt服务端,支持websocket,端口1888,/mqtt +* 内置ModbusTcp驱动 +* 内置西门子PLC驱动 * 支持驱动二次开发(短期内会提供西门子三菱通讯) * 数据通过mqtt推送,支持thingsboard * 目前只支持遥测数据上传,后续支持属性的双向通信 @@ -31,7 +33,7 @@ ## linux docker运行 1. docker pull registry.cn-hangzhou.aliyuncs.com/wanghaidong/iotgateway 2. docker tag registry.cn-hangzhou.aliyuncs.com/wanghaidong/iotgateway 15261671110/iotgateway -3. docker run -d -p 518:518 --name iotgateway --restart always 15261671110/iotgateway +3. docker run -d -p 518:518 -p 1888:1888 --name iotgateway --restart always 15261671110/iotgateway ## 登入系统 1. 用户名 admin,密码 000000 2. 打开发布文件路径下的ReadMe文件夹中的手摸手,按照顺序添加设备进行采集 @@ -60,6 +62,8 @@ ## 君子性非异也,善假于物也 1. [WTM(MIT)](https://github.com/dotnetcore/WTM) 2. [NModbus4(MIT)](https://github.com/NModbus4/NModbus4) +2. [S7NetPlus(MIT)](https://github.com/S7NetPlus/s7netplus) +2. [MQTTnet(MIT)](https://github.com/chkr1011/MQTTnet) 3. [EFCore(MIT)](https://github.com/dotnet/efcore) 4. [LayUI(MIT)](https://github.com/sentsin/layui) 5. [SQLite](https://github.com/sqlite/sqlite)