introduce new simplified, reflection based Modbus Lib

This commit is contained in:
ig 2023-04-20 15:03:13 +02:00
parent 41a8c3f272
commit 216ddda78f
25 changed files with 828 additions and 98 deletions

View File

@ -1,54 +1,35 @@
using InnovEnergy.Lib.Protocols.Modbus.Connections; using InnovEnergy.Lib.Protocols.Modbus.Connections;
using InnovEnergy.Lib.Protocols.Modbus.Conversions;
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
namespace InnovEnergy.Lib.Protocols.Modbus.Clients; namespace InnovEnergy.Lib.Protocols.Modbus.Clients;
using Coils = IReadOnlyList<Boolean>; using Booleans = IReadOnlyList<Boolean>;
using UInt16s = IReadOnlyList<UInt16>;
// TODO: ModbusClient = Framer(TCP/RTU) + Connection(Serial/TCP) + Encoder(binary/ascii) + quirk(+1/0) // TODO: ModbusClient = Framer(TCP/RTU) + Connection(Serial/TCP) + Encoder(binary/ascii) + quirk(+1/0)
// Transport // Transport
public abstract class ModbusClient public abstract class ModbusClient
{ {
protected ModbusConnection Connection { get; } internal ModbusConnection Connection { get; }
protected Byte SlaveId { get; } internal Byte SlaveId { get; }
// TODO: add additional functions: coils... // TODO: add additional functions: coils...
public abstract Coils ReadDiscreteInputs (UInt16 readAddress, UInt16 nValues); public abstract Booleans ReadCoils (UInt16 readAddress, UInt16 nValues);
public abstract ModbusRegisters ReadInputRegisters (UInt16 readAddress, UInt16 nValues); public abstract Booleans ReadDiscreteInputs (UInt16 readAddress, UInt16 nValues);
public abstract ModbusRegisters ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues); public abstract UInt16s ReadInputRegisters (UInt16 readAddress, UInt16 nValues);
public abstract UInt16 WriteMultipleCoils (UInt16 writeAddress, Coils coils); public abstract UInt16s ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues);
public abstract UInt16 WriteRegisters (UInt16 writeAddress, IReadOnlyList<UInt16> values); public abstract UInt16 WriteCoils (UInt16 writeAddress, Booleans coils);
public abstract ModbusRegisters ReadWriteRegisters (UInt16 readAddress, UInt16 nbToRead, public abstract UInt16 WriteRegisters (UInt16 writeAddress, UInt16s values);
UInt16 writeAddress, IReadOnlyList<UInt16> registersToWrite);
public UInt16 WriteMultipleCoils(UInt16 writeAddress, params Boolean[] coils) public abstract UInt16s ReadWriteRegisters (UInt16 readAddress,
{ UInt16 nbToRead,
return WriteMultipleCoils(writeAddress, (IReadOnlyList<Boolean>)coils); UInt16 writeAddress,
} UInt16s registersToWrite);
public UInt16 WriteRegisters(ModbusRegisters registers) protected ModbusClient(ModbusConnection connection, Byte slaveId)
{
return WriteRegisters(registers.StartRegister, registers);
}
public UInt16 WriteRegisters(UInt16 writeAddress, params UInt16[] values)
{
return WriteRegisters(writeAddress, (IReadOnlyList<UInt16>)values);
}
protected ModbusClient(ModbusConnection connection,
Byte slaveId)
{ {
Connection = connection; Connection = connection;
SlaveId = slaveId; SlaveId = slaveId;
} }
public void CloseConnection() => Connection.Close();
} }

View File

