From 380493c7ecd75ab68505cd844d29f43bbbaa87aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Tue, 2 Apr 2024 22:52:56 +0200 Subject: [PATCH] Implemented the Serial Server --- AMWD.Protocols.Modbus.Common/README.md | 6 +- .../ModbusSerialConnection.cs | 8 +- .../ModbusSerialServer.cs | 1174 +++++++++++++++++ AMWD.Protocols.Modbus.Serial/README.md | 2 +- AMWD.Protocols.Modbus.Tcp/README.md | 2 +- 5 files changed, 1186 insertions(+), 6 deletions(-) create mode 100644 AMWD.Protocols.Modbus.Serial/ModbusSerialServer.cs 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"); ```