diff --git a/AMWD.Protocols.Modbus.Proxy/AMWD.Protocols.Modbus.Proxy.csproj b/AMWD.Protocols.Modbus.Proxy/AMWD.Protocols.Modbus.Proxy.csproj index 5d6b82e..4ce791b 100644 --- a/AMWD.Protocols.Modbus.Proxy/AMWD.Protocols.Modbus.Proxy.csproj +++ b/AMWD.Protocols.Modbus.Proxy/AMWD.Protocols.Modbus.Proxy.csproj @@ -14,7 +14,6 @@ - @@ -23,8 +22,21 @@ + + + + + + + + + + + + + diff --git a/AMWD.Protocols.Modbus.Proxy/ModbusRtuProxy.cs b/AMWD.Protocols.Modbus.Proxy/ModbusRtuProxy.cs new file mode 100644 index 0000000..12e8cf9 --- /dev/null +++ b/AMWD.Protocols.Modbus.Proxy/ModbusRtuProxy.cs @@ -0,0 +1,867 @@ +using System; +using System.Collections.Generic; +using System.IO.Ports; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AMWD.Protocols.Modbus.Common; +using AMWD.Protocols.Modbus.Common.Contracts; +using AMWD.Protocols.Modbus.Common.Protocols; +using AMWD.Protocols.Modbus.Serial; + +namespace AMWD.Protocols.Modbus.Proxy +{ + /// + /// Implements a Modbus serial line RTU server proxying all requests to a Modbus client of choice. + /// + public class ModbusRtuProxy : IDisposable + { + #region Fields + + private bool _isDisposed; + + private readonly SerialPort _serialPort; + private CancellationTokenSource _stopCts; + + #endregion Fields + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The used to request the remote device, that should be proxied. + /// The name of the serial port to use. + /// The baud rate of the serial port (Default: 19.200). + public ModbusRtuProxy(ModbusClientBase client, string portName, BaudRate baudRate = BaudRate.Baud19200) + { + Client = client ?? throw new ArgumentNullException(nameof(client)); + + 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 Properties + + /// + /// Gets the Modbus client used to request the remote device, that should be proxied. + /// + public ModbusClientBase Client { get; } + + /// + 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(); + + _serialPort.Dispose(); + _stopCts?.Dispose(); + } + + 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]); + 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; + + switch ((ModbusFunctionCode)requestBytes[1]) + { + case ModbusFunctionCode.ReadCoils: + return HandleReadCoilsAsync(requestBytes, _stopCts.Token).Result; + + case ModbusFunctionCode.ReadDiscreteInputs: + return HandleReadDiscreteInputsAsync(requestBytes, _stopCts.Token).Result; + + case ModbusFunctionCode.ReadHoldingRegisters: + return HandleReadHoldingRegistersAsync(requestBytes, _stopCts.Token).Result; + + case ModbusFunctionCode.ReadInputRegisters: + return HandleReadInputRegistersAsync(requestBytes, _stopCts.Token).Result; + + case ModbusFunctionCode.WriteSingleCoil: + return HandleWriteSingleCoilAsync(requestBytes, _stopCts.Token).Result; + + case ModbusFunctionCode.WriteSingleRegister: + return HandleWriteSingleRegisterAsync(requestBytes, _stopCts.Token).Result; + + case ModbusFunctionCode.WriteMultipleCoils: + return HandleWriteMultipleCoilsAsync(requestBytes, _stopCts.Token).Result; + + case ModbusFunctionCode.WriteMultipleRegisters: + return HandleWriteMultipleRegistersAsync(requestBytes, _stopCts.Token).Result; + + case ModbusFunctionCode.EncapsulatedInterface: + return HandleEncapsulatedInterfaceAsync(requestBytes, _stopCts.Token).Result; + + 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 async Task HandleReadCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken) + { + if (requestBytes.Length < 8) + return null; + + byte unitId = requestBytes[0]; + ushort firstAddress = requestBytes.GetBigEndianUInt16(2); + ushort count = requestBytes.GetBigEndianUInt16(4); + + var responseBytes = new List(); + responseBytes.AddRange(requestBytes.Take(2)); + try + { + var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(false); + + byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)]; + for (int i = 0; i < coils.Count; i++) + { + if (coils[i].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 async Task HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken) + { + if (requestBytes.Length < 8) + return null; + + byte unitId = requestBytes[0]; + ushort firstAddress = requestBytes.GetBigEndianUInt16(2); + ushort count = requestBytes.GetBigEndianUInt16(4); + + var responseBytes = new List(); + responseBytes.AddRange(requestBytes.Take(2)); + try + { + var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(false); + + byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)]; + for (int i = 0; i < discreteInputs.Count; i++) + { + if (discreteInputs[i].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 async Task HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) + { + if (requestBytes.Length < 8) + return null; + + byte unitId = requestBytes[0]; + ushort firstAddress = requestBytes.GetBigEndianUInt16(2); + ushort count = requestBytes.GetBigEndianUInt16(4); + + var responseBytes = new List(); + responseBytes.AddRange(requestBytes.Take(2)); + try + { + var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(false); + + byte[] values = new byte[holdingRegisters.Count * 2]; + for (int i = 0; i < holdingRegisters.Count; i++) + { + values[i * 2] = holdingRegisters[i].HighByte; + values[i * 2 + 1] = holdingRegisters[i].LowByte; + } + + responseBytes.Add((byte)values.Length); + responseBytes.AddRange(values); + } + catch + { + responseBytes[1] |= 0x80; + responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); + } + + AddCrc(responseBytes); + return [.. responseBytes]; + } + + private async Task HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) + { + if (requestBytes.Length < 8) + return null; + + byte unitId = requestBytes[0]; + ushort firstAddress = requestBytes.GetBigEndianUInt16(2); + ushort count = requestBytes.GetBigEndianUInt16(4); + + var responseBytes = new List(); + responseBytes.AddRange(requestBytes.Take(2)); + try + { + var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(false); + + byte[] values = new byte[count * 2]; + for (int i = 0; i < count; i++) + { + values[i * 2] = inputRegisters[i].HighByte; + values[i * 2 + 1] = inputRegisters[i].LowByte; + } + + responseBytes.Add((byte)values.Length); + responseBytes.AddRange(values); + } + catch + { + responseBytes[1] |= 0x80; + responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); + } + + AddCrc(responseBytes); + return [.. responseBytes]; + } + + private async Task HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken) + { + 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 + { + var coil = new Coil + { + Address = address, + HighByte = requestBytes[4], + LowByte = requestBytes[5], + }; + + bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken).ConfigureAwait(false); + if (isSuccess) + { + // Response is an echo of the request + responseBytes.AddRange(requestBytes.Skip(2).Take(4)); + } + else + { + responseBytes[1] |= 0x80; + responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); + } + } + catch + { + responseBytes[1] |= 0x80; + responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); + } + + AddCrc(responseBytes); + return [.. responseBytes]; + } + + private async Task HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken) + { + if (requestBytes.Length < 8) + return null; + + var responseBytes = new List(); + responseBytes.AddRange(requestBytes.Take(2)); + + ushort address = requestBytes.GetBigEndianUInt16(2); + try + { + var register = new HoldingRegister + { + Address = address, + HighByte = requestBytes[4], + LowByte = requestBytes[5] + }; + + bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken).ConfigureAwait(false); + if (isSuccess) + { + // Response is an echo of the request + responseBytes.AddRange(requestBytes.Skip(2).Take(4)); + } + else + { + responseBytes[1] |= 0x80; + responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); + } + } + catch + { + responseBytes[1] |= 0x80; + responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); + } + + AddCrc(responseBytes); + return [.. responseBytes]; + } + + private async Task HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken) + { + 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; + var coils = new List(); + 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; + + coils.Add(new Coil + { + Address = address, + HighByte = value ? (byte)0xFF : (byte)0x00 + }); + } + + bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken).ConfigureAwait(false); + if (isSuccess) + { + // Response is an echo of the request + responseBytes.AddRange(requestBytes.Skip(2).Take(4)); + } + else + { + responseBytes[1] |= 0x80; + responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); + } + } + catch + { + responseBytes[1] |= 0x80; + responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); + } + + AddCrc(responseBytes); + return [.. responseBytes]; + } + + private async Task HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) + { + 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; + var list = new List(); + for (int i = 0; i < count; i++) + { + ushort address = (ushort)(firstAddress + i); + + list.Add(new HoldingRegister + { + Address = address, + HighByte = requestBytes[baseOffset + i * 2], + LowByte = requestBytes[baseOffset + i * 2 + 1] + }); + + bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken).ConfigureAwait(false); + if (isSuccess) + { + // Response is an echo of the request + responseBytes.AddRange(requestBytes.Skip(2).Take(4)); + } + else + { + responseBytes[1] |= 0x80; + responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); + } + } + } + catch + { + responseBytes[1] |= 0x80; + responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); + } + + AddCrc(responseBytes); + return [.. responseBytes]; + } + + private async Task HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken) + { + 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]; + } + + var firstObject = (ModbusDeviceIdentificationObject)requestBytes[4]; + 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 res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken).ConfigureAwait(false); + + var bodyBytes = new List(); + + // MEI, Category + bodyBytes.AddRange(requestBytes.Skip(2).Take(2)); + + // Conformity + bodyBytes.Add((byte)category); + if (res.IsIndividualAccessAllowed) + bodyBytes[2] |= 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 + 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, res); + + // 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, DeviceIdentification deviceIdentification) + { + var result = new List { objectId }; + switch ((ModbusDeviceIdentificationObject)objectId) + { + case ModbusDeviceIdentificationObject.VendorName: + { + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName); + result.Add((byte)bytes.Length); + result.AddRange(bytes); + } + break; + + case ModbusDeviceIdentificationObject.ProductCode: + { + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode); + result.Add((byte)bytes.Length); + result.AddRange(bytes); + } + break; + + case ModbusDeviceIdentificationObject.MajorMinorRevision: + { + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision); + result.Add((byte)bytes.Length); + result.AddRange(bytes); + } + break; + + case ModbusDeviceIdentificationObject.VendorUrl: + { + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl); + result.Add((byte)bytes.Length); + result.AddRange(bytes); + } + break; + + case ModbusDeviceIdentificationObject.ProductName: + { + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName); + result.Add((byte)bytes.Length); + result.AddRange(bytes); + } + break; + + case ModbusDeviceIdentificationObject.ModelName: + { + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName); + result.Add((byte)bytes.Length); + result.AddRange(bytes); + } + break; + + case ModbusDeviceIdentificationObject.UserApplicationName: + { + byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName); + result.Add((byte)bytes.Length); + result.AddRange(bytes); + } + break; + + default: + { + if (deviceIdentification.ExtendedObjects.ContainsKey(objectId)) + { + byte[] bytes = deviceIdentification.ExtendedObjects[objectId]; + result.Add((byte)bytes.Length); + result.AddRange(bytes); + } + else + { + 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 + } +} diff --git a/AMWD.Protocols.Modbus.Proxy/ModbusTcpProxy.cs b/AMWD.Protocols.Modbus.Proxy/ModbusTcpProxy.cs index f94fc66..2b50c4e 100644 --- a/AMWD.Protocols.Modbus.Proxy/ModbusTcpProxy.cs +++ b/AMWD.Protocols.Modbus.Proxy/ModbusTcpProxy.cs @@ -178,6 +178,8 @@ namespace AMWD.Protocols.Modbus.Proxy _clientListLock.Dispose(); _clients.Clear(); + + _stopCts?.Dispose(); } private void Assertions() diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fbcc0a..c42469b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Renamed `ModbusSerialServer` to `ModbusRtuServer` so clearify the protocol, that is used. +- Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used. - Made `Protocol` property of `ModbusClientBase` non-abstract.