@ -1,7 +1,6 @@
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using InnovEnergy.Lib.Protocols.Modbus.Connections; using InnovEnergy.Lib.Protocols.Modbus.Connections;
using InnovEnergy.Lib.Protocols.Modbus.Conversions;
using InnovEnergy.Lib.Protocols.Modbus.Protocol; using InnovEnergy.Lib.Protocols.Modbus.Protocol;
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames; using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames;
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Commands; using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Commands;
@ -23,12 +22,17 @@ public class ModbusRtuClient : ModbusClient
{ {
} }
public override IReadOnlyList<Boolean> ReadCoils(UInt16 readAddress, UInt16 nValues)
{
throw new NotImplementedException();
}
public override IReadOnlyList<Boolean> ReadDiscreteInputs(UInt16 readAddress, UInt16 nValues) public override IReadOnlyList<Boolean> ReadDiscreteInputs(UInt16 readAddress, UInt16 nValues)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override ModbusRegisters ReadInputRegisters(UInt16 readAddress, UInt16 nValues) public override UInt16s ReadInputRegisters(UInt16 readAddress, UInt16 nValues)
{ {
var cmd = new ReadInputRegistersCommandFrame(SlaveId, readAddress, nValues); var cmd = new ReadInputRegistersCommandFrame(SlaveId, readAddress, nValues);
var crc = CalcCrc(cmd); var crc = CalcCrc(cmd);
@ -48,11 +52,11 @@ public class ModbusRtuClient : ModbusClient
.Apply(ReadInputRegistersResponseFrame.Parse) .Apply(ReadInputRegistersResponseFrame.Parse)
.Apply(cmd.VerifyResponse); .Apply(cmd.VerifyResponse);
return new ModbusRegisters(readAddress, response.RegistersRead.ToArray()); return response.RegistersRead;
} }
public override ModbusRegisters ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues) public override UInt16s ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues)
{ {
var cmd = new ReadHoldingRegistersCommandFrame(SlaveId, readAddress, nValues); var cmd = new ReadHoldingRegistersCommandFrame(SlaveId, readAddress, nValues);
var crc = CalcCrc(cmd.Data); var crc = CalcCrc(cmd.Data);
@ -71,10 +75,10 @@ public class ModbusRtuClient : ModbusClient
.Apply(ReadHoldingRegistersResponseFrame.Parse) .Apply(ReadHoldingRegistersResponseFrame.Parse)
.Apply(cmd.VerifyResponse); .Apply(cmd.VerifyResponse);
return new ModbusRegisters(readAddress, response.RegistersRead.ToArray()); return response.RegistersRead;
} }
public override UInt16 WriteMultipleCoils(UInt16 writeAddress, IReadOnlyList<Boolean> coils) public override UInt16 WriteCoils(UInt16 writeAddress, IReadOnlyList<Boolean> coils)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -103,7 +107,7 @@ public class ModbusRtuClient : ModbusClient
return response.NbWritten; return response.NbWritten;
} }
public override ModbusRegisters ReadWriteRegisters(UInt16 readAddress, UInt16 nbToRead, UInt16 writeAddress, UInt16s registersToWrite) public override UInt16s ReadWriteRegisters(UInt16 readAddress, UInt16 nbToRead, UInt16 writeAddress, UInt16s registersToWrite)
{ {
var cmd = new ReadWriteRegistersCommandFrame(SlaveId, var cmd = new ReadWriteRegistersCommandFrame(SlaveId,
readAddress, readAddress,
@ -125,7 +129,7 @@ public class ModbusRtuClient : ModbusClient
.Apply(ReadWriteRegistersResponseFrame.Parse) .Apply(ReadWriteRegistersResponseFrame.Parse)
.Apply(cmd.VerifyResponse); .Apply(cmd.VerifyResponse);
return new ModbusRegisters(readAddress, response.RegistersRead.ToArray()); return response.RegistersRead;
} }
public static ArraySegment<Byte> AssertCrc(ArraySegment<Byte> data) public static ArraySegment<Byte> AssertCrc(ArraySegment<Byte> data)

View File

@ -0,0 +1,159 @@
using InnovEnergy.Lib.Protocols.Modbus.Reflection;
using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
namespace InnovEnergy.Lib.Protocols.Modbus.Clients;
public class ModbusSlave<S, C> 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<UInt16> ReadDiscreteInputs(Object statusRecord)
{
return from batch in statusRecord.GetModbusBooleanBatches<DiscreteInputAttribute>()
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<UInt16> ReadCoils(Object statusRecord)
{
return from batch in statusRecord.GetModbusBooleanBatches<CoilAttribute>()
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<UInt16> ReadInputRegisters(Object statusRecord)
{
return from batch in statusRecord.GetModbusRegisterBatches<InputRegisterAttribute>()
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<UInt16> ReadHoldingRegisters(Object statusRecord)
{
return from batch in statusRecord.GetModbusRegisterBatches<HoldingRegisterAttribute>()
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<CoilAttribute>())
{
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<HoldingRegisterAttribute>())
{
var values = batch.GetRawModbusValuesFromControlRecord();
_ModbusClient.WriteRegisters(batch[0].Address, values);
nBatches++;
}
return nBatches;
}
}
public class ModbusSlave<T> : ModbusSlave<T, T> where T : notnull, new()
{
public ModbusSlave(ModbusClient modbusClient) : base(modbusClient)
{
}
}
public static class ModbusSlave
{
public static ModbusSlave<T> Slave<T>(this ModbusClient modbusClient) where T : notnull, new()
{
return new ModbusSlave<T>(modbusClient);
}
public static ModbusSlave<S,C> Slave<S,C>(this ModbusClient modbusClient) where S : notnull, new() where C : notnull
{
return new ModbusSlave<S, C>(modbusClient);
}
}

View File

@ -1,15 +1,15 @@
using System.Diagnostics; using System.Diagnostics;
using InnovEnergy.Lib.Protocols.Modbus.Connections; using InnovEnergy.Lib.Protocols.Modbus.Connections;
using InnovEnergy.Lib.Protocols.Modbus.Conversions;
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Commands; using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Commands;
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Replies; using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Replies;
using InnovEnergy.Lib.Protocols.Modbus.Tcp; using InnovEnergy.Lib.Protocols.Modbus.Tcp;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.Lib.Protocols.Modbus.Clients; namespace InnovEnergy.Lib.Protocols.Modbus.Clients;
using UInt16s = IReadOnlyList<UInt16>; using UInt16s = IReadOnlyList<UInt16>;
using Coils = IReadOnlyList<Boolean>;
public class ModbusTcpClient : ModbusClient public class ModbusTcpClient : ModbusClient
{ {
@ -24,6 +24,25 @@ public class ModbusTcpClient : ModbusClient
{ {
} }
public override IReadOnlyList<Boolean> ReadCoils(UInt16 readAddress, UInt16 nValues)
{
var id = NextId(); // TODO: check response id
var cmd = new ReadCoilsCommandFrame(SlaveId, readAddress, nValues);
var hdr = new MbapHeader(id, cmd.Data.Count);
var frm = new ModbusTcpFrame(hdr, cmd);
Connection.Transmit(frm.Data);
var hData = Connection.Receive(MbapHeader.Size).ToArray(); // TODO: optimize .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;
}
public override IReadOnlyList<Boolean> ReadDiscreteInputs(UInt16 readAddress, UInt16 nValues) public override IReadOnlyList<Boolean> ReadDiscreteInputs(UInt16 readAddress, UInt16 nValues)
{ {
@ -39,12 +58,12 @@ public class ModbusTcpClient : ModbusClient
var rxHdr = new MbapHeader(hData); var rxHdr = new MbapHeader(hData);
var fData = Connection.Receive(rxHdr.FrameLength).ToArray(); // TODO: optimize .ToArray() ? var fData = Connection.Receive(rxHdr.FrameLength).ToArray(); // TODO: optimize .ToArray() ?
var rxFrm = ReadDiscreteInputResponseFrame.Parse(fData); var rxFrm = ReadDiscreteInputsResponseFrame.Parse(fData);
return cmd.VerifyResponse(rxFrm).Inputs; return cmd.VerifyResponse(rxFrm).Inputs;
} }
public override ModbusRegisters ReadInputRegisters(UInt16 readAddress, UInt16 nValues) public override IReadOnlyList<UInt16> ReadInputRegisters(UInt16 readAddress, UInt16 nValues)
{ {
var id = NextId(); // TODO: check response id var id = NextId(); // TODO: check response id
@ -62,13 +81,11 @@ public class ModbusTcpClient : ModbusClient
var verified = cmd.VerifyResponse(rxFrm); var verified = cmd.VerifyResponse(rxFrm);
return new ModbusRegisters(readAddress, verified.RegistersRead.ToArray()); return verified.RegistersRead;
} }
public override IReadOnlyList<UInt16> ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues)
public override ModbusRegisters ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues)
{ {
var id = NextId(); // TODO: check response id var id = NextId(); // TODO: check response id
var cmd = new ReadHoldingRegistersCommandFrame(SlaveId, readAddress, nValues); var cmd = new ReadHoldingRegistersCommandFrame(SlaveId, readAddress, nValues);
@ -77,18 +94,18 @@ public class ModbusTcpClient : ModbusClient
Connection.Transmit(frm.Data); Connection.Transmit(frm.Data);
var hData = Connection.Receive(MbapHeader.Size).ToArray(); var hData = Connection.Receive(MbapHeader.Size).ToArray(MbapHeader.Size);
var rxHdr = new MbapHeader(hData); var rxHdr = new MbapHeader(hData);
var fData = Connection.Receive(rxHdr.FrameLength).ToArray(); var fData = Connection.Receive(rxHdr.FrameLength).ToArray(rxHdr.FrameLength);
var rxFrm = ReadHoldingRegistersResponseFrame.Parse(fData); var rxFrm = ReadHoldingRegistersResponseFrame.Parse(fData);
var verified = cmd.VerifyResponse(rxFrm); var verified = cmd.VerifyResponse(rxFrm);
return new ModbusRegisters(readAddress, verified.RegistersRead.ToArray()); // TODO return verified.RegistersRead; // TODO
} }
public override UInt16 WriteMultipleCoils(UInt16 writeAddress, Coils coils) public override UInt16 WriteCoils(UInt16 writeAddress, IReadOnlyList<Boolean> coils)
{ {
var id = NextId(); // TODO: check response id var id = NextId(); // TODO: check response id
var cmd = new WriteCoilsCommandFrame(SlaveId, writeAddress, coils); var cmd = new WriteCoilsCommandFrame(SlaveId, writeAddress, coils);
@ -127,10 +144,8 @@ public class ModbusTcpClient : ModbusClient
return verified.NbWritten; return verified.NbWritten;
} }
public override ModbusRegisters ReadWriteRegisters(UInt16 readAddress, UInt16 nbToRead, UInt16 writeAddress, UInt16s registersToWrite) public override IReadOnlyList<UInt16> ReadWriteRegisters(UInt16 readAddress, UInt16 nbToRead, UInt16 writeAddress, UInt16s registersToWrite)
{ {
var id = NextId(); // TODO: check response id var id = NextId(); // TODO: check response id
var cmd = new ReadWriteRegistersCommandFrame(SlaveId, var cmd = new ReadWriteRegistersCommandFrame(SlaveId,
@ -144,15 +159,15 @@ public class ModbusTcpClient : ModbusClient
Connection.Transmit(frm.Data); Connection.Transmit(frm.Data);
var hData = Connection.Receive(MbapHeader.Size).ToArray(); var hData = Enumerable.ToArray(Connection.Receive(MbapHeader.Size));
var rxHdr = new MbapHeader(hData); var rxHdr = new MbapHeader(hData);
var fData = Connection.Receive(rxHdr.FrameLength).ToArray(); var fData = Enumerable.ToArray(Connection.Receive(rxHdr.FrameLength));
var rxFrm = ReadWriteRegistersResponseFrame.Parse(fData); var rxFrm = ReadWriteRegistersResponseFrame.Parse(fData);
var verified = cmd.VerifyResponse(rxFrm); var verified = cmd.VerifyResponse(rxFrm);
return new ModbusRegisters(readAddress, verified.RegistersRead.ToArray()); // TODO return verified.RegistersRead; // TODO
} }
} }

View File

@ -0,0 +1,38 @@
using System.Reactive.Linq;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.Lib.Protocols.Modbus.Connections;
public static class PushToPullHelper
{
// TODO: this is incredibly hacky, improve
public static Func<Int32, Task<IReadOnlyList<T>>> PushToPull<T>(this IObservable<T> src)
{
var buffer = new Queue<T>();
var nAvailable = src
.Do(buffer.Enqueue)
.Select(_ => buffer.Count)
.Publish();
nAvailable.Connect();
async Task<IReadOnlyList<T>> Read(Int32 n)
{
var available = buffer.Count;
if (available < n)
available = await nAvailable.FirstOrDefaultAsync(a => a >= n);
if (available < n)
throw new Exception("Connection closed");
return Enumerable
.Range(0, n)
.Select(_ => buffer.Dequeue())
.ToArray(n);
}
return Read;
}
}

View File

@ -0,0 +1,113 @@
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<Int32, Task<IReadOnlyList<Byte>>>? _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<Byte> Receive(UInt16 bytesToRead)
{
Open();
return _Read!(bytesToRead).Result;
}
public override void Transmit(IEnumerable<Byte> 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
}
}

View File

@ -5,6 +5,11 @@
</PropertyGroup> </PropertyGroup>
<Import Project="../../InnovEnergy.Lib.props" /> <Import Project="../../InnovEnergy.Lib.props" />
<PropertyGroup>
<IsTrimmable>false</IsTrimmable>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="../../Utils/Utils.csproj" /> <ProjectReference Include="../../Utils/Utils.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1,50 @@
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors;
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Replies;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Commands;
internal class ReadCoilsCommandFrame : ModbusFrame
{
private const Int32 Size = 6;
private MbWord ReadAddress => Data.WordAt(2);
private MbWord QuantityOfInputs => Data.WordAt(4);
public ReadCoilsCommandFrame(Byte slave, UInt16 readAddress, UInt16 nBits) : base(Size)
{
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);
}
private ReadCoilsCommandFrame(ArraySegment<Byte> data) : base(data)
{
if (data.Count != Size)
throw new ArgumentException($"Expecting an array of size {Size}", nameof(data));
AssertFunctionCode(Protocol.FunctionCode.ReadCoils);
}
public ReadCoilsResponseFrame VerifyResponse(ReadCoilsResponseFrame response)
{
if (response.SlaveAddress != SlaveAddress)
throw new UnexpectedResponseFieldException(nameof(response.SlaveAddress), SlaveAddress.ToString(), response.SlaveAddress);
if (response.Inputs.Count != Math.Ceiling(QuantityOfInputs / 8.0).ConvertTo<Int32>() * 8)
throw new UnexpectedResponseFieldException(nameof(response.Inputs), QuantityOfInputs.ToString(), response.Inputs.Count);
return response;
}
public static ReadCoilsCommandFrame Parse(ArraySegment<Byte> data)
{
return new ReadCoilsCommandFrame(data);
}
}

View File

@ -37,7 +37,7 @@ internal class ReadDiscreteInputsCommandFrame : ModbusFrame
AssertFunctionCode(ReadDiscreteInputs); AssertFunctionCode(ReadDiscreteInputs);
} }
public ReadDiscreteInputResponseFrame VerifyResponse(ReadDiscreteInputResponseFrame response) public ReadDiscreteInputsResponseFrame VerifyResponse(ReadDiscreteInputsResponseFrame response)
{ {
if (response.SlaveAddress != SlaveAddress) if (response.SlaveAddress != SlaveAddress)
throw new UnexpectedResponseFieldException(nameof(response.SlaveAddress), SlaveAddress.ToString(), response.SlaveAddress); throw new UnexpectedResponseFieldException(nameof(response.SlaveAddress), SlaveAddress.ToString(), response.SlaveAddress);

View File

@ -0,0 +1,51 @@
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Replies;
public class ReadCoilsResponseFrame : ModbusFrame
{
private new const Int32 MinSize = 3;
private MbByte ByteCount => Data.ByteAt(2);
public MbBits Inputs => Data.BitsAt(3);
public ReadCoilsResponseFrame(Byte slave, IReadOnlyList<Boolean> inputs) : base (inputs.Count)
{
var nBytes = Math.Ceiling(inputs.Count / 8.0).ConvertTo<UInt16>();
SlaveAddress .Set(slave);
FunctionCode .Set(Protocol.FunctionCode.ReadCoils);
ByteCount .Set(nBytes);
Inputs .Set(inputs);
}
private ReadCoilsResponseFrame(Byte[] data) : this(new ArraySegment<Byte>(data))
{ }
private ReadCoilsResponseFrame(ArraySegment<Byte> data) : base(data)
{
AssertFunctionCode(Protocol.FunctionCode.ReadCoils);
// TODO
// var expectedSize = ByteCount + MinSize;
// if (data.Count != expectedSize)
// throw new ArgumentException($"Expecting an array of size {expectedSize}", nameof(data));
// if (data.Count < MinSize)
// throw new ArgumentException($"Expecting an array of size {MinSize} or more", nameof(data));
}
public static ReadCoilsResponseFrame Parse(ModbusFrame rawFrame)
{
return new ReadCoilsResponseFrame(rawFrame.Data);
}
public static ReadCoilsResponseFrame Parse(ArraySegment<Byte> data)
{
return new ReadCoilsResponseFrame(data);
}
}

View File

@ -7,14 +7,14 @@ namespace InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Replies;
using Booleans = IReadOnlyList<Boolean>; using Booleans = IReadOnlyList<Boolean>;
using MbFc = MbByte<FunctionCode>; using MbFc = MbByte<FunctionCode>;
public class ReadDiscreteInputResponseFrame : ModbusFrame public class ReadDiscreteInputsResponseFrame : ModbusFrame
{ {
private new const Int32 MinSize = 3; private new const Int32 MinSize = 3;
private MbByte ByteCount => Data.ByteAt(2); private MbByte ByteCount => Data.ByteAt(2);
public MbBits Inputs => Data.BitsAt(3); public MbBits Inputs => Data.BitsAt(3);
public ReadDiscreteInputResponseFrame(Byte slave, Booleans inputs) : base (inputs.Count) public ReadDiscreteInputsResponseFrame(Byte slave, Booleans inputs) : base (inputs.Count)
{ {
var nBytes = Math.Ceiling(inputs.Count / 8.0).ConvertTo<UInt16>(); var nBytes = Math.Ceiling(inputs.Count / 8.0).ConvertTo<UInt16>();
@ -24,11 +24,11 @@ public class ReadDiscreteInputResponseFrame : ModbusFrame
Inputs .Set(inputs); Inputs .Set(inputs);
} }
private ReadDiscreteInputResponseFrame(Byte[] data) : this(new ArraySegment<Byte>(data)) private ReadDiscreteInputsResponseFrame(Byte[] data) : this(new ArraySegment<Byte>(data))
{ } { }
private ReadDiscreteInputResponseFrame(ArraySegment<Byte> data) : base(data) private ReadDiscreteInputsResponseFrame(ArraySegment<Byte> data) : base(data)
{ {
AssertFunctionCode(ReadDiscreteInputs); AssertFunctionCode(ReadDiscreteInputs);
@ -43,13 +43,13 @@ public class ReadDiscreteInputResponseFrame : ModbusFrame
} }
public static ReadDiscreteInputResponseFrame Parse(ModbusFrame rawFrame) public static ReadDiscreteInputsResponseFrame Parse(ModbusFrame rawFrame)
{ {
return new ReadDiscreteInputResponseFrame(rawFrame.Data); return new ReadDiscreteInputsResponseFrame(rawFrame.Data);
} }
public static ReadDiscreteInputResponseFrame Parse(ArraySegment<Byte> data) public static ReadDiscreteInputsResponseFrame Parse(ArraySegment<Byte> data)
{ {
return new ReadDiscreteInputResponseFrame(data); return new ReadDiscreteInputsResponseFrame(data);
} }
} }

View File

@ -1,39 +1,42 @@
using static InnovEnergy.Lib.Protocols.Modbus.Protocol.FunctionCode;
namespace InnovEnergy.Lib.Protocols.Modbus.Protocol; namespace InnovEnergy.Lib.Protocols.Modbus.Protocol;
[Flags] [Flags]
public enum FunctionCode : byte public enum FunctionCode : byte
{ {
ReadCoil = 1, ReadCoilError = ReadCoil | Error , ReadCoils = 1,
ReadDiscreteInputs = 2, ReadDiscreteInputsError = ReadDiscreteInputs | Error , ReadDiscreteInputs = 2,
ReadHoldingRegisters = 3, ReadHoldingRegistersError = ReadHoldingRegisters | Error , ReadHoldingRegisters = 3,
ReadInputRegisters = 4, ReadInputRegistersError = ReadInputRegisters | Error , ReadInputRegisters = 4,
WriteSingleCoil = 5, WriteSingleCoilError = WriteSingleCoil | Error , WriteSingleCoil = 5,
WriteSingleRegister = 6, WriteSingleRegisterError = WriteSingleRegister | Error , WriteSingleRegister = 6,
WriteMultipleCoils = 15, WriteMultipleCoilsError = WriteMultipleCoils | Error , WriteMultipleCoils = 15,
WriteMultipleRegisters = 16, WriteMultipleRegistersError = WriteMultipleRegisters | Error , WriteMultipleRegisters = 16,
ReadWriteMultipleRegisters = 23, ReadWriteMultipleRegistersError = ReadWriteMultipleRegisters | Error , ReadWriteMultipleRegisters = 23,
Error = 128 Error = 128
} }
public static class FunctionCodeExtensions public static class FunctionCodeExtensions
{ {
public static Boolean IsWriteOnly(this FunctionCode fc) public static Boolean IsWriteOnly(this FunctionCode fc)
{ {
return fc == FunctionCode.WriteSingleCoil || return fc is WriteSingleCoil
fc == FunctionCode.WriteSingleRegister || or WriteSingleRegister
fc == FunctionCode.WriteMultipleCoils || or WriteMultipleCoils
fc == FunctionCode.WriteMultipleRegisters; or WriteMultipleRegisters;
} }
public static Boolean IsMultiWrite(this FunctionCode fc) public static Boolean IsMultiWrite(this FunctionCode fc)
{ {
return fc == FunctionCode.WriteMultipleCoils || return fc is WriteMultipleCoils
fc == FunctionCode.WriteMultipleRegisters || or WriteMultipleRegisters
fc == FunctionCode.ReadWriteMultipleRegisters; or ReadWriteMultipleRegisters;
} }
public static Boolean IsError(this FunctionCode fc) public static Boolean IsError(this FunctionCode fc)
@ -48,5 +51,4 @@ public static class FunctionCodeExtensions
return fc | FunctionCode.Error; return fc | FunctionCode.Error;
} }
} }

