From 32876b51b939bbe3080cfe80a6d76a41f115b54f Mon Sep 17 00:00:00 2001 From: Adrian Stevens Date: Thu, 15 Aug 2024 12:00:16 -0700 Subject: [PATCH] Release 1.13.0 --- .../ModbusSerialTStatTests.cs | 79 +---- .../ModbusSerialVoltaicBatteryTests.cs | 47 +++ src/Meadow.Modbus.Unit.Tests/TStat8.cs | 75 +++++ src/Meadow.Modbus.Unit.Tests/V10x.cs | 95 ++++++ src/Meadow.Modbus/Clients/Extensions.cs | 204 ++++++++++++- src/Meadow.Modbus/Clients/IModbusBusClient.cs | 9 +- src/Meadow.Modbus/Clients/ModbusClientBase.cs | 25 +- src/Meadow.Modbus/Clients/ModbusRtuClient.cs | 2 +- src/Meadow.Modbus/Clients/ModbusTcpClient.cs | 99 +++++-- src/Meadow.Modbus/Meadow.Modbus.csproj | 2 +- src/Meadow.Modbus/ModbusErrorCode.cs | 5 + src/Meadow.Modbus/ModbusPolledDevice.cs | 276 +++++++++++++++--- 12 files changed, 767 insertions(+), 151 deletions(-) create mode 100644 src/Meadow.Modbus.Unit.Tests/ModbusSerialVoltaicBatteryTests.cs create mode 100644 src/Meadow.Modbus.Unit.Tests/TStat8.cs create mode 100644 src/Meadow.Modbus.Unit.Tests/V10x.cs diff --git a/src/Meadow.Modbus.Unit.Tests/ModbusSerialTStatTests.cs b/src/Meadow.Modbus.Unit.Tests/ModbusSerialTStatTests.cs index e1a6f8e..1d9a3fe 100644 --- a/src/Meadow.Modbus.Unit.Tests/ModbusSerialTStatTests.cs +++ b/src/Meadow.Modbus.Unit.Tests/ModbusSerialTStatTests.cs @@ -1,87 +1,16 @@ -using System; +using Meadow.Modbus.Temco; +using System; using System.Diagnostics; using System.Threading.Tasks; using Xunit; namespace Meadow.Modbus.Unit.Tests; -public class TStat8 : ModbusPolledDevice -{ - private float _currentSetPoint; - - private const ushort SetPointRegister = 345; - - public TStat8(ModbusRtuClient client, byte modbusAddress, TimeSpan? refreshPeriod = null) - : base(client, modbusAddress, refreshPeriod) - { - MapHoldingRegistersToProperty( - startRegister: 121, - registerCount: 1, - propertyName: nameof(Temperature), - scale: 0.10); // value is in 0.1 deg - - // map to a field, not a property as the property setter needs to perform an action - MapHoldingRegistersToField( - startRegister: SetPointRegister, - registerCount: 1, - fieldName: nameof(_currentSetPoint), - scale: 0.10); - - MapHoldingRegistersToProperty( - startRegister: 198, - registerCount: 1, - propertyName: nameof(Humidity)); - - MapHoldingRegistersToProperty( - startRegister: 364, // not scaled by 0.1 - registerCount: 1, - propertyName: nameof(PowerUpSetPoint)); - - MapHoldingRegistersToProperty( - startRegister: 365, - registerCount: 1, - propertyName: nameof(MaxSetPoint)); - - MapHoldingRegistersToProperty( - startRegister: 366, - registerCount: 1, - propertyName: nameof(MinSetPoint)); - - MapHoldingRegistersToProperty( - startRegister: 410, - registerCount: 7, - propertyName: nameof(Clock), - conversionFunction: ConvertRegistersToClockTime); - } - - private object ConvertRegistersToClockTime(ushort[] data) - { - // data[2] is week, so ignore - return new DateTime(data[0], data[1], data[3], data[4], data[5], data[6]); - } - - public DateTime Clock { get; private set; } - public int Humidity { get; private set; } - public float Temperature { get; private set; } - public float MinSetPoint { get; private set; } - public float MaxSetPoint { get; private set; } - public float PowerUpSetPoint { get; private set; } - - public float SetPoint - { - get => _currentSetPoint; - set - { - _ = WriteHoldingRegister(SetPointRegister, (ushort)(value * 10)); - } - } -} - public class ModbusSerialTStatTests { // this class assumes a connected serial Temco Controls TSTAT7 or TSTAT8 [Fact] - public async void PolledDevicetest() + public async void PolledDeviceTest() { using (var port = new SerialPortShim("COM3", 19200, Hardware.Parity.None, 8, Hardware.StopBits.One)) { @@ -264,4 +193,4 @@ public async void ReadWriteHoldingRegisterTest() Assert.Equal(newSetpoint, verifySetpoint[0]); } } -} \ No newline at end of file +} diff --git a/src/Meadow.Modbus.Unit.Tests/ModbusSerialVoltaicBatteryTests.cs b/src/Meadow.Modbus.Unit.Tests/ModbusSerialVoltaicBatteryTests.cs new file mode 100644 index 0000000..c23b871 --- /dev/null +++ b/src/Meadow.Modbus.Unit.Tests/ModbusSerialVoltaicBatteryTests.cs @@ -0,0 +1,47 @@ +using Meadow.Modbus.Voltaic; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Xunit; + +namespace Meadow.Modbus.Unit.Tests; + +public class ModbusSerialVoltaicBatteryTests +{ + // this class assumes a connected serial Voltaic V10x battery/controller + [Fact] + public async void PolledDeviceTest() + { + using (var port = new SerialPortShim("COM5", V10x.DefaultBaudRate, Hardware.Parity.None, 8, Hardware.StopBits.One)) + { + port.ReadTimeout = TimeSpan.FromSeconds(15); + port.Open(); + + var client = new ModbusRtuClient(port); + var controller = new V10x(client); + + controller.CommTimeout += (s, e) => Debug.WriteLine("Read Timeout"); + controller.CommError += (s, e) => Debug.WriteLine($"Error: {e.Message}"); + + controller.StartPolling(); + + var i = 0; + + while (true) + { + await Task.Delay(2000); + Debug.WriteLine($"---------------"); + Debug.WriteLine($"Battery voltage: {controller.BatteryVoltage.Volts:N2} V"); + Debug.WriteLine($"Input voltage: {controller.InputVoltage.Volts:N2} V"); + Debug.WriteLine($"Input current: {controller.InputCurrent.Amps:N2} A"); + Debug.WriteLine($"Load voltage: {controller.LoadVoltage.Volts:N2} V"); + Debug.WriteLine($"Load current: {controller.LoadCurrent.Amps:N2} A"); + Debug.WriteLine($"Environ temp: {controller.EnvironmentTemp.Fahrenheit:N2} F"); + Debug.WriteLine($"Controller temp: {controller.ControllerTemp.Fahrenheit:N2} F"); + // Debug.WriteLine($"Battery output: {controller.BatteryOutput}"); + + controller.BatteryOutput = (i % 2 == 0); + } + } + } +} diff --git a/src/Meadow.Modbus.Unit.Tests/TStat8.cs b/src/Meadow.Modbus.Unit.Tests/TStat8.cs new file mode 100644 index 0000000..035a14c --- /dev/null +++ b/src/Meadow.Modbus.Unit.Tests/TStat8.cs @@ -0,0 +1,75 @@ +using System; + +namespace Meadow.Modbus.Temco; + +public class TStat8 : ModbusPolledDevice +{ + private float _currentSetPoint; + + private const ushort SetPointRegister = 345; + + public TStat8(ModbusRtuClient client, byte modbusAddress, TimeSpan? refreshPeriod = null) + : base(client, modbusAddress, refreshPeriod) + { + MapHoldingRegistersToProperty( + startRegister: 121, + registerCount: 1, + propertyName: nameof(Temperature), + scale: 0.10); // value is in 0.1 deg + + // map to a field, not a property as the property setter needs to perform an action + MapHoldingRegistersToField( + startRegister: SetPointRegister, + registerCount: 1, + fieldName: nameof(_currentSetPoint), + scale: 0.10); + + MapHoldingRegistersToProperty( + startRegister: 198, + registerCount: 1, + propertyName: nameof(Humidity)); + + MapHoldingRegistersToProperty( + startRegister: 364, // not scaled by 0.1 + registerCount: 1, + propertyName: nameof(PowerUpSetPoint)); + + MapHoldingRegistersToProperty( + startRegister: 365, + registerCount: 1, + propertyName: nameof(MaxSetPoint)); + + MapHoldingRegistersToProperty( + startRegister: 366, + registerCount: 1, + propertyName: nameof(MinSetPoint)); + + MapHoldingRegistersToProperty( + startRegister: 410, + registerCount: 7, + propertyName: nameof(Clock), + conversionFunction: ConvertRegistersToClockTime); + } + + private object ConvertRegistersToClockTime(ushort[] data) + { + // data[2] is week, so ignore + return new DateTime(data[0], data[1], data[3], data[4], data[5], data[6]); + } + + public DateTime Clock { get; private set; } + public int Humidity { get; private set; } + public float Temperature { get; private set; } + public float MinSetPoint { get; private set; } + public float MaxSetPoint { get; private set; } + public float PowerUpSetPoint { get; private set; } + + public float SetPoint + { + get => _currentSetPoint; + set + { + _ = WriteHoldingRegister(SetPointRegister, (ushort)(value * 10)); + } + } +} diff --git a/src/Meadow.Modbus.Unit.Tests/V10x.cs b/src/Meadow.Modbus.Unit.Tests/V10x.cs new file mode 100644 index 0000000..c725781 --- /dev/null +++ b/src/Meadow.Modbus.Unit.Tests/V10x.cs @@ -0,0 +1,95 @@ +using Meadow.Units; +using System; + +namespace Meadow.Modbus.Voltaic; + +public class V10x : ModbusPolledDevice +{ + private double _rawBatteryVoltage; + private double _rawInputVoltage; + private double _rawInputCurrent; + private double _rawLoadVoltage; + private double _rawLoadCurrent; + private double _rawEnvironmentTemp; + private double _rawControllerTemp; + + public const int DefaultModbusAddress = 1; + public const int DefaultBaudRate = 9600; + + private const ushort BatteryOutputSwitchRegister = 0; + + public Voltage BatteryVoltage => new Voltage(_rawBatteryVoltage, Voltage.UnitType.Volts); + public Voltage InputVoltage => new Voltage(_rawInputVoltage, Voltage.UnitType.Volts); + public Current InputCurrent => new Current(_rawInputCurrent, Current.UnitType.Amps); + public Voltage LoadVoltage => new Voltage(_rawLoadVoltage, Voltage.UnitType.Volts); + public Current LoadCurrent => new Current(_rawLoadCurrent, Current.UnitType.Amps); + public Temperature EnvironmentTemp => new Temperature(_rawEnvironmentTemp, Temperature.UnitType.Celsius); + public Temperature ControllerTemp => new Temperature(_rawControllerTemp, Temperature.UnitType.Celsius); + + public V10x( + ModbusClientBase client, + byte modbusAddress = DefaultModbusAddress, + TimeSpan? refreshPeriod = null) + : base(client, modbusAddress, refreshPeriod) + { + MapInputRegistersToField( + startRegister: 0x30a0, + registerCount: 1, + fieldName: nameof(_rawBatteryVoltage), + conversionFunction: ConvertRegisterToRawValue + ); + + MapInputRegistersToField( + startRegister: 0x304e, + registerCount: 1, + fieldName: nameof(_rawInputVoltage), + conversionFunction: ConvertRegisterToRawValue + ); + + MapInputRegistersToField( + startRegister: 0x304f, + registerCount: 1, + fieldName: nameof(_rawInputCurrent), + conversionFunction: ConvertRegisterToRawValue + ); + + MapInputRegistersToField( + startRegister: 0x304a, + registerCount: 1, + fieldName: nameof(_rawLoadVoltage), + conversionFunction: ConvertRegisterToRawValue + ); + + MapInputRegistersToField( + startRegister: 0x304b, + registerCount: 1, + fieldName: nameof(_rawLoadCurrent), + conversionFunction: ConvertRegisterToRawValue + ); + + MapInputRegistersToField( + startRegister: 0x30a2, + registerCount: 1, + fieldName: nameof(_rawEnvironmentTemp), + conversionFunction: ConvertRegisterToRawValue + ); + + MapInputRegistersToField( + startRegister: 0x3037, + registerCount: 1, + fieldName: nameof(_rawControllerTemp), + conversionFunction: ConvertRegisterToRawValue + ); + } + + public bool BatteryOutput + { + set => _ = WriteCoil(BatteryOutputSwitchRegister, value); + } + + private object ConvertRegisterToRawValue(ushort[] registers) + { + // value is one register in 1/100 of a unit + return registers[0] / 100d; + } +} diff --git a/src/Meadow.Modbus/Clients/Extensions.cs b/src/Meadow.Modbus/Clients/Extensions.cs index bdc4d87..183fcdc 100644 --- a/src/Meadow.Modbus/Clients/Extensions.cs +++ b/src/Meadow.Modbus/Clients/Extensions.cs @@ -1,4 +1,7 @@ -namespace Meadow.Modbus; +using System; +using System.Linq; + +namespace Meadow.Modbus; /// /// Extension methods for Modbus functions @@ -24,4 +27,203 @@ public static int[] ConvertRegistersToInt32(this ushort[] registers) return values; } + + /// + /// Converts ushort registers to a single Int32, starting at a specific offset + /// + /// The registers + /// The offset in the registers to begine extraction + /// True to convert from big-endian words + public static int ExtractInt32(this Span registers, int startOffset = 0, bool swappedWords = false) + { + if (swappedWords) + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 0]) + .Concat(BitConverter.GetBytes(registers[startOffset + 1])) + .ToArray(); + + return BitConverter.ToInt32(value, 0); + } + else + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 1]) + .Concat(BitConverter.GetBytes(registers[startOffset + 0])) + .ToArray(); + + return BitConverter.ToInt32(value, 0); + } + } + + /// + /// Converts ushort registers to a single UInt32, starting at a specific offset + /// + /// The registers + /// The offset in the registers to begine extraction + /// True to convert from big-endian words + public static uint ExtractUInt32(this Span registers, int startOffset = 0, bool swappedWords = false) + { + if (swappedWords) + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 0]) + .Concat(BitConverter.GetBytes(registers[startOffset + 1])) + .ToArray(); + + return BitConverter.ToUInt32(value, 0); + } + else + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 1]) + .Concat(BitConverter.GetBytes(registers[startOffset + 0])) + .ToArray(); + + return BitConverter.ToUInt32(value, 0); + } + } + + /// + /// Converts ushort registers to a single Int64, starting at a specific offset + /// + /// The registers + /// The offset in the registers to begine extraction + /// True to convert from big-endian words + public static long ExtractInt64(this Span registers, int startOffset = 0, bool swappedWords = false) + { + if (swappedWords) + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 0]) + .Concat(BitConverter.GetBytes(registers[startOffset + 1])) + .Concat(BitConverter.GetBytes(registers[startOffset + 2])) + .Concat(BitConverter.GetBytes(registers[startOffset + 3])) + .ToArray(); + + return BitConverter.ToInt64(value, 0); + } + else + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 3]) + .Concat(BitConverter.GetBytes(registers[startOffset + 2])) + .Concat(BitConverter.GetBytes(registers[startOffset + 1])) + .Concat(BitConverter.GetBytes(registers[startOffset + 0])) + .ToArray(); + + return BitConverter.ToInt64(value, 0); + } + } + + /// + /// Converts ushort registers to a single UInt64, starting at a specific offset + /// + /// The registers + /// The offset in the registers to begine extraction + /// True to convert from big-endian words + public static ulong ExtractUInt64(this Span registers, int startOffset = 0, bool swappedWords = false) + { + if (swappedWords) + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 0]) + .Concat(BitConverter.GetBytes(registers[startOffset + 1])) + .Concat(BitConverter.GetBytes(registers[startOffset + 2])) + .Concat(BitConverter.GetBytes(registers[startOffset + 3])) + .ToArray(); + + return BitConverter.ToUInt64(value, 0); + } + else + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 3]) + .Concat(BitConverter.GetBytes(registers[startOffset + 2])) + .Concat(BitConverter.GetBytes(registers[startOffset + 1])) + .Concat(BitConverter.GetBytes(registers[startOffset + 0])) + .ToArray(); + + return BitConverter.ToUInt64(value, 0); + } + } + + /// + /// Converts 4 ushort registers to a IEEE 64 floating point, starting at a specific offset + /// + /// The registers + /// The offset in the registers to begine extraction + /// True to convert from big-endian words + public static double ExtractDouble(this Span registers, int startOffset = 0, bool swappedWords = false) + { + if (swappedWords) + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 0]) + .Concat(BitConverter.GetBytes(registers[startOffset + 1])) + .Concat(BitConverter.GetBytes(registers[startOffset + 2])) + .Concat(BitConverter.GetBytes(registers[startOffset + 3])) + .ToArray(); + + return BitConverter.ToDouble(value, 0); + } + else + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 3]) + .Concat(BitConverter.GetBytes(registers[startOffset + 2])) + .Concat(BitConverter.GetBytes(registers[startOffset + 1])) + .Concat(BitConverter.GetBytes(registers[startOffset + 0])) + .ToArray(); + + return BitConverter.ToDouble(value, 0); + } + } + + /// + /// Converts 2 ushort registers to a IEEE 32 floating point, starting at a specific offset + /// + /// The registers + /// The offset in the registers to begine extraction + /// True to convert from big-endian words + public static float ExtractSingle(this Span registers, int startOffset = 0, bool swappedWords = false) + { + if (swappedWords) + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 0]) + .Concat(BitConverter.GetBytes(registers[startOffset + 1])) + .ToArray(); + + return BitConverter.ToSingle(value, 0); + } + else + { + byte[] value = BitConverter.GetBytes(registers[startOffset + 1]) + .Concat(BitConverter.GetBytes(registers[startOffset + 0])) + .ToArray(); + + return BitConverter.ToSingle(value, 0); + } + } + + /// + /// Converts three UInt16 values into an unsigned 48-bit Mod10 (modulo 10000) format (returned as an Int64). + /// + /// The registers + public static long ExtractMod10_48(this Span registers) + { + // Each registers range is -9,999 to +9,999: + long R1 = BitConverter.ToInt16(BitConverter.GetBytes(registers[0]).ToArray(), 0); + long R2 = BitConverter.ToInt16(BitConverter.GetBytes(registers[1]).ToArray(), 0); + long R3 = BitConverter.ToInt16(BitConverter.GetBytes(registers[2]).ToArray(), 0); + + // R3*10,000^2 + R2*10,000 + R1 + return (R3 * (long)Math.Pow(10000, 2)) + (R2 * 10000) + R1; + } + + /// + /// Converts four UInt16 values into an unsigned 48-bit Mod10 (modulo 10000) format (returned as an Int64). + /// + /// The registers + public static long ExtractMod10_64(this Span registers) + { + // Each registers range is -9,999 to +9,999: + long R1 = BitConverter.ToInt16(BitConverter.GetBytes(registers[0]).ToArray(), 0); + long R2 = BitConverter.ToInt16(BitConverter.GetBytes(registers[1]).ToArray(), 0); + long R3 = BitConverter.ToInt16(BitConverter.GetBytes(registers[2]).ToArray(), 0); + long R4 = BitConverter.ToInt16(BitConverter.GetBytes(registers[3]).ToArray(), 0); + + // R3*10,000^2 + R2*10,000 + R1 + return (R4 * (long)Math.Pow(10000, 3)) + (R3 * (long)Math.Pow(10000, 2)) + (R2 * 10000) + R1; + } } diff --git a/src/Meadow.Modbus/Clients/IModbusBusClient.cs b/src/Meadow.Modbus/Clients/IModbusBusClient.cs index 2cb4c7b..b46aaaf 100644 --- a/src/Meadow.Modbus/Clients/IModbusBusClient.cs +++ b/src/Meadow.Modbus/Clients/IModbusBusClient.cs @@ -41,7 +41,8 @@ public interface IModbusBusClient /// /// /// - Task WriteHoldingRegister(byte modbusAddress, ushort register, ushort value); + /// If the address is a Modbus logical address, 40001 will be subtracted to get the register address. + Task WriteHoldingRegister(byte modbusAddress, ushort register, ushort value, bool addressIsLogical = false); /// /// Writes multiple values to holding registers (modbus function 16) @@ -49,8 +50,9 @@ public interface IModbusBusClient /// The target device modbus address /// The first register to begin writing /// The registers (16-bit values) to write + /// If the address is a Modbus logical address, 40001 will be subtracted to get the register address. /// - Task WriteHoldingRegisters(byte modbusAddress, ushort startRegister, IEnumerable values); + Task WriteHoldingRegisters(byte modbusAddress, ushort startRegister, IEnumerable values, bool addressIsLogical = false); /// /// Reads the requested number of holding registers from a device @@ -58,8 +60,9 @@ public interface IModbusBusClient /// /// /// + /// If the address is a Modbus logical address, 40001 will be subtracted to get the register address. /// - Task ReadHoldingRegisters(byte modbusAddress, ushort startRegister, int registerCount); + Task ReadHoldingRegisters(byte modbusAddress, ushort startRegister, int registerCount, bool addressIsLogical = false); /// /// Reads the requested number of floats from the holding registers diff --git a/src/Meadow.Modbus/Clients/ModbusClientBase.cs b/src/Meadow.Modbus/Clients/ModbusClientBase.cs index 7ac21c7..e4a67a9 100644 --- a/src/Meadow.Modbus/Clients/ModbusClientBase.cs +++ b/src/Meadow.Modbus/Clients/ModbusClientBase.cs @@ -126,15 +126,21 @@ protected set } } + /// + /// Gets or sets the Timeout value for reading / writing to a Modbus device. + /// + public TimeSpan Timeout { get; protected set; } = TimeSpan.FromSeconds(5); + /// /// Writes a single value to a holding register on the Modbus device. /// /// The Modbus device address. /// The register number to write to. /// The value to write to the register. - public async Task WriteHoldingRegister(byte modbusAddress, ushort register, ushort value) + /// If the address is a Modbus logical address, 40001 will be subtracted to get the register address. + public async Task WriteHoldingRegister(byte modbusAddress, ushort register, ushort value, bool addressIsLogical = false) { - if (register > 40000) + if (addressIsLogical && register > 40000) { // holding registers are defined as starting at 40001, but the actual bus read doesn't use the address, but instead the offset // we'll support th user passing in the definition either way @@ -166,14 +172,15 @@ public async Task WriteHoldingRegister(byte modbusAddress, ushort register, usho /// The Modbus device address. /// The starting register number to write to. /// The values to write to the registers. - public async Task WriteHoldingRegisters(byte modbusAddress, ushort startRegister, IEnumerable values) + /// If the address is a Modbus logical address, 40001 will be subtracted to get the register address. + public async Task WriteHoldingRegisters(byte modbusAddress, ushort startRegister, IEnumerable values, bool addressIsLogical = false) { if (values.Count() > MaxRegisterWriteCount) { throw new ArgumentException($"A maximum of {MaxRegisterWriteCount} registers can be written at one time"); } - if (startRegister > 40000) + if (addressIsLogical && startRegister > 40000) { // holding registers are defined as starting at 40001, but the actual bus read doesn't use the address, but instead the offset // we'll support th user passing in the definition either way @@ -232,11 +239,12 @@ public async Task ReadHoldingRegistersFloat(byte modbusAddress, ushort /// The starting register number to read from. /// The number of registers to read. /// An array of ushort values representing the registers. - public async Task ReadHoldingRegisters(byte modbusAddress, ushort startRegister, int registerCount) + /// If the address is a Modbus logical address, 40001 will be subtracted to get the register address. + public async Task ReadHoldingRegisters(byte modbusAddress, ushort startRegister, int registerCount, bool addressIsLogical = false) { if (registerCount > MaxRegisterReadCount) throw new ArgumentException($"A maximum of {MaxRegisterReadCount} registers can be retrieved at one time"); - if (startRegister > 40000) + if (addressIsLogical && startRegister > 40000) { // holding registers are defined as starting at 40001, but the actual bus read doesn't use the address, but instead the offset // we'll support th user passing in the definition either way @@ -276,10 +284,11 @@ public async Task ReadHoldingRegisters(byte modbusAddress, ushort star /// The Modbus device address. /// The starting register number to read from. /// The number of registers to read. + /// If the address is a Modbus logical address, 30001 will be subtracted to get the register address. /// An array of ushort values representing the registers. - public async Task ReadInputRegisters(byte modbusAddress, ushort startRegister, int registerCount) + public async Task ReadInputRegisters(byte modbusAddress, ushort startRegister, int registerCount, bool addressIsLogical = false) { - if (startRegister > 30000) + if (addressIsLogical && startRegister > 30000) { // input registers are defined as starting at 30001, but the actual bus read doesn't use the address, but instead the offset // we'll support th user passing in the definition either way diff --git a/src/Meadow.Modbus/Clients/ModbusRtuClient.cs b/src/Meadow.Modbus/Clients/ModbusRtuClient.cs index 989129b..2f2a4b3 100644 --- a/src/Meadow.Modbus/Clients/ModbusRtuClient.cs +++ b/src/Meadow.Modbus/Clients/ModbusRtuClient.cs @@ -36,7 +36,7 @@ public class ModbusRtuClient : ModbusClientBase public ModbusRtuClient(ISerialPort port, IDigitalOutputPort? enablePort = null) { _port = port; - _port.WriteTimeout = _port.ReadTimeout = TimeSpan.FromSeconds(5); + _port.WriteTimeout = _port.ReadTimeout = Timeout; _enable = enablePort; } diff --git a/src/Meadow.Modbus/Clients/ModbusTcpClient.cs b/src/Meadow.Modbus/Clients/ModbusTcpClient.cs index d510001..7b06e43 100644 --- a/src/Meadow.Modbus/Clients/ModbusTcpClient.cs +++ b/src/Meadow.Modbus/Clients/ModbusTcpClient.cs @@ -19,12 +19,12 @@ public class ModbusTcpClient : ModbusClientBase, IDisposable /// /// Gets the destination IP address for the Modbus TCP client. /// - public IPAddress Destination { get; } + public IPAddress Destination { get; private set; } /// /// Gets the port used for the Modbus TCP communication. /// - public short Port { get; } + public short Port { get; private set; } private TcpClient _client; private ushort _transaction = 0; @@ -67,6 +67,42 @@ protected override void DisposeManagedResources() _client?.Dispose(); } + /// + /// Connect to an endpoint. Allows for reusing the ModbusTcpClient. + /// + /// + /// + public async Task Connect(IPEndPoint destination) + { + await Connect(destination.Address, (short)destination.Port); + } + + /// + /// Connect to an endpoint. Allows for reusing the ModbusTcpClient. + /// + /// + /// + /// + public async Task Connect(string destinationAddress, short port = DefaultModbusTCPPort) + { + await Connect(IPAddress.Parse(destinationAddress), port); + } + + /// + /// Connect to an endpoint. Allows for reusing the ModbusTcpClient. + /// + /// + /// + /// + public async Task Connect(IPAddress destination, short port = DefaultModbusTCPPort) + { + if (IsConnected) + Disconnect(); + Destination = destination; + Port = port; + await Connect(); + } + /// public override async Task Connect() { @@ -78,7 +114,13 @@ public override async Task Connect() } await _client.ConnectAsync(Destination, Port); - IsConnected = true; + + IsConnected = _client.Connected; + if (!IsConnected) + throw new TimeoutException(); + + _client.ReceiveTimeout = (int)Timeout.TotalMilliseconds; + _client.SendTimeout = (int)Timeout.TotalMilliseconds; } catch (Exception ex) { @@ -190,12 +232,26 @@ protected override async Task DeliverMessage(byte[] message) } if (!_client.Connected) { - return; + throw new SocketException(); } try { - await _client.GetStream().WriteAsync(message, 0, message.Length); + Task count = _client.GetStream().WriteAsync(message, 0, message.Length); + + // send timeout + var t = 0; + + while (!count.IsCompleted) + { + await Task.Delay(10); + t += 10; + if (Timeout.TotalMilliseconds > 0 && t > Timeout.TotalMilliseconds) + { + _client.Close(); + throw new TimeoutException(); + } + } } catch { @@ -221,16 +277,28 @@ protected override async Task ReadResult(ModbusFunction function) if (!_client.Connected) { - return new byte[0]; - // throw new System.Net.Sockets.SocketException(); + throw new SocketException(); } // responses (even an error) are at least 9 bytes - read enough to know the status Array.Clear(_responseBuffer, 0, _responseBuffer.Length); - var count = await _client.GetStream().ReadAsync(_responseBuffer, 0, 9); + Task count = _client.GetStream().ReadAsync(_responseBuffer, 0, 9); - // TODO: we assume we get 9 bytes back here - handle that *not* happening + // send timeout + var t = 0; + + // handles 9 bytes not being recieved + while (!count.IsCompleted) + { + await Task.Delay(10); + t += 10; + if (Timeout.TotalMilliseconds > 0 && t > Timeout.TotalMilliseconds) + { + _client.Close(); + throw new TimeoutException(); + } + } if ((_responseBuffer[7] & 0x80) != 0) { @@ -240,16 +308,13 @@ protected override async Task ReadResult(ModbusFunction function) } // read any remaining payload - try - { - count = await _client.GetStream().ReadAsync(_responseBuffer, 9, _responseBuffer[8]); - // TODO: if count < the expected, we need to keep reading - } - catch + count = _client.GetStream().ReadAsync(_responseBuffer, 9, _responseBuffer[8]); + await Task.WhenAny(count, Task.Delay(10)); // the data should already be here? + + if (!count.IsCompleted) { - IsConnected = false; _client.Close(); - throw; + throw new Exception("Incomplete Response"); } // if it's not an error, responseBuffer[8] is the payload length (as a byte) diff --git a/src/Meadow.Modbus/Meadow.Modbus.csproj b/src/Meadow.Modbus/Meadow.Modbus.csproj index 94ba68b..7e820b3 100644 --- a/src/Meadow.Modbus/Meadow.Modbus.csproj +++ b/src/Meadow.Modbus/Meadow.Modbus.csproj @@ -23,6 +23,6 @@ - + diff --git a/src/Meadow.Modbus/ModbusErrorCode.cs b/src/Meadow.Modbus/ModbusErrorCode.cs index 5b31fc5..42e653b 100644 --- a/src/Meadow.Modbus/ModbusErrorCode.cs +++ b/src/Meadow.Modbus/ModbusErrorCode.cs @@ -40,6 +40,11 @@ public enum ModbusErrorCode /// GatePathUnavailable = 10, + /// + /// Gateway Target Device Failed to Respond. + /// + GatewayTimeoutError = 11, + /// /// Send failed error code. /// diff --git a/src/Meadow.Modbus/ModbusPolledDevice.cs b/src/Meadow.Modbus/ModbusPolledDevice.cs index 7633b1c..5b24d5d 100644 --- a/src/Meadow.Modbus/ModbusPolledDevice.cs +++ b/src/Meadow.Modbus/ModbusPolledDevice.cs @@ -14,6 +14,16 @@ namespace Meadow.Modbus; /// public abstract class ModbusPolledDevice { + /// + /// Raised when communication with the target device times out + /// + public event EventHandler? CommTimeout; + + /// + /// Raised when a communication error with the target device occurs + /// + public event EventHandler? CommError; + /// /// Represents the possible formats of source registers /// @@ -65,7 +75,8 @@ private class RegisterMapping private Timer _timer; private int _refreshPeriosMs; - private List _mapping = new(); + private List _holdingRegisterMap = new(); + private List _inputRegisterMap = new(); /// /// Starts polling the Modbus device. @@ -109,8 +120,32 @@ protected async Task WriteHoldingRegister(ushort startRegister, params ushort[] { await _client.WriteHoldingRegister(_address, startRegister, data[0]); } + else + { + await _client.WriteHoldingRegisters(_address, startRegister, data); + } + } - await _client.WriteHoldingRegisters(_address, startRegister, data); + /// + /// Reads a value from a coil register of the Modbus device. + /// + /// The coil register address. + /// A task representing the asynchronous write operation. + protected async Task ReadCoil(ushort register) + { + var registers = await _client.ReadCoils(_address, register, 1); + return registers[0]; + } + + /// + /// Writes a value to a coil register of the Modbus device. + /// + /// The coil register address. + /// The value to be written to the coil registers. + /// A task representing the asynchronous write operation. + protected async Task WriteCoil(ushort register, bool value) + { + await _client.WriteCoil(_address, register, value); } /// @@ -136,7 +171,46 @@ protected void MapHoldingRegistersToProperty( var prop = this.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) ?? throw new ArgumentException($"Property '{propertyName}' not found"); - _mapping.Add(new RegisterMapping + _holdingRegisterMap.Add(new RegisterMapping + { + PropertyInfo = prop, + StartRegister = startRegister, + RegisterCount = registerCount, + Scale = scale, + Offset = offset, + SourceFormat = sourceFormat + }); + } + finally + { + _mapLock.Release(); + } + } + + /// + /// Maps a range of input registers to a property of the Modbus device. + /// + /// The starting register address. + /// The number of registers to map. + /// The name of the property to map the registers to. + /// The optional scale factor to apply to the register values. + /// The optional offset to apply to the register values. + /// The format of the source registers + protected void MapInputRegistersToProperty( + ushort startRegister, + int registerCount, + string propertyName, + double? scale = null, + double? offset = null, + SourceFormat sourceFormat = SourceFormat.LittleEndianInteger) + { + _mapLock.Wait(); + try + { + var prop = this.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) + ?? throw new ArgumentException($"Property '{propertyName}' not found"); + + _inputRegisterMap.Add(new RegisterMapping { PropertyInfo = prop, StartRegister = startRegister, @@ -175,7 +249,7 @@ protected void MapHoldingRegistersToField( var field = this.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) ?? throw new ArgumentException($"Field '{fieldName}' not found"); - _mapping.Add(new RegisterMapping + _holdingRegisterMap.Add(new RegisterMapping { FieldInfo = field, StartRegister = startRegister, @@ -206,7 +280,7 @@ protected void MapHoldingRegistersToProperty(ushort startRegister, int registerC var prop = this.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) ?? throw new ArgumentException($"Property '{propertyName}' not found"); - _mapping.Add(new RegisterMapping + _holdingRegisterMap.Add(new RegisterMapping { PropertyInfo = prop, StartRegister = startRegister, @@ -221,11 +295,11 @@ protected void MapHoldingRegistersToProperty(ushort startRegister, int registerC } /// - /// Maps a range of holding registers to a field of the Modbus device. + /// Maps a range of holding registers to a class field /// /// The starting register address. /// The number of registers to map. - /// The name of the property to map the registers to. + /// The name of the field to map the registers to. /// The custom conversion function to transform raw register values to the field type. protected void MapHoldingRegistersToField(ushort startRegister, int registerCount, string fieldName, Func conversionFunction) { @@ -235,7 +309,7 @@ protected void MapHoldingRegistersToField(ushort startRegister, int registerCoun var field = this.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) ?? throw new ArgumentException($"Field '{fieldName}' not found"); - _mapping.Add(new RegisterMapping + _holdingRegisterMap.Add(new RegisterMapping { FieldInfo = field, StartRegister = startRegister, @@ -249,50 +323,44 @@ protected void MapHoldingRegistersToField(ushort startRegister, int registerCoun } } - private async void RefreshTimerProc(object _) + /// + /// Maps a range of input registers to a class field + /// + /// The starting register address. + /// The number of registers to map. + /// The name of the field to map the registers to. + /// The custom conversion function to transform raw register values to the field type. + protected void MapInputRegistersToField(ushort startRegister, int registerCount, string fieldName, Func conversionFunction) { - var start = Environment.TickCount; - - await _mapLock.WaitAsync(); + _mapLock.Wait(); try { - // TODO: add support for group reads (i.e. contiguously mapped registers) + var field = this.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) + ?? throw new ArgumentException($"Field '{fieldName}' not found"); - foreach (var r in _mapping) + _inputRegisterMap.Add(new RegisterMapping { - // read registers - ushort[] data; - - try - { - data = await _client.ReadHoldingRegisters(_address, r.StartRegister, r.RegisterCount); - } - catch (TimeoutException) - { - break; - } - - if (data.Length == 0) - { - // TODO: should we notify or log or something? - return; - } + FieldInfo = field, + StartRegister = startRegister, + RegisterCount = registerCount, + ConversionFunction = conversionFunction + }); + } + finally + { + _mapLock.Release(); + } + } - if (r.PropertyInfo != null) - { - UpdateProperty(data, r); - } - else if (r.FieldInfo != null) - { - UpdateField(data, r); - } - else - { - // no field or prop - should not be possible - throw new ArgumentException(); - } + private async void RefreshTimerProc(object _) + { + var start = Environment.TickCount; - } + await _mapLock.WaitAsync(); + try + { + await MoveHoldingRegistersToProperties(); + await MoveInputRegistersToProperties(); } finally { @@ -305,6 +373,98 @@ private async void RefreshTimerProc(object _) _timer.Change(delay > MinimumPollDelayMs ? delay : MinimumPollDelayMs, -1); } + private async Task MoveHoldingRegistersToProperties() + { + // TODO: add support for group reads (i.e. contiguously mapped registers) + foreach (var r in _holdingRegisterMap) + { + // read registers + ushort[] data; + + try + { + data = await _client.ReadHoldingRegisters(_address, r.StartRegister, r.RegisterCount); + } + catch (TimeoutException) + { + CommTimeout?.Invoke(this, EventArgs.Empty); + break; + } + catch (Exception ex) + { + CommError?.Invoke(this, ex); + break; + } + + if (data.Length == 0) + { + // TODO: should we notify or log or something? + return; + } + + if (r.PropertyInfo != null) + { + UpdateProperty(data, r); + } + else if (r.FieldInfo != null) + { + UpdateField(data, r); + } + else + { + // no field or prop - should not be possible + throw new ArgumentException(); + } + + } + } + + private async Task MoveInputRegistersToProperties() + { + // TODO: add support for group reads (i.e. contiguously mapped registers) + foreach (var r in _inputRegisterMap) + { + // read registers + ushort[] data; + + try + { + data = await _client.ReadInputRegisters(_address, r.StartRegister, r.RegisterCount); + } + catch (TimeoutException) + { + CommTimeout?.Invoke(this, EventArgs.Empty); + break; + } + catch (Exception ex) + { + CommError?.Invoke(this, ex); + break; + } + + if (data.Length == 0) + { + // TODO: should we notify or log or something? + return; + } + + if (r.PropertyInfo != null) + { + UpdateProperty(data, r); + } + else if (r.FieldInfo != null) + { + UpdateField(data, r); + } + else + { + // no field or prop - should not be possible + throw new ArgumentException(); + } + + } + } + private void UpdateProperty(ushort[] data, RegisterMapping mapping) { if (mapping.ConversionFunction != null) @@ -328,6 +488,11 @@ private void UpdateProperty(ushort[] data, RegisterMapping mapping) { UpdateIntegerProperty(data, mapping); } + else if ( + mapping.PropertyInfo!.PropertyType == typeof(bool)) + { + UpdateBooleanProperty(data, mapping); + } else { throw new NotSupportedException(); @@ -358,6 +523,11 @@ private void UpdateField(ushort[] data, RegisterMapping mapping) { UpdateIntegerField(data, mapping); } + else if ( + mapping.PropertyInfo!.PropertyType == typeof(bool)) + { + UpdateBooleanField(data, mapping); + } else { throw new NotSupportedException(); @@ -448,6 +618,14 @@ private void UpdateIntegerProperty(ushort[] data, RegisterMapping mapping) } } + private void UpdateBooleanProperty(ushort[] data, RegisterMapping mapping) + { + if (mapping.PropertyInfo!.PropertyType == typeof(bool)) + { + mapping.PropertyInfo!.SetValue(this, data[0] != 0); + } + } + private void UpdateDoubleProperty(ushort[] data, RegisterMapping mapping) { var final = Convert.ToDouble(GetValueByFormat(data, mapping.SourceFormat)); @@ -524,4 +702,12 @@ private void UpdateDoubleField(ushort[] data, RegisterMapping mapping) mapping.FieldInfo!.SetValue(this, Convert.ToSingle(final)); } } + + private void UpdateBooleanField(ushort[] data, RegisterMapping mapping) + { + if (mapping.FieldInfo!.FieldType == typeof(bool)) + { + mapping.FieldInfo!.SetValue(this, data[0] != 0); + } + } }