introduce new simplified, reflection based Modbus Lib
This commit is contained in:
parent
41a8c3f272
commit
216ddda78f
|
@ -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 ModbusRegisters ReadInputRegisters (UInt16 readAddress, UInt16 nValues);
|
|
||||||
public abstract ModbusRegisters ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues);
|
|
||||||
public abstract UInt16 WriteMultipleCoils (UInt16 writeAddress, Coils coils);
|
|
||||||
public abstract UInt16 WriteRegisters (UInt16 writeAddress, IReadOnlyList<UInt16> values);
|
|
||||||
public abstract ModbusRegisters ReadWriteRegisters (UInt16 readAddress, UInt16 nbToRead,
|
|
||||||
UInt16 writeAddress, IReadOnlyList<UInt16> registersToWrite);
|
|
||||||
|
|
||||||
public UInt16 WriteMultipleCoils(UInt16 writeAddress, params Boolean[] coils)
|
|
||||||
{
|
|
||||||
return WriteMultipleCoils(writeAddress, (IReadOnlyList<Boolean>)coils);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UInt16 WriteRegisters(ModbusRegisters registers)
|
|
||||||
{
|
|
||||||
return WriteRegisters(registers.StartRegister, registers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UInt16 WriteRegisters(UInt16 writeAddress, params UInt16[] values)
|
public abstract Booleans ReadCoils (UInt16 readAddress, UInt16 nValues);
|
||||||
{
|
public abstract Booleans ReadDiscreteInputs (UInt16 readAddress, UInt16 nValues);
|
||||||
return WriteRegisters(writeAddress, (IReadOnlyList<UInt16>)values);
|
public abstract UInt16s ReadInputRegisters (UInt16 readAddress, UInt16 nValues);
|
||||||
}
|
public abstract UInt16s ReadHoldingRegisters(UInt16 readAddress, UInt16 nValues);
|
||||||
|
public abstract UInt16 WriteCoils (UInt16 writeAddress, Booleans coils);
|
||||||
protected ModbusClient(ModbusConnection connection,
|
public abstract UInt16 WriteRegisters (UInt16 writeAddress, UInt16s values);
|
||||||
Byte slaveId)
|
|
||||||
{
|
|
||||||
Connection = connection;
|
|
||||||
SlaveId = slaveId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CloseConnection() => Connection.Close();
|
public abstract UInt16s ReadWriteRegisters (UInt16 readAddress,
|
||||||
|
UInt16 nbToRead,
|
||||||
|
UInt16 writeAddress,
|
||||||
|
UInt16s registersToWrite);
|
||||||
|
|
||||||
|
protected ModbusClient(ModbusConnection connection, Byte slaveId)
|
||||||
}
|
{
|
||||||
|
Connection = connection;
|
||||||
|
SlaveId = slaveId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,14 +129,14 @@ 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)
|
||||||
{
|
{
|
||||||
var expectedCrc = data.SkipLast(CrcSize).Apply(CalcCrc);
|
var expectedCrc = data.SkipLast(CrcSize).Apply(CalcCrc);
|
||||||
var actualCrc = data.TakeLast(CrcSize);
|
var actualCrc = data.TakeLast(CrcSize);
|
||||||
|
|
||||||
if (!actualCrc.SequenceEqual(expectedCrc))
|
if (!actualCrc.SequenceEqual(expectedCrc))
|
||||||
throw new CrcException(expectedCrc, actualCrc);
|
throw new CrcException(expectedCrc, actualCrc);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,7 +24,26 @@ 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)
|
||||||
{
|
{
|
||||||
var id = NextId(); // TODO: check response id
|
var id = NextId(); // TODO: check response id
|
||||||
|
@ -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,20 +94,20 @@ 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);
|
||||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||||
|
@ -105,8 +122,8 @@ public class ModbusTcpClient : ModbusClient
|
||||||
|
|
||||||
return cmd.VerifyResponse(rxFrm).NbWritten;
|
return cmd.VerifyResponse(rxFrm).NbWritten;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public override UInt16 WriteRegisters(UInt16 writeAddress, UInt16s values)
|
public override UInt16 WriteRegisters(UInt16 writeAddress, UInt16s values)
|
||||||
{
|
{
|
||||||
var id = NextId(); // TODO: check response id
|
var id = NextId(); // TODO: check response id
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@ public class ModbusTcpConnection : ModbusConnection
|
||||||
public ModbusTcpConnection(String hostname, Int32 port = 502)
|
public ModbusTcpConnection(String hostname, Int32 port = 502)
|
||||||
{
|
{
|
||||||
Hostname = hostname;
|
Hostname = hostname;
|
||||||
Port = port;
|
Port = port;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IReadOnlyList<Byte> Receive(UInt16 bytesToRead)
|
public override IReadOnlyList<Byte> Receive(UInt16 bytesToRead)
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,11 @@
|
||||||
<Platforms>AnyCPU;linux-arm</Platforms>
|
<Platforms>AnyCPU;linux-arm</Platforms>
|
||||||
</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" />
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
@ -47,6 +50,5 @@ public static class FunctionCodeExtensions
|
||||||
{
|
{
|
||||||
return fc | FunctionCode.Error;
|
return fc | FunctionCode.Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
|
||||||
|
|
||||||
|
public class CoilAttribute : ModbusBooleanAttribute
|
||||||
|
{
|
||||||
|
public CoilAttribute(UInt16 address) : base(address) { }
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
|
||||||
|
|
||||||
|
public class DiscreteInputAttribute : ModbusBooleanAttribute
|
||||||
|
{
|
||||||
|
public DiscreteInputAttribute(UInt16 address) : base(address) { }
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
|
||||||
|
|
||||||
|
public class HoldingRegisterAttribute : ModbusRegisterAttribute
|
||||||
|
{
|
||||||
|
public HoldingRegisterAttribute(UInt16 address) : base(address) { }
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
|
||||||
|
|
||||||
|
public class InputRegisterAttribute : ModbusRegisterAttribute
|
||||||
|
{
|
||||||
|
public InputRegisterAttribute(UInt16 address) : base(address) { }
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
|
||||||
|
|
||||||
|
public class ModbusBooleanAttribute : ModbusAttribute
|
||||||
|
{
|
||||||
|
protected ModbusBooleanAttribute(UInt16 address) : base(address)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace InnovEnergy.Lib.Protocols.Modbus.Reflection;
|
||||||
|
|
||||||
|
internal interface IAddress
|
||||||
|
{
|
||||||
|
UInt16 Address { get; }
|
||||||
|
}
|
|
@ -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>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue