From fb67e0b77eca0526d75267d78837e47ead68112a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Thu, 23 Jan 2025 22:01:45 +0100 Subject: [PATCH] Added VirtualModbusClient to Common --- .../Events/CoilWrittenEventArgs.cs | 13 +- .../Events/RegisterWrittenEventArgs.cs | 20 +- .../Extensions/ArrayExtensions.cs | 5 +- .../Models/HoldingRegister.cs | 8 +- .../Models/InputRegister.cs | 4 +- .../Models/ModbusDevice.cs | 2 +- .../Protocols/AsciiProtocol.cs | 28 +- .../Protocols/RtuOverTcpProtocol.cs | 32 +- .../Protocols/RtuProtocol.cs | 28 +- .../Protocols/TcpProtocol.cs | 32 +- .../Protocols/VirtualProtocol.cs | 478 ++++++++++++++++++ AMWD.Protocols.Modbus.Common/README.md | 3 +- .../Utils/VirtualModbusClient.cs | 179 +++++++ CHANGELOG.md | 1 + 14 files changed, 752 insertions(+), 81 deletions(-) create mode 100644 AMWD.Protocols.Modbus.Common/Protocols/VirtualProtocol.cs create mode 100644 AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs diff --git a/AMWD.Protocols.Modbus.Common/Events/CoilWrittenEventArgs.cs b/AMWD.Protocols.Modbus.Common/Events/CoilWrittenEventArgs.cs index a60b154..4755dd3 100644 --- a/AMWD.Protocols.Modbus.Common/Events/CoilWrittenEventArgs.cs +++ b/AMWD.Protocols.Modbus.Common/Events/CoilWrittenEventArgs.cs @@ -8,19 +8,26 @@ namespace AMWD.Protocols.Modbus.Common.Events [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class CoilWrittenEventArgs : EventArgs { + internal CoilWrittenEventArgs(byte unitId, ushort address, bool value) + { + UnitId = unitId; + Address = address; + Value = value; + } + /// /// Gets or sets the unit id. /// - public byte UnitId { get; set; } + public byte UnitId { get; } /// /// Gets or sets the coil address. /// - public ushort Address { get; set; } + public ushort Address { get; } /// /// Gets or sets the coil value. /// - public bool Value { get; set; } + public bool Value { get; } } } diff --git a/AMWD.Protocols.Modbus.Common/Events/RegisterWrittenEventArgs.cs b/AMWD.Protocols.Modbus.Common/Events/RegisterWrittenEventArgs.cs index 51ea589..cfc786d 100644 --- a/AMWD.Protocols.Modbus.Common/Events/RegisterWrittenEventArgs.cs +++ b/AMWD.Protocols.Modbus.Common/Events/RegisterWrittenEventArgs.cs @@ -8,29 +8,39 @@ namespace AMWD.Protocols.Modbus.Common.Events [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class RegisterWrittenEventArgs : EventArgs { + internal RegisterWrittenEventArgs(byte unitId, ushort address, byte highByte, byte lowByte) + { + UnitId = unitId; + Address = address; + HighByte = highByte; + LowByte = lowByte; + + Value = new[] { highByte, lowByte }.GetBigEndianUInt16(); + } + /// /// Gets or sets the unit id. /// - public byte UnitId { get; set; } + public byte UnitId { get; } /// /// Gets or sets the address of the register. /// - public ushort Address { get; set; } + public ushort Address { get; } /// /// Gets or sets the value of the register. /// - public ushort Value { get; set; } + public ushort Value { get; } /// /// Gets or sets the high byte of the register. /// - public byte HighByte { get; set; } + public byte HighByte { get; } /// /// Gets or sets the low byte of the register. /// - public byte LowByte { get; set; } + public byte LowByte { get; } } } diff --git a/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs b/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs index 78623d7..09535bc 100644 --- a/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs +++ b/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace AMWD.Protocols.Modbus.Common @@ -12,14 +13,14 @@ namespace AMWD.Protocols.Modbus.Common Array.Reverse(bytes); } - public static ushort GetBigEndianUInt16(this byte[] bytes, int offset = 0) + public static ushort GetBigEndianUInt16(this IReadOnlyList bytes, int offset = 0) { byte[] b = bytes.Skip(offset).Take(2).ToArray(); b.SwapBigEndian(); return BitConverter.ToUInt16(b, 0); } - public static byte[] ToBigEndianBytes(this ushort value) + public static IReadOnlyList ToBigEndianBytes(this ushort value) { byte[] b = BitConverter.GetBytes(value); b.SwapBigEndian(); diff --git a/AMWD.Protocols.Modbus.Common/Models/HoldingRegister.cs b/AMWD.Protocols.Modbus.Common/Models/HoldingRegister.cs index 67f9329..9f51a8f 100644 --- a/AMWD.Protocols.Modbus.Common/Models/HoldingRegister.cs +++ b/AMWD.Protocols.Modbus.Common/Models/HoldingRegister.cs @@ -17,15 +17,11 @@ namespace AMWD.Protocols.Modbus.Common { get { - byte[] blob = [HighByte, LowByte]; - blob.SwapBigEndian(); - return BitConverter.ToUInt16(blob, 0); + return new[] { HighByte, LowByte }.GetBigEndianUInt16(); } set { - byte[] blob = BitConverter.GetBytes(value); - blob.SwapBigEndian(); - + var blob = value.ToBigEndianBytes(); HighByte = blob[0]; LowByte = blob[1]; } diff --git a/AMWD.Protocols.Modbus.Common/Models/InputRegister.cs b/AMWD.Protocols.Modbus.Common/Models/InputRegister.cs index 8b2a186..7c24538 100644 --- a/AMWD.Protocols.Modbus.Common/Models/InputRegister.cs +++ b/AMWD.Protocols.Modbus.Common/Models/InputRegister.cs @@ -17,9 +17,7 @@ namespace AMWD.Protocols.Modbus.Common { get { - byte[] blob = [HighByte, LowByte]; - blob.SwapBigEndian(); - return BitConverter.ToUInt16(blob, 0); + return new[] { HighByte, LowByte }.GetBigEndianUInt16(); } } diff --git a/AMWD.Protocols.Modbus.Common/Models/ModbusDevice.cs b/AMWD.Protocols.Modbus.Common/Models/ModbusDevice.cs index 97fc22e..e4f67db 100644 --- a/AMWD.Protocols.Modbus.Common/Models/ModbusDevice.cs +++ b/AMWD.Protocols.Modbus.Common/Models/ModbusDevice.cs @@ -11,7 +11,7 @@ namespace AMWD.Protocols.Modbus.Common.Models /// Initializes a new instance of the class. /// /// The ID. - public class ModbusDevice(byte id) : IDisposable + internal class ModbusDevice(byte id) : IDisposable { private readonly ReaderWriterLockSlim _rwLockCoils = new(); private readonly ReaderWriterLockSlim _rwLockDiscreteInputs = new(); diff --git a/AMWD.Protocols.Modbus.Common/Protocols/AsciiProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/AsciiProtocol.cs index 03ebb6c..62c4342 100644 --- a/AMWD.Protocols.Modbus.Common/Protocols/AsciiProtocol.cs +++ b/AMWD.Protocols.Modbus.Common/Protocols/AsciiProtocol.cs @@ -92,11 +92,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadCoils:X2}"; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; // LRC @@ -151,11 +151,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadDiscreteInputs:X2}"; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; // LRC @@ -209,11 +209,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadHoldingRegisters:X2}"; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; // LRC @@ -264,11 +264,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadInputRegisters:X2}"; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; // LRC @@ -383,7 +383,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleCoil:X2}"; // Starting address - byte[] addrBytes = coil.Address.ToBigEndianBytes(); + var addrBytes = coil.Address.ToBigEndianBytes(); request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; // Value @@ -426,7 +426,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleRegister:X2}"; // Starting address - byte[] addrBytes = register.Address.ToBigEndianBytes(); + var addrBytes = register.Address.ToBigEndianBytes(); request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; // Value @@ -497,11 +497,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleCoils:X2}"; // Starting address - byte[] addrBytes = firstAddress.ToBigEndianBytes(); + var addrBytes = firstAddress.ToBigEndianBytes(); request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; // Quantity - byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); + var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; // Byte count @@ -567,11 +567,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleRegisters:X2}"; // Starting address - byte[] addrBytes = firstAddress.ToBigEndianBytes(); + var addrBytes = firstAddress.ToBigEndianBytes(); request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}"; // Quantity - byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); + var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); request += $"{countBytes[0]:X2}{countBytes[1]:X2}"; // Byte count diff --git a/AMWD.Protocols.Modbus.Common/Protocols/RtuOverTcpProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/RtuOverTcpProtocol.cs index 3815822..c85a94a 100644 --- a/AMWD.Protocols.Modbus.Common/Protocols/RtuOverTcpProtocol.cs +++ b/AMWD.Protocols.Modbus.Common/Protocols/RtuOverTcpProtocol.cs @@ -119,12 +119,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.ReadCoils; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -182,12 +182,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -245,12 +245,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -305,12 +305,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.ReadInputRegisters; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -432,7 +432,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols // Function code request[7] = (byte)ModbusFunctionCode.WriteSingleCoil; - byte[] addrBytes = coil.Address.ToBigEndianBytes(); + var addrBytes = coil.Address.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; @@ -479,7 +479,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols // Function code request[7] = (byte)ModbusFunctionCode.WriteSingleRegister; - byte[] addrBytes = register.Address.ToBigEndianBytes(); + var addrBytes = register.Address.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; @@ -542,12 +542,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils; // Starting address - byte[] addrBytes = firstAddress.ToBigEndianBytes(); + var addrBytes = firstAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); + var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -622,12 +622,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters; // Starting address - byte[] addrBytes = firstAddress.ToBigEndianBytes(); + var addrBytes = firstAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); + var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -747,7 +747,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols // Transaction id ushort txId = GetNextTransacitonId(); - byte[] txBytes = txId.ToBigEndianBytes(); + var txBytes = txId.ToBigEndianBytes(); header[0] = txBytes[0]; header[1] = txBytes[1]; @@ -756,7 +756,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols header[3] = 0x00; // Number of following bytes - byte[] countBytes = ((ushort)followingBytes).ToBigEndianBytes(); + var countBytes = ((ushort)followingBytes).ToBigEndianBytes(); header[4] = countBytes[0]; header[5] = countBytes[1]; diff --git a/AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs index 5a7109a..cdd9a0b 100644 --- a/AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs +++ b/AMWD.Protocols.Modbus.Common/Protocols/RtuProtocol.cs @@ -96,12 +96,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[1] = (byte)ModbusFunctionCode.ReadCoils; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[2] = addrBytes[0]; request[3] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[4] = countBytes[0]; request[5] = countBytes[1]; @@ -156,12 +156,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[1] = (byte)ModbusFunctionCode.ReadDiscreteInputs; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[2] = addrBytes[0]; request[3] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[4] = countBytes[0]; request[5] = countBytes[1]; @@ -216,12 +216,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[1] = (byte)ModbusFunctionCode.ReadHoldingRegisters; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[2] = addrBytes[0]; request[3] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[4] = countBytes[0]; request[5] = countBytes[1]; @@ -273,12 +273,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[1] = (byte)ModbusFunctionCode.ReadInputRegisters; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[2] = addrBytes[0]; request[3] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[4] = countBytes[0]; request[5] = countBytes[1]; @@ -394,7 +394,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols // Function code request[1] = (byte)ModbusFunctionCode.WriteSingleCoil; - byte[] addrBytes = coil.Address.ToBigEndianBytes(); + var addrBytes = coil.Address.ToBigEndianBytes(); request[2] = addrBytes[0]; request[3] = addrBytes[1]; @@ -438,7 +438,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols // Function code request[1] = (byte)ModbusFunctionCode.WriteSingleRegister; - byte[] addrBytes = register.Address.ToBigEndianBytes(); + var addrBytes = register.Address.ToBigEndianBytes(); request[2] = addrBytes[0]; request[3] = addrBytes[1]; @@ -495,11 +495,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[1] = (byte)ModbusFunctionCode.WriteMultipleCoils; - byte[] addrBytes = firstAddress.ToBigEndianBytes(); + var addrBytes = firstAddress.ToBigEndianBytes(); request[2] = addrBytes[0]; request[3] = addrBytes[1]; - byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); + var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); request[4] = countBytes[0]; request[5] = countBytes[1]; @@ -565,11 +565,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[0] = unitId; request[1] = (byte)ModbusFunctionCode.WriteMultipleRegisters; - byte[] addrBytes = firstAddress.ToBigEndianBytes(); + var addrBytes = firstAddress.ToBigEndianBytes(); request[2] = addrBytes[0]; request[3] = addrBytes[1]; - byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); + var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); request[4] = countBytes[0]; request[5] = countBytes[1]; diff --git a/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs index 4cd1d5c..21f142d 100644 --- a/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs +++ b/AMWD.Protocols.Modbus.Common/Protocols/TcpProtocol.cs @@ -101,12 +101,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.ReadCoils; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -159,12 +159,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -217,12 +217,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -272,12 +272,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.ReadInputRegisters; // Starting address - byte[] addrBytes = startAddress.ToBigEndianBytes(); + var addrBytes = startAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = count.ToBigEndianBytes(); + var countBytes = count.ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -389,7 +389,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols // Function code request[7] = (byte)ModbusFunctionCode.WriteSingleCoil; - byte[] addrBytes = coil.Address.ToBigEndianBytes(); + var addrBytes = coil.Address.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; @@ -431,7 +431,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols // Function code request[7] = (byte)ModbusFunctionCode.WriteSingleRegister; - byte[] addrBytes = register.Address.ToBigEndianBytes(); + var addrBytes = register.Address.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; @@ -489,12 +489,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils; // Starting address - byte[] addrBytes = firstAddress.ToBigEndianBytes(); + var addrBytes = firstAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); + var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -564,12 +564,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters; // Starting address - byte[] addrBytes = firstAddress.ToBigEndianBytes(); + var addrBytes = firstAddress.ToBigEndianBytes(); request[8] = addrBytes[0]; request[9] = addrBytes[1]; // Quantity - byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); + var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes(); request[10] = countBytes[0]; request[11] = countBytes[1]; @@ -678,7 +678,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols // Transaction id ushort txId = GetNextTransacitonId(); - byte[] txBytes = txId.ToBigEndianBytes(); + var txBytes = txId.ToBigEndianBytes(); header[0] = txBytes[0]; header[1] = txBytes[1]; @@ -687,7 +687,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols header[3] = 0x00; // Number of following bytes - byte[] countBytes = ((ushort)followingBytes).ToBigEndianBytes(); + var countBytes = ((ushort)followingBytes).ToBigEndianBytes(); header[4] = countBytes[0]; header[5] = countBytes[1]; diff --git a/AMWD.Protocols.Modbus.Common/Protocols/VirtualProtocol.cs b/AMWD.Protocols.Modbus.Common/Protocols/VirtualProtocol.cs new file mode 100644 index 0000000..3d0ef6d --- /dev/null +++ b/AMWD.Protocols.Modbus.Common/Protocols/VirtualProtocol.cs @@ -0,0 +1,478 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AMWD.Protocols.Modbus.Common.Contracts; +using AMWD.Protocols.Modbus.Common.Events; +using AMWD.Protocols.Modbus.Common.Models; + +namespace AMWD.Protocols.Modbus.Common.Protocols +{ + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal class VirtualProtocol : IModbusProtocol, IDisposable + { + #region Fields + + private bool _isDisposed; + + private readonly ReaderWriterLockSlim _deviceListLock = new(); + private readonly Dictionary _devices = []; + + #endregion Fields + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + + _deviceListLock.Dispose(); + + foreach (var device in _devices.Values) + device.Dispose(); + + _devices.Clear(); + } + + #region Events + + public event EventHandler CoilWritten; + + public event EventHandler RegisterWritten; + + #endregion Events + + #region Properties + + public string Name => nameof(VirtualProtocol); + + #endregion Properties + + #region Protocol + + public bool CheckResponseComplete(IReadOnlyList responseBytes) => true; + + public IReadOnlyList DeserializeReadCoils(IReadOnlyList response) + { + if (!_devices.TryGetValue(response[0], out var device)) + throw new TimeoutException("Device not found."); + + ushort start = response.GetBigEndianUInt16(1); + ushort count = response.GetBigEndianUInt16(3); + + return Enumerable.Range(0, count) + .Select(i => device.GetCoil((ushort)(start + i))) + .ToList(); + } + + public DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList response) + { + if (!_devices.TryGetValue(response[0], out var device)) + throw new TimeoutException("Device not found."); + + var result = new DeviceIdentificationRaw + { + AllowsIndividualAccess = false, + MoreRequestsNeeded = false, + Objects = [] + }; + + if (response[1] >= 1) + { + string version = GetType().Assembly + .GetCustomAttribute() + .InformationalVersion; + + result.Objects.Add(0, Encoding.UTF8.GetBytes("AM.WD")); + result.Objects.Add(1, Encoding.UTF8.GetBytes("AMWD.Protocols.Modbus")); + result.Objects.Add(2, Encoding.UTF8.GetBytes(version)); + } + + if (response[1] >= 2) + { + result.Objects.Add(3, Encoding.UTF8.GetBytes("https://github.com/AM-WD/AMWD.Protocols.Modbus")); + result.Objects.Add(4, Encoding.UTF8.GetBytes("Modbus Protocol for .NET")); + result.Objects.Add(5, Encoding.UTF8.GetBytes("Virtual Device")); + result.Objects.Add(6, Encoding.UTF8.GetBytes("Virtual Modbus Client")); + } + + if (response[1] >= 3) + { + for (int i = 128; i < 256; i++) + result.Objects.Add((byte)i, []); + } + + return result; + } + + public IReadOnlyList DeserializeReadDiscreteInputs(IReadOnlyList response) + { + if (!_devices.TryGetValue(response[0], out var device)) + throw new TimeoutException("Device not found."); + + ushort start = response.GetBigEndianUInt16(1); + ushort count = response.GetBigEndianUInt16(3); + + return Enumerable.Range(0, count) + .Select(i => device.GetDiscreteInput((ushort)(start + i))) + .ToList(); + } + + public IReadOnlyList DeserializeReadHoldingRegisters(IReadOnlyList response) + { + if (!_devices.TryGetValue(response[0], out var device)) + throw new TimeoutException("Device not found."); + + ushort start = response.GetBigEndianUInt16(1); + ushort count = response.GetBigEndianUInt16(3); + + return Enumerable.Range(0, count) + .Select(i => device.GetHoldingRegister((ushort)(start + i))) + .ToList(); + } + + public IReadOnlyList DeserializeReadInputRegisters(IReadOnlyList response) + { + if (!_devices.TryGetValue(response[0], out var device)) + throw new TimeoutException("Device not found."); + + ushort start = response.GetBigEndianUInt16(1); + ushort count = response.GetBigEndianUInt16(3); + + return Enumerable.Range(0, count) + .Select(i => device.GetInputRegister((ushort)(start + i))) + .ToList(); + } + + public (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList response) + { + if (!_devices.TryGetValue(response[0], out var device)) + throw new TimeoutException("Device not found."); + + ushort start = response.GetBigEndianUInt16(1); + ushort count = response.GetBigEndianUInt16(3); + + for (int i = 0; i < count; i++) + { + var coil = new Coil + { + Address = (ushort)(start + i), + HighByte = response[5 + i] + }; + device.SetCoil(coil); + + Task.Run(() => + { + try + { + CoilWritten?.Invoke(this, new CoilWrittenEventArgs( + unitId: response[0], + address: coil.Address, + value: coil.Value)); + } + catch + { + // ignore + } + }); + } + + return (start, count); + } + + public (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList response) + { + if (!_devices.TryGetValue(response[0], out var device)) + throw new TimeoutException("Device not found."); + + ushort start = response.GetBigEndianUInt16(1); + ushort count = response.GetBigEndianUInt16(3); + + for (int i = 0; i < count; i++) + { + var register = new HoldingRegister + { + Address = (ushort)(start + i), + HighByte = response[5 + i * 2], + LowByte = response[5 + i * 2 + 1] + }; + device.SetHoldingRegister(register); + + Task.Run(() => + { + try + { + RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs( + unitId: response[0], + address: register.Address, + highByte: register.HighByte, + lowByte: register.LowByte)); + } + catch + { + // ignore + } + }); + } + + return (start, count); + } + + public Coil DeserializeWriteSingleCoil(IReadOnlyList response) + { + if (!_devices.TryGetValue(response[0], out var device)) + throw new TimeoutException("Device not found."); + + var coil = new Coil + { + Address = response.GetBigEndianUInt16(1), + HighByte = response[3] + }; + device.SetCoil(coil); + + Task.Run(() => + { + try + { + CoilWritten?.Invoke(this, new CoilWrittenEventArgs( + unitId: response[0], + address: coil.Address, + value: coil.Value)); + } + catch + { + // ignore + } + }); + + return coil; + } + + public HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList response) + { + if (!_devices.TryGetValue(response[0], out var device)) + throw new TimeoutException("Device not found."); + + var register = new HoldingRegister + { + Address = response.GetBigEndianUInt16(1), + HighByte = response[3], + LowByte = response[4] + }; + device.SetHoldingRegister(register); + + Task.Run(() => + { + try + { + RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs( + unitId: response[0], + address: register.Address, + highByte: register.HighByte, + lowByte: register.LowByte)); + } + catch + { + // ignore + } + }); + + return register; + } + + public IReadOnlyList SerializeReadCoils(byte unitId, ushort startAddress, ushort count) + { + return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()]; + } + + public IReadOnlyList SerializeReadDeviceIdentification(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId) + { + if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category)) + throw new ArgumentOutOfRangeException(nameof(category)); + + return [unitId, (byte)category]; + } + + public IReadOnlyList SerializeReadDiscreteInputs(byte unitId, ushort startAddress, ushort count) + { + return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()]; + } + + public IReadOnlyList SerializeReadHoldingRegisters(byte unitId, ushort startAddress, ushort count) + { + return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()]; + } + + public IReadOnlyList SerializeReadInputRegisters(byte unitId, ushort startAddress, ushort count) + { + return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()]; + } + + public IReadOnlyList SerializeWriteMultipleCoils(byte unitId, IReadOnlyList coils) + { + ushort start = coils.OrderBy(c => c.Address).First().Address; + ushort count = (ushort)coils.Count; + byte[] values = coils.Select(c => c.HighByte).ToArray(); + + return [unitId, .. start.ToBigEndianBytes(), .. count.ToBigEndianBytes(), .. values]; + } + + public IReadOnlyList SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList registers) + { + ushort start = registers.OrderBy(c => c.Address).First().Address; + ushort count = (ushort)registers.Count; + byte[] values = registers.SelectMany(r => new[] { r.HighByte, r.LowByte }).ToArray(); + + return [unitId, .. start.ToBigEndianBytes(), .. count.ToBigEndianBytes(), .. values]; + } + + public IReadOnlyList SerializeWriteSingleCoil(byte unitId, Coil coil) + { + return [unitId, .. coil.Address.ToBigEndianBytes(), coil.HighByte]; + } + + public IReadOnlyList SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register) + { + return [unitId, .. register.Address.ToBigEndianBytes(), register.HighByte, register.LowByte]; + } + + public void ValidateResponse(IReadOnlyList request, IReadOnlyList response) + { + if (!request.SequenceEqual(response)) + throw new InvalidOperationException("Request and response have to be the same on virtual protocol."); + } + + #endregion Protocol + + #region Device Handling + + public bool AddDevice(byte unitId) + { + Assertions(); + using (_deviceListLock.GetWriteLock()) + { + if (_devices.ContainsKey(unitId)) + return false; + + _devices.Add(unitId, new ModbusDevice(unitId)); + return true; + } + } + + public bool RemoveDevice(byte unitId) + { + Assertions(); + using (_deviceListLock.GetWriteLock()) + { + if (_devices.ContainsKey(unitId)) + return false; + + return _devices.Remove(unitId); + } + } + + #endregion Device Handling + + #region Entity Handling + + public Coil GetCoil(byte unitId, ushort address) + { + Assertions(); + using (_deviceListLock.GetReadLock()) + { + return _devices.TryGetValue(unitId, out var device) + ? device.GetCoil(address) + : null; + } + } + + public void SetCoil(byte unitId, Coil coil) + { + Assertions(); + using (_deviceListLock.GetWriteLock()) + { + if (_devices.TryGetValue(unitId, out var device)) + device.SetCoil(coil); + } + } + + public DiscreteInput GetDiscreteInput(byte unitId, ushort address) + { + Assertions(); + using (_deviceListLock.GetReadLock()) + { + return _devices.TryGetValue(unitId, out var device) + ? device.GetDiscreteInput(address) + : null; + } + } + + public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput) + { + Assertions(); + using (_deviceListLock.GetWriteLock()) + { + if (_devices.TryGetValue(unitId, out var device)) + device.SetDiscreteInput(discreteInput); + } + } + + public HoldingRegister GetHoldingRegister(byte unitId, ushort address) + { + Assertions(); + using (_deviceListLock.GetReadLock()) + { + return _devices.TryGetValue(unitId, out var device) + ? device.GetHoldingRegister(address) + : null; + } + } + + public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister) + { + Assertions(); + using (_deviceListLock.GetWriteLock()) + { + if (_devices.TryGetValue(unitId, out var device)) + device.SetHoldingRegister(holdingRegister); + } + } + + public InputRegister GetInputRegister(byte unitId, ushort address) + { + Assertions(); + using (_deviceListLock.GetReadLock()) + { + return _devices.TryGetValue(unitId, out var device) + ? device.GetInputRegister(address) + : null; + } + } + + public void SetInputRegister(byte unitId, InputRegister inputRegister) + { + Assertions(); + using (_deviceListLock.GetWriteLock()) + { + if (_devices.TryGetValue(unitId, out var device)) + device.SetInputRegister(inputRegister); + } + } + + #endregion Entity Handling + + private void Assertions() + { +#if NET8_0_OR_GREATER + ObjectDisposedException.ThrowIf(_isDisposed, this); +#else + if (_isDisposed) + throw new ObjectDisposedException(GetType().Name); +#endif + } + } +} diff --git a/AMWD.Protocols.Modbus.Common/README.md b/AMWD.Protocols.Modbus.Common/README.md index 43326f1..97c462b 100644 --- a/AMWD.Protocols.Modbus.Common/README.md +++ b/AMWD.Protocols.Modbus.Common/README.md @@ -50,7 +50,8 @@ The different types handled by the Modbus Protocol. In addition, you'll find the `DeviceIdentification` there. It is used for a "special" function called _Read Device Identification_ (0x2B / 43), not supported on all devices. -The `ModbusDevice` is used for the server implementations in the derived packages. +The `ModbusDevice` is used for the `VirtualModbusClient`. +In combination with the *Proxy implementations (in the derived packages) it can be used as server. ### Protocols diff --git a/AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs b/AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs new file mode 100644 index 0000000..26dd52f --- /dev/null +++ b/AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using AMWD.Protocols.Modbus.Common.Contracts; +using AMWD.Protocols.Modbus.Common.Events; +using AMWD.Protocols.Modbus.Common.Models; +using AMWD.Protocols.Modbus.Common.Protocols; + +namespace AMWD.Protocols.Modbus.Common.Utils +{ + /// + /// Implements a virtual Modbus client. + /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class VirtualModbusClient : ModbusClientBase + { + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// DO NOT MODIFY connection or protocol. + public VirtualModbusClient() + : base(new VirtualConnection()) + { + Protocol = new VirtualProtocol(); + + TypedProtocol.CoilWritten += (sender, e) => CoilWritten?.Invoke(this, e); + TypedProtocol.RegisterWritten += (sender, e) => RegisterWritten?.Invoke(this, e); + } + + #endregion Constructor + + #region Events + + /// + /// Indicates that a -value received through a remote client has been written. + /// + public event EventHandler CoilWritten; + + /// + /// Indicates that a -value received from a remote client has been written. + /// + public event EventHandler RegisterWritten; + + #endregion Events + + #region Properties + + internal VirtualProtocol TypedProtocol => Protocol as VirtualProtocol; + + #endregion Properties + + #region Device Handling + + /// + /// Adds a device to the virtual client. + /// + /// The unit id of the device. + /// if the device was added successfully, otherwise. + public bool AddDevice(byte unitId) + => TypedProtocol.AddDevice(unitId); + + /// + /// Removes a device from the virtual client. + /// + /// The unit id of the device. + /// if the device was removed successfully, otherwise. + public bool RemoveDevice(byte unitId) + => TypedProtocol.RemoveDevice(unitId); + + #endregion Device Handling + + #region Entity Handling + + /// + /// Gets a from the specified . + /// + /// The unit ID of the device. + /// The address of the . + public Coil GetCoil(byte unitId, ushort address) + => TypedProtocol.GetCoil(unitId, address); + + /// + /// Sets a to the specified . + /// + /// The unit ID of the device. + /// The to set. + public void SetCoil(byte unitId, Coil coil) + => TypedProtocol.SetCoil(unitId, coil); + + /// + /// Gets a from the specified . + /// + /// The unit ID of the device. + /// The address of the . + public DiscreteInput GetDiscreteInput(byte unitId, ushort address) + => TypedProtocol.GetDiscreteInput(unitId, address); + + /// + /// Sets a to the specified . + /// + /// The unit ID of the device. + /// The to set. + public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput) + => TypedProtocol.SetDiscreteInput(unitId, discreteInput); + + /// + /// Gets a from the specified . + /// + /// The unit ID of the device. + /// The address of the . + public HoldingRegister GetHoldingRegister(byte unitId, ushort address) + => TypedProtocol.GetHoldingRegister(unitId, address); + + /// + /// Sets a to the specified . + /// + /// The unit ID of the device. + /// The to set. + public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister) + => TypedProtocol.SetHoldingRegister(unitId, holdingRegister); + + /// + /// Gets a from the specified . + /// + /// The unit ID of the device. + /// The address of the . + public InputRegister GetInputRegister(byte unitId, ushort address) + => TypedProtocol.GetInputRegister(unitId, address); + + /// + /// Sets a to the specified . + /// + /// The unit ID of the device. + /// The to set. + public void SetInputRegister(byte unitId, InputRegister inputRegister) + => TypedProtocol.SetInputRegister(unitId, inputRegister); + + #endregion Entity Handling + + #region Methods + + /// + protected override void Dispose(bool disposing) + { + TypedProtocol.Dispose(); + base.Dispose(disposing); + } + + #endregion Methods + + #region Connection + + internal class VirtualConnection : IModbusConnection + { + public string Name => nameof(VirtualConnection); + + public TimeSpan IdleTimeout { get; set; } + + public TimeSpan ConnectTimeout { get; set; } + + public TimeSpan ReadTimeout { get; set; } + + public TimeSpan WriteTimeout { get; set; } + + public void Dispose() + { /* nothing to do */ } + + public Task> InvokeAsync( + IReadOnlyList request, + Func, bool> validateResponseComplete, + CancellationToken cancellationToken = default) => Task.FromResult(request); + } + + #endregion Connection + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6201a72..3ffa58f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Small CLI tool to test Modbus client communication. +- `VirtualModbusClient` added to `AMWD.Protocols.Modbus.Common`. ### Changed