View File

@ -0,0 +1,6 @@
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
public class CoilAttribute : ModbusBooleanAttribute
{
public CoilAttribute(UInt16 address) : base(address) { }
}

View File

@ -0,0 +1,8 @@
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
public class DiscreteInputAttribute : ModbusBooleanAttribute
{
public DiscreteInputAttribute(UInt16 address) : base(address) { }
}

View File

@ -0,0 +1,7 @@
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
public class HoldingRegisterAttribute : ModbusRegisterAttribute
{
public HoldingRegisterAttribute(UInt16 address) : base(address) { }
}

View File

@ -0,0 +1,7 @@
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
public class InputRegisterAttribute : ModbusRegisterAttribute
{
public InputRegisterAttribute(UInt16 address) : base(address) { }
}

View File

@ -0,0 +1,8 @@
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public abstract class ModbusAttribute : Attribute
{
public UInt16 Address { get; }
protected ModbusAttribute(UInt16 address) { Address = address; }
}

View File

@ -0,0 +1,8 @@
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
public class ModbusBooleanAttribute : ModbusAttribute
{
protected ModbusBooleanAttribute(UInt16 address) : base(address)
{
}
}

View File

@ -0,0 +1,11 @@
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;
}

View File

@ -0,0 +1,100 @@
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<ModbusRegister>;
using ModbusBooleans = IReadOnlyList<ModbusBoolean>;
using UInt16s = IReadOnlyList<UInt16>;
using Booleans = IReadOnlyList<Boolean>;
internal static class Batches
{
internal static IEnumerable<ModbusRegister[]> GetModbusRegisterBatches<T>(this Object statusRecord) where T : ModbusRegisterAttribute
{
var modbusRegisters = from mi in statusRecord.GetDataMembers()
from a in mi.GetCustomAttributes<T>()
select new ModbusRegister(a, mi, statusRecord);
return modbusRegisters.SplitIntoFrameSizedBatches();
}
internal static IEnumerable<ModbusBoolean[]> GetModbusBooleanBatches<T>(this Object statusRecord) where T : ModbusBooleanAttribute
{
var modbusBooleans = from mi in statusRecord.GetDataMembers()
from a in mi.GetCustomAttributes<T>()
select new ModbusBoolean(a, mi, statusRecord);
return modbusBooleans.SplitIntoFrameSizedBatches();
}
private static IEnumerable<MemberInfo> 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<MemberInfo>(props);
}
private static IEnumerable<T[]> SplitIntoFrameSizedBatches<T>(this IEnumerable<T> 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<UInt16> 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<UInt16> 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);
}
}

View File

@ -0,0 +1,6 @@
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection;
internal interface IAddress
{
UInt16 Address { get; }
}

View File

@ -0,0 +1,54 @@
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<IConvertible>()
.ConvertTo<Boolean>();
}
}

View File

@ -0,0 +1,98 @@
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<IConvertible>()
.ConvertTo<Decimal>() / Scale - Offset;
return IsUnsigned
? transformed.ConvertTo<UInt16>()
: transformed.ConvertTo<UInt16>().CastTo<UInt16>();
}
}

View File

@ -1,4 +1,3 @@
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames;
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors; using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors;
namespace InnovEnergy.Lib.Protocols.Modbus.Tcp; namespace InnovEnergy.Lib.Protocols.Modbus.Tcp;