From 570c1a613c533802ae5393a28f7e30f6be08ffff Mon Sep 17 00:00:00 2001 From: dd <535915157@qq.com> Date: Thu, 16 Dec 2021 16:25:02 +0800 Subject: [PATCH] =?UTF-8?q?1.=E5=A2=9E=E5=8A=A0Mqtt=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E9=9B=86=E6=88=902.=E5=A2=9E=E5=8A=A0=E8=A5=BF=E9=97=A8?= =?UTF-8?q?=E5=AD=90PLC=E9=A9=B1=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vs/IoTGateway/v17/.suo | Bin 156672 -> 185344 bytes Dockerfile | 10 + IoTGateway.sln | 21 +- .../BasicData/Views/DeviceConfig/Edit.cshtml | 6 +- IoTGateway/Dockerfile | 30 - IoTGateway/IoTGateway.csproj | 6 + IoTGateway/Plugin.dll | Bin 26112 -> 0 bytes IoTGateway/Program.cs | 3 +- IoTGateway/Startup.cs | 32 +- IoTGateway/appsettings.json | 8 +- IoTGateway/iotgateway.db | Bin 274432 -> 278528 bytes .../DesignTimeBuild/.dtbcache.v2 | Bin 0 -> 64320 bytes .../.vs/DriverModbusMaster/v17/.futdcache.v1 | Bin 0 -> 137 bytes .../.vs/DriverModbusMaster/v17/.suo | Bin 0 -> 3584 bytes .../DriverModbusMaster.csproj} | 0 .../ModbusMaster.cs} | 10 +- .../NModbus4/Data/DataStore.cs | 0 .../NModbus4/Data/DataStoreEventArgs.cs | 0 .../NModbus4/Data/DataStoreFactory.cs | 0 .../NModbus4/Data/DiscreteCollection.cs | 0 .../Data/IModbusMessageDataCollection.cs | 0 .../NModbus4/Data/ModbusDataCollection.cs | 0 .../NModbus4/Data/ModbusDataType.cs | 0 .../NModbus4/Data/RegisterCollection.cs | 0 .../NModbus4/Device/IModbusMaster.cs | 0 .../NModbus4/Device/IModbusSerialMaster.cs | 0 .../NModbus4/Device/ModbusDevice.cs | 0 .../NModbus4/Device/ModbusIpMaster.cs | 0 .../NModbus4/Device/ModbusMaster.cs | 0 .../Device/ModbusMasterTcpConnection.cs | 0 .../NModbus4/Device/ModbusSerialMaster.cs | 0 .../NModbus4/Device/ModbusSerialSlave.cs | 0 .../NModbus4/Device/ModbusSlave.cs | 0 .../Device/ModbusSlaveRequestEventArgs.cs | 0 .../NModbus4/Device/ModbusTcpSlave.cs | 0 .../NModbus4/Device/ModbusUdpSlave.cs | 0 .../NModbus4/Device/TcpConnectionEventArgs.cs | 0 .../NModbus4/Extensions/Enron/EnronModbus.cs | 0 .../NModbus4/GlobalSuppressions.cs | 0 .../NModbus4/IO/EmptyTransport.cs | 0 .../NModbus4/IO/IStreamResource.cs | 0 .../NModbus4/IO/ModbusAsciiTransport.cs | 0 .../NModbus4/IO/ModbusIpTransport.cs | 0 .../NModbus4/IO/ModbusRtuTransport.cs | 0 .../NModbus4/IO/ModbusSerialTransport.cs | 0 .../NModbus4/IO/ModbusTransport.cs | 0 .../NModbus4/IO/StreamResourceUtility.cs | 0 .../NModbus4/IO/TcpClientAdapter.cs | 0 .../NModbus4/IO/UdpClientAdapter.cs | 0 .../NModbus4/InvalidModbusRequestException.cs | 0 .../NModbus4/Message/AbstractModbusMessage.cs | 0 .../Message/AbstractModbusMessageWithData.cs | 0 .../Message/DiagnosticsRequestResponse.cs | 0 .../NModbus4/Message/IModbusMessage.cs | 0 .../NModbus4/Message/IModbusRequest.cs | 0 .../NModbus4/Message/ModbusMessageFactory.cs | 0 .../NModbus4/Message/ModbusMessageImpl.cs | 0 .../Message/ReadCoilsInputsRequest.cs | 0 .../Message/ReadCoilsInputsResponse.cs | 0 .../ReadHoldingInputRegistersRequest.cs | 0 .../ReadHoldingInputRegistersResponse.cs | 0 .../ReadWriteMultipleRegistersRequest.cs | 0 .../Message/SlaveExceptionResponse.cs | 0 .../Message/WriteMultipleCoilsRequest.cs | 0 .../Message/WriteMultipleCoilsResponse.cs | 0 .../Message/WriteMultipleRegistersRequest.cs | 0 .../Message/WriteMultipleRegistersResponse.cs | 0 .../Message/WriteSingleCoilRequestResponse.cs | 0 .../WriteSingleRegisterRequestResponse.cs | 0 .../NModbus4/Modbus.cs | 0 .../NModbus4/Properties/AssemblyInfo.cs | 0 .../NModbus4/Resources.cs | 0 .../NModbus4/SerialPortAdapter.cs | 0 .../NModbus4/SlaveException.cs | 0 .../NModbus4/Unme.Common/DisposableUtility.cs | 0 .../NModbus4/Unme.Common/SequenceUtility.cs | 0 .../NModbus4/Utility/DiscriminatedUnion.cs | 0 .../NModbus4/Utility/ModbusUtility.cs | 0 .../System.IO.Ports.dll | Bin .../DriverSiemensS7/DriverSiemensS7.csproj | 12 + .../Drivers/DriverSiemensS7/S7.Net/COTP.cs | 118 ++++ .../S7.Net/Compat/TcpClientMixins.cs | 19 + .../DriverSiemensS7/S7.Net/Conversion.cs | 226 +++++++ .../Drivers/DriverSiemensS7/S7.Net/Enums.cs | 211 +++++++ .../S7.Net/Helper/MemoryStreamExtension.cs | 18 + .../S7.Net/Internal/TaskQueue.cs | 28 + .../S7.Net/InvalidDataException.cs | 43 ++ Plugins/Drivers/DriverSiemensS7/S7.Net/PLC.cs | 330 ++++++++++ .../DriverSiemensS7/S7.Net/PLCAddress.cs | 207 +++++++ .../DriverSiemensS7/S7.Net/PLCExceptions.cs | 113 ++++ .../DriverSiemensS7/S7.Net/PLCHelpers.cs | 266 ++++++++ .../DriverSiemensS7/S7.Net/PlcAsynchronous.cs | 566 ++++++++++++++++++ .../DriverSiemensS7/S7.Net/PlcException.cs | 39 ++ .../DriverSiemensS7/S7.Net/PlcSynchronous.cs | 475 +++++++++++++++ .../S7.Net/Properties/AssemblyInfo.cs | 3 + .../S7.Net/Properties/S7.Net.snk | Bin 0 -> 596 bytes .../S7.Net/Protocol/ConnectionRequest.cs | 28 + .../S7.Net/Protocol/ReadWriteErrorCode.cs | 15 + .../S7.Net/Protocol/S7/DataItemAddress.cs | 37 ++ .../S7.Net/Protocol/S7WriteMultiple.cs | 163 +++++ .../S7.Net/Protocol/Serialization.cs | 98 +++ .../DriverSiemensS7/S7.Net/Protocol/Tsap.cs | 31 + .../S7.Net/Protocol/TsapPair.cs | 96 +++ .../S7.Net/StreamExtensions.cs | 57 ++ .../Drivers/DriverSiemensS7/S7.Net/TPKT.cs | 66 ++ .../DriverSiemensS7/S7.Net/Types/Bit.cs | 43 ++ .../DriverSiemensS7/S7.Net/Types/Boolean.cs | 64 ++ .../DriverSiemensS7/S7.Net/Types/Byte.cs | 33 + .../DriverSiemensS7/S7.Net/Types/ByteArray.cs | 63 ++ .../DriverSiemensS7/S7.Net/Types/Class.cs | 340 +++++++++++ .../DriverSiemensS7/S7.Net/Types/Counter.cs | 63 ++ .../DriverSiemensS7/S7.Net/Types/DInt.cs | 65 ++ .../DriverSiemensS7/S7.Net/Types/DWord.cs | 73 +++ .../DriverSiemensS7/S7.Net/Types/DataItem.cs | 104 ++++ .../DriverSiemensS7/S7.Net/Types/DateTime.cs | 156 +++++ .../S7.Net/Types/DateTimeLong.cs | 185 ++++++ .../DriverSiemensS7/S7.Net/Types/Double.cs | 68 +++ .../DriverSiemensS7/S7.Net/Types/Int.cs | 87 +++ .../DriverSiemensS7/S7.Net/Types/LReal.cs | 57 ++ .../DriverSiemensS7/S7.Net/Types/Real.cs | 75 +++ .../DriverSiemensS7/S7.Net/Types/S7String.cs | 69 +++ .../S7.Net/Types/S7StringAttribute.cs | 67 +++ .../DriverSiemensS7/S7.Net/Types/S7WString.cs | 72 +++ .../DriverSiemensS7/S7.Net/Types/Single.cs | 68 +++ .../DriverSiemensS7/S7.Net/Types/String.cs | 37 ++ .../DriverSiemensS7/S7.Net/Types/StringEx.cs | 15 + .../DriverSiemensS7/S7.Net/Types/Struct.cs | 322 ++++++++++ .../DriverSiemensS7/S7.Net/Types/Timer.cs | 82 +++ .../S7.Net/Types/TypeHelper.cs | 43 ++ .../DriverSiemensS7/S7.Net/Types/Word.cs | 71 +++ Plugins/Drivers/DriverSiemensS7/SiemensS7.cs | 184 ++++++ README.md | 8 +- 132 files changed, 5845 insertions(+), 60 deletions(-) delete mode 100644 IoTGateway/Dockerfile delete mode 100644 IoTGateway/Plugin.dll create mode 100644 Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/DesignTimeBuild/.dtbcache.v2 create mode 100644 Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/v17/.futdcache.v1 create mode 100644 Plugins/Drivers/DriverModbusMaster/.vs/DriverModbusMaster/v17/.suo rename Plugins/Drivers/{DriverModbusTCP/DriverModbusTCP.csproj => DriverModbusMaster/DriverModbusMaster.csproj} (100%) rename Plugins/Drivers/{DriverModbusTCP/ModbusTCP.cs => DriverModbusMaster/ModbusMaster.cs} (98%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Data/DataStore.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Data/DataStoreEventArgs.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Data/DataStoreFactory.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Data/DiscreteCollection.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Data/IModbusMessageDataCollection.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Data/ModbusDataCollection.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Data/ModbusDataType.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Data/RegisterCollection.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/IModbusMaster.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/IModbusSerialMaster.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/ModbusDevice.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/ModbusIpMaster.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/ModbusMaster.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/ModbusMasterTcpConnection.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/ModbusSerialMaster.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/ModbusSerialSlave.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/ModbusSlave.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/ModbusSlaveRequestEventArgs.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/ModbusTcpSlave.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/ModbusUdpSlave.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Device/TcpConnectionEventArgs.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Extensions/Enron/EnronModbus.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/GlobalSuppressions.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/IO/EmptyTransport.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/IO/IStreamResource.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/IO/ModbusAsciiTransport.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/IO/ModbusIpTransport.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/IO/ModbusRtuTransport.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/IO/ModbusSerialTransport.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/IO/ModbusTransport.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/IO/StreamResourceUtility.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/IO/TcpClientAdapter.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/IO/UdpClientAdapter.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/InvalidModbusRequestException.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/AbstractModbusMessage.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/AbstractModbusMessageWithData.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/DiagnosticsRequestResponse.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/IModbusMessage.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/IModbusRequest.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/ModbusMessageFactory.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/ModbusMessageImpl.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/ReadCoilsInputsRequest.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/ReadCoilsInputsResponse.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/ReadHoldingInputRegistersRequest.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/ReadHoldingInputRegistersResponse.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/ReadWriteMultipleRegistersRequest.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/SlaveExceptionResponse.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/WriteMultipleCoilsRequest.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/WriteMultipleCoilsResponse.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/WriteMultipleRegistersRequest.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/WriteMultipleRegistersResponse.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/WriteSingleCoilRequestResponse.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Message/WriteSingleRegisterRequestResponse.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Modbus.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Properties/AssemblyInfo.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Resources.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/SerialPortAdapter.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/SlaveException.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Unme.Common/DisposableUtility.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Unme.Common/SequenceUtility.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Utility/DiscriminatedUnion.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/NModbus4/Utility/ModbusUtility.cs (100%) rename Plugins/Drivers/{DriverModbusTCP => DriverModbusMaster}/System.IO.Ports.dll (100%) create mode 100644 Plugins/Drivers/DriverSiemensS7/DriverSiemensS7.csproj create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/COTP.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Compat/TcpClientMixins.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Conversion.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Enums.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Helper/MemoryStreamExtension.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Internal/TaskQueue.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/InvalidDataException.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/PLC.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/PLCAddress.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/PLCExceptions.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/PLCHelpers.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/PlcAsynchronous.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/PlcException.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/PlcSynchronous.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Properties/AssemblyInfo.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Properties/S7.Net.snk create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/ConnectionRequest.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/ReadWriteErrorCode.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/S7/DataItemAddress.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/S7WriteMultiple.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/Serialization.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/Tsap.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Protocol/TsapPair.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/StreamExtensions.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/TPKT.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Bit.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Boolean.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Byte.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/ByteArray.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Class.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Counter.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DInt.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DWord.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DataItem.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DateTime.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/DateTimeLong.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Double.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Int.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/LReal.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Real.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7String.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7StringAttribute.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/S7WString.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Single.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/String.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/StringEx.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Struct.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Timer.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/TypeHelper.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/S7.Net/Types/Word.cs create mode 100644 Plugins/Drivers/DriverSiemensS7/SiemensS7.cs diff --git a/.vs/IoTGateway/v17/.suo b/.vs/IoTGateway/v17/.suo index 28a75b3ca6ffcd7c15a8d8792865a0f194be73e6..f4aa5d924c76a03441a6f4fc4023c8b2926db829 100644 GIT binary patch delta 30387 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 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 2378f81847e1ba0601a8ee02186c784ac32eee83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26112 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 XU;k@gz4HGVanHQ}wHEkaJ@EeomTU~q 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 142e91fd1b4aac04c4e8db12fb17116c724ebfeb..5335f6b3aa1e27b5c92a8157a4e5c16e556a1dbd 100644 GIT binary patch delta 11846 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 delta 4087 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 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 0000000000000000000000000000000000000000..9e3295943663f335803771636da24a99d526a1a2 GIT binary patch literal 64320 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 + + + 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 0000000000000000000000000000000000000000..191a65d2442463baa7df49218ce30ecda77a9e08 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096k5Hj0U_6=mekm?-5Bzszmn_+06d(B2l z2hJ`+T>^jkK$>K_4nH?>jugcV`DD=9A?2=%jdZ<=f}g~jC)YM9{cBi=k7(}mIkY$oKs)qr35+kOQ^^_w3V(`nQ+1EwTcXO)s}IK++9 zxew729U2sJlNf4)r{jHv66rJ^52^&1@ii(i8sOIhr%1)^86H~#7lq#IlJ$Gw(#rls zZLW9I7BY<0`S!K{%^nIICGgDvx$q}BEueKuZ+*^EJ{V?-xPUa!XL z^DM%iaBZk_9PTAEV41gaP79Fe-P}~18rmM3=T96dp+o@ z-N}lju2>=S9p~Wo8vN5GcNsr1`LpzGs0}2Q9!i|N)>**Nlzv4gd!y4%52ZRTbIY8UcZqQi$D>TK)?(^=r zacAV%ZR6@?g__~9?|;j-Sd>~itWh{vN{< + /// 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)