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 } } }