diff --git a/csharp/Lib/Protocols/Modbus/Channels/Channel.cs b/csharp/Lib/Protocols/Modbus/Channels/Channel.cs new file mode 100644 index 000000000..9fd8ec731 --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Channels/Channel.cs @@ -0,0 +1,7 @@ +namespace InnovEnergy.Lib.Protocols.Modbus.Channels; + +public abstract class Channel +{ + public abstract IReadOnlyList Read(Int32 nBytes); + public abstract void Write(IReadOnlyList bytes); +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Channels/ConnectionChannel.cs b/csharp/Lib/Protocols/Modbus/Channels/ConnectionChannel.cs new file mode 100644 index 000000000..bafbe947c --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Channels/ConnectionChannel.cs @@ -0,0 +1,82 @@ +namespace InnovEnergy.Lib.Protocols.Modbus.Channels; + +public abstract class ConnectionChannel : Channel, IDisposable +{ + private readonly Func _CloseAfterException ; + private readonly Boolean _CloseAfterSuccessfulRead ; + private readonly Boolean _CloseAfterSuccessfulWrite ; + + protected abstract T Open(); + protected abstract void Close(T connection); + + protected abstract IReadOnlyList Read (T connection, Int32 nBytes); + protected abstract void Write(T connection, IReadOnlyList data); + + private T? _Connection; + + protected ConnectionChannel(Boolean closeAfterSuccessfulRead = false, + Boolean closeAfterSuccessfulWrite = false, + Func? closeAfterException = null) + { + _CloseAfterSuccessfulRead = closeAfterSuccessfulRead; + _CloseAfterSuccessfulWrite = closeAfterSuccessfulWrite; + _CloseAfterException = closeAfterException ?? (_ => true); + } + + public override IReadOnlyList Read(Int32 nBytes) + { + try + { + return Read(Connection, nBytes); + } + catch (Exception e) + { + if (_CloseAfterException(e)) + Close(); + + throw; + } + finally + { + if (_CloseAfterSuccessfulRead) + Close(); + } + } + + + public override void Write(IReadOnlyList data) + { + try + { + Write(Connection, data); + } + catch (Exception e) + { + if (_CloseAfterException(e)) + Close(); + + throw; + } + finally + { + if (_CloseAfterSuccessfulWrite) + Close(); + } + } + + + private T Connection => _Connection ??= Open(); + + + private void Close() + { + if (_Connection is null) + return; + + Close(_Connection); + _Connection = default; + } + + + public void Dispose() => Close(); +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Connections/PushToPullHelper.cs b/csharp/Lib/Protocols/Modbus/Channels/PushToPullHelper.cs similarity index 75% rename from csharp/Lib/Protocols/Modbus/Connections/PushToPullHelper.cs rename to csharp/Lib/Protocols/Modbus/Channels/PushToPullHelper.cs index ad78f46e8..115b97795 100644 --- a/csharp/Lib/Protocols/Modbus/Connections/PushToPullHelper.cs +++ b/csharp/Lib/Protocols/Modbus/Channels/PushToPullHelper.cs @@ -1,7 +1,7 @@ using System.Reactive.Linq; using InnovEnergy.Lib.Utils; -namespace InnovEnergy.Lib.Protocols.Modbus.Connections; +namespace InnovEnergy.Lib.Protocols.Modbus.Channels; public static class PushToPullHelper { @@ -14,12 +14,16 @@ public static class PushToPullHelper var nAvailable = src .Do(buffer.Enqueue) .Select(_ => buffer.Count) - .Publish(); - - nAvailable.Connect(); + .Publish() + .RefCount(); + nAvailable.SelectError() + .Subscribe(e => e.WriteLine()); + async Task> Read(Int32 n) { + Console.WriteLine($"requesting {n}"); + var available = buffer.Count; if (available < n) available = await nAvailable.FirstOrDefaultAsync(a => a >= n); diff --git a/csharp/Lib/Protocols/Modbus/Channels/RemoteSerialChannel.cs b/csharp/Lib/Protocols/Modbus/Channels/RemoteSerialChannel.cs new file mode 100644 index 000000000..46fd27f72 --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Channels/RemoteSerialChannel.cs @@ -0,0 +1,207 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO.Ports; +using System.Reactive.Linq; +using CliWrap; +using InnovEnergy.Lib.Utils; + +namespace InnovEnergy.Lib.Protocols.Modbus.Channels; + +public record RemoteSerialConnection + ( + Func> Read, + Action> Write, + Action Close + ); + + +// public class RemoteSerialChannel : ConnectionChannel +// { +// private readonly Command _Command; +// +// public RemoteSerialChannel(SshHost host, +// String tty, +// Int32 baudRate, +// Parity parity, +// Int32 stopBits, +// Int32 dataBits) +// { +// tty = tty.EnsureStartsWith("/dev/"); +// +// var configureTty = ConfigureTty(tty, baudRate, parity, stopBits, dataBits); +// var redirectStreams = RedirectStreams(tty); +// +// var call = $"{configureTty}; {redirectStreams}"; +// +// _Command = host +// .Command +// .AppendArgument(call); +// +// _Command.WriteLine(); +// } +// +// +// protected override RemoteSerialConnection Open() +// { +// var observableProcess = new ObservableProcess(_Command); +// +// observableProcess.Start(); +// +// +// IReadOnlyList Read(Int32 i) +// { +// return observableProcess.Read(i).Result; +// } +// +// void Write(IReadOnlyList data) => observableProcess.StdIn.OnNext(data.ToArray()); +// +// return new RemoteSerialConnection(Read, Write, observableProcess.Interrupt); +// } +// +// protected override void Close(RemoteSerialConnection connection) +// { +// connection.Close(); +// } +// +// protected override IReadOnlyList Read(RemoteSerialConnection connection, Int32 nBytes) +// { +// return connection.Read(nBytes); +// } +// +// protected override void Write(RemoteSerialConnection connection, IReadOnlyList data) +// { +// connection.Write(data); +// } +// +// +// private static String RedirectStreams(String tty) +// { +// // https://unix.stackexchange.com/questions/19604/all-about-ssh-proxycommand +// +// return $"exec 3<>{tty}; " + +// $"cat <&3 & cat >&3; " + +// // $"(cat <&3 | tee -a ~/read) & cat | tee -a ~/write >&3; " + +// $"kill $!"; +// +// // var call = $"trap 'kill -HUP $(jobs -lp) 2>/dev/null || true' EXIT; " + +// // $"{configure} ; "+ +// // $"dd if={tty} of=/dev/stdout bs=1 & " + +// // $"dd if=/dev/stdin of={tty} bs=1 ;" +// } +// +// [SuppressMessage("ReSharper", "StringLiteralTypo")] +// private static String ConfigureTty(String tty, Int32 baudRate, Parity parity, Int32 stopBits, Int32 dataBits) +// { +// var oParity = parity switch +// { +// Parity.Even => "parenb -parodd", +// Parity.Odd => "parenb parodd", +// Parity.None => "-parenb", +// _ => throw new NotImplementedException() +// }; +// +// var oStopBits = stopBits switch +// { +// 1 => "-cstopb", +// 2 => "cstopb", +// _ => throw new NotImplementedException() +// }; +// +// var oDataBits = "cs" + dataBits; +// +// return $"stty -F {tty} {baudRate} {oDataBits} {oStopBits} {oParity}"; +// } +// +// } + +public class RemoteSerialChannel : ConnectionChannel +{ + private readonly Command _Command; + private readonly TcpChannel _TcpChannel; + + const String SsDir = "/opt/victronenergy/serial-starter"; + const String KillTasks = "kill $!"; + + private CancellationTokenSource _CancellationTokenSource = new CancellationTokenSource(); + + private CommandTask? _CommandTask; + + public RemoteSerialChannel(SshHost host, + String tty, + Int32 baudRate, + Parity parity, + Int32 stopBits, + Int32 dataBits) + { + const Int32 port = 6855; + + tty = tty.EnsureStartsWith("/dev/"); + + var configureTty = ConfigureTty(tty, baudRate, parity, stopBits, dataBits); + + var stopTty = $"{SsDir}/stop-tty.sh {tty}"; + var startTty = $"{SsDir}/start-tty.sh {tty}"; + + // ReSharper disable once StringLiteralTypo + var socat = $"socat TCP-LISTEN:{port},nodelay {tty},raw"; + + //var script = $"-n -o RemoteCommand='{stopTty}; {configureTty} && {socat} ; {startTty} ; {KillTasks}'"; + var script = $"{stopTty}; {configureTty} && {socat} ; {startTty} ; {KillTasks}"; + + _Command = host.Command.AppendArgument(script); + + _Command.WriteLine(); + + _TcpChannel = new TcpChannel(host.HostName, port); + } + + private static String ConfigureTty(String tty, Int32 baudRate, Parity parity, Int32 stopBits, Int32 dataBits) + { + var oParity = parity switch + { + Parity.Even => "parenb -parodd", + Parity.Odd => "parenb parodd", + Parity.None => "-parenb", + _ => throw new NotImplementedException() + }; + + var oStopBits = stopBits switch + { + 1 => "-cstopb", + 2 => "cstopb", + _ => throw new NotImplementedException() + }; + + var oDataBits = "cs" + dataBits; + + return $"stty -F {tty} {baudRate} {oDataBits} {oStopBits} {oParity}"; + } + + + protected override TcpChannel Open() + { + _CommandTask ??= _Command.ExecuteAsync(_CancellationTokenSource.Token); + + Thread.Sleep(2000); // wait until socat is ready + return _TcpChannel; + } + + protected override void Close(TcpChannel connection) + { + _CancellationTokenSource.Cancel(); + connection.Dispose(); + + _CommandTask = null; + + _CancellationTokenSource = new CancellationTokenSource(); + } + + protected override IReadOnlyList Read(TcpChannel connection, Int32 nBytes) + { + return connection.Read(nBytes); + } + + protected override void Write(TcpChannel connection, IReadOnlyList data) + { + connection.Write(data); + } +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Channels/SerialPortChannel.cs b/csharp/Lib/Protocols/Modbus/Channels/SerialPortChannel.cs new file mode 100644 index 000000000..904be5b29 --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Channels/SerialPortChannel.cs @@ -0,0 +1,67 @@ +using System.IO.Ports; +using CliWrap; +using InnovEnergy.Lib.Protocols.Modbus.Protocol; + +namespace InnovEnergy.Lib.Protocols.Modbus.Channels; + +public class SerialPortChannel : ConnectionChannel +{ + private readonly Func _Open; + + public SerialPortChannel(String portName, + Int32 baudRate, + Parity parity, + Int32 dataBits, + Int32 stopBits, + Boolean closeAfterSuccessfulRead = false, + Boolean closeAfterSuccessfulWrite = false) + : + base( + closeAfterSuccessfulRead, + closeAfterSuccessfulWrite + ) + { + var sb = stopBits switch + { + 0 => StopBits.None, + 1 => StopBits.One, + 2 => StopBits.Two, + _ => StopBits.OnePointFive + }; + + _Open = () => + { + var serialPort = new SerialPort(portName, baudRate, parity, dataBits, sb); + serialPort.Open(); + return serialPort; + }; + } + + protected override SerialPort Open() => _Open(); + + protected override void Close(SerialPort serialPort) => serialPort.Dispose(); + + protected override IReadOnlyList Read(SerialPort serialPort, Int32 nBytes) + { + var buffer = new Byte[nBytes]; + + var bytesReceived = 0; + do + { + var received = serialPort.Read(buffer, bytesReceived, nBytes - bytesReceived); + if (received < 0) + throw new NotConnectedException("Serial Connection has been closed"); + + bytesReceived += received; + } + while (bytesReceived < nBytes); + + return buffer; + } + + protected override void Write(SerialPort serialPort, IReadOnlyList data) + { + var array = data.ToArray(); + serialPort.Write(array, 0, array.Length); + } +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Channels/TcpChannel.cs b/csharp/Lib/Protocols/Modbus/Channels/TcpChannel.cs new file mode 100644 index 000000000..f21946408 --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Channels/TcpChannel.cs @@ -0,0 +1,48 @@ +using System.Net.Sockets; +using InnovEnergy.Lib.Protocols.Modbus.Protocol; + +namespace InnovEnergy.Lib.Protocols.Modbus.Channels; + +public class TcpChannel : ConnectionChannel +{ + public TcpChannel(String hostname, + Int32 port, + Boolean closeAfterSuccessfulRead = false, + Boolean closeAfterSuccessfulWrite = false) : base(closeAfterSuccessfulRead, closeAfterSuccessfulWrite) + { + TcpClient Open() => new(hostname, port); + _Open = Open; + } + + private readonly Func _Open; + + protected override TcpClient Open() => _Open(); + protected override void Close(TcpClient tcpClient) => tcpClient.Close(); + + protected override IReadOnlyList Read(TcpClient tcpClient, Int32 nToRead) + { + var buffer = new Byte[nToRead]; + + var stream = tcpClient.GetStream(); + var nReceived = 0; + + do + { + var nRemaining = nToRead - nReceived; + var read = stream.Read(buffer, nReceived, nRemaining); + if (read <= 0) + throw new NotConnectedException("The TCP Connection was closed"); + + nReceived += read; + } + while (nReceived < nToRead); + + return buffer; + } + + protected override void Write(TcpClient tcpClient, IReadOnlyList data) + { + var array = data.ToArray(); + tcpClient.GetStream().Write(array, 0, array.Length); + } +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Clients/ModbusClient.cs b/csharp/Lib/Protocols/Modbus/Clients/ModbusClient.cs index 0e2e7d158..2aba23c4d 100644 --- a/csharp/Lib/Protocols/Modbus/Clients/ModbusClient.cs +++ b/csharp/Lib/Protocols/Modbus/Clients/ModbusClient.cs @@ -1,35 +1,38 @@ -using InnovEnergy.Lib.Protocols.Modbus.Connections; +using InnovEnergy.Lib.Protocols.Modbus.Channels; +using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; namespace InnovEnergy.Lib.Protocols.Modbus.Clients; -using Booleans = IReadOnlyList; -using UInt16s = IReadOnlyList; +using Booleans = IReadOnlyCollection; +using UInt16s = IReadOnlyCollection; // TODO: ModbusClient = Framer(TCP/RTU) + Connection(Serial/TCP) + Encoder(binary/ascii) + quirk(+1/0) // Transport public abstract class ModbusClient { - internal ModbusConnection Connection { get; } - internal Byte SlaveId { get; } + internal Channel Channel { get; } + internal Byte SlaveId { get; } + internal Endian Endian { get; } // TODO: add additional functions: coils... - public abstract Booleans ReadCoils (UInt16 readAddress, UInt16 nValues); - public abstract Booleans ReadDiscreteInputs (UInt16 readAddress, UInt16 nValues); - public abstract UInt16s ReadInputRegisters (UInt16 readAddress, UInt16 nValues); - public abstract UInt16s ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues); - public abstract UInt16 WriteCoils (UInt16 writeAddress, Booleans coils); - public abstract UInt16 WriteRegisters (UInt16 writeAddress, UInt16s values); + public abstract MbData ReadCoils (UInt16 readAddress, UInt16 nValues); + public abstract MbData ReadDiscreteInputs (UInt16 readAddress, UInt16 nValues); + public abstract MbData ReadInputRegisters (UInt16 readAddress, UInt16 nValues); + public abstract MbData ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues); + public abstract UInt16 WriteCoils (UInt16 writeAddress, Booleans coils); + public abstract UInt16 WriteRegisters (UInt16 writeAddress, UInt16s values); - public abstract UInt16s ReadWriteRegisters (UInt16 readAddress, - UInt16 nbToRead, - UInt16 writeAddress, - UInt16s registersToWrite); + public abstract MbData ReadWriteRegisters (UInt16 readAddress, + UInt16 nbToRead, + UInt16 writeAddress, + UInt16s registersToWrite); - protected ModbusClient(ModbusConnection connection, Byte slaveId) + protected ModbusClient(Channel channel, Byte slaveId, Endian endian = Endian.Little) { - Connection = connection; - SlaveId = slaveId; + Channel = channel; + SlaveId = slaveId; + Endian = endian; } } \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Clients/ModbusRtuClient.cs b/csharp/Lib/Protocols/Modbus/Clients/ModbusRtuClient.cs index 4e3afe361..b98107af1 100644 --- a/csharp/Lib/Protocols/Modbus/Clients/ModbusRtuClient.cs +++ b/csharp/Lib/Protocols/Modbus/Clients/ModbusRtuClient.cs @@ -1,8 +1,9 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -using InnovEnergy.Lib.Protocols.Modbus.Connections; +using InnovEnergy.Lib.Protocols.Modbus.Channels; using InnovEnergy.Lib.Protocols.Modbus.Protocol; using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames; +using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Commands; using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Replies; using InnovEnergy.Lib.Protocols.Modbus.Util; @@ -12,73 +13,69 @@ using static System.Runtime.CompilerServices.MethodImplOptions; namespace InnovEnergy.Lib.Protocols.Modbus.Clients; -using UInt16s = IReadOnlyList; +using UInt16s = IReadOnlyCollection; +using Booleans = IReadOnlyCollection; public class ModbusRtuClient : ModbusClient { private const Int32 CrcSize = 2; - public ModbusRtuClient(ModbusConnection connection, Byte slaveId) : base(connection, slaveId) + public ModbusRtuClient(Channel channel, Byte slaveId) : base(channel, slaveId) { } - public override IReadOnlyList ReadCoils(UInt16 readAddress, UInt16 nValues) + public override MbData ReadCoils(UInt16 readAddress, UInt16 nValues) { throw new NotImplementedException(); } - public override IReadOnlyList ReadDiscreteInputs(UInt16 readAddress, UInt16 nValues) + public override MbData ReadDiscreteInputs(UInt16 readAddress, UInt16 nValues) { throw new NotImplementedException(); } - public override UInt16s ReadInputRegisters(UInt16 readAddress, UInt16 nValues) + public override MbData ReadInputRegisters(UInt16 readAddress, UInt16 nValues) { var cmd = new ReadInputRegistersCommandFrame(SlaveId, readAddress, nValues); var crc = CalcCrc(cmd); // TX - cmd.Data.Concat(crc).Apply(Connection.Transmit); - - var nToRead = cmd.ExpectedResponseSize + CrcSize; + cmd.Data.Concat(crc).ToList().Apply(Channel.Write); // RX - var response = nToRead - .ConvertTo() - .Apply(Connection.Receive) + var response = Channel + .Read(cmd.ExpectedResponseSize + CrcSize) .ToArraySegment() .Apply(AssertCrc) .SkipLast(CrcSize) .Apply(ReadInputRegistersResponseFrame.Parse) .Apply(cmd.VerifyResponse); - return response.RegistersRead; + return new MbData(response.RegistersRead.RawData, readAddress, Endian); } - public override UInt16s ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues) + public override MbData ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues) { var cmd = new ReadHoldingRegistersCommandFrame(SlaveId, readAddress, nValues); var crc = CalcCrc(cmd.Data); // TX - cmd.Data.Concat(crc).Apply(Connection.Transmit); + cmd.Data.Concat(crc).ToList().Apply(Channel.Write); // RX - var nToRead = cmd.ExpectedResponseSize + CrcSize; - var response = nToRead - .ConvertTo() - .Apply(Connection.Receive) + var response = Channel + .Read(cmd.ExpectedResponseSize + CrcSize) .ToArraySegment() .Apply(AssertCrc) .SkipLast(CrcSize) .Apply(ReadHoldingRegistersResponseFrame.Parse) .Apply(cmd.VerifyResponse); - return response.RegistersRead; + return new MbData(response.RegistersRead.RawData, readAddress, Endian); } - public override UInt16 WriteCoils(UInt16 writeAddress, IReadOnlyList coils) + public override UInt16 WriteCoils(UInt16 writeAddress, Booleans coils) { throw new NotImplementedException(); } @@ -88,16 +85,13 @@ public class ModbusRtuClient : ModbusClient { var cmd = new WriteRegistersCommandFrame(SlaveId, writeAddress, values); var crc = CalcCrc(cmd); - var nToRead = cmd.ExpectedResponseSize + CrcSize; // TX - cmd.Data.Concat(crc).Apply(Connection.Transmit); - + cmd.Data.Concat(crc).ToList().Apply(Channel.Write); // RX - var response = nToRead - .ConvertTo() - .Apply(Connection.Receive) + var response = Channel + .Read(cmd.ExpectedResponseSize + CrcSize) .ToArraySegment() .Apply(AssertCrc) .SkipLast(CrcSize) @@ -107,7 +101,7 @@ public class ModbusRtuClient : ModbusClient return response.NbWritten; } - public override UInt16s ReadWriteRegisters(UInt16 readAddress, UInt16 nbToRead, UInt16 writeAddress, UInt16s registersToWrite) + public override MbData ReadWriteRegisters(UInt16 readAddress, UInt16 nbToRead, UInt16 writeAddress, UInt16s registersToWrite) { var cmd = new ReadWriteRegistersCommandFrame(SlaveId, readAddress, @@ -117,19 +111,18 @@ public class ModbusRtuClient : ModbusClient var crc = CalcCrc(cmd); // TX - cmd.Data.Concat(crc).Apply(Connection.Transmit); + cmd.Data.Concat(crc).ToList().Apply(Channel.Write); // RX var nToRead = cmd.ExpectedResponseSize + CrcSize; var response = nToRead - .ConvertTo() - .Apply(Connection.Receive) + .Apply(Channel.Read) .ToArraySegment() .SkipLast(CrcSize) .Apply(ReadWriteRegistersResponseFrame.Parse) .Apply(cmd.VerifyResponse); - return response.RegistersRead; + return new MbData(response.RegistersRead.RawData, readAddress, Endian); } public static ArraySegment AssertCrc(ArraySegment data) diff --git a/csharp/Lib/Protocols/Modbus/Clients/ModbusSlave.cs b/csharp/Lib/Protocols/Modbus/Clients/ModbusSlave.cs deleted file mode 100644 index 0d854cb9f..000000000 --- a/csharp/Lib/Protocols/Modbus/Clients/ModbusSlave.cs +++ /dev/null @@ -1,159 +0,0 @@ -using InnovEnergy.Lib.Protocols.Modbus.Reflection; -using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; - -namespace InnovEnergy.Lib.Protocols.Modbus.Clients; - -public class ModbusSlave where S : notnull, new() where C : notnull -{ - private readonly ModbusClient _ModbusClient; - - public ModbusSlave(ModbusClient modbusClient) => _ModbusClient = modbusClient; - - public S? Read() - { - try - { - return ReadInto(new S()); - } - catch (Exception e) - { - return default; // TODO :stdErr - } - } - - public Exception? Write(C controlRecord) - { - try - { - WriteHoldingRegisters(controlRecord); - WriteCoils(controlRecord); - } - catch (Exception e) - { - _ModbusClient.Connection.Close(); - return e; - } - - return default; - } - - - public S ReadInto(S statusRecord) - { - Int32 holdingRegisters; - Int32 coils; - Int32 inputRegisters; - Int32 discreteInputs; - - try - { - holdingRegisters = ReadHoldingRegisters(statusRecord).Count(); // force enumeration! - inputRegisters = ReadInputRegisters(statusRecord).Count(); - discreteInputs = ReadDiscreteInputs(statusRecord).Count(); - coils = ReadCoils(statusRecord).Count(); - } - catch (Exception e) - { - _ModbusClient.Connection.Close(); - throw; - } - - - var nUpdated = holdingRegisters - + inputRegisters - + discreteInputs - + coils; - - if (nUpdated == 0) - throw new ArgumentException(nameof(statusRecord)); - - return statusRecord; - } - - // returns an enumerable of read addresses - private IEnumerable ReadDiscreteInputs(Object statusRecord) - { - return from batch in statusRecord.GetModbusBooleanBatches() - let received = _ModbusClient.ReadDiscreteInputs(batch[0].Address, (UInt16)batch.Length) - from address in batch.SetStatusRecordMembersFromRawModbusValues(received) - select address; - } - - // returns an enumerable of read addresses - private IEnumerable ReadCoils(Object statusRecord) - { - return from batch in statusRecord.GetModbusBooleanBatches() - let received = _ModbusClient.ReadCoils(batch[0].Address, (UInt16)batch.Length) - from address in batch.SetStatusRecordMembersFromRawModbusValues(received) - select address; - } - - // returns an enumerable of read addresses - private IEnumerable ReadInputRegisters(Object statusRecord) - { - return from batch in statusRecord.GetModbusRegisterBatches() - let received = _ModbusClient.ReadInputRegisters(batch[0].Address, (UInt16)batch.Length) - from address in batch.SetStatusRecordMembersFromRawModbusValues(received) - select address; - } - - // returns an enumerable of read addresses - private IEnumerable ReadHoldingRegisters(Object statusRecord) - { - return from batch in statusRecord.GetModbusRegisterBatches() - let received = _ModbusClient.ReadHoldingRegisters(batch[0].Address, (UInt16)batch.Length) - from address in batch.SetStatusRecordMembersFromRawModbusValues(received) - select address; - } - - private Int32 WriteCoils(C controlRecord) - { - var nBatches = 0; - - foreach (var batch in controlRecord.GetModbusBooleanBatches()) - { - var values = batch.GetRawModbusValuesFromControlRecord(); - _ModbusClient.WriteCoils(batch[0].Address, values); - nBatches++; - } - - return nBatches; - } - - - private Int32 WriteHoldingRegisters(C controlRecord) - { - var nBatches = 0; - - foreach (var batch in controlRecord.GetModbusRegisterBatches()) - { - var values = batch.GetRawModbusValuesFromControlRecord(); - _ModbusClient.WriteRegisters(batch[0].Address, values); - nBatches++; - } - - return nBatches; - } - - -} - -public class ModbusSlave : ModbusSlave where T : notnull, new() -{ - public ModbusSlave(ModbusClient modbusClient) : base(modbusClient) - { - } -} - -public static class ModbusSlave -{ - public static ModbusSlave Slave(this ModbusClient modbusClient) where T : notnull, new() - { - return new ModbusSlave(modbusClient); - } - - public static ModbusSlave Slave(this ModbusClient modbusClient) where S : notnull, new() where C : notnull - { - return new ModbusSlave(modbusClient); - } -} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Clients/ModbusTcpClient.cs b/csharp/Lib/Protocols/Modbus/Clients/ModbusTcpClient.cs index badcff48a..ff206717f 100644 --- a/csharp/Lib/Protocols/Modbus/Clients/ModbusTcpClient.cs +++ b/csharp/Lib/Protocols/Modbus/Clients/ModbusTcpClient.cs @@ -1,5 +1,6 @@ using System.Diagnostics; -using InnovEnergy.Lib.Protocols.Modbus.Connections; +using InnovEnergy.Lib.Protocols.Modbus.Channels; +using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Commands; using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Replies; using InnovEnergy.Lib.Protocols.Modbus.Tcp; @@ -8,8 +9,8 @@ using InnovEnergy.Lib.Utils; namespace InnovEnergy.Lib.Protocols.Modbus.Clients; -using UInt16s = IReadOnlyList; - +using UInt16s = IReadOnlyCollection; +using Booleans = IReadOnlyCollection; public class ModbusTcpClient : ModbusClient { @@ -20,11 +21,11 @@ public class ModbusTcpClient : ModbusClient private UInt16 NextId() => unchecked(++_Id); - public ModbusTcpClient(ModbusConnection connection, Byte slaveId) : base(connection, slaveId) + public ModbusTcpClient(Channel channel, Byte slaveId) : base(channel, slaveId) { } - public override IReadOnlyList ReadCoils(UInt16 readAddress, UInt16 nValues) + public override MbData ReadCoils(UInt16 readAddress, UInt16 nValues) { var id = NextId(); // TODO: check response id @@ -32,19 +33,22 @@ public class ModbusTcpClient : ModbusClient var hdr = new MbapHeader(id, cmd.Data.Count); var frm = new ModbusTcpFrame(hdr, cmd); - Connection.Transmit(frm.Data); + Channel.Write(frm.Data); - var hData = Connection.Receive(MbapHeader.Size).ToArray(); // TODO: optimize .ToArray() ? + var hData = Channel.Read(MbapHeader.Size).ToArray(); var rxHdr = new MbapHeader(hData); - var fData = Connection.Receive(rxHdr.FrameLength).ToArray(); // TODO: optimize .ToArray() ? - var rxFrm = ReadCoilsResponseFrame.Parse(fData); - - return cmd.VerifyResponse(rxFrm).Inputs; + var rxFrm = Channel + .Read(rxHdr.FrameLength) + .ToArray() // TODO: optimize .ToArray() ? + .Apply(ReadCoilsResponseFrame.Parse) + .Apply(cmd.VerifyResponse); + + return new MbData(rxFrm.Coils.RawData, readAddress, Endian); } - public override IReadOnlyList ReadDiscreteInputs(UInt16 readAddress, UInt16 nValues) + public override MbData ReadDiscreteInputs(UInt16 readAddress, UInt16 nValues) { var id = NextId(); // TODO: check response id @@ -52,18 +56,21 @@ public class ModbusTcpClient : ModbusClient var hdr = new MbapHeader(id, cmd.Data.Count); var frm = new ModbusTcpFrame(hdr, cmd); - Connection.Transmit(frm.Data); + Channel.Write(frm.Data); - var hData = Connection.Receive(MbapHeader.Size).ToArray(); // TODO: optimize .ToArray() ? + var hData = Channel.Read(MbapHeader.Size).ToArray(); // TODO: optimize .ToArray() ? var rxHdr = new MbapHeader(hData); - var fData = Connection.Receive(rxHdr.FrameLength).ToArray(); // TODO: optimize .ToArray() ? - var rxFrm = ReadDiscreteInputsResponseFrame.Parse(fData); - - return cmd.VerifyResponse(rxFrm).Inputs; + var rxFrm = Channel + .Read(rxHdr.FrameLength) + .ToArray() // TODO: optimize .ToArray() ? + .Apply(ReadDiscreteInputsResponseFrame.Parse) + .Apply(cmd.VerifyResponse); + + return new MbData(rxFrm.Inputs.RawData, readAddress, Endian); } - public override IReadOnlyList ReadInputRegisters(UInt16 readAddress, UInt16 nValues) + public override MbData ReadInputRegisters(UInt16 readAddress, UInt16 nValues) { var id = NextId(); // TODO: check response id @@ -71,56 +78,62 @@ public class ModbusTcpClient : ModbusClient var hdr = new MbapHeader(id, cmd.Data.Count); var frm = new ModbusTcpFrame(hdr, cmd); - Connection.Transmit(frm.Data); + Channel.Write(frm.Data); - var hData = Connection.Receive(MbapHeader.Size).ToArray(); + var hData = Channel.Read(MbapHeader.Size).ToArray(); var rxHdr = new MbapHeader(hData); - var fData = Connection.Receive(rxHdr.FrameLength).ToArray(); - var rxFrm = ReadInputRegistersResponseFrame.Parse(fData); + var rxFrm = Channel + .Read(rxHdr.FrameLength) + .ToArray() + .Apply(ReadInputRegistersResponseFrame.Parse) + .Apply(cmd.VerifyResponse); - var verified = cmd.VerifyResponse(rxFrm); - - return verified.RegistersRead; + return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian); } - public override IReadOnlyList ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues) + public override MbData ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues) { var id = NextId(); // TODO: check response id + var cmd = new ReadHoldingRegistersCommandFrame(SlaveId, readAddress, nValues); var hdr = new MbapHeader(id, cmd.Data.Count); var frm = new ModbusTcpFrame(hdr, cmd); - Connection.Transmit(frm.Data); + Channel.Write(frm.Data); - var hData = Connection.Receive(MbapHeader.Size).ToArray(MbapHeader.Size); + var hData = Channel.Read(MbapHeader.Size).ToArray(); var rxHdr = new MbapHeader(hData); - var fData = Connection.Receive(rxHdr.FrameLength).ToArray(rxHdr.FrameLength); - var rxFrm = ReadHoldingRegistersResponseFrame.Parse(fData); + var rxFrm = Channel + .Read(rxHdr.FrameLength) + .ToArray() + .Apply(ReadHoldingRegistersResponseFrame.Parse) + .Apply(cmd.VerifyResponse); - var verified = cmd.VerifyResponse(rxFrm); - - return verified.RegistersRead; // TODO + return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian); } - public override UInt16 WriteCoils(UInt16 writeAddress, IReadOnlyList coils) + public override UInt16 WriteCoils(UInt16 writeAddress, Booleans coils) { var id = NextId(); // TODO: check response id var cmd = new WriteCoilsCommandFrame(SlaveId, writeAddress, coils); var hdr = new MbapHeader(id, cmd.Data.Count); var frm = new ModbusTcpFrame(hdr, cmd); - Connection.Transmit(frm.Data); + Channel.Write(frm.Data); - var hData = Connection.Receive(MbapHeader.Size).ToArray(); + var hData = Channel.Read(MbapHeader.Size).ToArray(); var rxHdr = new MbapHeader(hData); - var fData = Connection.Receive(rxHdr.FrameLength).ToArray(); - var rxFrm = WriteCoilsResponseFrame.Parse(fData); + var rxFrm = Channel + .Read(rxHdr.FrameLength) + .ToArray() + .Apply(WriteCoilsResponseFrame.Parse) + .Apply(cmd.VerifyResponse); - return cmd.VerifyResponse(rxFrm).NbWritten; + return rxFrm.NbWritten; } @@ -131,20 +144,21 @@ public class ModbusTcpClient : ModbusClient var hdr = new MbapHeader(id, cmd.Data.Count); var frm = new ModbusTcpFrame(hdr, cmd); - Connection.Transmit(frm.Data); + Channel.Write(frm.Data); - var hData = Connection.Receive(MbapHeader.Size).ToArray(); + var hData = Channel.Read(MbapHeader.Size).ToArray(); var rxHdr = new MbapHeader(hData); - var fData = Connection.Receive(rxHdr.FrameLength).ToArray(); - var rxFrm = WriteRegistersResponseFrame.Parse(fData); + var rxFrm = Channel + .Read(rxHdr.FrameLength) + .ToArray() + .Apply(WriteRegistersResponseFrame.Parse) + .Apply(cmd.VerifyResponse); - var verified = cmd.VerifyResponse(rxFrm); - - return verified.NbWritten; + return rxFrm.NbWritten; } - public override IReadOnlyList ReadWriteRegisters(UInt16 readAddress, UInt16 nbToRead, UInt16 writeAddress, UInt16s registersToWrite) + public override MbData ReadWriteRegisters(UInt16 readAddress, UInt16 nbToRead, UInt16 writeAddress, UInt16s registersToWrite) { var id = NextId(); // TODO: check response id @@ -157,17 +171,17 @@ public class ModbusTcpClient : ModbusClient var hdr = new MbapHeader(id, cmd.Data.Count); var frm = new ModbusTcpFrame(hdr, cmd); - Connection.Transmit(frm.Data); + Channel.Write(frm.Data); - var hData = Enumerable.ToArray(Connection.Receive(MbapHeader.Size)); + var hData = Enumerable.ToArray(Channel.Read(MbapHeader.Size)); var rxHdr = new MbapHeader(hData); - var fData = Enumerable.ToArray(Connection.Receive(rxHdr.FrameLength)); - var rxFrm = ReadWriteRegistersResponseFrame.Parse(fData); + var fData = Enumerable.ToArray(Channel.Read(rxHdr.FrameLength)); + var rxFrm = fData + .Apply(ReadWriteRegistersResponseFrame.Parse) + .Apply(cmd.VerifyResponse); - var verified = cmd.VerifyResponse(rxFrm); - - return verified.RegistersRead; // TODO + return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian); } } \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Connections/ModbusConnection.cs b/csharp/Lib/Protocols/Modbus/Connections/ModbusConnection.cs deleted file mode 100644 index 30ad8d5a9..000000000 --- a/csharp/Lib/Protocols/Modbus/Connections/ModbusConnection.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace InnovEnergy.Lib.Protocols.Modbus.Connections; - - - -public abstract class ModbusConnection -{ - public abstract IReadOnlyList Receive(UInt16 nBytes); - public abstract void Transmit(IEnumerable bytes); - - public abstract void Open(); // calls to opening an already open connection must be ignored - public abstract void Close(); // calls to closing an already closed connection must be ignored, must not throw -} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Connections/ModbusSerialConnection.cs b/csharp/Lib/Protocols/Modbus/Connections/ModbusSerialConnection.cs deleted file mode 100644 index 434bb467f..000000000 --- a/csharp/Lib/Protocols/Modbus/Connections/ModbusSerialConnection.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.IO.Ports; -using InnovEnergy.Lib.Protocols.Modbus.Protocol; - -namespace InnovEnergy.Lib.Protocols.Modbus.Connections; - -public class ModbusSerialConnection : ModbusConnection -{ - private String PortName { get; } - private Int32 BaudRate { get; } - private Parity Parity { get; } - private Int32 DataBits { get; } - private StopBits StopBits { get; } - private Int32 TimeoutMs { get; } - - private Byte[] Buffer { get; } = new Byte[1024]; - private SerialPort? _SerialPort; - - public ModbusSerialConnection(String portName, Int32 baudRate, Parity parity, Int32 dataBits, StopBits stopBits, TimeSpan timeout) - { - PortName = portName; - BaudRate = baudRate; - Parity = parity; - DataBits = dataBits; - StopBits = stopBits; - TimeoutMs = (Int32) timeout.TotalMilliseconds; - } - - - public override IReadOnlyList Receive(UInt16 bytesToRead) - { - var bytesReceived = 0; - var serialPort = SerialPort(); - - do - { - var received = serialPort.Read(Buffer, bytesReceived, Buffer.Length - bytesReceived); - if (received < 0) - throw new NotConnectedException("Serial Connection has been closed"); - - bytesReceived += received; - } - while (bytesReceived < bytesToRead); - - return new ArraySegment(Buffer, 0, bytesToRead); - } - - public override void Transmit(IEnumerable data) - { - var array = data.ToArray(); - SerialPort().Write(array, 0, array.Length); - } - - public override void Open() => SerialPort(); - - private SerialPort SerialPort() - { - if (_SerialPort == null) - { - _SerialPort = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits) { ReadTimeout = TimeoutMs, WriteTimeout = TimeoutMs}; - _SerialPort.Open(); - } - - return _SerialPort; - } - - public override void Close() - { - if (_SerialPort != null) - { - try - { - _SerialPort.Close(); - _SerialPort?.Dispose(); - } - catch (Exception) - { - // ignored - } - } - - _SerialPort = null; - } - -} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Connections/ModbusTcpConnection.cs b/csharp/Lib/Protocols/Modbus/Connections/ModbusTcpConnection.cs deleted file mode 100644 index 17b4c96c1..000000000 --- a/csharp/Lib/Protocols/Modbus/Connections/ModbusTcpConnection.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Net.Sockets; -using InnovEnergy.Lib.Protocols.Modbus.Protocol; - -namespace InnovEnergy.Lib.Protocols.Modbus.Connections; - -public class ModbusTcpConnection : ModbusConnection -{ - private String Hostname { get; } - private Int32 Port { get; } - - private Byte[] Buffer { get; } = new Byte[1024]; - private TcpClient? _TcpClient; - - public ModbusTcpConnection(String hostname, Int32 port = 502) - { - Hostname = hostname; - Port = port; - } - - public override IReadOnlyList Receive(UInt16 bytesToRead) - { - var bytesReceived = 0; - var stream = TcpClient().GetStream(); - - do - { - var maxBytes = Math.Min(bytesToRead, Buffer.Length) - bytesReceived; - var read = stream.Read(Buffer, bytesReceived, maxBytes); - if (read <= 0) - throw new NotConnectedException("The TCP Connection was closed"); - - bytesReceived += read; - } - while (bytesReceived < bytesToRead); - - return new ArraySegment(Buffer, 0, bytesToRead); - } - - public override void Transmit(IEnumerable data) - { - var array = data.ToArray(); - TcpClient().GetStream().Write(array, 0, array.Length); - } - - public override void Open() => TcpClient(); - - private TcpClient TcpClient() - { - return _TcpClient ??= new TcpClient(Hostname, Port); - } - - public override void Close() - { - try - { - _TcpClient?.Dispose(); - } - catch (Exception) - { - // ignored - } - - _TcpClient = null; - } - -} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Connections/RemoteSerialConnection.cs b/csharp/Lib/Protocols/Modbus/Connections/RemoteSerialConnection.cs deleted file mode 100644 index 7140b62ed..000000000 --- a/csharp/Lib/Protocols/Modbus/Connections/RemoteSerialConnection.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.IO.Ports; -using System.Reactive.Linq; -using CliWrap; -using InnovEnergy.Lib.Utils; - -namespace InnovEnergy.Lib.Protocols.Modbus.Connections; - -public class RemoteSerialConnection : ModbusConnection -{ - private readonly Command _Command; - private ObservableProcess? _ObservableProcess; - private Func>>? _Read; - - - public RemoteSerialConnection(SshHost host, - String tty, - Int32 baudRate, - Parity parity, - Int32 stopBits, - Int32 dataBits) - { - tty = tty.EnsureStartsWith("/dev/"); - - var configure = ConfigureTty(tty, baudRate, parity, stopBits, dataBits); - - // https://unix.stackexchange.com/questions/19604/all-about-ssh-proxycommand - - var call = $"{configure}; " + - $"exec 3<>{tty}; " + - $"cat <&3 & cat >&3; " + - $"kill $!"; - - _Command = host - .Command - .AppendArgument(call); - - _Command.WriteLine(); - } - - [SuppressMessage("ReSharper", "StringLiteralTypo")] - private static String ConfigureTty(String tty, Int32 baudRate, Parity parity, Int32 stopBits, Int32 dataBits) - { - var oParity = parity switch - { - Parity.Even => "parenb -parodd", - Parity.Odd => "parenb parodd", - Parity.None => "-parenb", - _ => throw new NotImplementedException() - }; - - var oStopBits = stopBits switch - { - 1 => "-cstopb", - 2 => "cstopb", - _ => throw new NotImplementedException() - }; - - var oDataBits = "cs" + dataBits; - - return $"stty -F {tty} {baudRate} {oDataBits} {oStopBits} {oParity}"; - } - - public override IReadOnlyList Receive(UInt16 bytesToRead) - { - Open(); - - return _Read!(bytesToRead).Result; - } - - public override void Transmit(IEnumerable bytes) - { - Open(); - try - { - _ObservableProcess!.StdIn.OnNext(bytes.ToArray()); - } - catch - { - Close(); - throw; - } - } - - - public override void Open() - { - if (_ObservableProcess is not null) - return; - - _ObservableProcess = new ObservableProcess(_Command); - - _ObservableProcess.Start(); - - _Read = _ObservableProcess - .StdOut - .Select(b => b.ToArray()) - .SelectMany(t => t) - .PushToPull(); - } - - public override void Close() - { - throw new NotImplementedException(); - - // if (_ObservableProcess is null) - // return; - - // https://stackoverflow.com/questions/6960520/when-to-dispose-cancellationtokensource - - - } -} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Doc/Modbus_Application_Protocol_V1_1b.pdf b/csharp/Lib/Protocols/Modbus/Doc/Modbus_Application_Protocol_V1_1b.pdf new file mode 100644 index 000000000..7c458fa6c Binary files /dev/null and b/csharp/Lib/Protocols/Modbus/Doc/Modbus_Application_Protocol_V1_1b.pdf differ diff --git a/csharp/Lib/Protocols/Modbus/Modbus.csproj b/csharp/Lib/Protocols/Modbus/Modbus.csproj index 0bcc73c8c..85aaefa65 100644 --- a/csharp/Lib/Protocols/Modbus/Modbus.csproj +++ b/csharp/Lib/Protocols/Modbus/Modbus.csproj @@ -9,7 +9,6 @@ false - @@ -18,7 +17,6 @@ - diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/Accessors.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/Accessors.cs index 2a2e9e2c5..6c27b7f72 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/Accessors.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/Accessors.cs @@ -6,5 +6,5 @@ public static class Accessors public static MbWord WordAt (this ArraySegment data, Byte i) => new MbWord(data, i); public static MbWords WordsAt (this ArraySegment data, Byte i) => new MbWords(data, i); public static MbBits BitsAt (this ArraySegment data, Byte i) => new MbBits(data, i); - public static MbByte ByteAt(this ArraySegment data,Byte i) where T : struct, IConvertible => new MbByte(data, i); + public static MbByte ByteAt(this ArraySegment data, Byte i) where T : struct, IConvertible => new MbByte(data, i); } \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/Endian.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/Endian.cs new file mode 100644 index 000000000..5ec75397d --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/Endian.cs @@ -0,0 +1,7 @@ +namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; + +public enum Endian +{ + Little, + Big +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbBits.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbBits.cs index 137c377f3..ac990f5d8 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbBits.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbBits.cs @@ -3,11 +3,15 @@ using InnovEnergy.Lib.Utils; namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; -public readonly struct MbBits : IReadOnlyList +public readonly struct MbBits : IReadOnlyList, IReadOnlyList { - private readonly ArraySegment _Data; + public readonly ArraySegment RawData; - internal MbBits(ArraySegment data, Byte startIndex) : this(data, startIndex, CountBits(data, startIndex)) + internal MbBits(Int32 bitCount) : this(new Byte[NbRegistersFromNbBits(bitCount)]) + { + } + + internal MbBits(ArraySegment data, Byte startIndex = 0) : this(data, startIndex, CountBits(data, startIndex)) { } @@ -24,23 +28,32 @@ public readonly struct MbBits : IReadOnlyList internal MbBits(Byte[] data, Byte startIndex, UInt16 bitCount) { - _Data = new ArraySegment(data, startIndex, Math.Ceiling(bitCount / 8.0).ConvertTo()); + RawData = new ArraySegment(data, startIndex, NbRegistersFromNbBits(bitCount)); } - internal void Set(IReadOnlyList values) + private static UInt16 NbRegistersFromNbBits(Int32 bitCount) { - for (var i = 0; i < values.Count; i++) - { - SetBit((UInt16)i, values[i]); - } + return Math.Ceiling(bitCount / 8.0).ConvertTo(); } + internal void Set(IReadOnlyCollection values) + { + var i = 0; + foreach (var value in values) + SetBit((UInt16)i++, value); + } - public IEnumerator GetEnumerator() => Enumerable.Range(0, Count).Select(GetBit).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetBits() + .OfType() + .GetEnumerator(); + + public IEnumerator GetEnumerator() => GetBits().GetEnumerator(); + + private IEnumerable GetBits() => Enumerable.Range(0, Count).Select(GetBit); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Int32 Count => _Data.Count * 8; + public Int32 Count => RawData.Count * 8; private Boolean GetBit(Int32 index) @@ -48,10 +61,10 @@ public readonly struct MbBits : IReadOnlyList var byteIndex = index / 8; var bitIndex = index % 8; - return (_Data[byteIndex] & (1 << bitIndex)) != 0; + return (RawData[byteIndex] & (1 << bitIndex)) != 0; } - private void SetBit(UInt16 index, Boolean value) + internal void SetBit(UInt16 index, Boolean value) { var byteIndex = index / 8; var bitIndex = index % 8; @@ -59,12 +72,13 @@ public readonly struct MbBits : IReadOnlyList var mask = 1 << bitIndex; // !! needs the u suffix! if (value) - _Data[byteIndex] |= (Byte)mask; + RawData[byteIndex] |= (Byte)mask; else - _Data[byteIndex] &= (Byte)~mask; + RawData[byteIndex] &= (Byte)~mask; } public Boolean this[Int32 index] => GetBit(index); + IConvertible IReadOnlyList.this[Int32 index] => this[index]; } diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbData.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbData.cs new file mode 100644 index 000000000..42f4efc9d --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbData.cs @@ -0,0 +1,232 @@ +using InnovEnergy.Lib.Utils; + +namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; + +using Float32 = Single; +using Float64 = Double; + +// switch exhaustion +#pragma warning disable CS8524 + +public struct MbData +{ + + //TODO: use System.Buffers.Binary.BinaryPrimitives + // see Decimal class + + private readonly ArraySegment _Data; + private readonly Endian _Endian; + private readonly UInt16 _StartAddress; + + public static MbData Registers(UInt16 startAddress, UInt16 nRegisters) + { + if (nRegisters > Constants.MaxRegs) + throw new ArgumentException(nameof(nRegisters)); + + var nBytes = nRegisters * 2; + var data = new Byte[nBytes]; + return new MbData(data, startAddress, Endian.Big); // endian has no influence on coils + } + + public static MbData Coils(UInt16 startAddress, UInt16 nCoils) + { + if (nCoils > Constants.MaxCoils) + throw new ArgumentException(nameof(nCoils)); + + var nBytes = Math.Ceiling(nCoils / 8.0).ConvertTo(); + var data = new Byte[nBytes]; + return new MbData(data, startAddress, Endian.Big); // endian has no influence on coils + } + + internal MbData(ArraySegment data, UInt16 startAddress, Endian endian) + { + if (endian != Endian.Big && endian != Endian.Little) + throw new ArgumentOutOfRangeException(nameof(endian), endian, null); + + _Endian = endian; + _StartAddress = startAddress; + _Data = data; + } + + #region Coils + + public IReadOnlyList GetCoils() + { + IEnumerable GetBits(Byte b) => 1.Unfold(m => m << 1) + .Take(8) + .Select(m => (b & m) > 0); + + return _Data.SelectMany(GetBits).ToList(); + } + + public Boolean GetInput(UInt16 address) => GetCoil(address); + + public Boolean GetCoil(UInt16 address) + { + var index = address - _StartAddress; + + var byteIndex = index / 8; + var bitIndex = index % 8; + + return (_Data[byteIndex] & (1 << bitIndex)) != 0; + } + + public void SetCoil(UInt16 address, Boolean value) + { + var index = address - _StartAddress; + + var byteIndex = index / 8; + var bitIndex = index % 8; + + _Data[byteIndex] = (Byte)(_Data[byteIndex] | (1 << bitIndex)) ; + } + + #endregion Coils + + #region 16Bit + + public UInt16 GetUInt16(UInt16 address) + { + return GetRegister((address - _StartAddress) * 2); + } + + public Int16 GetInt16(UInt16 address) + { + return (Int16) GetUInt16(address); + } + + public UInt16 SetUInt16(UInt16 address, UInt16 value) + { + var i = (address - _StartAddress) * 2; + + _Data[i ] = (Byte)(value >> 8); + _Data[i + 1] = (Byte)(value & 0xFF); + + return value; + } + + public void SetInt16(UInt16 address, Int16 value) + { + SetUInt16(address, (UInt16)value); + } + + #endregion 16Bit + + #region 32Bit + + public UInt32 GetUInt32(UInt16 address) + { + var hi = (UInt32) GetUInt16(address); + var lo = (UInt32) GetUInt16(++address); + + return _Endian switch + { + Endian.Big => hi << 16 | lo, + Endian.Little => lo << 16 | hi, + }; + } + + public Int32 GetInt32(UInt16 address) => (Int32)GetUInt32(address); + + public void SetUInt32(UInt16 address, UInt32 value) + { + var hi = (UInt16)(value >> 16); + var lo = (UInt16)(value & 0xFF_FF); + + if (_Endian == Endian.Big) + { + SetUInt16(address, hi); + SetUInt16(++address, lo); + } + else + { + SetUInt16(address, lo); + SetUInt16(++address, hi); + } + } + + public void SetInt32(UInt16 address, Int64 value) => SetUInt32(address, (UInt32)value); + + public Float32 GetFloat32(UInt16 address) + { + return address + .Apply(GetUInt32) + .Apply(BitConverter.UInt32BitsToSingle); + } + + public void SetFloat32(UInt16 address, Float32 value) + { + SetUInt32(address, BitConverter.SingleToUInt32Bits(value)); + } + + #endregion 32Bit + + #region 64Bit + + public UInt64 GetUInt64(UInt16 address) + { + var hi = (UInt64) GetUInt32(address); + var lo = (UInt64) GetUInt32(++address); + + return _Endian switch + { + Endian.Big => hi << 32 | lo, + Endian.Little => lo << 32 | hi, + }; + } + + public Int64 GetInt64(UInt16 address) => (Int32)GetUInt64(address); + + public void SetUInt64(UInt16 address, UInt64 value) + { + var hi = (UInt32)(value >> 32); + var lo = (UInt32)(value & 0xFF_FF_FF_FF); + + if (_Endian == Endian.Big) + { + SetUInt32(address, hi); + SetUInt32((UInt16)(address + 2), lo); + } + else + { + SetUInt32(address, lo); + SetUInt32((UInt16)(address + 2), hi); + } + } + + public void SetInt64(UInt16 address, Int64 value) => SetUInt64(address, (UInt64)value); + + public Float64 GetFloat64(UInt16 address) + { + return address + .Apply(GetUInt64) + .Apply(BitConverter.UInt64BitsToDouble); + } + + public void SetFloat64(UInt16 address, Float64 value) + { + SetUInt64(address, BitConverter.DoubleToUInt64Bits(value)); + } + + #endregion 64Bit + + + private UInt16 GetRegister(Int32 i) + { + var hi = _Data[i] << 8; + var lo = _Data[i + 1]; + + return (UInt16)(hi | lo); + } + + public IReadOnlyList GetRegisters() + { + var nRegisters = _Data.Count / 2; + + return Enumerable + .Range(0, nRegisters) + .Select(GetRegister) + .ToArray(nRegisters); + } + +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbRegisters.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbRegisters.cs deleted file mode 100644 index 6ff463460..000000000 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbRegisters.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Collections; -using InnovEnergy.Lib.Utils; - -namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; - -using Float32 = Single; - -public struct MbRegisters : IReadOnlyList -{ - private MbWords Words { get; } - private UInt16 StartRegister { get; } - - public MbRegisters(MbWords words, UInt16 startRegister) - { - Words = words; - StartRegister = startRegister; - } - - public IEnumerator GetEnumerator() => Words.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public Int32 Count => Words.Count; - - public UInt16 this[Int32 index] => Words[index]; - - private Byte MapIndex(UInt16 index) - { - var i = index - StartRegister; - if (i is < Byte.MinValue or > Byte.MaxValue) - throw new IndexOutOfRangeException(); - - return (Byte)i; - } - - public UInt16 GetUInt16(UInt16 index) - { - return index - .Apply(MapIndex) - .Apply(Words.GetUInt16); - } - - public void SetUInt16(UInt16 index, UInt16 value) - { - Words.SetUInt16(MapIndex(index), value); - } - - public Int16 GetInt16(UInt16 index) - { - return (Int16) GetUInt16(index); - } - - public void SetUInt16(UInt16 index, Int16 value) - { - SetUInt16(index, (UInt16)value); - } - - public UInt32 GetUInt32(UInt16 index) - { - var i = MapIndex(index); - - var hi = (UInt32) GetUInt16(i); - var lo = (UInt32) GetUInt16(++i); - - return hi << 16 | lo; - } - - // TODO - // public void SetUInt32(UInt32 index, UInt32 value) - - public Int32 GetInt32(UInt16 index) => (Int32)GetUInt32(index); - - // TODO - // public void SetInt32(Int32 index, Int32 value) - - - public Float32 GetFloat32(UInt16 index) - { - return index - .Apply(GetInt32) - .Apply(BitConverter.Int32BitsToSingle); - } - - // TODO - // public void SetFloat32(Float32 index, Float32 value) - - - - -} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbWords.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbWords.cs index 5511c5c63..5ff9232f9 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbWords.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Accessors/MbWords.cs @@ -3,9 +3,9 @@ using InnovEnergy.Lib.Utils; namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; -public readonly struct MbWords : IReadOnlyList +public readonly struct MbWords : IReadOnlyList , IReadOnlyList { - internal readonly ArraySegment Data; + public readonly ArraySegment RawData; internal MbWords(ArraySegment data, Byte startIndex = 0) : this(data, startIndex, CountWords(data, startIndex)) @@ -13,7 +13,7 @@ public readonly struct MbWords : IReadOnlyList internal MbWords(ArraySegment data, Byte startIndex, UInt16 wordCount) { - Data = new ArraySegment(data.Array!, startIndex + data.Offset, wordCount * 2); + RawData = new ArraySegment(data.Array!, startIndex + data.Offset, wordCount * 2); } private static UInt16 CountWords(ArraySegment data, Byte startIndex) @@ -24,28 +24,40 @@ public readonly struct MbWords : IReadOnlyList internal IReadOnlyCollection Set(IReadOnlyCollection values) { - if (values.Count != Data.Count / 2) + if (values.Count != RawData.Count / 2) throw new ArgumentException($"Expecting an list of size {values.Count}!", nameof(values)); var i = 0; foreach (var value in values) { - Data[i++] = (Byte) (value >> 8); - Data[i++] = (Byte) (value & 0xFF); + RawData[i++] = (Byte) (value >> 8); + RawData[i++] = (Byte) (value & 0xFF); } return values; } - - public IEnumerator GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() { - var end = Data.Count; + var end = RawData.Count; for (var i = 0; i < end; ) { - var hi = Data[i++] << 8; - var lo = Data[i++]; + var hi = RawData[i++] << 8; + var lo = RawData[i++]; + + yield return (UInt16) (hi | lo); + } + } + + public IEnumerator GetEnumerator() + { + var end = RawData.Count; + + for (var i = 0; i < end; ) + { + var hi = RawData[i++] << 8; + var lo = RawData[i++]; yield return (UInt16) (hi | lo); } @@ -53,7 +65,7 @@ public readonly struct MbWords : IReadOnlyList IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Int32 Count => Data.Count / 2; + public Int32 Count => RawData.Count / 2; public UInt16 this[Int32 index] { @@ -63,8 +75,8 @@ public readonly struct MbWords : IReadOnlyList { var i = index * 2; - var hi = Data[i] << 8; - var lo = Data[i+1]; + var hi = RawData[i] << 8; + var lo = RawData[i+1]; return (UInt16) (hi | lo); } @@ -76,7 +88,9 @@ public readonly struct MbWords : IReadOnlyList public void SetUInt16(Byte index, UInt16 value) { var i = index * 2; - Data[i + 0] = (Byte)(value >> 8); - Data[i + 1] = (Byte)(value & 0xFF); + RawData[i + 0] = (Byte)(value >> 8); + RawData[i + 1] = (Byte)(value & 0xFF); } + + IConvertible IReadOnlyList.this[Int32 index] => this[index]; } \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadCoilsCommandFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadCoilsCommandFrame.cs index edb4548de..e5034f8de 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadCoilsCommandFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadCoilsCommandFrame.cs @@ -1,6 +1,7 @@ using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Replies; using InnovEnergy.Lib.Utils; +using static InnovEnergy.Lib.Protocols.Modbus.Protocol.FunctionCode; namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Commands; @@ -8,8 +9,8 @@ internal class ReadCoilsCommandFrame : ModbusFrame { private const Int32 Size = 6; - private MbWord ReadAddress => Data.WordAt(2); - private MbWord QuantityOfInputs => Data.WordAt(4); + public MbWord ReadAddress => Data.WordAt(2); + public MbWord NumberOfCoils => Data.WordAt(4); public ReadCoilsCommandFrame(Byte slave, UInt16 readAddress, UInt16 nBits) : base(Size) @@ -17,10 +18,10 @@ internal class ReadCoilsCommandFrame : ModbusFrame if (nBits > Constants.MaxCoils) throw new ArgumentOutOfRangeException($"Maximum number of registers ({Constants.MaxCoils}) exceeeded!", nameof(nBits)); - SlaveAddress .Set(slave); - FunctionCode .Set(Protocol.FunctionCode.ReadCoils); - ReadAddress .Set(readAddress); - QuantityOfInputs.Set(nBits); + SlaveAddress .Set(slave); + FunctionCode .Set(ReadCoils); + ReadAddress .Set(readAddress); + NumberOfCoils.Set(nBits); } @@ -29,7 +30,7 @@ internal class ReadCoilsCommandFrame : ModbusFrame if (data.Count != Size) throw new ArgumentException($"Expecting an array of size {Size}", nameof(data)); - AssertFunctionCode(Protocol.FunctionCode.ReadCoils); + AssertFunctionCode(ReadCoils); } public ReadCoilsResponseFrame VerifyResponse(ReadCoilsResponseFrame response) @@ -37,8 +38,8 @@ internal class ReadCoilsCommandFrame : ModbusFrame if (response.SlaveAddress != SlaveAddress) throw new UnexpectedResponseFieldException(nameof(response.SlaveAddress), SlaveAddress.ToString(), response.SlaveAddress); - if (response.Inputs.Count != Math.Ceiling(QuantityOfInputs / 8.0).ConvertTo() * 8) - throw new UnexpectedResponseFieldException(nameof(response.Inputs), QuantityOfInputs.ToString(), response.Inputs.Count); + if (response.Coils.Count != Math.Ceiling(NumberOfCoils / 8.0).ConvertTo() * 8) + throw new UnexpectedResponseFieldException(nameof(response.Coils), NumberOfCoils.ToString(), response.Coils.Count); return response; } diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadHoldingRegistersCommandFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadHoldingRegistersCommandFrame.cs index 9134b0be9..65b072dc2 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadHoldingRegistersCommandFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadHoldingRegistersCommandFrame.cs @@ -40,12 +40,10 @@ internal class ReadHoldingRegistersCommandFrame : ModbusFrame } - public ReadHoldingRegistersResponseFrame VerifyResponse(ModbusFrame response) + public ReadHoldingRegistersResponseFrame VerifyResponse(ReadHoldingRegistersResponseFrame r) { - if (response.SlaveAddress != SlaveAddress) - throw new UnexpectedResponseFieldException(nameof(response.SlaveAddress), SlaveAddress.ToString(), response.SlaveAddress); - - var r = ReadHoldingRegistersResponseFrame.Parse(response.Data); + if (r.SlaveAddress != SlaveAddress) + throw new UnexpectedResponseFieldException(nameof(r.SlaveAddress), SlaveAddress.ToString(), r.SlaveAddress); if (r.RegistersRead.Count != NbToRead) throw new UnexpectedResponseFieldException(nameof(r.RegistersRead), NbToRead.ToString(), r.RegistersRead.Count); diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadInputRegistersCommandFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadInputRegistersCommandFrame.cs index a17fe34f8..c873c1d9d 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadInputRegistersCommandFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadInputRegistersCommandFrame.cs @@ -40,7 +40,6 @@ internal class ReadInputRegistersCommandFrame : ModbusFrame if (response.SlaveAddress != SlaveAddress) throw new UnexpectedResponseFieldException(nameof(response.SlaveAddress), SlaveAddress.ToString(), response.SlaveAddress); - if (response.RegistersRead.Count != NbToRead) throw new UnexpectedResponseFieldException(nameof(response.RegistersRead), NbToRead.ToString(), response.RegistersRead.Count); diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadWriteRegistersCommandFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadWriteRegistersCommandFrame.cs index e280c071d..c11d73563 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadWriteRegistersCommandFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/ReadWriteRegistersCommandFrame.cs @@ -5,7 +5,7 @@ using static InnovEnergy.Lib.Protocols.Modbus.Protocol.FunctionCode; namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Commands; -using UInt16s = IReadOnlyList; +using UInt16s = IReadOnlyCollection; internal class ReadWriteRegistersCommandFrame : ModbusFrame { @@ -21,10 +21,10 @@ internal class ReadWriteRegistersCommandFrame : ModbusFrame public Int32 ExpectedResponseSize => ReadWriteRegistersResponseFrame.ExpectedSize(NbToRead); public ReadWriteRegistersCommandFrame(Byte slave, - UInt16 readAddress, - UInt16 nbToRead, - UInt16 writeAddress, - UInt16s registersToWrite) : base(MinSize + registersToWrite.Count * 2) + UInt16 readAddress, + UInt16 nbToRead, + UInt16 writeAddress, + UInt16s registersToWrite) : base(MinSize + registersToWrite.Count * 2) { if (nbToRead > MaxRegs) throw new ArgumentOutOfRangeException($"Maximum number of registers ({MaxRegs}) exceeeded!", nameof(nbToRead)); diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/WriteCoilsCommandFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/WriteCoilsCommandFrame.cs index e366616cb..1873fd34b 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/WriteCoilsCommandFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/WriteCoilsCommandFrame.cs @@ -7,7 +7,7 @@ using static InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Constants; namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Commands; -using Booleans = IReadOnlyList; +using Booleans = IReadOnlyCollection; using MbFc = MbByte; public class WriteCoilsCommandFrame : ModbusFrame @@ -17,7 +17,7 @@ public class WriteCoilsCommandFrame : ModbusFrame private MbWord WriteAddress => Data.WordAt(2); private MbWord NbOfCoils => Data.WordAt(4); private MbByte ByteCount => Data.ByteAt(6); - public MbBits CoilsToWrite => Data.BitsAt(7); + private MbBits CoilsToWrite => Data.BitsAt(7); public WriteCoilsCommandFrame(Byte slave, UInt16 writeAddress, Booleans coils) : base(MinSize + NbBytes(coils)) { diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/WriteRegistersCommandFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/WriteRegistersCommandFrame.cs index b317767c1..e934392c1 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/WriteRegistersCommandFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Commands/WriteRegistersCommandFrame.cs @@ -4,8 +4,8 @@ using static InnovEnergy.Lib.Protocols.Modbus.Protocol.FunctionCode; namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Commands; -using UInt16s = IReadOnlyList; -using MbFc = MbByte; +using UInt16s = IReadOnlyCollection; +using MbFc = MbByte; internal class WriteRegistersCommandFrame : ModbusFrame { diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadCoilsResponseFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadCoilsResponseFrame.cs index 916bf0d96..8dd78b859 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadCoilsResponseFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadCoilsResponseFrame.cs @@ -7,8 +7,8 @@ public class ReadCoilsResponseFrame : ModbusFrame { private new const Int32 MinSize = 3; - private MbByte ByteCount => Data.ByteAt(2); - public MbBits Inputs => Data.BitsAt(3); + private MbByte ByteCount => Data.ByteAt(2); + internal MbBits Coils => Data.BitsAt(3); public ReadCoilsResponseFrame(Byte slave, IReadOnlyList inputs) : base (inputs.Count) { @@ -17,7 +17,7 @@ public class ReadCoilsResponseFrame : ModbusFrame SlaveAddress .Set(slave); FunctionCode .Set(Protocol.FunctionCode.ReadCoils); ByteCount .Set(nBytes); - Inputs .Set(inputs); + Coils .Set(inputs); } private ReadCoilsResponseFrame(Byte[] data) : this(new ArraySegment(data)) @@ -39,13 +39,7 @@ public class ReadCoilsResponseFrame : ModbusFrame } - public static ReadCoilsResponseFrame Parse(ModbusFrame rawFrame) - { - return new ReadCoilsResponseFrame(rawFrame.Data); - } + public static ReadCoilsResponseFrame Parse(Byte[] data) => new(data); - public static ReadCoilsResponseFrame Parse(ArraySegment data) - { - return new ReadCoilsResponseFrame(data); - } + public static ReadCoilsResponseFrame Parse(ArraySegment data) => new(data); } \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadDiscreteInputsResponseFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadDiscreteInputsResponseFrame.cs index e05234866..7b92fd651 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadDiscreteInputsResponseFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadDiscreteInputsResponseFrame.cs @@ -43,11 +43,11 @@ public class ReadDiscreteInputsResponseFrame : ModbusFrame } - public static ReadDiscreteInputsResponseFrame Parse(ModbusFrame rawFrame) + public static ReadDiscreteInputsResponseFrame Parse(Byte[] data) { - return new ReadDiscreteInputsResponseFrame(rawFrame.Data); + return new ReadDiscreteInputsResponseFrame(data); } - + public static ReadDiscreteInputsResponseFrame Parse(ArraySegment data) { return new ReadDiscreteInputsResponseFrame(data); diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadHoldingRegistersResponseFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadHoldingRegistersResponseFrame.cs index 7fcd77983..5e594a059 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadHoldingRegistersResponseFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadHoldingRegistersResponseFrame.cs @@ -43,8 +43,7 @@ internal class ReadHoldingRegistersResponseFrame : ModbusFrame throw new ArgumentException(nameof(RegistersRead)); } - - public static ReadHoldingRegistersResponseFrame Parse(ModbusFrame frame) => new ReadHoldingRegistersResponseFrame(frame.Data); + public static ReadHoldingRegistersResponseFrame Parse(Byte[] data) => new ReadHoldingRegistersResponseFrame(data); public static ReadHoldingRegistersResponseFrame Parse(ArraySegment data) => new ReadHoldingRegistersResponseFrame(data); } \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadInputRegistersResponseFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadInputRegistersResponseFrame.cs index 29cebda39..05e8ed3bd 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadInputRegistersResponseFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/ReadInputRegistersResponseFrame.cs @@ -46,9 +46,9 @@ internal class ReadInputRegistersResponseFrame : ModbusFrame - public static ReadInputRegistersResponseFrame Parse(ModbusFrame rawFrame) + public static ReadInputRegistersResponseFrame Parse(Byte[] data) { - return new ReadInputRegistersResponseFrame(rawFrame.Data); + return new ReadInputRegistersResponseFrame(data); } public static ReadInputRegistersResponseFrame Parse(ArraySegment data) diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/WriteCoilsResponseFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/WriteCoilsResponseFrame.cs index 76518a0e5..dc83498f0 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/WriteCoilsResponseFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/WriteCoilsResponseFrame.cs @@ -33,7 +33,6 @@ public class WriteCoilsResponseFrame : ModbusFrame AssertFunctionCode(WriteMultipleCoils); } - public static WriteCoilsResponseFrame Parse(ArraySegment data) => new WriteCoilsResponseFrame(data); - public static WriteCoilsResponseFrame Parse(Byte[] data) => new WriteCoilsResponseFrame(data); - public static WriteCoilsResponseFrame Parse(ModbusFrame frame) => new WriteCoilsResponseFrame(frame.Data); + public static WriteCoilsResponseFrame Parse(Byte[] data) => new WriteCoilsResponseFrame(data); + } diff --git a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/WriteRegistersResponseFrame.cs b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/WriteRegistersResponseFrame.cs index 76a0a6b7d..f77e7793c 100644 --- a/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/WriteRegistersResponseFrame.cs +++ b/csharp/Lib/Protocols/Modbus/Protocol/Frames/Replies/WriteRegistersResponseFrame.cs @@ -35,5 +35,5 @@ internal class WriteRegistersResponseFrame : ModbusFrame public static WriteRegistersResponseFrame Parse(ArraySegment data) => new WriteRegistersResponseFrame(data); public static WriteRegistersResponseFrame Parse(Byte[] data) => new WriteRegistersResponseFrame(data); - public static WriteRegistersResponseFrame Parse(ModbusFrame frame) => new WriteRegistersResponseFrame(frame.Data); + } \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/AddressOffsetAttribute.cs b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/AddressOffsetAttribute.cs new file mode 100644 index 000000000..48e35cdd0 --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/AddressOffsetAttribute.cs @@ -0,0 +1,11 @@ + +using static System.AttributeTargets; + +namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; + +[AttributeUsage(Class | Struct)] +public class AddressOffsetAttribute : Attribute +{ + public Int32 Offset { get; } + public AddressOffsetAttribute(Int32 offset) => Offset = offset; +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/DiscreteInputAttribute.cs b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/DiscreteInputAttribute.cs index 6902b0454..1f5719fdd 100644 --- a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/DiscreteInputAttribute.cs +++ b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/DiscreteInputAttribute.cs @@ -4,5 +4,4 @@ namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; public class DiscreteInputAttribute : ModbusBooleanAttribute { public DiscreteInputAttribute(UInt16 address) : base(address) { } - } \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/HoldingRegisterAttribute.cs b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/HoldingRegisterAttribute.cs index 8ecb7f333..5b1d9cf85 100644 --- a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/HoldingRegisterAttribute.cs +++ b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/HoldingRegisterAttribute.cs @@ -2,6 +2,7 @@ namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; public class HoldingRegisterAttribute : ModbusRegisterAttribute { - public HoldingRegisterAttribute(UInt16 address) : base(address) { } - -} \ No newline at end of file + public HoldingRegisterAttribute(UInt16 address, TypeCode modbusType = TypeCode.UInt16) : base(address, modbusType) { } +} + + diff --git a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/InputRegisterAttribute.cs b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/InputRegisterAttribute.cs index 6c53233c3..76649a15d 100644 --- a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/InputRegisterAttribute.cs +++ b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/InputRegisterAttribute.cs @@ -3,5 +3,6 @@ namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; public class InputRegisterAttribute : ModbusRegisterAttribute { - public InputRegisterAttribute(UInt16 address) : base(address) { } -} \ No newline at end of file + public InputRegisterAttribute(UInt16 address, TypeCode modbusType = TypeCode.UInt16) : base(address, modbusType) { } +} + diff --git a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusAttribute.cs b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusAttribute.cs index c67548fe6..057a8e799 100644 --- a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusAttribute.cs +++ b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusAttribute.cs @@ -1,8 +1,32 @@ +using static System.AttributeTargets; + namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] -public abstract class ModbusAttribute : Attribute +[AttributeUsage(Field | Property)] +public class ModbusAttribute : Attribute { - public UInt16 Address { get; } - protected ModbusAttribute(UInt16 address) { Address = address; } + public UInt16 Address { get; } + public UInt16 Size { get; } + public TypeCode ModbusType { get; } + + protected ModbusAttribute(UInt16 address, TypeCode modbusType) + { + Address = address; + ModbusType = modbusType; + + Size = modbusType switch + { + TypeCode.Boolean => 1, + TypeCode.Int16 => 1, + TypeCode.UInt16 => 1, + TypeCode.Int32 => 2, + TypeCode.UInt32 => 2, + TypeCode.Single => 2, + TypeCode.Int64 => 4, + TypeCode.UInt64 => 4, + TypeCode.Double => 4, + _ => throw new ArgumentException(nameof(modbusType)) + }; + + } } \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusBooleanAttribute.cs b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusBooleanAttribute.cs index df2f55c11..d2a10da79 100644 --- a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusBooleanAttribute.cs +++ b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusBooleanAttribute.cs @@ -1,8 +1,9 @@ + namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; public class ModbusBooleanAttribute : ModbusAttribute { - protected ModbusBooleanAttribute(UInt16 address) : base(address) + protected ModbusBooleanAttribute(UInt16 address) : base(address, TypeCode.Boolean) { } } \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusRegisterAttribute.cs b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusRegisterAttribute.cs index e8e1d8501..1ee6a3907 100644 --- a/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusRegisterAttribute.cs +++ b/csharp/Lib/Protocols/Modbus/Reflection/Attributes/ModbusRegisterAttribute.cs @@ -1,11 +1,30 @@ + + namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; public abstract class ModbusRegisterAttribute : ModbusAttribute { - protected ModbusRegisterAttribute(UInt16 address) : base(address) - { - } - public Double Scale { get; init; } = 1; public Double Offset { get; init; } = 0; -} \ No newline at end of file + + protected ModbusRegisterAttribute(UInt16 address, TypeCode modbusType) : base(address, modbusType) + { + if (!SupportedTypes.Contains(modbusType)) + throw new ArgumentException(nameof(modbusType)); + } + + + internal static readonly TypeCode[] SupportedTypes = + { + TypeCode.UInt16, + TypeCode.Int16, + TypeCode.UInt32, + TypeCode.Int32, + TypeCode.UInt64, + TypeCode.Int64, + TypeCode.Single, + TypeCode.Double + }; + +} + diff --git a/csharp/Lib/Protocols/Modbus/Reflection/Batch.cs b/csharp/Lib/Protocols/Modbus/Reflection/Batch.cs new file mode 100644 index 000000000..fee6090ae --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Reflection/Batch.cs @@ -0,0 +1,115 @@ +using InnovEnergy.Lib.Protocols.Modbus.Clients; +using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames; +using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; +using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; + +namespace InnovEnergy.Lib.Protocols.Modbus.Reflection; + +#pragma warning disable CS8509 + +internal record Batch(Action Read, Action Write); + +public static class Batches +{ + internal static IEnumerable> MakeBatchesFor(this ModbusClient modbusClient, Int32 addressOffset) + { + var members = ModbusMembers + .From(addressOffset) + .OrderBy(m => m.Attribute.GetType().Name) + .ThenBy(m => m.StartAddress) + .ThenBy(m => m.EndAddress); + + return MakeBatches(modbusClient, members); + } + + private static IEnumerable> MakeBatches(ModbusClient mb, IEnumerable modbusMembers) + { + var batchMembers = new List(); + + foreach (var member in modbusMembers) + { + if (CloseBatch(member)) + { + yield return MakeBatch(mb, batchMembers); + batchMembers = new List(); + } + + batchMembers.Add(member); + } + + if (batchMembers.Count > 0) + yield return MakeBatch(mb, batchMembers); + + Boolean CloseBatch(ModbusMember m) + { + if (batchMembers.Count == 0) + return false; + + return m.StartAddress > batchMembers[^1].EndAddress // gap between registers + || m.EndAddress > batchMembers[0].StartAddress + Constants.MaxRegs // max batch size reached + || m.Attribute.GetType() != batchMembers[0].Attribute.GetType(); // different ModbusType + } + } + + + private static Batch MakeBatch(ModbusClient modbusClient, IReadOnlyList members) + { + var startAddress = members[0].StartAddress; + var endAddress = members[^1].EndAddress; + var count = (UInt16)(endAddress - startAddress); + var attribute = members[0].Attribute; + var isWritable = attribute is HoldingRegisterAttribute or CoilAttribute; + + return new Batch(Read(), Write()); + + Action Read() + { + Func readModbus = attribute switch + { + InputRegisterAttribute => () => modbusClient.ReadInputRegisters (startAddress, count), + HoldingRegisterAttribute => () => modbusClient.ReadHoldingRegisters(startAddress, count), + DiscreteInputAttribute => () => modbusClient.ReadDiscreteInputs (startAddress, count), + CoilAttribute => () => modbusClient.ReadCoils (startAddress, count), + }; + + return record => + { + foreach (var member in members) + { + var mbData = readModbus(); + member.ModbusToRecord(mbData, record!); + } + }; + } + + Action Write() + { + if (!isWritable) + return _ => { }; // nop + + Func createMbData = attribute switch + { + HoldingRegisterAttribute => () => MbData.Registers(startAddress, count), + CoilAttribute => () => MbData.Coils (startAddress, count), + }; + + + Action writeModbus = attribute switch + { + HoldingRegisterAttribute => d => modbusClient.WriteRegisters(startAddress, d.GetRegisters()), + CoilAttribute => d => modbusClient.WriteCoils (startAddress, d.GetCoils()), + }; + + + return rec => + { + foreach (var member in members) + { + var mbData = createMbData(); + member.RecordToModbus(rec!, mbData); + writeModbus(mbData); + } + }; + } + } +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Reflection/Batches.cs b/csharp/Lib/Protocols/Modbus/Reflection/Batches.cs deleted file mode 100644 index c98cc1aa4..000000000 --- a/csharp/Lib/Protocols/Modbus/Reflection/Batches.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Reflection; -using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames; -using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; -using InnovEnergy.Lib.Utils; -using static System.Reflection.BindingFlags; - -namespace InnovEnergy.Lib.Protocols.Modbus.Reflection; - -using ModbusRegisters = IReadOnlyList; -using ModbusBooleans = IReadOnlyList; -using UInt16s = IReadOnlyList; -using Booleans = IReadOnlyList; - -internal static class Batches -{ - - internal static IEnumerable GetModbusRegisterBatches(this Object statusRecord) where T : ModbusRegisterAttribute - { - var modbusRegisters = from mi in statusRecord.GetDataMembers() - from a in mi.GetCustomAttributes() - select new ModbusRegister(a, mi, statusRecord); - - return modbusRegisters.SplitIntoFrameSizedBatches(); - } - - - internal static IEnumerable GetModbusBooleanBatches(this Object statusRecord) where T : ModbusBooleanAttribute - { - var modbusBooleans = from mi in statusRecord.GetDataMembers() - from a in mi.GetCustomAttributes() - select new ModbusBoolean(a, mi, statusRecord); - - return modbusBooleans.SplitIntoFrameSizedBatches(); - } - - - private static IEnumerable GetDataMembers(this Object statusRecord) - { - const BindingFlags bindingFlags = Instance - | Public - | NonPublic - | FlattenHierarchy; - - var type = statusRecord.GetType(); - - var fields = type.GetFields(bindingFlags); - var props = type.GetProperties(bindingFlags); - - return fields.Concat(props); - } - - - private static IEnumerable SplitIntoFrameSizedBatches(this IEnumerable members) where T : IAddress - { - return members - .OrderBy(m => m.Address) - .GroupWhile((a, b) => b.Address - a.Address > 1) // split when there is a gap between consecutive addresses - .SelectMany(g => g.Chunk(Constants.MaxRegs)); // group cannot be larger than Constants.MaxRegs - } - - - public static IEnumerable SetStatusRecordMembersFromRawModbusValues(this ModbusRegisters modbusBooleans, UInt16s rawRegisterValues) - { - foreach (var modbusBoolean in modbusBooleans) - { - var index = modbusBoolean.Address - modbusBooleans[0].Address; - var rawRegisterValue = rawRegisterValues[index]; - modbusBoolean.SetStatusRecordMemberFromRawModbusValue(rawRegisterValue); - yield return modbusBoolean.Address; - } - } - - - public static UInt16[] GetRawModbusValuesFromControlRecord(this ModbusRegisters modbusBooleans) - { - return modbusBooleans - .Select(v => v.GetRawModbusValueFromControlRecord()) - .ToArray(modbusBooleans.Count); - } - - - public static IEnumerable SetStatusRecordMembersFromRawModbusValues(this ModbusBooleans modbusBooleans, Booleans rawRegisterValues) - { - foreach (var modbusBoolean in modbusBooleans) - { - var index = modbusBoolean.Address - modbusBooleans[0].Address; - var rawRegisterValue = rawRegisterValues[index]; - modbusBoolean.SetStatusRecordMemberFromRawModbusValue(rawRegisterValue); - yield return modbusBoolean.Address; - } - } - - - public static Boolean[] GetRawModbusValuesFromControlRecord(this ModbusBooleans modbusBooleans) - { - return modbusBooleans - .Select(v => v.GetRawModbusValueFromControlRecord()) - .ToArray(modbusBooleans.Count); - } -} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Reflection/IAddress.cs b/csharp/Lib/Protocols/Modbus/Reflection/IAddress.cs deleted file mode 100644 index 5228d8661..000000000 --- a/csharp/Lib/Protocols/Modbus/Reflection/IAddress.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace InnovEnergy.Lib.Protocols.Modbus.Reflection; - -internal interface IAddress -{ - UInt16 Address { get; } -} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Reflection/ModbusBoolean.cs b/csharp/Lib/Protocols/Modbus/Reflection/ModbusBoolean.cs deleted file mode 100644 index 0178f681d..000000000 --- a/csharp/Lib/Protocols/Modbus/Reflection/ModbusBoolean.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Reflection; -using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; -using InnovEnergy.Lib.Utils; - -namespace InnovEnergy.Lib.Protocols.Modbus.Reflection; - -internal readonly struct ModbusBoolean : IAddress -{ - public UInt16 Address => _Attribute.Address; - - private readonly ModbusBooleanAttribute _Attribute ; - private readonly MemberInfo _MemberInfo ; - private readonly Object _Record; - - [Obsolete] public ModbusBoolean() => throw new Exception("Forbidden"); - - public ModbusBoolean(ModbusBooleanAttribute attribute, MemberInfo memberInfo, Object record) - { - _Attribute = attribute; - _MemberInfo = memberInfo; - _Record = record; - } - - - public UInt16 SetStatusRecordMemberFromRawModbusValue(Boolean rawBooleanValue) - { - if (_MemberInfo is FieldInfo fi) - { - var converted = rawBooleanValue.ConvertTo(fi.FieldType); - fi.SetValue(_Record, converted); - } - else if (_MemberInfo is PropertyInfo pi) - { - var converted = rawBooleanValue.ConvertTo(pi.PropertyType); - pi.SetValue(_Record, converted); - } - - return Address; - } - - public Boolean GetRawModbusValueFromControlRecord() - { - var value = _MemberInfo switch - { - FieldInfo fi => fi.GetValue(_Record)!, - PropertyInfo pi => pi.GetValue(_Record)!, - _ => throw new ArgumentException(nameof(_MemberInfo)) - }; - - return value - .CastTo() - .ConvertTo(); - } -} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Reflection/ModbusMember.cs b/csharp/Lib/Protocols/Modbus/Reflection/ModbusMember.cs new file mode 100644 index 000000000..cd435a711 --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Reflection/ModbusMember.cs @@ -0,0 +1,179 @@ +using System.Reflection; +using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; +using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; +using InnovEnergy.Lib.Utils; +using static System.Reflection.BindingFlags; + +namespace InnovEnergy.Lib.Protocols.Modbus.Reflection; + +#pragma warning disable CS8509 + +internal record ModbusMember +( + UInt16 StartAddress, + UInt16 EndAddress, + ModbusAttribute Attribute, + Action ModbusToRecord, + Action RecordToModbus +); + +internal static class ModbusMembers +{ + private static readonly (Double scale, Double offset) NoTransform = (scale:1, offset:0); + + internal static IEnumerable From(Int32 addressOffset) + { + return GetDataMembers() + .Where(HasAttribute) + .Select(m => m.CreateModbusMember(addressOffset)); + } + + + private static ModbusMember CreateModbusMember(this MemberInfo info, Int32 addressOffset) + { + var attribute = info.GetCustomAttributes().Single(); + var startAddress = (UInt16)(attribute.Address + addressOffset); + var endAddress = (UInt16)(startAddress + attribute.Size); + var modbusType = attribute.ModbusType; + var transform = attribute is ModbusRegisterAttribute mra + ? (mra.Scale, mra.Offset) + : NoTransform; + + var modbusToRecord = info.ModbusToRecord(startAddress, modbusType, transform); + var recordToModbus = info.RecordToModbus(startAddress, modbusType, transform); + + return new ModbusMember + ( + startAddress, + endAddress, + attribute, + modbusToRecord, + recordToModbus + ); + } + + private static Action ModbusToRecord(this MemberInfo info, UInt16 address, TypeCode modbusType, (Double scale, Double offset) transform) + { + var decode = ConvertModbusToRecord(transform); + + Func readFromMbData = modbusType switch + { + TypeCode.Boolean => d => d.GetInput (address), + TypeCode.UInt16 => d => d.GetUInt16 (address), + TypeCode.Int16 => d => d.GetInt16 (address), + TypeCode.UInt32 => d => d.GetUInt32 (address), + TypeCode.Int32 => d => d.GetInt32 (address), + TypeCode.Single => d => d.GetFloat32(address), + TypeCode.UInt64 => d => d.GetUInt64 (address), + TypeCode.Int64 => d => d.GetInt64 (address), + TypeCode.Double => d => d.GetFloat64(address), + _ => throw new ArgumentException(nameof(modbusType)) + }; + + Action set = info switch + { + FieldInfo fi => (rec, value) => fi.SetValue(rec, value.ConvertTo(fi.FieldType)), + PropertyInfo pi => (rec, value) => pi.SetValue(rec, value.ConvertTo(pi.PropertyType)), + }; + + + return (mbData, rec) => + { + var rawModbusValue = readFromMbData(mbData); + var decoded = decode(rawModbusValue); + + set(rec, decoded); + }; + } + + private static Func ConvertModbusToRecord((Double scale, Double offset) transform) + { + if (transform == NoTransform) + return Nop; + + var scale = transform.scale.ConvertTo(); + var offset = transform.offset.ConvertTo(); + + return c => + { + var value = c.ConvertTo(); + + return (value + offset) * scale; + }; + } + + + private static Action RecordToModbus(this MemberInfo info, UInt16 addr, TypeCode modbusType, (Double scale, Double offset) transform) + { + var encode = ConvertRecordToModbus(transform); + + Func get = info switch + { + FieldInfo fi => rec => (IConvertible)fi.GetValue(rec)!, + PropertyInfo pi => rec => (IConvertible)pi.GetValue(rec)!, + }; + + Action writeToMbData = modbusType switch + { + TypeCode.Boolean => (value, mbData) => mbData.SetCoil (addr, value.ConvertTo()), + TypeCode.UInt16 => (value, mbData) => mbData.SetUInt16 (addr, value.ConvertTo()), + TypeCode.Int16 => (value, mbData) => mbData.SetInt16 (addr, value.ConvertTo()), + TypeCode.UInt32 => (value, mbData) => mbData.SetUInt32 (addr, value.ConvertTo()), + TypeCode.Int32 => (value, mbData) => mbData.SetInt32 (addr, value.ConvertTo()), + TypeCode.Single => (value, mbData) => mbData.SetFloat32(addr, value.ConvertTo()), + TypeCode.UInt64 => (value, mbData) => mbData.SetUInt64 (addr, value.ConvertTo()), + TypeCode.Int64 => (value, mbData) => mbData.SetInt64 (addr, value.ConvertTo()), + TypeCode.Double => (value, mbData) => mbData.SetFloat64(addr, value.ConvertTo()), + _ => throw new ArgumentException(nameof(modbusType)) + }; + + return (rec, mbData) => + { + var memberValue = get(rec); + var encoded = encode(memberValue); + + writeToMbData(encoded, mbData); + }; + } + + private static Func ConvertRecordToModbus((Double scale, Double offset) transform) + { + if (transform == NoTransform) + return Nop; + + var scale = transform.scale.ConvertTo(); + var offset = transform.offset.ConvertTo(); + + return c => + { + var value = c.ConvertTo(); + + return value / scale - offset; + }; + } + + + private static T Nop(T c) => c; + + + private static IEnumerable GetDataMembers() + { + var recordType = typeof(R); + + const BindingFlags bindingFlags = Instance + | Public + | NonPublic + | FlattenHierarchy; + + var fields = recordType.GetFields(bindingFlags); + var props = recordType.GetProperties(bindingFlags); + + return fields.Concat(props); + } + + private static Boolean HasAttribute(MemberInfo i) where T : Attribute + { + return i.GetCustomAttributes().Any(); + } +} + \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Reflection/ModbusRegister.cs b/csharp/Lib/Protocols/Modbus/Reflection/ModbusRegister.cs deleted file mode 100644 index 25942638a..000000000 --- a/csharp/Lib/Protocols/Modbus/Reflection/ModbusRegister.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Reflection; -using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; -using InnovEnergy.Lib.Utils; - -#pragma warning disable CS8509 - -namespace InnovEnergy.Lib.Protocols.Modbus.Reflection; - -internal readonly struct ModbusRegister : IAddress -{ - - public UInt16 Address => _Attribute.Address; - public Decimal Offset => (Decimal)_Attribute.Offset; - public Decimal Scale => (Decimal)_Attribute.Scale; - - private readonly Object _Record; - private readonly ModbusRegisterAttribute _Attribute; - private readonly MemberInfo _MemberInfo; - - private static readonly Type[] UnsignedTypes = - { - typeof(Byte), - typeof(UInt16), - typeof(UInt32), - typeof(UInt64), - }; - - private Boolean IsUnsigned - { - get - { - var type = _MemberInfo switch - { - FieldInfo fi => fi.FieldType, - PropertyInfo pi => pi.PropertyType - }; - - type = type.IsEnum - ? Enum.GetUnderlyingType(type) - : type; - - return UnsignedTypes.Contains(type); - } - } - - - public ModbusRegister(ModbusRegisterAttribute attribute, MemberInfo memberInfo, Object record) - { - _Record = record; - _Attribute = attribute; - _MemberInfo = memberInfo; - } - - - [Obsolete] public ModbusRegister() => throw new Exception("Forbidden"); - - public UInt16 SetStatusRecordMemberFromRawModbusValue(UInt16 rawRegisterValue) - { - var raw = IsUnsigned - ? (Decimal)rawRegisterValue - : (Int16)rawRegisterValue; // if it is signed we must first reinterpret cast the native UInt16 to Int16 - - var transformed = (raw + Offset) * Scale; - - if (_MemberInfo is FieldInfo fi) - { - var converted = transformed.ConvertTo(fi.FieldType); - fi.SetValue(_Record, converted); - } - else if (_MemberInfo is PropertyInfo pi) - { - var converted = transformed.ConvertTo(pi.PropertyType); - pi.SetValue(_Record, converted); - } - - return Address; - } - - public UInt16 GetRawModbusValueFromControlRecord() - { - var value = _MemberInfo switch - { - FieldInfo fi => fi.GetValue(_Record), - PropertyInfo pi => pi.GetValue(_Record), - _ => throw new ArgumentException(nameof(_MemberInfo)) - }; - - var transformed = value! - .CastTo() - .ConvertTo() / Scale - Offset; - - return IsUnsigned - ? transformed.ConvertTo() - : transformed.ConvertTo().CastTo(); - } - - -} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Reflection/Record.cs b/csharp/Lib/Protocols/Modbus/Reflection/Record.cs new file mode 100644 index 000000000..da37a95d3 --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Reflection/Record.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes; + +#pragma warning disable CS8509 + +namespace InnovEnergy.Lib.Protocols.Modbus.Reflection; + +public static class Record +{ + internal static Int32 GetAddressOffset() + { + return typeof(R) + .GetCustomAttributes() + .Select(a => a.Offset) + .FirstOrDefault(); + } +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Slaves/ModbusDevice.cs b/csharp/Lib/Protocols/Modbus/Slaves/ModbusDevice.cs new file mode 100644 index 000000000..b8bf2b2ca --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Slaves/ModbusDevice.cs @@ -0,0 +1,53 @@ +using InnovEnergy.Lib.Protocols.Modbus.Clients; +using InnovEnergy.Lib.Protocols.Modbus.Reflection; + +namespace InnovEnergy.Lib.Protocols.Modbus.Slaves; + +using Float32 = Single; +using Float64 = Double; + +public class ModbusDevice where R : notnull +{ + private readonly IReadOnlyList> _Batches; + + public ModbusDevice(ModbusClient modbusClient, Int32 addressOffset = 0) + { + var offset = addressOffset + Record.GetAddressOffset(); + _Batches = modbusClient.MakeBatchesFor(offset).ToList(); + } + + public R Read() + { + R r; + + try + { + r = Activator.CreateInstance(); + } + catch (Exception e) + { + throw new Exception + ( + $"type {typeof(R).Name} seems to lack a parameterless constructor. " + + $"Either create one or use the other overload of{nameof(Read)} instead.", + e + ); + } + + return Read(r); + } + + public R Read(R record) + { + foreach (var batch in _Batches) + batch.Read(record); + + return record; + } + + public void Write(R record) + { + foreach (var batch in _Batches) + batch.Write(record); + } +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Slaves/ModbusEncoding.cs b/csharp/Lib/Protocols/Modbus/Slaves/ModbusEncoding.cs new file mode 100644 index 000000000..7e749cb3b --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Slaves/ModbusEncoding.cs @@ -0,0 +1,7 @@ +namespace InnovEnergy.Lib.Protocols.Modbus.Slaves; + +public enum ModbusEncoding +{ + Binary, + Ascii +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Slaves/ModbusProtocol.cs b/csharp/Lib/Protocols/Modbus/Slaves/ModbusProtocol.cs new file mode 100644 index 000000000..74ec6c3c3 --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Slaves/ModbusProtocol.cs @@ -0,0 +1,7 @@ +namespace InnovEnergy.Lib.Protocols.Modbus.Slaves; + +public enum ModbusProtocol +{ + Rtu, + Tcp +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Slaves/ModbusSlave.cs b/csharp/Lib/Protocols/Modbus/Slaves/ModbusSlave.cs new file mode 100644 index 000000000..3b1870058 --- /dev/null +++ b/csharp/Lib/Protocols/Modbus/Slaves/ModbusSlave.cs @@ -0,0 +1,55 @@ +using InnovEnergy.Lib.Protocols.Modbus.Channels; +using InnovEnergy.Lib.Protocols.Modbus.Clients; + +namespace InnovEnergy.Lib.Protocols.Modbus.Slaves; + +public static class ModbusSlave +{ + + public static Func ModbusTcp(this Channel channel) + { + ModbusTcpClient SlaveId(Byte slaveId) => new ModbusTcpClient(channel, slaveId); + return SlaveId; + } + + public static Func ModbusRtu(this Channel channel) + { + ModbusRtuClient SlaveId(Byte slaveId) => new ModbusRtuClient(channel, slaveId); + return SlaveId; + } + + + public static Func ModbusTcp(this Channel channel) where R : notnull, new() + { + ModbusTcpClient SlaveId(Byte slaveId) + { + return new ModbusTcpClient(channel, slaveId); + } + + return SlaveId; + } + + public static Func ModbusRtu(this Channel channel) where R : notnull, new() + { + ModbusRtuClient SlaveId(Byte slaveId) => new ModbusRtuClient(channel, slaveId); + return SlaveId; + } + + public static ModbusDevice TcpSlave(this Channel channel, Byte slaveId) where T : notnull, new() + { + var client = new ModbusTcpClient(channel, slaveId); + return new ModbusDevice(client); + } + + public static ModbusDevice RtuSlave(this Channel channel, Byte slaveId) where T : notnull, new() + { + var client = new ModbusRtuClient(channel, slaveId); + return new ModbusDevice(client); + } + + public static ModbusDevice Slave(this ModbusClient modbusClient) where T : notnull, new() + { + return new ModbusDevice(modbusClient); + } + +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Util/ArraySegmentExtensions.cs b/csharp/Lib/Protocols/Modbus/Util/ArraySegmentExtensions.cs index 1e0217ff6..82b1e4de4 100644 --- a/csharp/Lib/Protocols/Modbus/Util/ArraySegmentExtensions.cs +++ b/csharp/Lib/Protocols/Modbus/Util/ArraySegmentExtensions.cs @@ -1,17 +1,15 @@ +using InnovEnergy.Lib.Utils; + namespace InnovEnergy.Lib.Protocols.Modbus.Util; public static class ArraySegmentExtensions { - public static ArraySegment ToArraySegment(this IEnumerable enumerable) + public static ArraySegment ToArraySegment(this IEnumerable enumerable) => enumerable switch { - if (enumerable is T[] array) - return new ArraySegment(array); - - if (enumerable is ArraySegment arraySegment) - return arraySegment; // already an ArraySegment, doh! - - return new ArraySegment(enumerable.ToArray()); - } + ArraySegment ars => ars, + IReadOnlyList rol => rol.ToArray(), + _ => enumerable.ToArray() + }; public static ArraySegment Skip(this ArraySegment seg, Int32 count) {