diff --git a/AMWD.Protocols.Modbus.Common/README.md b/AMWD.Protocols.Modbus.Common/README.md
index 4f29a1b..ce2a2f2 100644
--- a/AMWD.Protocols.Modbus.Common/README.md
+++ b/AMWD.Protocols.Modbus.Common/README.md
@@ -14,7 +14,7 @@ If you want to speak a custom type of protocol with the clients, you can impleme
**ModbusBaseClient**
This abstract base client contains all the basic methods and handlings required to communicate via Modbus Protocol.
-The packages `AMWD.Protocols.Modbus.Serial` _(in progress)_ and `AMWD.Protocols.Modbus.Tcp` have specific derived implementations to match the communication types.
+The packages `AMWD.Protocols.Modbus.Serial` and `AMWD.Protocols.Modbus.Tcp` have specific derived implementations to match the communication types.
### Enums
@@ -63,8 +63,8 @@ Here you have the specific default implementations for the Modbus Protocol.
- TCP
**NOTE:**
-The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) referring to the specification.
-This validation is _not_ implemented here due to real world experience, that some manufactures do not care about it.
+The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) and maximum unit ID of 247 referring to the specification.
+This validation is _not_ implemented here due to real world experience, that some manufactures don't care about it.
---
diff --git a/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs b/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs
index 35b1873..6c3e62c 100644
--- a/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs
+++ b/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs
@@ -90,7 +90,13 @@ namespace AMWD.Protocols.Modbus.Serial
///
/// Gets or sets a wait-time between requests.
///
- public virtual TimeSpan InterRequestDelay { get; set; } = TimeSpan.Zero;
+ ///
+ /// The specification says:
+ ///
+ /// For baud rates greater than 19.2k Bps, fixed values for the two timers should be used:
+ /// [...] a value of 1.750ms for inter-frame delay (t_3.5).
+ ///
+ public virtual TimeSpan InterRequestDelay { get; set; } = TimeSpan.FromMilliseconds(1.75);
#region SerialPort Properties
diff --git a/AMWD.Protocols.Modbus.Serial/ModbusSerialServer.cs b/AMWD.Protocols.Modbus.Serial/ModbusSerialServer.cs
new file mode 100644
index 0000000..9b2305e
--- /dev/null
+++ b/AMWD.Protocols.Modbus.Serial/ModbusSerialServer.cs
@@ -0,0 +1,1174 @@
+using System;
+using System.Collections.Generic;
+using System.IO.Ports;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Protocols.Modbus.Common;
+using AMWD.Protocols.Modbus.Common.Events;
+using AMWD.Protocols.Modbus.Common.Models;
+using AMWD.Protocols.Modbus.Common.Protocols;
+
+namespace AMWD.Protocols.Modbus.Serial
+{
+ ///
+ /// A basic implementation of a Modbus serial line server.
+ ///
+ [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public class ModbusSerialServer : IDisposable
+ {
+ #region Fields
+
+ private bool _isDisposed;
+
+ private SerialPort _serialPort;
+ private CancellationTokenSource _stopCts;
+
+ private readonly ReaderWriterLockSlim _deviceListLock = new();
+ private readonly Dictionary _devices = [];
+
+ #endregion Fields
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the serial port to use.
+ /// The baud rate of the serial port (Default: 19.200).
+ public ModbusSerialServer(string portName, BaudRate baudRate = BaudRate.Baud19200)
+ {
+ if (string.IsNullOrWhiteSpace(portName))
+ throw new ArgumentNullException(nameof(portName));
+
+ if (!Enum.IsDefined(typeof(BaudRate), baudRate))
+ throw new ArgumentOutOfRangeException(nameof(baudRate));
+
+ if (!ModbusSerialClient.AvailablePortNames.Contains(portName))
+ throw new ArgumentException($"The serial port ({portName}) is not available.", nameof(portName));
+
+ _serialPort = new SerialPort
+ {
+ PortName = portName,
+ BaudRate = (int)baudRate,
+ Handshake = Handshake.None,
+ DataBits = 8,
+ ReadTimeout = 1000,
+ RtsEnable = false,
+ StopBits = StopBits.One,
+ WriteTimeout = 1000,
+ Parity = Parity.Even
+ };
+ }
+
+ #endregion Constructors
+
+ #region Events
+
+ ///
+ /// Occurs when a is written.
+ ///
+ public event EventHandler CoilWritten;
+
+ ///
+ /// Occurs when a is written.
+ ///
+ public event EventHandler RegisterWritten;
+
+ #endregion Events
+
+ #region Properties
+
+ ///
+ public string PortName => _serialPort.PortName;
+
+ ///
+ /// Gets or sets the baud rate of the serial port.
+ ///
+ public BaudRate BaudRate
+ {
+ get => (BaudRate)_serialPort.BaudRate;
+ set => _serialPort.BaudRate = (int)value;
+ }
+
+ ///
+ public Handshake Handshake
+ {
+ get => _serialPort.Handshake;
+ set => _serialPort.Handshake = value;
+ }
+
+ ///
+ public int DataBits
+ {
+ get => _serialPort.DataBits;
+ set => _serialPort.DataBits = value;
+ }
+
+ ///
+ public bool IsOpen => _serialPort.IsOpen;
+
+ ///
+ /// Gets or sets the before a time-out occurs when a read operation does not finish.
+ ///
+ public TimeSpan ReadTimeout
+ {
+ get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
+ set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
+ }
+
+ ///
+ public bool RtsEnable
+ {
+ get => _serialPort.RtsEnable;
+ set => _serialPort.RtsEnable = value;
+ }
+
+ ///
+ public StopBits StopBits
+ {
+ get => _serialPort.StopBits;
+ set => _serialPort.StopBits = value;
+ }
+
+ ///
+ /// Gets or sets the before a time-out occurs when a write operation does not finish.
+ ///
+ public TimeSpan WriteTimeout
+ {
+ get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
+ set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
+ }
+
+ ///
+ public Parity Parity
+ {
+ get => _serialPort.Parity;
+ set => _serialPort.Parity = value;
+ }
+
+ #endregion Properties
+
+ #region Control Methods
+
+ ///
+ /// Starts the server.
+ ///
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ Assertions();
+
+ _stopCts?.Cancel();
+ _serialPort.Close();
+ _serialPort.DataReceived -= OnDataReceived;
+
+ _stopCts?.Dispose();
+ _stopCts = new CancellationTokenSource();
+
+ _serialPort.DataReceived += OnDataReceived;
+ _serialPort.Open();
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Stops the server.
+ ///
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public Task StopAsync(CancellationToken cancellationToken = default)
+ {
+ Assertions();
+ return StopAsyncInternal(cancellationToken);
+ }
+
+ private Task StopAsyncInternal(CancellationToken cancellationToken)
+ {
+ _stopCts.Cancel();
+
+ _serialPort.Close();
+ _serialPort.DataReceived -= OnDataReceived;
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Releases all managed and unmanaged resources used by the .
+ ///
+ public void Dispose()
+ {
+ if (_isDisposed)
+ return;
+
+ _isDisposed = true;
+
+ StopAsyncInternal(CancellationToken.None).Wait();
+
+ _deviceListLock.Dispose();
+ _devices.Clear();
+ }
+
+ private void Assertions()
+ {
+#if NET8_0_OR_GREATER
+ ObjectDisposedException.ThrowIf(_isDisposed, this);
+#else
+ if (_isDisposed)
+ throw new ObjectDisposedException(GetType().FullName);
+#endif
+ }
+
+ #endregion Control Methods
+
+ #region Client Handling
+
+ private void OnDataReceived(object _, SerialDataReceivedEventArgs evArgs)
+ {
+ try
+ {
+ var requestBytes = new List();
+ do
+ {
+ byte[] buffer = new byte[RtuProtocol.MAX_ADU_LENGTH];
+ int count = _serialPort.Read(buffer, 0, buffer.Length);
+ requestBytes.AddRange(buffer.Take(count));
+
+ _stopCts.Token.ThrowIfCancellationRequested();
+ }
+ while (_serialPort.BytesToRead > 0);
+
+ _stopCts.Token.ThrowIfCancellationRequested();
+ byte[] responseBytes = HandleRequest(requestBytes.ToArray());
+ if (responseBytes == null)
+ return;
+
+ _stopCts.Token.ThrowIfCancellationRequested();
+ _serialPort.Write(responseBytes, 0, responseBytes.Length);
+ }
+ catch
+ { /* keep it quiet */ }
+ }
+
+ #endregion Client Handling
+
+ #region Request Handling
+
+ private byte[] HandleRequest(byte[] requestBytes)
+ {
+ byte[] recvCrc = requestBytes.Skip(requestBytes.Length - 2).ToArray();
+ byte[] calcCrc = RtuProtocol.CRC16(requestBytes, 0, requestBytes.Length - 2);
+ if (!recvCrc.SequenceEqual(calcCrc))
+ return null;
+
+ using (_deviceListLock.GetReadLock())
+ {
+ // No response is sent, if the device is not known
+ if (!_devices.TryGetValue(requestBytes[0], out var device))
+ return null;
+
+ switch ((ModbusFunctionCode)requestBytes[1])
+ {
+ case ModbusFunctionCode.ReadCoils:
+ return HandleReadCoils(device, requestBytes);
+
+ case ModbusFunctionCode.ReadDiscreteInputs:
+ return HandleReadDiscreteInputs(device, requestBytes);
+
+ case ModbusFunctionCode.ReadHoldingRegisters:
+ return HandleReadHoldingRegisters(device, requestBytes);
+
+ case ModbusFunctionCode.ReadInputRegisters:
+ return HandleReadInputRegisters(device, requestBytes);
+
+ case ModbusFunctionCode.WriteSingleCoil:
+ return HandleWriteSingleCoil(device, requestBytes);
+
+ case ModbusFunctionCode.WriteSingleRegister:
+ return HandleWriteSingleRegister(device, requestBytes);
+
+ case ModbusFunctionCode.WriteMultipleCoils:
+ return HandleWriteMultipleCoils(device, requestBytes);
+
+ case ModbusFunctionCode.WriteMultipleRegisters:
+ return HandleWriteMultipleRegisters(device, requestBytes);
+
+ case ModbusFunctionCode.EncapsulatedInterface:
+ return HandleEncapsulatedInterface(requestBytes);
+
+ default: // unknown function
+ {
+ byte[] responseBytes = new byte[5];
+ Array.Copy(requestBytes, 0, responseBytes, 0, 2);
+
+ // Mark as error
+ responseBytes[1] |= 0x80;
+
+ responseBytes[2] = (byte)ModbusErrorCode.IllegalFunction;
+
+ SetCrc(responseBytes);
+ return responseBytes;
+ }
+ }
+ }
+ }
+
+ private static byte[] HandleReadCoils(ModbusDevice device, byte[] requestBytes)
+ {
+ if (requestBytes.Length < 8)
+ return null;
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(2));
+
+ ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
+ ushort count = requestBytes.GetBigEndianUInt16(4);
+
+ if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_COUNT)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ if (firstAddress + count > ushort.MaxValue)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ try
+ {
+ byte[] values = new byte[(int)Math.Ceiling(count / 8.0)];
+ for (int i = 0; i < count; i++)
+ {
+ ushort address = (ushort)(firstAddress + i);
+ if (device.GetCoil(address).Value)
+ {
+ int byteIndex = i / 8;
+ int bitIndex = i % 8;
+
+ values[byteIndex] |= (byte)(1 << bitIndex);
+ }
+ }
+
+ responseBytes.Add((byte)values.Length);
+ responseBytes.AddRange(values);
+ }
+ catch
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ private static byte[] HandleReadDiscreteInputs(ModbusDevice device, byte[] requestBytes)
+ {
+ if (requestBytes.Length < 8)
+ return null;
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(2));
+
+ ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
+ ushort count = requestBytes.GetBigEndianUInt16(4);
+
+ if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_COUNT)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ if (firstAddress + count > ushort.MaxValue)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ try
+ {
+ byte[] values = new byte[(int)Math.Ceiling(count / 8.0)];
+ for (int i = 0; i < count; i++)
+ {
+ ushort address = (ushort)(firstAddress + i);
+ if (device.GetDiscreteInput(address).Value)
+ {
+ int byteIndex = i / 8;
+ int bitIndex = i % 8;
+
+ values[byteIndex] |= (byte)(1 << bitIndex);
+ }
+ }
+
+ responseBytes.Add((byte)values.Length);
+ responseBytes.AddRange(values);
+ }
+ catch
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ private static byte[] HandleReadHoldingRegisters(ModbusDevice device, byte[] requestBytes)
+ {
+ if (requestBytes.Length < 8)
+ return null;
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(2));
+
+ ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
+ ushort count = requestBytes.GetBigEndianUInt16(4);
+
+ if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_COUNT)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ if (firstAddress + count > ushort.MaxValue)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ try
+ {
+ byte[] values = new byte[count * 2];
+ for (int i = 0; i < count; i++)
+ {
+ ushort address = (ushort)(firstAddress + i);
+ var register = device.GetHoldingRegister(address);
+
+ values[i * 2] = register.HighByte;
+ values[i * 2 + 1] = register.LowByte;
+ }
+
+ responseBytes.Add((byte)values.Length);
+ responseBytes.AddRange(values);
+ }
+ catch
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ private static byte[] HandleReadInputRegisters(ModbusDevice device, byte[] requestBytes)
+ {
+ if (requestBytes.Length < 8)
+ return null;
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(2));
+
+ ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
+ ushort count = requestBytes.GetBigEndianUInt16(4);
+
+ if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_COUNT)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ if (firstAddress + count > ushort.MaxValue)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ try
+ {
+ byte[] values = new byte[count * 2];
+ for (int i = 0; i < count; i++)
+ {
+ ushort address = (ushort)(firstAddress + i);
+ var register = device.GetInputRegister(address);
+
+ values[i * 2] = register.HighByte;
+ values[i * 2 + 1] = register.LowByte;
+ }
+
+ responseBytes.Add((byte)values.Length);
+ responseBytes.AddRange(values);
+ }
+ catch
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ private byte[] HandleWriteSingleCoil(ModbusDevice device, byte[] requestBytes)
+ {
+ if (requestBytes.Length < 8)
+ return null;
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(2));
+
+ ushort address = requestBytes.GetBigEndianUInt16(2);
+
+ if (requestBytes[4] != 0x00 && requestBytes[4] != 0xFF)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ try
+ {
+ device.SetCoil(new Coil
+ {
+ Address = address,
+ HighByte = requestBytes[4]
+ });
+
+ // Response is an echo of the request
+ responseBytes.AddRange(requestBytes.Skip(2).Take(4));
+
+ // Notify that the coil was written
+ Task.Run(() =>
+ {
+ try
+ {
+ CoilWritten?.Invoke(this, new CoilWrittenEventArgs
+ {
+ UnitId = device.Id,
+ Address = address,
+ Value = requestBytes[10] == 0xFF
+ });
+ }
+ catch
+ {
+ // keep everything quiet
+ }
+ });
+ }
+ catch
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ private byte[] HandleWriteSingleRegister(ModbusDevice device, byte[] requestBytes)
+ {
+ if (requestBytes.Length < 8)
+ return null;
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(2));
+
+ ushort address = requestBytes.GetBigEndianUInt16(2);
+ ushort value = requestBytes.GetBigEndianUInt16(4);
+
+ try
+ {
+ device.SetHoldingRegister(new HoldingRegister
+ {
+ Address = address,
+ HighByte = requestBytes[4],
+ LowByte = requestBytes[5]
+ });
+
+ // Response is an echo of the request
+ responseBytes.AddRange(requestBytes.Skip(2).Take(4));
+
+ // Notify that the register was written
+ Task.Run(() =>
+ {
+ try
+ {
+ RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs
+ {
+ UnitId = device.Id,
+ Address = address,
+ Value = value,
+ HighByte = requestBytes[10],
+ LowByte = requestBytes[11]
+ });
+ }
+ catch
+ {
+ // keep everything quiet
+ }
+ });
+ }
+ catch
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ private byte[] HandleWriteMultipleCoils(ModbusDevice device, byte[] requestBytes)
+ {
+ if (requestBytes.Length < 9)
+ return null;
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(2));
+
+ ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
+ ushort count = requestBytes.GetBigEndianUInt16(4);
+
+ int byteCount = (int)Math.Ceiling(count / 8.0);
+ if (requestBytes.Length < 9 + byteCount)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ try
+ {
+ int baseOffset = 7;
+ for (int i = 0; i < count; i++)
+ {
+ int bytePosition = i / 8;
+ int bitPosition = i % 8;
+
+ ushort address = (ushort)(firstAddress + i);
+ bool value = (requestBytes[baseOffset + bytePosition] & (1 << bitPosition)) > 0;
+
+ device.SetCoil(new Coil
+ {
+ Address = address,
+ HighByte = value ? (byte)0xFF : (byte)0x00
+ });
+
+ // Notify that the coil was written
+ Task.Run(() =>
+ {
+ try
+ {
+ CoilWritten?.Invoke(this, new CoilWrittenEventArgs
+ {
+ UnitId = device.Id,
+ Address = address,
+ Value = value
+ });
+ }
+ catch
+ {
+ // keep everything quiet
+ }
+ });
+ }
+
+ responseBytes.AddRange(requestBytes.Skip(2).Take(4));
+ }
+ catch
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ private byte[] HandleWriteMultipleRegisters(ModbusDevice device, byte[] requestBytes)
+ {
+ if (requestBytes.Length < 9)
+ return null;
+
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(8));
+
+ ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
+ ushort count = requestBytes.GetBigEndianUInt16(4);
+
+ int byteCount = count * 2;
+ if (requestBytes.Length < 9 + byteCount)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ try
+ {
+ int baseOffset = 7;
+ for (int i = 0; i < count; i++)
+ {
+ ushort address = (ushort)(firstAddress + i);
+
+ device.SetHoldingRegister(new HoldingRegister
+ {
+ Address = address,
+ HighByte = requestBytes[baseOffset + i * 2],
+ LowByte = requestBytes[baseOffset + i * 2 + 1]
+ });
+
+ // Notify that the coil was written
+ Task.Run(() =>
+ {
+ try
+ {
+ RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs
+ {
+ UnitId = device.Id,
+ Address = address,
+ Value = requestBytes.GetBigEndianUInt16(baseOffset + i * 2),
+ HighByte = requestBytes[baseOffset + i * 2],
+ LowByte = requestBytes[baseOffset + i * 2 + 1]
+ });
+ }
+ catch
+ {
+ // keep everything quiet
+ }
+ });
+ }
+
+ responseBytes.AddRange(requestBytes.Skip(2).Take(4));
+ }
+ catch
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+ }
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ private byte[] HandleEncapsulatedInterface(byte[] requestBytes)
+ {
+ var responseBytes = new List();
+ responseBytes.AddRange(requestBytes.Take(2));
+
+ if (requestBytes[2] != 0x0E)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ if (0x06 < requestBytes[4] && requestBytes[4] < 0x80)
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ var category = (ModbusDeviceIdentificationCategory)requestBytes[3];
+ if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+
+ try
+ {
+ var bodyBytes = new List();
+ // MEI, Category
+ bodyBytes.AddRange(requestBytes.Skip(2).Take(2));
+ // Conformity
+ bodyBytes.Add((byte)(category + 0x80));
+ // More, NextId, NumberOfObjects
+ bodyBytes.AddRange(new byte[3]);
+
+ int maxObjectId;
+ switch (category)
+ {
+ case ModbusDeviceIdentificationCategory.Basic:
+ maxObjectId = 0x02;
+ break;
+
+ case ModbusDeviceIdentificationCategory.Regular:
+ maxObjectId = 0x06;
+ break;
+
+ case ModbusDeviceIdentificationCategory.Extended:
+ maxObjectId = 0xFF;
+ break;
+
+ default: // Individual
+ {
+ if (requestBytes[4] < 0x03)
+ bodyBytes[2] = 0x81;
+ else if (requestBytes[4] < 0x80)
+ bodyBytes[2] = 0x82;
+ else
+ bodyBytes[2] = 0x83;
+
+ maxObjectId = requestBytes[4];
+ }
+
+ break;
+ }
+
+ byte numberOfObjects = 0;
+ for (int i = requestBytes[4]; i <= maxObjectId; i++)
+ {
+ // Reserved
+ if (0x07 <= i && i <= 0x7F)
+ continue;
+
+ byte[] objBytes = GetDeviceObject((byte)i);
+
+ // We need to split the response if it would exceed the max ADU size
+ if (responseBytes.Count + bodyBytes.Count + objBytes.Length > RtuProtocol.MAX_ADU_LENGTH)
+ {
+ bodyBytes[3] = 0xFF;
+ bodyBytes[4] = (byte)i;
+
+ bodyBytes[5] = numberOfObjects;
+ responseBytes.AddRange(bodyBytes);
+ return [.. responseBytes];
+ }
+
+ bodyBytes.AddRange(objBytes);
+ numberOfObjects++;
+ }
+
+ bodyBytes[5] = numberOfObjects;
+ responseBytes.AddRange(bodyBytes);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+ catch
+ {
+ responseBytes[1] |= 0x80;
+ responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
+
+ AddCrc(responseBytes);
+ return [.. responseBytes];
+ }
+ }
+
+ private byte[] GetDeviceObject(byte objectId)
+ {
+ var result = new List { objectId };
+ switch ((ModbusDeviceIdentificationObject)objectId)
+ {
+ case ModbusDeviceIdentificationObject.VendorName:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes("AMWD");
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.ProductCode:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes("AMWD-MBS-RTU");
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.MajorMinorRevision:
+ {
+ string version = GetType().Assembly
+ .GetCustomAttribute()
+ .InformationalVersion;
+
+ byte[] bytes = Encoding.UTF8.GetBytes(version);
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.VendorUrl:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes("https://github.com/AM-WD/AMWD.Protocols.Modbus");
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.ProductName:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes("AM.WD Modbus Library");
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.ModelName:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes("Serial Server");
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ case ModbusDeviceIdentificationObject.UserApplicationName:
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes("Modbus RTU Server");
+ result.Add((byte)bytes.Length);
+ result.AddRange(bytes);
+ }
+ break;
+
+ default:
+ result.Add(0x00);
+ break;
+ }
+
+ return [.. result];
+ }
+
+ private static void SetCrc(byte[] bytes)
+ {
+ byte[] crc = RtuProtocol.CRC16(bytes, 0, bytes.Length - 2);
+ bytes[bytes.Length - 2] = crc[0];
+ bytes[bytes.Length - 1] = crc[1];
+ }
+
+ private static void AddCrc(List bytes)
+ {
+ byte[] crc = RtuProtocol.CRC16(bytes);
+ bytes.Add(crc[0]);
+ bytes.Add(crc[1]);
+ }
+
+ #endregion Request Handling
+
+ #region Device Handling
+
+ ///
+ /// Adds a new device to the server.
+ ///
+ /// The unit ID of the device.
+ /// if the device was added, otherwise.
+ public bool AddDevice(byte unitId)
+ {
+ Assertions();
+
+ using (_deviceListLock.GetWriteLock())
+ {
+ if (_devices.ContainsKey(unitId))
+ return false;
+
+ _devices.Add(unitId, new ModbusDevice(unitId));
+ return true;
+ }
+ }
+
+ ///
+ /// Removes a device from the server.
+ ///
+ /// The unit ID of the device.
+ /// if the device was removed, otherwise.
+ public bool RemoveDevice(byte unitId)
+ {
+ Assertions();
+
+ using (_deviceListLock.GetWriteLock())
+ {
+ if (_devices.TryGetValue(unitId, out var device))
+ device.Dispose();
+
+ return _devices.Remove(unitId);
+ }
+ }
+
+ ///
+ /// Gets a from the specified .
+ ///
+ /// The unit ID of the device.
+ /// The address of the coil.
+ public Coil GetCoil(byte unitId, ushort address)
+ {
+ Assertions();
+
+ using (_deviceListLock.GetReadLock())
+ {
+ if (!_devices.TryGetValue(unitId, out var device))
+ return null;
+
+ return device.GetCoil(address);
+ }
+ }
+
+ ///
+ /// Sets a to the specified .
+ ///
+ /// The unit ID of the device.
+ /// The to set.
+ public void SetCoil(byte unitId, Coil coil)
+ {
+ Assertions();
+
+ using (_deviceListLock.GetReadLock())
+ {
+ if (!_devices.TryGetValue(unitId, out var device))
+ return;
+
+ device.SetCoil(coil);
+ }
+ }
+
+ ///
+ /// Gets a from the specified .
+ ///
+ /// The unit ID of the device.
+ /// The address of the .
+ public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
+ {
+ Assertions();
+
+ using (_deviceListLock.GetReadLock())
+ {
+ if (!_devices.TryGetValue(unitId, out var device))
+ return null;
+
+ return device.GetDiscreteInput(address);
+ }
+ }
+
+ ///
+ /// Sets a to the specified .
+ ///
+ /// The unit ID of the device.
+ /// The to set.
+ public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
+ {
+ Assertions();
+
+ using (_deviceListLock.GetReadLock())
+ {
+ if (!_devices.TryGetValue(unitId, out var device))
+ return;
+
+ device.SetDiscreteInput(discreteInput);
+ }
+ }
+
+ ///
+ /// Gets a from the specified .
+ ///
+ /// The unit ID of the device.
+ /// The address of the .
+ public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
+ {
+ Assertions();
+
+ using (_deviceListLock.GetReadLock())
+ {
+ if (!_devices.TryGetValue(unitId, out var device))
+ return null;
+
+ return device.GetHoldingRegister(address);
+ }
+ }
+
+ ///
+ /// Sets a to the specified .
+ ///
+ /// The unit ID of the device.
+ /// The to set.
+ public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
+ {
+ Assertions();
+
+ using (_deviceListLock.GetReadLock())
+ {
+ if (!_devices.TryGetValue(unitId, out var device))
+ return;
+
+ device.SetHoldingRegister(holdingRegister);
+ }
+ }
+
+ ///
+ /// Gets a from the specified .
+ ///
+ /// The unit ID of the device.
+ /// The address of the .
+ public InputRegister GetInputRegister(byte unitId, ushort address)
+ {
+ Assertions();
+
+ using (_deviceListLock.GetReadLock())
+ {
+ if (!_devices.TryGetValue(unitId, out var device))
+ return null;
+
+ return device.GetInputRegister(address);
+ }
+ }
+
+ ///
+ /// Sets a to the specified .
+ ///
+ /// The unit ID of the device.
+ /// The to set.
+ public void SetInputRegister(byte unitId, InputRegister inputRegister)
+ {
+ Assertions();
+
+ using (_deviceListLock.GetReadLock())
+ {
+ if (!_devices.TryGetValue(unitId, out var device))
+ return;
+
+ device.SetInputRegister(inputRegister);
+ }
+ }
+
+ #endregion Device Handling
+ }
+}
diff --git a/AMWD.Protocols.Modbus.Serial/README.md b/AMWD.Protocols.Modbus.Serial/README.md
index 683feab..232e1fd 100644
--- a/AMWD.Protocols.Modbus.Serial/README.md
+++ b/AMWD.Protocols.Modbus.Serial/README.md
@@ -19,7 +19,7 @@ ushort count = 2;
var registers = await client.ReadHoldingRegistersAsync(unitId, startAddress, count);
float voltage = registers.GetSingle();
-Console.WriteLine($"The voltage between L1 and N is: {voltage:N2}V");
+Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
```
diff --git a/AMWD.Protocols.Modbus.Tcp/README.md b/AMWD.Protocols.Modbus.Tcp/README.md
index db54acf..9d2cdec 100644
--- a/AMWD.Protocols.Modbus.Tcp/README.md
+++ b/AMWD.Protocols.Modbus.Tcp/README.md
@@ -20,7 +20,7 @@ ushort count = 2;
var registers = await client.ReadHoldingRegistersAsync(unitId, startAddress, count);
float voltage = registers.GetSingle();
-Console.WriteLine($"The voltage between L1 and N is: {voltage:N2}V");
+Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
```