引入了NModbus源码,并优化完善Modbus驱动支持多种模式

This commit is contained in:
王海东 2021-12-16 00:16:22 +08:00
parent 507b0c2114
commit 7369e8da24
69 changed files with 6338 additions and 22 deletions

Binary file not shown.

View File

@ -5,15 +5,19 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<OutputPath>../../../IoTGateway/bin/Debug/net5.0/drivers</OutputPath> <OutputPath>../../../IoTGateway/bin/Debug/net5.0/drivers</OutputPath>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="NModbus4.Core" Version="1.0.2" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\PluginInterface\PluginInterface.csproj" /> <ProjectReference Include="..\..\PluginInterface\PluginInterface.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Reference Include="System.IO.Ports">
<HintPath>System.IO.Ports.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup>
</Project> </Project>

View File

@ -1,7 +1,9 @@
using Modbus.Device; using Modbus.Device;
using Modbus.Serial;
using PluginInterface; using PluginInterface;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO.Ports;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
@ -9,12 +11,16 @@ namespace DriverModbusTCP
{ {
[DriverSupported("ModbusTCP")] [DriverSupported("ModbusTCP")]
[DriverSupported("ModbusUDP")] [DriverSupported("ModbusUDP")]
[DriverInfoAttribute("ModbusTCP", "V1.0.0", "Copyright WHD© 2021-12-19")] [DriverSupported("ModbusRtu")]
[DriverSupported("ModbusAscii")]
[DriverInfoAttribute("ModbusMaster", "V1.0.0", "Copyright WHD© 2021-12-19")]
public class ModbusTCP : IDriver public class ModbusTCP : IDriver
{ {
private TcpClient client = null; private TcpClient clientTcp = null;
private ModbusIpMaster master = null; private UdpClient clientUdp = null;
private SerialPort port = null;
private ModbusMaster master = null;
private SerialPortAdapter adapter = null;
#region #region
[ConfigParameter("设备Id")] [ConfigParameter("设备Id")]
@ -23,14 +29,32 @@ namespace DriverModbusTCP
[ConfigParameter("PLC类型")] [ConfigParameter("PLC类型")]
public PLC_TYPE PLCType { get; set; } = PLC_TYPE.S71200; public PLC_TYPE PLCType { get; set; } = PLC_TYPE.S71200;
[ConfigParameter("主站类型")]
public Master_TYPE Master_TYPE { get; set; } = Master_TYPE.Tcp;
[ConfigParameter("IP地址")] [ConfigParameter("IP地址")]
public string IpAddress { get; set; } = "127.0.0.1"; public string IpAddress { get; set; } = "127.0.0.1";
[ConfigParameter("端口号")] [ConfigParameter("端口号")]
public int Port { get; set; } = 502; public int Port { get; set; } = 502;
[ConfigParameter("站号")] [ConfigParameter("串口名")]
public byte SlaveId { get; set; } = 1; public string PortName { get; set; } = "COM1";
[ConfigParameter("波特率")]
public int BaudRate { get; set; } = 9600;
[ConfigParameter("数据位")]
public int DataBits { get; set; } = 8;
[ConfigParameter("校验位")]
public Parity Parity { get; set; } = Parity.None;
[ConfigParameter("停止位")]
public StopBits StopBits { get; set; } = StopBits.One;
[ConfigParameter("从站号")]
public byte SlaveAddress { get; set; } = 1;
[ConfigParameter("超时时间ms")] [ConfigParameter("超时时间ms")]
public uint Timeout { get; set; } = 3000; public uint Timeout { get; set; } = 3000;
@ -50,7 +74,7 @@ namespace DriverModbusTCP
{ {
get get
{ {
return client != null && master != null && client.Connected; return clientTcp != null && master != null && clientTcp.Connected;
} }
} }
@ -58,8 +82,47 @@ namespace DriverModbusTCP
{ {
try try
{ {
client = new TcpClient(IpAddress.ToString(), Port); switch (Master_TYPE)
master = ModbusIpMaster.CreateIp(client); {
case Master_TYPE.Tcp:
clientTcp = new TcpClient(IpAddress.ToString(), Port);
master = ModbusIpMaster.CreateIp(clientTcp);
break;
case Master_TYPE.Udp:
clientUdp = new UdpClient(IpAddress.ToString(), Port);
master = ModbusIpMaster.CreateIp(clientUdp);
break;
case Master_TYPE.Rtu:
port = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits);
port.Open();
adapter = new SerialPortAdapter(port);
master = ModbusSerialMaster.CreateRtu(adapter);
break;
case Master_TYPE.RtuOnTcp:
clientTcp = new TcpClient(IpAddress.ToString(), Port);
master = ModbusSerialMaster.CreateRtu(clientTcp);
break;
case Master_TYPE.RtuOnUdp:
clientUdp = new UdpClient(IpAddress.ToString(), Port);
master = ModbusSerialMaster.CreateRtu(clientUdp);
break;
case Master_TYPE.Ascii:
port = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits);
port.Open();
adapter = new SerialPortAdapter(port);
master = ModbusSerialMaster.CreateAscii(adapter);
break;
case Master_TYPE.AsciiOnTcp:
clientTcp = new TcpClient(IpAddress.ToString(), Port);
master = ModbusSerialMaster.CreateAscii(clientTcp);
break;
case Master_TYPE.AsciiOnUdp:
clientUdp = new UdpClient(IpAddress.ToString(), Port);
master = ModbusSerialMaster.CreateAscii(clientUdp);
break;
default:
break;
}
} }
catch (Exception) catch (Exception)
{ {
@ -72,7 +135,9 @@ namespace DriverModbusTCP
{ {
try try
{ {
client?.Close(); clientTcp?.Close();
clientUdp?.Close();
port?.Close();
return !IsConnected; return !IsConnected;
} }
catch (Exception) catch (Exception)
@ -86,7 +151,9 @@ namespace DriverModbusTCP
{ {
try try
{ {
client?.Dispose(); clientTcp?.Dispose();
clientUdp?.Dispose();
port?.Dispose();
master?.Dispose(); master?.Dispose();
} }
catch (Exception) catch (Exception)
@ -158,7 +225,7 @@ namespace DriverModbusTCP
private DriverReturnValueModel ReadRegistersBuffers(byte FunCode, DriverAddressIoArgModel ioarg) private DriverReturnValueModel ReadRegistersBuffers(byte FunCode, DriverAddressIoArgModel ioarg)
{ {
DriverReturnValueModel ret = new() { StatusType = VaribaleStatusTypeEnum.Good }; DriverReturnValueModel ret = new() { StatusType = VaribaleStatusTypeEnum.Good };
if (!client.Connected) if (!clientTcp.Connected)
ret.StatusType = VaribaleStatusTypeEnum.Bad; ret.StatusType = VaribaleStatusTypeEnum.Bad;
else else
{ {
@ -172,9 +239,9 @@ namespace DriverModbusTCP
{ {
var rawBuffers = new ushort[] { }; var rawBuffers = new ushort[] { };
if (FunCode == 3) if (FunCode == 3)
rawBuffers = master.ReadHoldingRegisters(SlaveId, startAddress, count); rawBuffers = master.ReadHoldingRegisters(SlaveAddress, startAddress, count);
else if (FunCode == 4) else if (FunCode == 4)
rawBuffers = master.ReadHoldingRegisters(SlaveId, startAddress, count); rawBuffers = master.ReadHoldingRegisters(SlaveAddress, startAddress, count);
var retBuffers = ChangeBuffersOrder(rawBuffers, ioarg.ValueType); var retBuffers = ChangeBuffersOrder(rawBuffers, ioarg.ValueType);
if (ioarg.ValueType.ToString().Contains("Uint16")) if (ioarg.ValueType.ToString().Contains("Uint16"))
@ -280,4 +347,16 @@ namespace DriverModbusTCP
S71200 = 3, S71200 = 3,
S71500 = 4, S71500 = 4,
} }
public enum Master_TYPE
{
Tcp = 0,
Udp = 1,
Rtu = 2,
RtuOnTcp = 3,
RtuOnUdp = 4,
Ascii = 5,
AsciiOnTcp = 6,
AsciiOnUdp = 7,
}
} }

View File

@ -0,0 +1,176 @@
namespace Modbus.Data
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Unme.Common;
/// <summary>
/// Object simulation of device memory map.
/// The underlying collections are thread safe when using the ModbusMaster API to read/write values.
/// You can use the SyncRoot property to synchronize direct access to the DataStore collections.
/// </summary>
public class DataStore
{
private readonly object _syncRoot = new object();
/// <summary>
/// Initializes a new instance of the <see cref="DataStore" /> class.
/// </summary>
public DataStore()
{
CoilDiscretes = new ModbusDataCollection<bool> { ModbusDataType = ModbusDataType.Coil };
InputDiscretes = new ModbusDataCollection<bool> { ModbusDataType = ModbusDataType.Input };
HoldingRegisters = new ModbusDataCollection<ushort> { ModbusDataType = ModbusDataType.HoldingRegister };
InputRegisters = new ModbusDataCollection<ushort> { ModbusDataType = ModbusDataType.InputRegister };
}
/// <summary>
/// Initializes a new instance of the <see cref="DataStore"/> class.
/// </summary>
/// <param name="coilDiscretes">List of discrete coil values.</param>
/// <param name="inputDiscretes">List of discrete input values</param>
/// <param name="holdingRegisters">List of holding register values.</param>
/// <param name="inputRegisters">List of input register values.</param>
internal DataStore(
IList<bool> coilDiscretes,
IList<bool> inputDiscretes,
IList<ushort> holdingRegisters,
IList<ushort> inputRegisters)
{
CoilDiscretes = new ModbusDataCollection<bool>(coilDiscretes) { ModbusDataType = ModbusDataType.Coil };
InputDiscretes = new ModbusDataCollection<bool>(inputDiscretes) { ModbusDataType = ModbusDataType.Input };
HoldingRegisters = new ModbusDataCollection<ushort>(holdingRegisters) { ModbusDataType = ModbusDataType.HoldingRegister };
InputRegisters = new ModbusDataCollection<ushort>(inputRegisters) { ModbusDataType = ModbusDataType.InputRegister };
}
/// <summary>
/// Occurs when the DataStore is written to via a Modbus command.
/// </summary>
public event EventHandler<DataStoreEventArgs> DataStoreWrittenTo;
/// <summary>
/// Occurs when the DataStore is read from via a Modbus command.
/// </summary>
public event EventHandler<DataStoreEventArgs> DataStoreReadFrom;
/// <summary>
/// Gets the discrete coils.
/// </summary>
public ModbusDataCollection<bool> CoilDiscretes { get; }
/// <summary>
/// Gets the discrete inputs.
/// </summary>
public ModbusDataCollection<bool> InputDiscretes { get; }
/// <summary>
/// Gets the holding registers.
/// </summary>
public ModbusDataCollection<ushort> HoldingRegisters { get; }
/// <summary>
/// Gets the input registers.
/// </summary>
public ModbusDataCollection<ushort> InputRegisters { get; }
/// <summary>
/// An object that can be used to synchronize direct access to the DataStore collections.
/// </summary>
public object SyncRoot
{
get { return _syncRoot; }
}
/// <summary>
/// Retrieves subset of data from collection.
/// </summary>
/// <typeparam name="T">The collection type.</typeparam>
/// <typeparam name="U">The type of elements in the collection.</typeparam>
internal static T ReadData<T, U>(
DataStore dataStore,
ModbusDataCollection<U> dataSource,
ushort startAddress,
ushort count,
object syncRoot)
where T : Collection<U>, new()
{
DataStoreEventArgs dataStoreEventArgs;
int startIndex = startAddress + 1;
if (startIndex < 0 || dataSource.Count < startIndex + count)
{
throw new InvalidModbusRequestException(Modbus.IllegalDataAddress);
}
U[] dataToRetrieve;
lock (syncRoot)
{
dataToRetrieve = dataSource.Slice(startIndex, count).ToArray();
}
T result = new T();
for (int i = 0; i < count; i++)
{
result.Add(dataToRetrieve[i]);
}
dataStoreEventArgs = DataStoreEventArgs.CreateDataStoreEventArgs(startAddress, dataSource.ModbusDataType, result);
dataStore.DataStoreReadFrom?.Invoke(dataStore, dataStoreEventArgs);
return result;
}
/// <summary>
/// Write data to data store.
/// </summary>
/// <typeparam name="TData">The type of the data.</typeparam>
internal static void WriteData<TData>(
DataStore dataStore,
IEnumerable<TData> items,
ModbusDataCollection<TData> destination,
ushort startAddress,
object syncRoot)
{
DataStoreEventArgs dataStoreEventArgs;
int startIndex = startAddress + 1;
if (startIndex < 0 || destination.Count < startIndex + items.Count())
{
throw new InvalidModbusRequestException(Modbus.IllegalDataAddress);
}
lock (syncRoot)
{
Update(items, destination, startIndex);
}
dataStoreEventArgs = DataStoreEventArgs.CreateDataStoreEventArgs(
startAddress,
destination.ModbusDataType,
items);
dataStore.DataStoreWrittenTo?.Invoke(dataStore, dataStoreEventArgs);
}
/// <summary>
/// Updates subset of values in a collection.
/// </summary>
internal static void Update<T>(IEnumerable<T> items, IList<T> destination, int startIndex)
{
if (startIndex < 0 || destination.Count < startIndex + items.Count())
{
throw new InvalidModbusRequestException(Modbus.IllegalDataAddress);
}
int index = startIndex;
foreach (T item in items)
{
destination[index] = item;
++index;
}
}
}
}

View File

@ -0,0 +1,73 @@
namespace Modbus.Data
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Utility;
/// <summary>
/// Event args for read write actions performed on the DataStore.
/// </summary>
public class DataStoreEventArgs : EventArgs
{
private DataStoreEventArgs(ushort startAddress, ModbusDataType modbusDataType)
{
StartAddress = startAddress;
ModbusDataType = modbusDataType;
}
/// <summary>
/// Type of Modbus data (e.g. Holding register).
/// </summary>
public ModbusDataType ModbusDataType { get; }
/// <summary>
/// Start address of data.
/// </summary>
public ushort StartAddress { get; }
/// <summary>
/// Data that was read or written.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
public DiscriminatedUnion<ReadOnlyCollection<bool>, ReadOnlyCollection<ushort>> Data { get; private set; }
internal static DataStoreEventArgs CreateDataStoreEventArgs<T>(ushort startAddress, ModbusDataType modbusDataType, IEnumerable<T> data)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
DataStoreEventArgs eventArgs;
if (typeof(T) == typeof(bool))
{
var a = new ReadOnlyCollection<bool>(data.Cast<bool>().ToArray());
eventArgs = new DataStoreEventArgs(startAddress, modbusDataType)
{
Data = DiscriminatedUnion<ReadOnlyCollection<bool>, ReadOnlyCollection<ushort>>.CreateA(a)
};
}
else if (typeof(T) == typeof(ushort))
{
var b = new ReadOnlyCollection<ushort>(data.Cast<ushort>().ToArray());
eventArgs = new DataStoreEventArgs(startAddress, modbusDataType)
{
Data = DiscriminatedUnion<ReadOnlyCollection<bool>, ReadOnlyCollection<ushort>>.CreateB(b)
};
}
else
{
throw new ArgumentException("Generic type T should be of type bool or ushort");
}
return eventArgs;
}
}
}

View File

@ -0,0 +1,53 @@
namespace Modbus.Data
{
/// <summary>
/// Data story factory.
/// </summary>
public static class DataStoreFactory
{
/// <summary>
/// Factory method for default data store - register values set to 0 and discrete values set to false.
/// </summary>
public static DataStore CreateDefaultDataStore()
{
return CreateDefaultDataStore(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, ushort.MaxValue);
}
/// <summary>
/// Factory method for default data store - register values set to 0 and discrete values set to false.
/// </summary>
/// <param name="coilsCount">Number of discrete coils.</param>
/// <param name="inputsCount">Number of discrete inputs.</param>
/// <param name="holdingRegistersCount">Number of holding registers.</param>
/// <param name="inputRegistersCount">Number of input registers.</param>
/// <returns>New instance of Data store with defined inputs/outputs.</returns>
public static DataStore CreateDefaultDataStore(ushort coilsCount, ushort inputsCount, ushort holdingRegistersCount, ushort inputRegistersCount)
{
var coils = new bool[coilsCount];
var inputs = new bool[inputsCount];
var holdingRegs = new ushort[holdingRegistersCount];
var inputRegs = new ushort[inputRegistersCount];
return new DataStore(coils, inputs, holdingRegs, inputRegs);
}
/// <summary>
/// Factory method for test data store.
/// </summary>
internal static DataStore CreateTestDataStore()
{
DataStore dataStore = new DataStore();
for (int i = 1; i < 3000; i++)
{
bool value = i % 2 > 0;
dataStore.CoilDiscretes.Add(value);
dataStore.InputDiscretes.Add(!value);
dataStore.HoldingRegisters.Add((ushort)i);
dataStore.InputRegisters.Add((ushort)(i * 10));
}
return dataStore;
}
}
}

View File

@ -0,0 +1,124 @@
namespace Modbus.Data
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
/// <summary>
/// Collection of discrete values.
/// </summary>
public class DiscreteCollection : Collection<bool>, IModbusMessageDataCollection
{
/// <summary>
/// Number of bits per byte.
/// </summary>
private const int BitsPerByte = 8;
private readonly List<bool> _discretes;
/// <summary>
/// Initializes a new instance of the <see cref="DiscreteCollection" /> class.
/// </summary>
public DiscreteCollection()
: this(new List<bool>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DiscreteCollection" /> class.
/// </summary>
/// <param name="bits">Array for discrete collection.</param>
public DiscreteCollection(params bool[] bits)
: this((IList<bool>)bits)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DiscreteCollection" /> class.
/// </summary>
/// <param name="bytes">Array for discrete collection.</param>
public DiscreteCollection(params byte[] bytes)
: this()
{
if (bytes == null)
{
throw new ArgumentNullException(nameof(bytes));
}
_discretes.Capacity = bytes.Length * BitsPerByte;
foreach (byte b in bytes)
{
_discretes.Add((b & 1) == 1);
_discretes.Add((b & 2) == 2);
_discretes.Add((b & 4) == 4);
_discretes.Add((b & 8) == 8);
_discretes.Add((b & 16) == 16);
_discretes.Add((b & 32) == 32);
_discretes.Add((b & 64) == 64);
_discretes.Add((b & 128) == 128);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="DiscreteCollection" /> class.
/// </summary>
/// <param name="bits">List for discrete collection.</param>
public DiscreteCollection(IList<bool> bits)
: this(new List<bool>(bits))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DiscreteCollection" /> class.
/// </summary>
/// <param name="bits">List for discrete collection.</param>
internal DiscreteCollection(List<bool> bits)
: base(bits)
{
Debug.Assert(bits != null, "Discrete bits is null.");
_discretes = bits;
}
/// <summary>
/// Gets the network bytes.
/// </summary>
public byte[] NetworkBytes
{
get
{
byte[] bytes = new byte[ByteCount];
for (int index = 0; index < _discretes.Count; index++)
{
if (_discretes[index])
{
bytes[index / BitsPerByte] |= (byte)(1 << (index % BitsPerByte));
}
}
return bytes;
}
}
/// <summary>
/// Gets the byte count.
/// </summary>
public byte ByteCount
{
get { return (byte)((Count + 7) / 8); }
}
/// <summary>
/// Returns a <see cref="T:System.String" /> that represents the current <see cref="T:System.Object" />.
/// </summary>
/// <returns>
/// A <see cref="T:System.String" /> that represents the current <see cref="T:System.Object" />.
/// </returns>
public override string ToString()
{
return string.Concat("{", string.Join(", ", this.Select(discrete => discrete ? "1" : "0").ToArray()), "}");
}
}
}

View File

@ -0,0 +1,22 @@
namespace Modbus.Data
{
using System.Diagnostics.CodeAnalysis;
/// <summary>
/// Modbus message containing data.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
public interface IModbusMessageDataCollection
{
/// <summary>
/// Gets the network bytes.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
byte[] NetworkBytes { get; }
/// <summary>
/// Gets the byte count.
/// </summary>
byte ByteCount { get; }
}
}

View File

@ -0,0 +1,128 @@
namespace Modbus.Data
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
/// <summary>
/// A 1 origin collection represetative of the Modbus Data Model.
/// </summary>
public class ModbusDataCollection<TData> : Collection<TData>
{
private bool _allowZeroElement = true;
/// <summary>
/// Initializes a new instance of the <see cref="ModbusDataCollection&lt;TData&gt;" /> class.
/// </summary>
public ModbusDataCollection()
{
AddDefault(this);
_allowZeroElement = false;
}
/// <summary>
/// Initializes a new instance of the <see cref="ModbusDataCollection&lt;TData&gt;" /> class.
/// </summary>
/// <param name="data">The data.</param>
public ModbusDataCollection(params TData[] data)
: this((IList<TData>)data)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ModbusDataCollection&lt;TData&gt;" /> class.
/// </summary>
/// <param name="data">The data.</param>
public ModbusDataCollection(IList<TData> data)
: base(AddDefault(data.IsReadOnly ? new List<TData>(data) : data))
{
_allowZeroElement = false;
}
internal ModbusDataType ModbusDataType { get; set; }
/// <summary>
/// Inserts an element into the <see cref="T:System.Collections.ObjectModel.Collection`1"></see> at the specified
/// index.
/// </summary>
/// <param name="index">The zero-based index at which item should be inserted.</param>
/// <param name="item">The object to insert. The value can be null for reference types.</param>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// index is less than zero.-or-index is greater than
/// <see cref="P:System.Collections.ObjectModel.Collection`1.Count"></see>.
/// </exception>
protected override void InsertItem(int index, TData item)
{
if (!_allowZeroElement && index == 0)
{
throw new ArgumentOutOfRangeException(
nameof(index),
"0 is not a valid address for a Modbus data collection.");
}
base.InsertItem(index, item);
}
/// <summary>
/// Replaces the element at the specified index.
/// </summary>
/// <param name="index">The zero-based index of the element to replace.</param>
/// <param name="item">The new value for the element at the specified index. The value can be null for reference types.</param>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// index is less than zero.-or-index is greater than
/// <see cref="P:System.Collections.ObjectModel.Collection`1.Count"></see>.
/// </exception>
protected override void SetItem(int index, TData item)
{
if (index == 0)
{
throw new ArgumentOutOfRangeException(
nameof(index),
"0 is not a valid address for a Modbus data collection.");
}
base.SetItem(index, item);
}
/// <summary>
/// Removes the element at the specified index of the <see cref="T:System.Collections.ObjectModel.Collection`1"></see>.
/// </summary>
/// <param name="index">The zero-based index of the element to remove.</param>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// index is less than zero.-or-index is equal to or greater than
/// <see cref="P:System.Collections.ObjectModel.Collection`1.Count"></see>.
/// </exception>
protected override void RemoveItem(int index)
{
if (index == 0)
{
throw new ArgumentOutOfRangeException(
nameof(index),
"0 is not a valid address for a Modbus data collection.");
}
base.RemoveItem(index);
}
/// <summary>
/// Removes all elements from the <see cref="T:System.Collections.ObjectModel.Collection`1"></see>.
/// </summary>
protected override void ClearItems()
{
_allowZeroElement = true;
base.ClearItems();
AddDefault(this);
_allowZeroElement = false;
}
/// <summary>
/// Adds a default element to the collection.
/// </summary>
/// <param name="data">The data.</param>
private static IList<TData> AddDefault(IList<TData> data)
{
data.Insert(0, default(TData));
return data;
}
}
}

View File

@ -0,0 +1,28 @@
namespace Modbus.Data
{
/// <summary>
/// Types of data supported by the Modbus protocol.
/// </summary>
public enum ModbusDataType
{
/// <summary>
/// Read/write register.
/// </summary>
HoldingRegister,
/// <summary>
/// Readonly register.
/// </summary>
InputRegister,
/// <summary>
/// Read/write discrete.
/// </summary>
Coil,
/// <summary>
/// Readonly discrete.
/// </summary>
Input
}
}

View File

@ -0,0 +1,86 @@
namespace Modbus.Data
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net;
using Utility;
/// <summary>
/// Collection of 16 bit registers.
/// </summary>
public class RegisterCollection : Collection<ushort>, IModbusMessageDataCollection
{
/// <summary>
/// Initializes a new instance of the <see cref="RegisterCollection" /> class.
/// </summary>
public RegisterCollection()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RegisterCollection" /> class.
/// </summary>
/// <param name="bytes">Array for register collection.</param>
public RegisterCollection(byte[] bytes)
: this((IList<ushort>)ModbusUtility.NetworkBytesToHostUInt16(bytes))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RegisterCollection" /> class.
/// </summary>
/// <param name="registers">Array for register collection.</param>
public RegisterCollection(params ushort[] registers)
: this((IList<ushort>)registers)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RegisterCollection" /> class.
/// </summary>
/// <param name="registers">List for register collection.</param>
public RegisterCollection(IList<ushort> registers)
: base(registers.IsReadOnly ? new List<ushort>(registers) : registers)
{
}
public byte[] NetworkBytes
{
get
{
var bytes = new MemoryStream(ByteCount);
foreach (ushort register in this)
{
var b = BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((short)register));
bytes.Write(b, 0, b.Length);
}
return bytes.ToArray();
}
}
/// <summary>
/// Gets the byte count.
/// </summary>
public byte ByteCount
{
get { return (byte)(Count * 2); }
}
/// <summary>
/// Returns a <see cref="T:System.String" /> that represents the current <see cref="T:System.Object" />.
/// </summary>
/// <returns>
/// A <see cref="T:System.String" /> that represents the current <see cref="T:System.Object" />.
/// </returns>
public override string ToString()
{
return string.Concat("{", string.Join(", ", this.Select(v => v.ToString()).ToArray()), "}");
}
}
}

View File

@ -0,0 +1,191 @@
namespace Modbus.Device
{
using System;
using System.Threading.Tasks;
using IO;
/// <summary>
/// Modbus master device.
/// </summary>
public interface IModbusMaster : IDisposable
{
/// <summary>
/// Transport used by this master.
/// </summary>
ModbusTransport Transport { get; }
/// <summary>
/// Reads from 1 to 2000 contiguous coils status.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of coils to read.</param>
/// <returns>Coils status.</returns>
bool[] ReadCoils(byte slaveAddress, ushort startAddress, ushort numberOfPoints);
/// <summary>
/// Asynchronously reads from 1 to 2000 contiguous coils status.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of coils to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
Task<bool[]> ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints);
/// <summary>
/// Reads from 1 to 2000 contiguous discrete input status.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of discrete inputs to read.</param>
/// <returns>Discrete inputs status.</returns>
bool[] ReadInputs(byte slaveAddress, ushort startAddress, ushort numberOfPoints);
/// <summary>
/// Asynchronously reads from 1 to 2000 contiguous discrete input status.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of discrete inputs to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
Task<bool[]> ReadInputsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints);
/// <summary>
/// Reads contiguous block of holding registers.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>Holding registers status.</returns>
ushort[] ReadHoldingRegisters(byte slaveAddress, ushort startAddress, ushort numberOfPoints);
/// <summary>
/// Asynchronously reads contiguous block of holding registers.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
Task<ushort[]> ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints);
/// <summary>
/// Reads contiguous block of input registers.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>Input registers status.</returns>
ushort[] ReadInputRegisters(byte slaveAddress, ushort startAddress, ushort numberOfPoints);
/// <summary>
/// Asynchronously reads contiguous block of input registers.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
Task<ushort[]> ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints);
/// <summary>
/// Writes a single coil value.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="coilAddress">Address to write value to.</param>
/// <param name="value">Value to write.</param>
void WriteSingleCoil(byte slaveAddress, ushort coilAddress, bool value);
/// <summary>
/// Asynchronously writes a single coil value.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="coilAddress">Address to write value to.</param>
/// <param name="value">Value to write.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value);
/// <summary>
/// Writes a single holding register.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="registerAddress">Address to write.</param>
/// <param name="value">Value to write.</param>
void WriteSingleRegister(byte slaveAddress, ushort registerAddress, ushort value);
/// <summary>
/// Asynchronously writes a single holding register.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="registerAddress">Address to write.</param>
/// <param name="value">Value to write.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value);
/// <summary>
/// Writes a block of 1 to 123 contiguous registers.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
void WriteMultipleRegisters(byte slaveAddress, ushort startAddress, ushort[] data);
/// <summary>
/// Asynchronously writes a block of 1 to 123 contiguous registers.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] data);
/// <summary>
/// Writes a sequence of coils.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
void WriteMultipleCoils(byte slaveAddress, ushort startAddress, bool[] data);
/// <summary>
/// Asynchronously writes a sequence of coils.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
Task WriteMultipleCoilsAsync(byte slaveAddress, ushort startAddress, bool[] data);
/// <summary>
/// Performs a combination of one read operation and one write operation in a single Modbus transaction.
/// The write operation is performed before the read.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startReadAddress">Address to begin reading (Holding registers are addressed starting at 0).</param>
/// <param name="numberOfPointsToRead">Number of registers to read.</param>
/// <param name="startWriteAddress">Address to begin writing (Holding registers are addressed starting at 0).</param>
/// <param name="writeData">Register values to write.</param>
ushort[] ReadWriteMultipleRegisters(
byte slaveAddress,
ushort startReadAddress,
ushort numberOfPointsToRead,
ushort startWriteAddress,
ushort[] writeData);
/// <summary>
/// Asynchronously performs a combination of one read operation and one write operation in a single Modbus transaction.
/// The write operation is performed before the read.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startReadAddress">Address to begin reading (Holding registers are addressed starting at 0).</param>
/// <param name="numberOfPointsToRead">Number of registers to read.</param>
/// <param name="startWriteAddress">Address to begin writing (Holding registers are addressed starting at 0).</param>
/// <param name="writeData">Register values to write.</param>
/// <returns>A task that represents the asynchronous operation</returns>
Task<ushort[]> ReadWriteMultipleRegistersAsync(
byte slaveAddress,
ushort startReadAddress,
ushort numberOfPointsToRead,
ushort startWriteAddress,
ushort[] writeData);
}
}

View File

@ -0,0 +1,26 @@
namespace Modbus.Device
{
using IO;
/// <summary>
/// Modbus Serial Master device.
/// </summary>
public interface IModbusSerialMaster : IModbusMaster
{
/// <summary>
/// Transport for used by this master.
/// </summary>
new ModbusSerialTransport Transport { get; }
/// <summary>
/// Serial Line only.
/// Diagnostic function which loops back the original data.
/// NModbus only supports looping back one ushort value, this is a
/// limitation of the "Best Effort" implementation of the RTU protocol.
/// </summary>
/// <param name="slaveAddress">Address of device to test.</param>
/// <param name="data">Data to return.</param>
/// <returns>Return true if slave device echoed data.</returns>
bool ReturnQueryData(byte slaveAddress, ushort data);
}
}

View File

@ -0,0 +1,53 @@
namespace Modbus.Device
{
using System;
using IO;
using Unme.Common;
/// <summary>
/// Modbus device.
/// </summary>
public abstract class ModbusDevice : IDisposable
{
private ModbusTransport _transport;
internal ModbusDevice(ModbusTransport transport)
{
_transport = transport;
}
/// <summary>
/// Gets the Modbus Transport.
/// </summary>
public ModbusTransport Transport
{
get { return _transport; }
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing">
/// <c>true</c> to release both managed and unmanaged resources;
/// <c>false</c> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
DisposableUtility.Dispose(ref _transport);
}
}
}
}

View File

@ -0,0 +1,313 @@
namespace Modbus.Device
{
using System;
using System.Diagnostics.CodeAnalysis;
#if SERIAL
using System.IO.Ports;
#endif
using System.Net.Sockets;
using System.Threading.Tasks;
using IO;
/// <summary>
/// Modbus IP master device.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Breaking change.")]
public class ModbusIpMaster : ModbusMaster
{
/// <summary>
/// Modbus IP master device.
/// </summary>
/// <param name="transport">Transport used by this master.</param>
private ModbusIpMaster(ModbusTransport transport)
: base(transport)
{
}
/// <summary>
/// Modbus IP master factory method.
/// </summary>
/// <returns>New instance of Modbus IP master device using provided TCP client.</returns>
[SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Breaking change.")]
public static ModbusIpMaster CreateIp(TcpClient tcpClient)
{
if (tcpClient == null)
{
throw new ArgumentNullException(nameof(tcpClient));
}
return CreateIp(new TcpClientAdapter(tcpClient));
}
/// <summary>
/// Modbus IP master factory method.
/// </summary>
/// <returns>New instance of Modbus IP master device using provided UDP client.</returns>
[SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Breaking change.")]
public static ModbusIpMaster CreateIp(UdpClient udpClient)
{
if (udpClient == null)
{
throw new ArgumentNullException(nameof(udpClient));
}
if (!udpClient.Client.Connected)
{
throw new InvalidOperationException(Resources.UdpClientNotConnected);
}
return CreateIp(new UdpClientAdapter(udpClient));
}
#if SERIAL
/// <summary>
/// Modbus IP master factory method.
/// </summary>
/// <returns>New instance of Modbus IP master device using provided serial port.</returns>
[SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Breaking change.")]
public static ModbusIpMaster CreateIp(SerialPort serialPort)
{
if (serialPort == null)
{
throw new ArgumentNullException(nameof(serialPort));
}
return CreateIp(new SerialPortAdapter(serialPort));
}
#endif
/// <summary>
/// Modbus IP master factory method.
/// </summary>
/// <returns>New instance of Modbus IP master device using provided stream resource.</returns>
[SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Breaking change.")]
public static ModbusIpMaster CreateIp(IStreamResource streamResource)
{
if (streamResource == null)
{
throw new ArgumentNullException(nameof(streamResource));
}
return new ModbusIpMaster(new ModbusIpTransport(streamResource));
}
/// <summary>
/// Reads from 1 to 2000 contiguous coils status.
/// </summary>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of coils to read.</param>
/// <returns>Coils status.</returns>
public bool[] ReadCoils(ushort startAddress, ushort numberOfPoints)
{
return base.ReadCoils(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints);
}
/// <summary>
/// Asynchronously reads from 1 to 2000 contiguous coils status.
/// </summary>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of coils to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
public Task<bool[]> ReadCoilsAsync(ushort startAddress, ushort numberOfPoints)
{
return base.ReadCoilsAsync(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints);
}
/// <summary>
/// Reads from 1 to 2000 contiguous discrete input status.
/// </summary>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of discrete inputs to read.</param>
/// <returns>Discrete inputs status.</returns>
public bool[] ReadInputs(ushort startAddress, ushort numberOfPoints)
{
return base.ReadInputs(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints);
}
/// <summary>
/// Asynchronously reads from 1 to 2000 contiguous discrete input status.
/// </summary>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of discrete inputs to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
public Task<bool[]> ReadInputsAsync(ushort startAddress, ushort numberOfPoints)
{
return base.ReadInputsAsync(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints);
}
/// <summary>
/// Reads contiguous block of holding registers.
/// </summary>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>Holding registers status.</returns>
public ushort[] ReadHoldingRegisters(ushort startAddress, ushort numberOfPoints)
{
return base.ReadHoldingRegisters(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints);
}
/// <summary>
/// Asynchronously reads contiguous block of holding registers.
/// </summary>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
public Task<ushort[]> ReadHoldingRegistersAsync(ushort startAddress, ushort numberOfPoints)
{
return base.ReadHoldingRegistersAsync(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints);
}
/// <summary>
/// Reads contiguous block of input registers.
/// </summary>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>Input registers status.</returns>
public ushort[] ReadInputRegisters(ushort startAddress, ushort numberOfPoints)
{
return base.ReadInputRegisters(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints);
}
/// <summary>
/// Asynchronously reads contiguous block of input registers.
/// </summary>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
public Task<ushort[]> ReadInputRegistersAsync(ushort startAddress, ushort numberOfPoints)
{
return base.ReadInputRegistersAsync(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints);
}
/// <summary>
/// Writes a single coil value.
/// </summary>
/// <param name="coilAddress">Address to write value to.</param>
/// <param name="value">Value to write.</param>
public void WriteSingleCoil(ushort coilAddress, bool value)
{
base.WriteSingleCoil(Modbus.DefaultIpSlaveUnitId, coilAddress, value);
}
/// <summary>
/// Asynchronously writes a single coil value.
/// </summary>
/// <param name="coilAddress">Address to write value to.</param>
/// <param name="value">Value to write.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public Task WriteSingleCoilAsync(ushort coilAddress, bool value)
{
return base.WriteSingleCoilAsync(Modbus.DefaultIpSlaveUnitId, coilAddress, value);
}
/// <summary>
/// Write a single holding register.
/// </summary>
/// <param name="registerAddress">Address to write.</param>
/// <param name="value">Value to write.</param>
public void WriteSingleRegister(ushort registerAddress, ushort value)
{
base.WriteSingleRegister(Modbus.DefaultIpSlaveUnitId, registerAddress, value);
}
/// <summary>
/// Asynchronously writes a single holding register.
/// </summary>
/// <param name="registerAddress">Address to write.</param>
/// <param name="value">Value to write.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public Task WriteSingleRegisterAsync(ushort registerAddress, ushort value)
{
return base.WriteSingleRegisterAsync(Modbus.DefaultIpSlaveUnitId, registerAddress, value);
}
/// <summary>
/// Write a block of 1 to 123 contiguous registers.
/// </summary>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
public void WriteMultipleRegisters(ushort startAddress, ushort[] data)
{
base.WriteMultipleRegisters(Modbus.DefaultIpSlaveUnitId, startAddress, data);
}
/// <summary>
/// Asynchronously writes a block of 1 to 123 contiguous registers.
/// </summary>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public Task WriteMultipleRegistersAsync(ushort startAddress, ushort[] data)
{
return base.WriteMultipleRegistersAsync(Modbus.DefaultIpSlaveUnitId, startAddress, data);
}
/// <summary>
/// Force each coil in a sequence of coils to a provided value.
/// </summary>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
public void WriteMultipleCoils(ushort startAddress, bool[] data)
{
base.WriteMultipleCoils(Modbus.DefaultIpSlaveUnitId, startAddress, data);
}
/// <summary>
/// Asynchronously writes a sequence of coils.
/// </summary>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
/// <returns>A task that represents the asynchronous write operation</returns>
public Task WriteMultipleCoilsAsync(ushort startAddress, bool[] data)
{
return base.WriteMultipleCoilsAsync(Modbus.DefaultIpSlaveUnitId, startAddress, data);
}
/// <summary>
/// Performs a combination of one read operation and one write operation in a single MODBUS transaction.
/// The write operation is performed before the read.
/// Message uses default TCP slave id of 0.
/// </summary>
/// <param name="startReadAddress">Address to begin reading (Holding registers are addressed starting at 0).</param>
/// <param name="numberOfPointsToRead">Number of registers to read.</param>
/// <param name="startWriteAddress">Address to begin writing (Holding registers are addressed starting at 0).</param>
/// <param name="writeData">Register values to write.</param>
public ushort[] ReadWriteMultipleRegisters(
ushort startReadAddress,
ushort numberOfPointsToRead,
ushort startWriteAddress,
ushort[] writeData)
{
return base.ReadWriteMultipleRegisters(
Modbus.DefaultIpSlaveUnitId,
startReadAddress,
numberOfPointsToRead,
startWriteAddress,
writeData);
}
/// <summary>
/// Asynchronously performs a combination of one read operation and one write operation in a single Modbus transaction.
/// The write operation is performed before the read.
/// </summary>
/// <param name="startReadAddress">Address to begin reading (Holding registers are addressed starting at 0).</param>
/// <param name="numberOfPointsToRead">Number of registers to read.</param>
/// <param name="startWriteAddress">Address to begin writing (Holding registers are addressed starting at 0).</param>
/// <param name="writeData">Register values to write.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task<ushort[]> ReadWriteMultipleRegistersAsync(
ushort startReadAddress,
ushort numberOfPointsToRead,
ushort startWriteAddress,
ushort[] writeData)
{
return base.ReadWriteMultipleRegistersAsync(
Modbus.DefaultIpSlaveUnitId,
startReadAddress,
numberOfPointsToRead,
startWriteAddress,
writeData);
}
}
}

View File

@ -0,0 +1,452 @@
namespace Modbus.Device
{
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Data;
using IO;
using Message;
/// <summary>
/// Modbus master device.
/// </summary>
public abstract class ModbusMaster : ModbusDevice, IModbusMaster
{
internal ModbusMaster(ModbusTransport transport)
: base(transport)
{
}
/// <summary>
/// Reads from 1 to 2000 contiguous coils status.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of coils to read.</param>
/// <returns>Coils status.</returns>
public bool[] ReadCoils(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
{
ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 2000);
var request = new ReadCoilsInputsRequest(
Modbus.ReadCoils,
slaveAddress,
startAddress,
numberOfPoints);
return PerformReadDiscretes(request);
}
/// <summary>
/// Asynchronously reads from 1 to 2000 contiguous coils status.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of coils to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
public Task<bool[]> ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
{
ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 2000);
var request = new ReadCoilsInputsRequest(
Modbus.ReadCoils,
slaveAddress,
startAddress,
numberOfPoints);
return PerformReadDiscretesAsync(request);
}
/// <summary>
/// Reads from 1 to 2000 contiguous discrete input status.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of discrete inputs to read.</param>
/// <returns>Discrete inputs status.</returns>
public bool[] ReadInputs(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
{
ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 2000);
var request = new ReadCoilsInputsRequest(
Modbus.ReadInputs,
slaveAddress,
startAddress,
numberOfPoints);
return PerformReadDiscretes(request);
}
/// <summary>
/// Asynchronously reads from 1 to 2000 contiguous discrete input status.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of discrete inputs to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
public Task<bool[]> ReadInputsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
{
ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 2000);
var request = new ReadCoilsInputsRequest(
Modbus.ReadInputs,
slaveAddress,
startAddress,
numberOfPoints);
return PerformReadDiscretesAsync(request);
}
/// <summary>
/// Reads contiguous block of holding registers.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>Holding registers status.</returns>
public ushort[] ReadHoldingRegisters(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
{
ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 125);
var request = new ReadHoldingInputRegistersRequest(
Modbus.ReadHoldingRegisters,
slaveAddress,
startAddress,
numberOfPoints);
return PerformReadRegisters(request);
}
/// <summary>
/// Asynchronously reads contiguous block of holding registers.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
public Task<ushort[]> ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
{
ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 125);
var request = new ReadHoldingInputRegistersRequest(
Modbus.ReadHoldingRegisters,
slaveAddress,
startAddress,
numberOfPoints);
return PerformReadRegistersAsync(request);
}
/// <summary>
/// Reads contiguous block of input registers.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>Input registers status.</returns>
public ushort[] ReadInputRegisters(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
{
ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 125);
var request = new ReadHoldingInputRegistersRequest(
Modbus.ReadInputRegisters,
slaveAddress,
startAddress,
numberOfPoints);
return PerformReadRegisters(request);
}
/// <summary>
/// Asynchronously reads contiguous block of input registers.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
public Task<ushort[]> ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
{
ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 125);
var request = new ReadHoldingInputRegistersRequest(
Modbus.ReadInputRegisters,
slaveAddress,
startAddress,
numberOfPoints);
return PerformReadRegistersAsync(request);
}
/// <summary>
/// Writes a single coil value.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="coilAddress">Address to write value to.</param>
/// <param name="value">Value to write.</param>
public void WriteSingleCoil(byte slaveAddress, ushort coilAddress, bool value)
{
var request = new WriteSingleCoilRequestResponse(slaveAddress, coilAddress, value);
Transport.UnicastMessage<WriteSingleCoilRequestResponse>(request);
}
/// <summary>
/// Asynchronously writes a single coil value.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="coilAddress">Address to write value to.</param>
/// <param name="value">Value to write.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value)
{
var request = new WriteSingleCoilRequestResponse(slaveAddress, coilAddress, value);
return PerformWriteRequestAsync<WriteSingleCoilRequestResponse>(request);
}
/// <summary>
/// Writes a single holding register.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="registerAddress">Address to write.</param>
/// <param name="value">Value to write.</param>
public void WriteSingleRegister(byte slaveAddress, ushort registerAddress, ushort value)
{
var request = new WriteSingleRegisterRequestResponse(
slaveAddress,
registerAddress,
value);
Transport.UnicastMessage<WriteSingleRegisterRequestResponse>(request);
}
/// <summary>
/// Asynchronously writes a single holding register.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="registerAddress">Address to write.</param>
/// <param name="value">Value to write.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value)
{
var request = new WriteSingleRegisterRequestResponse(
slaveAddress,
registerAddress,
value);
return PerformWriteRequestAsync<WriteSingleRegisterRequestResponse>(request);
}
/// <summary>
/// Write a block of 1 to 123 contiguous 16 bit holding registers.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
public void WriteMultipleRegisters(byte slaveAddress, ushort startAddress, ushort[] data)
{
ValidateData("data", data, 123);
var request = new WriteMultipleRegistersRequest(
slaveAddress,
startAddress,
new RegisterCollection(data));
Transport.UnicastMessage<WriteMultipleRegistersResponse>(request);
}
/// <summary>
/// Asynchronously writes a block of 1 to 123 contiguous registers.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] data)
{
ValidateData("data", data, 123);
var request = new WriteMultipleRegistersRequest(
slaveAddress,
startAddress,
new RegisterCollection(data));
return PerformWriteRequestAsync<WriteMultipleRegistersResponse>(request);
}
/// <summary>
/// Writes a sequence of coils.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
public void WriteMultipleCoils(byte slaveAddress, ushort startAddress, bool[] data)
{
ValidateData("data", data, 1968);
var request = new WriteMultipleCoilsRequest(
slaveAddress,
startAddress,
new DiscreteCollection(data));
Transport.UnicastMessage<WriteMultipleCoilsResponse>(request);
}
/// <summary>
/// Asynchronously writes a sequence of coils.
/// </summary>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public Task WriteMultipleCoilsAsync(byte slaveAddress, ushort startAddress, bool[] data)
{
ValidateData("data", data, 1968);
var request = new WriteMultipleCoilsRequest(
slaveAddress,
startAddress,
new DiscreteCollection(data));
return PerformWriteRequestAsync<WriteMultipleCoilsResponse>(request);
}
/// <summary>
/// Performs a combination of one read operation and one write operation in a single Modbus transaction.
/// The write operation is performed before the read.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startReadAddress">Address to begin reading (Holding registers are addressed starting at 0).</param>
/// <param name="numberOfPointsToRead">Number of registers to read.</param>
/// <param name="startWriteAddress">Address to begin writing (Holding registers are addressed starting at 0).</param>
/// <param name="writeData">Register values to write.</param>
public ushort[] ReadWriteMultipleRegisters(
byte slaveAddress,
ushort startReadAddress,
ushort numberOfPointsToRead,
ushort startWriteAddress,
ushort[] writeData)
{
ValidateNumberOfPoints("numberOfPointsToRead", numberOfPointsToRead, 125);
ValidateData("writeData", writeData, 121);
var request = new ReadWriteMultipleRegistersRequest(
slaveAddress,
startReadAddress,
numberOfPointsToRead,
startWriteAddress,
new RegisterCollection(writeData));
return PerformReadRegisters(request);
}
/// <summary>
/// Asynchronously performs a combination of one read operation and one write operation in a single Modbus transaction.
/// The write operation is performed before the read.
/// </summary>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startReadAddress">Address to begin reading (Holding registers are addressed starting at 0).</param>
/// <param name="numberOfPointsToRead">Number of registers to read.</param>
/// <param name="startWriteAddress">Address to begin writing (Holding registers are addressed starting at 0).</param>
/// <param name="writeData">Register values to write.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task<ushort[]> ReadWriteMultipleRegistersAsync(
byte slaveAddress,
ushort startReadAddress,
ushort numberOfPointsToRead,
ushort startWriteAddress,
ushort[] writeData)
{
ValidateNumberOfPoints("numberOfPointsToRead", numberOfPointsToRead, 125);
ValidateData("writeData", writeData, 121);
var request = new ReadWriteMultipleRegistersRequest(
slaveAddress,
startReadAddress,
numberOfPointsToRead,
startWriteAddress,
new RegisterCollection(writeData));
return PerformReadRegistersAsync(request);
}
/// <summary>
/// Executes the custom message.
/// </summary>
/// <typeparam name="TResponse">The type of the response.</typeparam>
/// <param name="request">The request.</param>
[SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
[SuppressMessage("Microsoft.Usage", "CA2223:MembersShouldDifferByMoreThanReturnType")]
public TResponse ExecuteCustomMessage<TResponse>(IModbusMessage request)
where TResponse : IModbusMessage, new()
{
return Transport.UnicastMessage<TResponse>(request);
}
private static void ValidateData<T>(string argumentName, T[] data, int maxDataLength)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
if (data.Length == 0 || data.Length > maxDataLength)
{
string msg = $"The length of argument {argumentName} must be between 1 and {maxDataLength} inclusive.";
throw new ArgumentException(msg);
}
}
private static void ValidateNumberOfPoints(string argumentName, ushort numberOfPoints, ushort maxNumberOfPoints)
{
if (numberOfPoints < 1 || numberOfPoints > maxNumberOfPoints)
{
string msg = $"Argument {argumentName} must be between 1 and {maxNumberOfPoints} inclusive.";
throw new ArgumentException(msg);
}
}
private bool[] PerformReadDiscretes(ReadCoilsInputsRequest request)
{
ReadCoilsInputsResponse response = Transport.UnicastMessage<ReadCoilsInputsResponse>(request);
return response.Data.Take(request.NumberOfPoints).ToArray();
}
private Task<bool[]> PerformReadDiscretesAsync(ReadCoilsInputsRequest request)
{
return Task.Factory.StartNew(() => PerformReadDiscretes(request));
}
private ushort[] PerformReadRegisters(ReadHoldingInputRegistersRequest request)
{
ReadHoldingInputRegistersResponse response =
Transport.UnicastMessage<ReadHoldingInputRegistersResponse>(request);
return response.Data.Take(request.NumberOfPoints).ToArray();
}
private Task<ushort[]> PerformReadRegistersAsync(ReadHoldingInputRegistersRequest request)
{
return Task.Factory.StartNew(() => PerformReadRegisters(request));
}
private ushort[] PerformReadRegisters(ReadWriteMultipleRegistersRequest request)
{
ReadHoldingInputRegistersResponse response =
Transport.UnicastMessage<ReadHoldingInputRegistersResponse>(request);
return response.Data.Take(request.ReadRequest.NumberOfPoints).ToArray();
}
private Task<ushort[]> PerformReadRegistersAsync(ReadWriteMultipleRegistersRequest request)
{
return Task.Factory.StartNew(() => PerformReadRegisters(request));
}
private Task PerformWriteRequestAsync<T>(IModbusMessage request)
where T : IModbusMessage, new()
{
return Task.Factory.StartNew(() => Transport.UnicastMessage<T>(request));
}
}
}

View File

@ -0,0 +1,122 @@
namespace Modbus.Device
{
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using IO;
using Message;
/// <summary>
/// Represents an incoming connection from a Modbus master. Contains the slave's logic to process the connection.
/// </summary>
internal class ModbusMasterTcpConnection : ModbusDevice, IDisposable
{
private readonly TcpClient _client;
private readonly string _endPoint;
private readonly Stream _stream;
private readonly ModbusTcpSlave _slave;
private readonly Task _requestHandlerTask;
private readonly byte[] _mbapHeader = new byte[6];
private byte[] _messageFrame;
public ModbusMasterTcpConnection(TcpClient client, ModbusTcpSlave slave)
: base(new ModbusIpTransport(new TcpClientAdapter(client)))
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}
if (slave == null)
{
throw new ArgumentNullException(nameof(slave));
}
_client = client;
_endPoint = client.Client.RemoteEndPoint.ToString();
_stream = client.GetStream();
_slave = slave;
_requestHandlerTask = Task.Run((Func<Task>)HandleRequestAsync);
}
/// <summary>
/// Occurs when a Modbus master TCP connection is closed.
/// </summary>
public event EventHandler<TcpConnectionEventArgs> ModbusMasterTcpConnectionClosed;
public string EndPoint
{
get { return _endPoint; }
}
public Stream Stream
{
get { return _stream; }
}
public TcpClient TcpClient
{
get { return _client; }
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_stream.Dispose();
}
base.Dispose(disposing);
}
private async Task HandleRequestAsync()
{
while (true)
{
Debug.WriteLine($"Begin reading header from Master at IP: {EndPoint}");
int readBytes = await Stream.ReadAsync(_mbapHeader, 0, 6).ConfigureAwait(false);
if (readBytes == 0)
{
Debug.WriteLine($"0 bytes read, Master at {EndPoint} has closed Socket connection.");
ModbusMasterTcpConnectionClosed?.Invoke(this, new TcpConnectionEventArgs(EndPoint));
return;
}
ushort frameLength = (ushort)IPAddress.HostToNetworkOrder(BitConverter.ToInt16(_mbapHeader, 4));
Debug.WriteLine($"Master at {EndPoint} sent header: \"{string.Join(", ", _mbapHeader)}\" with {frameLength} bytes in PDU");
_messageFrame = new byte[frameLength];
readBytes = await Stream.ReadAsync(_messageFrame, 0, frameLength).ConfigureAwait(false);
if (readBytes == 0)
{
Debug.WriteLine($"0 bytes read, Master at {EndPoint} has closed Socket connection.");
ModbusMasterTcpConnectionClosed?.Invoke(this, new TcpConnectionEventArgs(EndPoint));
return;
}
Debug.WriteLine($"Read frame from Master at {EndPoint} completed {readBytes} bytes");
byte[] frame = _mbapHeader.Concat(_messageFrame).ToArray();
Debug.WriteLine($"RX from Master at {EndPoint}: {string.Join(", ", frame)}");
var request = ModbusMessageFactory.CreateModbusRequest(_messageFrame);
request.TransactionId = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 0));
// perform action and build response
IModbusMessage response = _slave.ApplyRequest(request);
response.TransactionId = request.TransactionId;
// write response
byte[] responseFrame = Transport.BuildMessageFrame(response);
Debug.WriteLine($"TX to Master at {EndPoint}: {string.Join(", ", responseFrame)}");
await Stream.WriteAsync(responseFrame, 0, responseFrame.Length).ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,175 @@
namespace Modbus.Device
{
using System;
using System.Diagnostics.CodeAnalysis;
#if SERIAL
using System.IO.Ports;
#endif
using System.Net.Sockets;
using Data;
using IO;
using Message;
/// <summary>
/// Modbus serial master device.
/// </summary>
public class ModbusSerialMaster : ModbusMaster, IModbusSerialMaster
{
private ModbusSerialMaster(ModbusTransport transport)
: base(transport)
{
}
/// <summary>
/// Gets the Modbus Transport.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
ModbusSerialTransport IModbusSerialMaster.Transport
{
get { return (ModbusSerialTransport)Transport; }
}
#if SERIAL
/// <summary>
/// Modbus ASCII master factory method.
/// </summary>
public static ModbusSerialMaster CreateAscii(SerialPort serialPort)
{
if (serialPort == null)
{
throw new ArgumentNullException(nameof(serialPort));
}
return CreateAscii(new SerialPortAdapter(serialPort));
}
#endif
/// <summary>
/// Modbus ASCII master factory method.
/// </summary>
public static ModbusSerialMaster CreateAscii(TcpClient tcpClient)
{
if (tcpClient == null)
{
throw new ArgumentNullException(nameof(tcpClient));
}
return CreateAscii(new TcpClientAdapter(tcpClient));
}
/// <summary>
/// Modbus ASCII master factory method.
/// </summary>
public static ModbusSerialMaster CreateAscii(UdpClient udpClient)
{
if (udpClient == null)
{
throw new ArgumentNullException(nameof(udpClient));
}
if (!udpClient.Client.Connected)
{
throw new InvalidOperationException(Resources.UdpClientNotConnected);
}
return CreateAscii(new UdpClientAdapter(udpClient));
}
/// <summary>
/// Modbus ASCII master factory method.
/// </summary>
public static ModbusSerialMaster CreateAscii(IStreamResource streamResource)
{
if (streamResource == null)
{
throw new ArgumentNullException(nameof(streamResource));
}
return new ModbusSerialMaster(new ModbusAsciiTransport(streamResource));
}
#if SERIAL
/// <summary>
/// Modbus RTU master factory method.
/// </summary>
public static ModbusSerialMaster CreateRtu(SerialPort serialPort)
{
if (serialPort == null)
{
throw new ArgumentNullException(nameof(serialPort));
}
return CreateRtu(new SerialPortAdapter(serialPort));
}
#endif
/// <summary>
/// Modbus RTU master factory method.
/// </summary>
public static ModbusSerialMaster CreateRtu(TcpClient tcpClient)
{
if (tcpClient == null)
{
throw new ArgumentNullException(nameof(tcpClient));
}
return CreateRtu(new TcpClientAdapter(tcpClient));
}
/// <summary>
/// Modbus RTU master factory method.
/// </summary>
public static ModbusSerialMaster CreateRtu(UdpClient udpClient)
{
if (udpClient == null)
{
throw new ArgumentNullException(nameof(udpClient));
}
if (!udpClient.Client.Connected)
{
throw new InvalidOperationException(Resources.UdpClientNotConnected);
}
return CreateRtu(new UdpClientAdapter(udpClient));
}
/// <summary>
/// Modbus RTU master factory method.
/// </summary>
public static ModbusSerialMaster CreateRtu(IStreamResource streamResource)
{
if (streamResource == null)
{
throw new ArgumentNullException(nameof(streamResource));
}
return new ModbusSerialMaster(new ModbusRtuTransport(streamResource));
}
/// <summary>
/// Serial Line only.
/// Diagnostic function which loops back the original data.
/// NModbus only supports looping back one ushort value, this is a limitation of the "Best Effort" implementation of
/// the RTU protocol.
/// </summary>
/// <param name="slaveAddress">Address of device to test.</param>
/// <param name="data">Data to return.</param>
/// <returns>Return true if slave device echoed data.</returns>
public bool ReturnQueryData(byte slaveAddress, ushort data)
{
DiagnosticsRequestResponse request;
DiagnosticsRequestResponse response;
request = new DiagnosticsRequestResponse(
Modbus.DiagnosticsReturnQueryData,
slaveAddress,
new RegisterCollection(data));
response = Transport.UnicastMessage<DiagnosticsRequestResponse>(request);
return response.Data[0] == data;
}
}
}

View File

@ -0,0 +1,153 @@
namespace Modbus.Device
{
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
#if SERIAL
using System.IO.Ports;
#endif
using IO;
using Message;
/// <summary>
/// Modbus serial slave device.
/// </summary>
public class ModbusSerialSlave : ModbusSlave
{
private ModbusSerialSlave(byte unitId, ModbusTransport transport)
: base(unitId, transport)
{
}
private ModbusSerialTransport SerialTransport
{
get
{
var transport = Transport as ModbusSerialTransport;
if (transport == null)
{
throw new ObjectDisposedException("SerialTransport");
}
return transport;
}
}
#if SERIAL
/// <summary>
/// Modbus ASCII slave factory method.
/// </summary>
public static ModbusSerialSlave CreateAscii(byte unitId, SerialPort serialPort)
{
if (serialPort == null)
{
throw new ArgumentNullException(nameof(serialPort));
}
return CreateAscii(unitId, new SerialPortAdapter(serialPort));
}
#endif
/// <summary>
/// Modbus ASCII slave factory method.
/// </summary>
public static ModbusSerialSlave CreateAscii(byte unitId, IStreamResource streamResource)
{
if (streamResource == null)
{
throw new ArgumentNullException(nameof(streamResource));
}
return new ModbusSerialSlave(unitId, new ModbusAsciiTransport(streamResource));
}
#if SERIAL
/// <summary>
/// Modbus RTU slave factory method.
/// </summary>
public static ModbusSerialSlave CreateRtu(byte unitId, SerialPort serialPort)
{
if (serialPort == null)
{
throw new ArgumentNullException(nameof(serialPort));
}
return CreateRtu(unitId, new SerialPortAdapter(serialPort));
}
#endif
/// <summary>
/// Modbus RTU slave factory method.
/// </summary>
public static ModbusSerialSlave CreateRtu(byte unitId, IStreamResource streamResource)
{
if (streamResource == null)
{
throw new ArgumentNullException(nameof(streamResource));
}
return new ModbusSerialSlave(unitId, new ModbusRtuTransport(streamResource));
}
/// <summary>
/// Start slave listening for requests.
/// </summary>
public override async Task ListenAsync()
{
while (true)
{
try
{
try
{
//TODO: remove deleay once async will be implemented in transport level
await Task.Delay(20).ConfigureAwait(false);
// read request and build message
byte[] frame = SerialTransport.ReadRequest();
IModbusMessage request = ModbusMessageFactory.CreateModbusRequest(frame);
if (SerialTransport.CheckFrame && !SerialTransport.ChecksumsMatch(request, frame))
{
string msg = $"Checksums failed to match {string.Join(", ", request.MessageFrame)} != {string.Join(", ", frame)}.";
Debug.WriteLine(msg);
throw new IOException(msg);
}
// only service requests addressed to this particular slave
if (request.SlaveAddress != UnitId)
{
Debug.WriteLine($"NModbus Slave {UnitId} ignoring request intended for NModbus Slave {request.SlaveAddress}");
continue;
}
// perform action
IModbusMessage response = ApplyRequest(request);
// write response
SerialTransport.Write(response);
}
catch (IOException ioe)
{
Debug.WriteLine($"IO Exception encountered while listening for requests - {ioe.Message}");
SerialTransport.DiscardInBuffer();
}
catch (TimeoutException te)
{
Debug.WriteLine($"Timeout Exception encountered while listening for requests - {te.Message}");
SerialTransport.DiscardInBuffer();
}
// TODO better exception handling here, missing FormatException, NotImplemented...
}
catch (InvalidOperationException)
{
// when the underlying transport is disposed
break;
}
}
}
}
}

View File

@ -0,0 +1,271 @@
namespace Modbus.Device
{
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Data;
using IO;
using Message;
/// <summary>
/// Modbus slave device.
/// </summary>
public abstract class ModbusSlave : ModbusDevice
{
internal ModbusSlave(byte unitId, ModbusTransport transport)
: base(transport)
{
DataStore = DataStoreFactory.CreateDefaultDataStore();
UnitId = unitId;
}
/// <summary>
/// Raised when a Modbus slave receives a request, before processing request function.
/// </summary>
/// <exception cref="InvalidModbusRequestException">The Modbus request was invalid, and an error response the specified exception should be sent.</exception>
public event EventHandler<ModbusSlaveRequestEventArgs> ModbusSlaveRequestReceived;
/// <summary>
/// Raised when a Modbus slave receives a write request, after processing the write portion of the function.
/// </summary>
/// <remarks>For Read/Write Multiple registers (function code 23), this method is raised after writing and before reading.</remarks>
public event EventHandler<ModbusSlaveRequestEventArgs> WriteComplete;
/// <summary>
/// Gets or sets the data store.
/// </summary>
public DataStore DataStore { get; set; }
/// <summary>
/// Gets or sets the unit ID.
/// </summary>
public byte UnitId { get; set; }
/// <summary>
/// Start slave listening for requests.
/// </summary>
public abstract Task ListenAsync();
internal static ReadCoilsInputsResponse ReadDiscretes(
ReadCoilsInputsRequest request,
DataStore dataStore,
ModbusDataCollection<bool> dataSource)
{
DiscreteCollection data;
ReadCoilsInputsResponse response;
data = DataStore.ReadData<DiscreteCollection, bool>(
dataStore,
dataSource,
request.StartAddress,
request.NumberOfPoints,
dataStore.SyncRoot);
response = new ReadCoilsInputsResponse(
request.FunctionCode,
request.SlaveAddress,
data.ByteCount,
data);
return response;
}
internal static ReadHoldingInputRegistersResponse ReadRegisters(
ReadHoldingInputRegistersRequest request,
DataStore dataStore,
ModbusDataCollection<ushort> dataSource)
{
RegisterCollection data;
ReadHoldingInputRegistersResponse response;
data = DataStore.ReadData<RegisterCollection, ushort>(
dataStore,
dataSource,
request.StartAddress,
request.NumberOfPoints,
dataStore.SyncRoot);
response = new ReadHoldingInputRegistersResponse(
request.FunctionCode,
request.SlaveAddress,
data);
return response;
}
internal static WriteSingleCoilRequestResponse WriteSingleCoil(
WriteSingleCoilRequestResponse request,
DataStore dataStore,
ModbusDataCollection<bool> dataSource)
{
DataStore.WriteData(
dataStore,
new DiscreteCollection(request.Data[0] == Modbus.CoilOn),
dataSource,
request.StartAddress,
dataStore.SyncRoot);
return request;
}
internal static WriteMultipleCoilsResponse WriteMultipleCoils(
WriteMultipleCoilsRequest request,
DataStore dataStore,
ModbusDataCollection<bool> dataSource)
{
WriteMultipleCoilsResponse response;
DataStore.WriteData(
dataStore,
request.Data.Take(request.NumberOfPoints),
dataSource,
request.StartAddress,
dataStore.SyncRoot);
response = new WriteMultipleCoilsResponse(
request.SlaveAddress,
request.StartAddress,
request.NumberOfPoints);
return response;
}
internal static WriteSingleRegisterRequestResponse WriteSingleRegister(
WriteSingleRegisterRequestResponse request,
DataStore dataStore,
ModbusDataCollection<ushort> dataSource)
{
DataStore.WriteData(
dataStore,
request.Data,
dataSource,
request.StartAddress,
dataStore.SyncRoot);
return request;
}
internal static WriteMultipleRegistersResponse WriteMultipleRegisters(
WriteMultipleRegistersRequest request,
DataStore dataStore,
ModbusDataCollection<ushort> dataSource)
{
WriteMultipleRegistersResponse response;
DataStore.WriteData(
dataStore,
request.Data,
dataSource,
request.StartAddress,
dataStore.SyncRoot);
response = new WriteMultipleRegistersResponse(
request.SlaveAddress,
request.StartAddress,
request.NumberOfPoints);
return response;
}
[SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Cast is not unneccessary.")]
internal IModbusMessage ApplyRequest(IModbusMessage request)
{
IModbusMessage response;
try
{
Debug.WriteLine(request.ToString());
var eventArgs = new ModbusSlaveRequestEventArgs(request);
ModbusSlaveRequestReceived?.Invoke(this, eventArgs);
switch (request.FunctionCode)
{
case Modbus.ReadCoils:
response = ReadDiscretes(
(ReadCoilsInputsRequest)request,
DataStore,
DataStore.CoilDiscretes);
break;
case Modbus.ReadInputs:
response = ReadDiscretes(
(ReadCoilsInputsRequest)request,
DataStore,
DataStore.InputDiscretes);
break;
case Modbus.ReadHoldingRegisters:
response = ReadRegisters(
(ReadHoldingInputRegistersRequest)request,
DataStore,
DataStore.HoldingRegisters);
break;
case Modbus.ReadInputRegisters:
response = ReadRegisters(
(ReadHoldingInputRegistersRequest)request,
DataStore,
DataStore.InputRegisters);
break;
case Modbus.Diagnostics:
response = request;
break;
case Modbus.WriteSingleCoil:
response = WriteSingleCoil(
(WriteSingleCoilRequestResponse)request,
DataStore,
DataStore.CoilDiscretes);
WriteComplete?.Invoke(this, eventArgs);
break;
case Modbus.WriteSingleRegister:
response = WriteSingleRegister(
(WriteSingleRegisterRequestResponse)request,
DataStore,
DataStore.HoldingRegisters);
WriteComplete?.Invoke(this, eventArgs);
break;
case Modbus.WriteMultipleCoils:
response = WriteMultipleCoils(
(WriteMultipleCoilsRequest)request,
DataStore,
DataStore.CoilDiscretes);
WriteComplete?.Invoke(this, eventArgs);
break;
case Modbus.WriteMultipleRegisters:
response = WriteMultipleRegisters(
(WriteMultipleRegistersRequest)request,
DataStore,
DataStore.HoldingRegisters);
WriteComplete?.Invoke(this, eventArgs);
break;
case Modbus.ReadWriteMultipleRegisters:
ReadWriteMultipleRegistersRequest readWriteRequest = (ReadWriteMultipleRegistersRequest)request;
WriteMultipleRegisters(
readWriteRequest.WriteRequest,
DataStore,
DataStore.HoldingRegisters);
WriteComplete?.Invoke(this, eventArgs);
response = ReadRegisters(
readWriteRequest.ReadRequest,
DataStore,
DataStore.HoldingRegisters);
break;
default:
string msg = $"Unsupported function code {request.FunctionCode}.";
Debug.WriteLine(msg);
throw new InvalidModbusRequestException(Modbus.IllegalFunction);
}
}
catch (InvalidModbusRequestException ex)
{
// Catches the exception for an illegal function or a custom exception from the ModbusSlaveRequestReceived event.
response = new SlaveExceptionResponse(
request.SlaveAddress,
(byte)(Modbus.ExceptionOffset + request.FunctionCode),
ex.ExceptionCode);
}
return response;
}
}
}

View File

@ -0,0 +1,27 @@
namespace Modbus.Device
{
using System;
using Message;
/// <summary>
/// Modbus Slave request event args containing information on the message.
/// </summary>
public class ModbusSlaveRequestEventArgs : EventArgs
{
private readonly IModbusMessage _message;
internal ModbusSlaveRequestEventArgs(IModbusMessage message)
{
_message = message;
}
/// <summary>
/// Gets the message.
/// </summary>
public IModbusMessage Message
{
get { return _message; }
}
}
}

View File

@ -0,0 +1,203 @@
namespace Modbus.Device
{
using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Sockets;
using System.Threading.Tasks;
#if TIMER
using System.Timers;
#endif
using IO;
/// <summary>
/// Modbus TCP slave device.
/// </summary>
public class ModbusTcpSlave : ModbusSlave
{
private const int TimeWaitResponse = 1000;
private readonly object _serverLock = new object();
private readonly ConcurrentDictionary<string, ModbusMasterTcpConnection> _masters =
new ConcurrentDictionary<string, ModbusMasterTcpConnection>();
private TcpListener _server;
#if TIMER
private Timer _timer;
#endif
private ModbusTcpSlave(byte unitId, TcpListener tcpListener)
: base(unitId, new EmptyTransport())
{
if (tcpListener == null)
{
throw new ArgumentNullException(nameof(tcpListener));
}
_server = tcpListener;
}
#if TIMER
private ModbusTcpSlave(byte unitId, TcpListener tcpListener, double timeInterval)
: base(unitId, new EmptyTransport())
{
if (tcpListener == null)
{
throw new ArgumentNullException(nameof(tcpListener));
}
_server = tcpListener;
_timer = new Timer(timeInterval);
_timer.Elapsed += OnTimer;
_timer.Enabled = true;
}
#endif
/// <summary>
/// Gets the Modbus TCP Masters connected to this Modbus TCP Slave.
/// </summary>
public ReadOnlyCollection<TcpClient> Masters
{
get
{
return new ReadOnlyCollection<TcpClient>(_masters.Values.Select(mc => mc.TcpClient).ToList());
}
}
/// <summary>
/// Gets the server.
/// </summary>
/// <value>The server.</value>
/// <remarks>
/// This property is not thread safe, it should only be consumed within a lock.
/// </remarks>
private TcpListener Server
{
get
{
if (_server == null)
{
throw new ObjectDisposedException("Server");
}
return _server;
}
}
/// <summary>
/// Modbus TCP slave factory method.
/// </summary>
public static ModbusTcpSlave CreateTcp(byte unitId, TcpListener tcpListener)
{
return new ModbusTcpSlave(unitId, tcpListener);
}
#if TIMER
/// <summary>
/// Creates ModbusTcpSlave with timer which polls connected clients every
/// <paramref name="pollInterval"/> milliseconds on that they are connected.
/// </summary>
public static ModbusTcpSlave CreateTcp(byte unitId, TcpListener tcpListener, double pollInterval)
{
return new ModbusTcpSlave(unitId, tcpListener, pollInterval);
}
#endif
/// <summary>
/// Start slave listening for requests.
/// </summary>
public override async Task ListenAsync()
{
Debug.WriteLine("Start Modbus Tcp Server.");
// TODO: add state {stoped, listening} and check it before starting
Server.Start();
while (true)
{
TcpClient client = await Server.AcceptTcpClientAsync().ConfigureAwait(false);
var masterConnection = new ModbusMasterTcpConnection(client, this);
masterConnection.ModbusMasterTcpConnectionClosed += OnMasterConnectionClosedHandler;
_masters.TryAdd(client.Client.RemoteEndPoint.ToString(), masterConnection);
}
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources
/// </summary>
/// <param name="disposing">
/// <c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only
/// unmanaged resources.
/// </param>
/// <remarks>Dispose is thread-safe.</remarks>
protected override void Dispose(bool disposing)
{
if (disposing)
{
// double-check locking
if (_server != null)
{
lock (_serverLock)
{
if (_server != null)
{
_server.Stop();
_server = null;
#if TIMER
if (_timer != null)
{
_timer.Dispose();
_timer = null;
}
#endif
foreach (var key in _masters.Keys)
{
ModbusMasterTcpConnection connection;
if (_masters.TryRemove(key, out connection))
{
connection.ModbusMasterTcpConnectionClosed -= OnMasterConnectionClosedHandler;
connection.Dispose();
}
}
}
}
}
}
}
private static bool IsSocketConnected(Socket socket)
{
bool poll = socket.Poll(TimeWaitResponse, SelectMode.SelectRead);
bool available = (socket.Available == 0);
return poll && available;
}
#if TIMER
private void OnTimer(object sender, ElapsedEventArgs e)
{
foreach (var master in _masters.ToList())
{
if (IsSocketConnected(master.Value.TcpClient.Client) == false)
{
master.Value.Dispose();
}
}
}
#endif
private void OnMasterConnectionClosedHandler(object sender, TcpConnectionEventArgs e)
{
ModbusMasterTcpConnection connection;
if (!_masters.TryRemove(e.EndPoint, out connection))
{
string msg = $"EndPoint {e.EndPoint} cannot be removed, it does not exist.";
throw new ArgumentException(msg);
}
Debug.WriteLine($"Removed Master {e.EndPoint}");
}
}
}

View File

@ -0,0 +1,87 @@
namespace Modbus.Device
{
using System;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using IO;
using Message;
using Unme.Common;
/// <summary>
/// Modbus UDP slave device.
/// </summary>
public class ModbusUdpSlave : ModbusSlave
{
private readonly UdpClient _udpClient;
private ModbusUdpSlave(byte unitId, UdpClient udpClient)
: base(unitId, new ModbusIpTransport(new UdpClientAdapter(udpClient)))
{
_udpClient = udpClient;
}
/// <summary>
/// Modbus UDP slave factory method.
/// Creates NModbus UDP slave with default
/// </summary>
public static ModbusUdpSlave CreateUdp(UdpClient client)
{
return new ModbusUdpSlave(Modbus.DefaultIpSlaveUnitId, client);
}
/// <summary>
/// Modbus UDP slave factory method.
/// </summary>
public static ModbusUdpSlave CreateUdp(byte unitId, UdpClient client)
{
return new ModbusUdpSlave(unitId, client);
}
/// <summary>
/// Start slave listening for requests.
/// </summary>
public override async Task ListenAsync()
{
Debug.WriteLine("Start Modbus Udp Server.");
try
{
while (true)
{
UdpReceiveResult receiveResult = await _udpClient.ReceiveAsync().ConfigureAwait(false);
IPEndPoint masterEndPoint = receiveResult.RemoteEndPoint;
byte[] frame = receiveResult.Buffer;
Debug.WriteLine($"Read Frame completed {frame.Length} bytes");
Debug.WriteLine($"RX: {string.Join(", ", frame)}");
IModbusMessage request =
ModbusMessageFactory.CreateModbusRequest(frame.Slice(6, frame.Length - 6).ToArray());
request.TransactionId = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 0));
// perform action and build response
IModbusMessage response = ApplyRequest(request);
response.TransactionId = request.TransactionId;
// write response
byte[] responseFrame = Transport.BuildMessageFrame(response);
Debug.WriteLine($"TX: {string.Join(", ", responseFrame)}");
await _udpClient.SendAsync(responseFrame, responseFrame.Length, masterEndPoint).ConfigureAwait(false);
}
}
catch (SocketException se)
{
// this hapens when slave stops
if (se.SocketErrorCode != SocketError.Interrupted)
{
throw;
}
}
}
}
}

View File

@ -0,0 +1,24 @@
namespace Modbus.Device
{
using System;
internal class TcpConnectionEventArgs : EventArgs
{
public TcpConnectionEventArgs(string endPoint)
{
if (endPoint == null)
{
throw new ArgumentNullException(nameof(endPoint));
}
if (endPoint == string.Empty)
{
throw new ArgumentException(Resources.EmptyEndPoint);
}
EndPoint = endPoint;
}
public string EndPoint { get; set; }
}
}

View File

@ -0,0 +1,163 @@
namespace Modbus.Extensions.Enron
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Device;
using Utility;
/// <summary>
/// Utility extensions for the Enron Modbus dialect.
/// </summary>
public static class EnronModbus
{
/// <summary>
/// Read contiguous block of 32 bit holding registers.
/// </summary>
/// <param name="master">The Modbus master.</param>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>Holding registers status</returns>
public static uint[] ReadHoldingRegisters32(
this ModbusMaster master,
byte slaveAddress,
ushort startAddress,
ushort numberOfPoints)
{
if (master == null)
{
throw new ArgumentNullException(nameof(master));
}
ValidateNumberOfPoints(numberOfPoints, 62);
// read 16 bit chunks and perform conversion
var rawRegisters = master.ReadHoldingRegisters(
slaveAddress,
startAddress,
(ushort)(numberOfPoints * 2));
return Convert(rawRegisters).ToArray();
}
/// <summary>
/// Read contiguous block of 32 bit input registers.
/// </summary>
/// <param name="master">The Modbus master.</param>
/// <param name="slaveAddress">Address of device to read values from.</param>
/// <param name="startAddress">Address to begin reading.</param>
/// <param name="numberOfPoints">Number of holding registers to read.</param>
/// <returns>Input registers status</returns>
public static uint[] ReadInputRegisters32(
this ModbusMaster master,
byte slaveAddress,
ushort startAddress,
ushort numberOfPoints)
{
if (master == null)
{
throw new ArgumentNullException(nameof(master));
}
ValidateNumberOfPoints(numberOfPoints, 62);
var rawRegisters = master.ReadInputRegisters(
slaveAddress,
startAddress,
(ushort)(numberOfPoints * 2));
return Convert(rawRegisters).ToArray();
}
/// <summary>
/// Write a single 16 bit holding register.
/// </summary>
/// <param name="master">The Modbus master.</param>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="registerAddress">Address to write.</param>
/// <param name="value">Value to write.</param>
public static void WriteSingleRegister32(
this ModbusMaster master,
byte slaveAddress,
ushort registerAddress,
uint value)
{
if (master == null)
{
throw new ArgumentNullException(nameof(master));
}
master.WriteMultipleRegisters32(slaveAddress, registerAddress, new[] { value });
}
/// <summary>
/// Write a block of contiguous 32 bit holding registers.
/// </summary>
/// <param name="master">The Modbus master.</param>
/// <param name="slaveAddress">Address of the device to write to.</param>
/// <param name="startAddress">Address to begin writing values.</param>
/// <param name="data">Values to write.</param>
public static void WriteMultipleRegisters32(
this ModbusMaster master,
byte slaveAddress,
ushort startAddress,
uint[] data)
{
if (master == null)
{
throw new ArgumentNullException(nameof(master));
}
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
if (data.Length == 0 || data.Length > 61)
{
throw new ArgumentException("The length of argument data must be between 1 and 61 inclusive.");
}
master.WriteMultipleRegisters(slaveAddress, startAddress, Convert(data).ToArray());
}
/// <summary>
/// Convert the 32 bit registers to two 16 bit values.
/// </summary>
private static IEnumerable<ushort> Convert(uint[] registers)
{
foreach (var register in registers)
{
// low order value
yield return BitConverter.ToUInt16(BitConverter.GetBytes(register), 0);
// high order value
yield return BitConverter.ToUInt16(BitConverter.GetBytes(register), 2);
}
}
/// <summary>
/// Convert the 16 bit registers to 32 bit registers.
/// </summary>
private static IEnumerable<uint> Convert(ushort[] registers)
{
for (int i = 0; i < registers.Length; i++)
{
yield return ModbusUtility.GetUInt32(registers[i + 1], registers[i]);
i++;
}
}
private static void ValidateNumberOfPoints(ushort numberOfPoints, ushort maxNumberOfPoints)
{
if (numberOfPoints < 1 || numberOfPoints > maxNumberOfPoints)
{
string msg = $"Argument numberOfPoints must be between 1 and {maxNumberOfPoints} inclusive.";
throw new ArgumentException(msg);
}
}
}
}

View File

@ -0,0 +1,8 @@
using System.Diagnostics.CodeAnalysis;
[module:
SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace",
Target = "Modbus")]
[module:
SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace",
Target = "Modbus.Utility")]

View File

@ -0,0 +1,33 @@
namespace Modbus.IO
{
using System;
using Message;
public class EmptyTransport : ModbusTransport
{
internal override byte[] ReadRequest()
{
throw new NotImplementedException();
}
internal override IModbusMessage ReadResponse<T>()
{
throw new NotImplementedException();
}
internal override byte[] BuildMessageFrame(Message.IModbusMessage message)
{
throw new NotImplementedException();
}
internal override void Write(IModbusMessage message)
{
throw new NotImplementedException();
}
internal override void OnValidateResponse(IModbusMessage request, IModbusMessage response)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,48 @@
namespace Modbus.IO
{
using System;
/// <summary>
/// Represents a serial resource.
/// Implementor - http://en.wikipedia.org/wiki/Bridge_Pattern
/// </summary>
public interface IStreamResource : IDisposable
{
/// <summary>
/// Indicates that no timeout should occur.
/// </summary>
int InfiniteTimeout { get; }
/// <summary>
/// Gets or sets the number of milliseconds before a timeout occurs when a read operation does not finish.
/// </summary>
int ReadTimeout { get; set; }
/// <summary>
/// Gets or sets the number of milliseconds before a timeout occurs when a write operation does not finish.
/// </summary>
int WriteTimeout { get; set; }
/// <summary>
/// Purges the receive buffer.
/// </summary>
void DiscardInBuffer();
/// <summary>
/// Reads a number of bytes from the input buffer and writes those bytes into a byte array at the specified offset.
/// </summary>
/// <param name="buffer">The byte array to write the input to.</param>
/// <param name="offset">The offset in the buffer array to begin writing.</param>
/// <param name="count">The number of bytes to read.</param>
/// <returns>The number of bytes read.</returns>
int Read(byte[] buffer, int offset, int count);
/// <summary>
/// Writes a specified number of bytes to the port from an output buffer, starting at the specified offset.
/// </summary>
/// <param name="buffer">The byte array that contains the data to write to the port.</param>
/// <param name="offset">The offset in the buffer array to begin writing.</param>
/// <param name="count">The number of bytes to write.</param>
void Write(byte[] buffer, int offset, int count);
}
}

View File

@ -0,0 +1,70 @@
namespace Modbus.IO
{
using System.Diagnostics;
using System.IO;
using System.Text;
using Message;
using Utility;
/// <summary>
/// Refined Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern
/// </summary>
internal class ModbusAsciiTransport : ModbusSerialTransport
{
internal ModbusAsciiTransport(IStreamResource streamResource)
: base(streamResource)
{
Debug.Assert(streamResource != null, "Argument streamResource cannot be null.");
}
internal override byte[] BuildMessageFrame(IModbusMessage message)
{
var msgFrame = message.MessageFrame;
var msgFrameAscii = ModbusUtility.GetAsciiBytes(msgFrame);
var lrcAscii = ModbusUtility.GetAsciiBytes(ModbusUtility.CalculateLrc(msgFrame));
var nlAscii = Encoding.UTF8.GetBytes(Modbus.NewLine.ToCharArray());
var frame = new MemoryStream(1 + msgFrameAscii.Length + lrcAscii.Length + nlAscii.Length);
frame.WriteByte((byte)':');
frame.Write(msgFrameAscii, 0, msgFrameAscii.Length);
frame.Write(lrcAscii, 0, lrcAscii.Length);
frame.Write(nlAscii, 0, nlAscii.Length);
return frame.ToArray();
}
internal override bool ChecksumsMatch(IModbusMessage message, byte[] messageFrame)
{
return ModbusUtility.CalculateLrc(message.MessageFrame) == messageFrame[messageFrame.Length - 1];
}
internal override byte[] ReadRequest()
{
return ReadRequestResponse();
}
internal override IModbusMessage ReadResponse<T>()
{
return CreateResponse<T>(ReadRequestResponse());
}
internal byte[] ReadRequestResponse()
{
// read message frame, removing frame start ':'
string frameHex = StreamResourceUtility.ReadLine(StreamResource).Substring(1);
// convert hex to bytes
byte[] frame = ModbusUtility.HexToBytes(frameHex);
Debug.WriteLine($"RX: {string.Join(", ", frame)}");
if (frame.Length < 3)
{
throw new IOException("Premature end of stream, message truncated.");
}
return frame;
}
}
}

View File

@ -0,0 +1,163 @@
namespace Modbus.IO
{
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using Message;
using Unme.Common;
/// <summary>
/// Transport for Internet protocols.
/// Refined Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern
/// </summary>
internal class ModbusIpTransport : ModbusTransport
{
private static readonly object _transactionIdLock = new object();
private ushort _transactionId;
internal ModbusIpTransport(IStreamResource streamResource)
: base(streamResource)
{
Debug.Assert(streamResource != null, "Argument streamResource cannot be null.");
}
internal static byte[] ReadRequestResponse(IStreamResource streamResource)
{
// read header
var mbapHeader = new byte[6];
int numBytesRead = 0;
while (numBytesRead != 6)
{
int bRead = streamResource.Read(mbapHeader, numBytesRead, 6 - numBytesRead);
if (bRead == 0)
{
throw new IOException("Read resulted in 0 bytes returned.");
}
numBytesRead += bRead;
}
Debug.WriteLine($"MBAP header: {string.Join(", ", mbapHeader)}");
var frameLength = (ushort)IPAddress.HostToNetworkOrder(BitConverter.ToInt16(mbapHeader, 4));
Debug.WriteLine($"{frameLength} bytes in PDU.");
// read message
var messageFrame = new byte[frameLength];
numBytesRead = 0;
while (numBytesRead != frameLength)
{
int bRead = streamResource.Read(messageFrame, numBytesRead, frameLength - numBytesRead);
if (bRead == 0)
{
throw new IOException("Read resulted in 0 bytes returned.");
}
numBytesRead += bRead;
}
Debug.WriteLine($"PDU: {frameLength}");
var frame = mbapHeader.Concat(messageFrame).ToArray();
Debug.WriteLine($"RX: {string.Join(", ", frame)}");
return frame;
}
internal static byte[] GetMbapHeader(IModbusMessage message)
{
byte[] transactionId = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)message.TransactionId));
byte[] length = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)(message.ProtocolDataUnit.Length + 1)));
var stream = new MemoryStream(7);
stream.Write(transactionId, 0, transactionId.Length);
stream.WriteByte(0);
stream.WriteByte(0);
stream.Write(length, 0, length.Length);
stream.WriteByte(message.SlaveAddress);
return stream.ToArray();
}
/// <summary>
/// Create a new transaction ID.
/// </summary>
internal virtual ushort GetNewTransactionId()
{
lock (_transactionIdLock)
{
_transactionId = _transactionId == ushort.MaxValue ? (ushort)1 : ++_transactionId;
}
return _transactionId;
}
internal IModbusMessage CreateMessageAndInitializeTransactionId<T>(byte[] fullFrame)
where T : IModbusMessage, new()
{
byte[] mbapHeader = fullFrame.Slice(0, 6).ToArray();
byte[] messageFrame = fullFrame.Slice(6, fullFrame.Length - 6).ToArray();
IModbusMessage response = CreateResponse<T>(messageFrame);
response.TransactionId = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(mbapHeader, 0));
return response;
}
internal override byte[] BuildMessageFrame(IModbusMessage message)
{
byte[] header = GetMbapHeader(message);
byte[] pdu = message.ProtocolDataUnit;
var messageBody = new MemoryStream(header.Length + pdu.Length);
messageBody.Write(header, 0, header.Length);
messageBody.Write(pdu, 0, pdu.Length);
return messageBody.ToArray();
}
internal override void Write(IModbusMessage message)
{
message.TransactionId = GetNewTransactionId();
byte[] frame = BuildMessageFrame(message);
Debug.WriteLine($"TX: {string.Join(", ", frame)}");
StreamResource.Write(frame, 0, frame.Length);
}
internal override byte[] ReadRequest()
{
return ReadRequestResponse(StreamResource);
}
internal override IModbusMessage ReadResponse<T>()
{
return CreateMessageAndInitializeTransactionId<T>(ReadRequestResponse(StreamResource));
}
internal override void OnValidateResponse(IModbusMessage request, IModbusMessage response)
{
if (request.TransactionId != response.TransactionId)
{
string msg = $"Response was not of expected transaction ID. Expected {request.TransactionId}, received {response.TransactionId}.";
throw new IOException(msg);
}
}
internal override bool OnShouldRetryResponse(IModbusMessage request, IModbusMessage response)
{
if (request.TransactionId > response.TransactionId && request.TransactionId - response.TransactionId < RetryOnOldResponseThreshold)
{
// This response was from a previous request
return true;
}
return base.OnShouldRetryResponse(request, response);
}
}
}

View File

@ -0,0 +1,142 @@
namespace Modbus.IO
{
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Message;
using Utility;
/// <summary>
/// Refined Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern
/// </summary>
internal class ModbusRtuTransport : ModbusSerialTransport
{
public const int RequestFrameStartLength = 7;
public const int ResponseFrameStartLength = 4;
internal ModbusRtuTransport(IStreamResource streamResource)
: base(streamResource)
{
Debug.Assert(streamResource != null, "Argument streamResource cannot be null.");
}
public static int RequestBytesToRead(byte[] frameStart)
{
byte functionCode = frameStart[1];
int numBytes;
switch (functionCode)
{
case Modbus.ReadCoils:
case Modbus.ReadInputs:
case Modbus.ReadHoldingRegisters:
case Modbus.ReadInputRegisters:
case Modbus.WriteSingleCoil:
case Modbus.WriteSingleRegister:
case Modbus.Diagnostics:
numBytes = 1;
break;
case Modbus.WriteMultipleCoils:
case Modbus.WriteMultipleRegisters:
byte byteCount = frameStart[6];
numBytes = byteCount + 2;
break;
default:
string msg = $"Function code {functionCode} not supported.";
Debug.WriteLine(msg);
throw new NotImplementedException(msg);
}
return numBytes;
}
public static int ResponseBytesToRead(byte[] frameStart)
{
byte functionCode = frameStart[1];
// exception response
if (functionCode > Modbus.ExceptionOffset)
{
return 1;
}
int numBytes;
switch (functionCode)
{
case Modbus.ReadCoils:
case Modbus.ReadInputs:
case Modbus.ReadHoldingRegisters:
case Modbus.ReadInputRegisters:
numBytes = frameStart[2] + 1;
break;
case Modbus.WriteSingleCoil:
case Modbus.WriteSingleRegister:
case Modbus.WriteMultipleCoils:
case Modbus.WriteMultipleRegisters:
case Modbus.Diagnostics:
numBytes = 4;
break;
default:
string msg = $"Function code {functionCode} not supported.";
Debug.WriteLine(msg);
throw new NotImplementedException(msg);
}
return numBytes;
}
public virtual byte[] Read(int count)
{
byte[] frameBytes = new byte[count];
int numBytesRead = 0;
while (numBytesRead != count)
{
numBytesRead += StreamResource.Read(frameBytes, numBytesRead, count - numBytesRead);
}
return frameBytes;
}
internal override byte[] BuildMessageFrame(IModbusMessage message)
{
var messageFrame = message.MessageFrame;
var crc = ModbusUtility.CalculateCrc(messageFrame);
var messageBody = new MemoryStream(messageFrame.Length + crc.Length);
messageBody.Write(messageFrame, 0, messageFrame.Length);
messageBody.Write(crc, 0, crc.Length);
return messageBody.ToArray();
}
internal override bool ChecksumsMatch(IModbusMessage message, byte[] messageFrame)
{
return BitConverter.ToUInt16(messageFrame, messageFrame.Length - 2) ==
BitConverter.ToUInt16(ModbusUtility.CalculateCrc(message.MessageFrame), 0);
}
internal override IModbusMessage ReadResponse<T>()
{
byte[] frameStart = Read(ResponseFrameStartLength);
byte[] frameEnd = Read(ResponseBytesToRead(frameStart));
byte[] frame = Enumerable.Concat(frameStart, frameEnd).ToArray();
Debug.WriteLine($"RX: {string.Join(", ", frame)}");
return CreateResponse<T>(frame);
}
internal override byte[] ReadRequest()
{
byte[] frameStart = Read(RequestFrameStartLength);
byte[] frameEnd = Read(RequestBytesToRead(frameStart));
byte[] frame = Enumerable.Concat(frameStart, frameEnd).ToArray();
Debug.WriteLine($"RX: {string.Join(", ", frame)}");
return frame;
}
}
}

View File

@ -0,0 +1,67 @@
namespace Modbus.IO
{
using System.Diagnostics;
using System.IO;
using Message;
/// <summary>
/// Transport for Serial protocols.
/// Refined Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern
/// </summary>
public abstract class ModbusSerialTransport : ModbusTransport
{
private bool _checkFrame = true;
internal ModbusSerialTransport(IStreamResource streamResource)
: base(streamResource)
{
Debug.Assert(streamResource != null, "Argument streamResource cannot be null.");
}
/// <summary>
/// Gets or sets a value indicating whether LRC/CRC frame checking is performed on messages.
/// </summary>
public bool CheckFrame
{
get { return _checkFrame; }
set { _checkFrame = value; }
}
internal void DiscardInBuffer()
{
StreamResource.DiscardInBuffer();
}
internal override void Write(IModbusMessage message)
{
DiscardInBuffer();
byte[] frame = BuildMessageFrame(message);
Debug.WriteLine($"TX: {string.Join(", ", frame)}");
StreamResource.Write(frame, 0, frame.Length);
}
internal override IModbusMessage CreateResponse<T>(byte[] frame)
{
IModbusMessage response = base.CreateResponse<T>(frame);
// compare checksum
if (CheckFrame && !ChecksumsMatch(response, frame))
{
string msg = $"Checksums failed to match {string.Join(", ", response.MessageFrame)} != {string.Join(", ", frame)}";
Debug.WriteLine(msg);
throw new IOException(msg);
}
return response;
}
internal abstract bool ChecksumsMatch(IModbusMessage message, byte[] messageFrame);
internal override void OnValidateResponse(IModbusMessage request, IModbusMessage response)
{
// no-op
}
}
}

View File

@ -0,0 +1,310 @@
namespace Modbus.IO
{
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Message;
using Unme.Common;
/// <summary>
/// Modbus transport.
/// Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern
/// </summary>
public abstract class ModbusTransport : IDisposable
{
private readonly object _syncLock = new object();
private int _retries = Modbus.DefaultRetries;
private int _waitToRetryMilliseconds = Modbus.DefaultWaitToRetryMilliseconds;
private IStreamResource _streamResource;
/// <summary>
/// This constructor is called by the NullTransport.
/// </summary>
internal ModbusTransport()
{
}
internal ModbusTransport(IStreamResource streamResource)
{
Debug.Assert(streamResource != null, "Argument streamResource cannot be null.");
_streamResource = streamResource;
}
/// <summary>
/// Number of times to retry sending message after encountering a failure such as an IOException,
/// TimeoutException, or a corrupt message.
/// </summary>
public int Retries
{
get { return _retries; }
set { _retries = value; }
}
/// <summary>
/// If non-zero, this will cause a second reply to be read if the first is behind the sequence number of the
/// request by less than this number. For example, set this to 3, and if when sending request 5, response 3 is
/// read, we will attempt to re-read responses.
/// </summary>
public uint RetryOnOldResponseThreshold { get; set; }
/// <summary>
/// If set, Slave Busy exception causes retry count to be used. If false, Slave Busy will cause infinite retries
/// </summary>
public bool SlaveBusyUsesRetryCount { get; set; }
/// <summary>
/// Gets or sets the number of milliseconds the tranport will wait before retrying a message after receiving
/// an ACKNOWLEGE or SLAVE DEVICE BUSY slave exception response.
/// </summary>
public int WaitToRetryMilliseconds
{
get
{
return _waitToRetryMilliseconds;
}
set
{
if (value < 0)
{
throw new ArgumentException(Resources.WaitRetryGreaterThanZero);
}
_waitToRetryMilliseconds = value;
}
}
/// <summary>
/// Gets or sets the number of milliseconds before a timeout occurs when a read operation does not finish.
/// </summary>
public int ReadTimeout
{
get { return StreamResource.ReadTimeout; }
set { StreamResource.ReadTimeout = value; }
}
/// <summary>
/// Gets or sets the number of milliseconds before a timeout occurs when a write operation does not finish.
/// </summary>
public int WriteTimeout
{
get { return StreamResource.WriteTimeout; }
set { StreamResource.WriteTimeout = value; }
}
/// <summary>
/// Gets the stream resource.
/// </summary>
internal IStreamResource StreamResource
{
get { return _streamResource; }
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
internal virtual T UnicastMessage<T>(IModbusMessage message)
where T : IModbusMessage, new()
{
IModbusMessage response = null;
int attempt = 1;
bool success = false;
do
{
try
{
lock (_syncLock)
{
Write(message);
bool readAgain;
do
{
readAgain = false;
response = ReadResponse<T>();
var exceptionResponse = response as SlaveExceptionResponse;
if (exceptionResponse != null)
{
// if SlaveExceptionCode == ACKNOWLEDGE we retry reading the response without resubmitting request
readAgain = exceptionResponse.SlaveExceptionCode == Modbus.Acknowledge;
if (readAgain)
{
Debug.WriteLine($"Received ACKNOWLEDGE slave exception response, waiting {_waitToRetryMilliseconds} milliseconds and retrying to read response.");
Sleep(WaitToRetryMilliseconds);
}
else
{
throw new SlaveException(exceptionResponse);
}
}
else if (ShouldRetryResponse(message, response))
{
readAgain = true;
}
}
while (readAgain);
}
ValidateResponse(message, response);
success = true;
}
catch (SlaveException se)
{
if (se.SlaveExceptionCode != Modbus.SlaveDeviceBusy)
{
throw;
}
if (SlaveBusyUsesRetryCount && attempt++ > _retries)
{
throw;
}
Debug.WriteLine($"Received SLAVE_DEVICE_BUSY exception response, waiting {_waitToRetryMilliseconds} milliseconds and resubmitting request.");
Sleep(WaitToRetryMilliseconds);
}
catch (Exception e)
{
if (e is FormatException ||
e is NotImplementedException ||
e is TimeoutException ||
e is IOException)
{
Debug.WriteLine($"{e.GetType().Name}, {(_retries - attempt + 1)} retries remaining - {e}");
if (attempt++ > _retries)
{
throw;
}
}
else
{
throw;
}
}
}
while (!success);
return (T)response;
}
internal virtual IModbusMessage CreateResponse<T>(byte[] frame)
where T : IModbusMessage, new()
{
byte functionCode = frame[1];
IModbusMessage response;
// check for slave exception response else create message from frame
if (functionCode > Modbus.ExceptionOffset)
{
response = ModbusMessageFactory.CreateModbusMessage<SlaveExceptionResponse>(frame);
}
else
{
response = ModbusMessageFactory.CreateModbusMessage<T>(frame);
}
return response;
}
internal void ValidateResponse(IModbusMessage request, IModbusMessage response)
{
// always check the function code and slave address, regardless of transport protocol
if (request.FunctionCode != response.FunctionCode)
{
string msg = $"Received response with unexpected Function Code. Expected {request.FunctionCode}, received {response.FunctionCode}.";
throw new IOException(msg);
}
if (request.SlaveAddress != response.SlaveAddress)
{
string msg = $"Response slave address does not match request. Expected {response.SlaveAddress}, received {request.SlaveAddress}.";
throw new IOException(msg);
}
// message specific validation
var req = request as IModbusRequest;
if (req != null)
{
req.ValidateResponse(response);
}
OnValidateResponse(request, response);
}
/// <summary>
/// Check whether we need to attempt to read another response before processing it (e.g. response was from previous request)
/// </summary>
internal bool ShouldRetryResponse(IModbusMessage request, IModbusMessage response)
{
// These checks are enforced in ValidateRequest, we don't want to retry for these
if (request.FunctionCode != response.FunctionCode)
{
return false;
}
if (request.SlaveAddress != response.SlaveAddress)
{
return false;
}
return OnShouldRetryResponse(request, response);
}
/// <summary>
/// Provide hook to check whether receiving a response should be retried
/// </summary>
internal virtual bool OnShouldRetryResponse(IModbusMessage request, IModbusMessage response)
{
return false;
}
/// <summary>
/// Provide hook to do transport level message validation.
/// </summary>
internal abstract void OnValidateResponse(IModbusMessage request, IModbusMessage response);
internal abstract byte[] ReadRequest();
internal abstract IModbusMessage ReadResponse<T>()
where T : IModbusMessage, new();
internal abstract byte[] BuildMessageFrame(IModbusMessage message);
internal abstract void Write(IModbusMessage message);
/// <summary>
/// Releases unmanaged and - optionally - managed resources
/// </summary>
/// <param name="disposing">
/// <c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only
/// unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
DisposableUtility.Dispose(ref _streamResource);
}
}
private static void Sleep(int millisecondsTimeout)
{
Task.Delay(millisecondsTimeout).Wait();
}
}
}

View File

@ -0,0 +1,27 @@
namespace Modbus.IO
{
using System.Linq;
using System.Text;
internal static class StreamResourceUtility
{
internal static string ReadLine(IStreamResource stream)
{
var result = new StringBuilder();
var singleByteBuffer = new byte[1];
do
{
if (stream.Read(singleByteBuffer, 0, 1) == 0)
{
continue;
}
result.Append(Encoding.UTF8.GetChars(singleByteBuffer).First());
}
while (!result.ToString().EndsWith(Modbus.NewLine));
return result.ToString().Substring(0, result.Length - Modbus.NewLine.Length);
}
}
}

View File

@ -0,0 +1,70 @@
namespace Modbus.IO
{
using System;
using System.Diagnostics;
using System.Net.Sockets;
using System.Threading;
using Unme.Common;
/// <summary>
/// Concrete Implementor - http://en.wikipedia.org/wiki/Bridge_Pattern
/// </summary>
internal class TcpClientAdapter : IStreamResource
{
private TcpClient _tcpClient;
public TcpClientAdapter(TcpClient tcpClient)
{
Debug.Assert(tcpClient != null, "Argument tcpClient cannot be null.");
_tcpClient = tcpClient;
}
public int InfiniteTimeout
{
get { return Timeout.Infinite; }
}
public int ReadTimeout
{
get { return _tcpClient.GetStream().ReadTimeout; }
set { _tcpClient.GetStream().ReadTimeout = value; }
}
public int WriteTimeout
{
get { return _tcpClient.GetStream().WriteTimeout; }
set { _tcpClient.GetStream().WriteTimeout = value; }
}
public void Write(byte[] buffer, int offset, int size)
{
_tcpClient.GetStream().Write(buffer, offset, size);
}
public int Read(byte[] buffer, int offset, int size)
{
return _tcpClient.GetStream().Read(buffer, offset, size);
}
public void DiscardInBuffer()
{
_tcpClient.GetStream().Flush();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
DisposableUtility.Dispose(ref _tcpClient);
}
}
}
}

View File

@ -0,0 +1,158 @@
namespace Modbus.IO
{
using System;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using Unme.Common;
/// <summary>
/// Concrete Implementor - http://en.wikipedia.org/wiki/Bridge_Pattern
/// </summary>
internal class UdpClientAdapter : IStreamResource
{
// strategy for cross platform r/w
private const int MaxBufferSize = ushort.MaxValue;
private UdpClient _udpClient;
private readonly byte[] _buffer = new byte[MaxBufferSize];
private int _bufferOffset;
public UdpClientAdapter(UdpClient udpClient)
{
if (udpClient == null)
{
throw new ArgumentNullException(nameof(udpClient));
}
_udpClient = udpClient;
}
public int InfiniteTimeout
{
get { return Timeout.Infinite; }
}
public int ReadTimeout
{
get { return _udpClient.Client.ReceiveTimeout; }
set { _udpClient.Client.ReceiveTimeout = value; }
}
public int WriteTimeout
{
get { return _udpClient.Client.SendTimeout; }
set { _udpClient.Client.SendTimeout = value; }
}
public void DiscardInBuffer()
{
// no-op
}
public int Read(byte[] buffer, int offset, int count)
{
if (buffer == null)
{
throw new ArgumentNullException(nameof(buffer));
}
if (offset < 0)
{
throw new ArgumentOutOfRangeException(
nameof(offset),
"Argument offset must be greater than or equal to 0.");
}
if (offset > buffer.Length)
{
throw new ArgumentOutOfRangeException(
nameof(offset),
"Argument offset cannot be greater than the length of buffer.");
}
if (count < 0)
{
throw new ArgumentOutOfRangeException(
nameof(count),
"Argument count must be greater than or equal to 0.");
}
if (count > buffer.Length - offset)
{
throw new ArgumentOutOfRangeException(
nameof(count),
"Argument count cannot be greater than the length of buffer minus offset.");
}
if (_bufferOffset == 0)
{
_bufferOffset = _udpClient.Client.Receive(_buffer);
}
if (_bufferOffset < count)
{
throw new IOException("Not enough bytes in the datagram.");
}
Buffer.BlockCopy(_buffer, 0, buffer, offset, count);
_bufferOffset -= count;
Buffer.BlockCopy(_buffer, count, _buffer, 0, _bufferOffset);
return count;
}
public void Write(byte[] buffer, int offset, int count)
{
if (buffer == null)
{
throw new ArgumentNullException(nameof(buffer));
}
if (offset < 0)
{
throw new ArgumentOutOfRangeException(
nameof(offset),
"Argument offset must be greater than or equal to 0.");
}
if (offset > buffer.Length)
{
throw new ArgumentOutOfRangeException(
nameof(offset),
"Argument offset cannot be greater than the length of buffer.");
}
if (count < 0)
{
throw new ArgumentOutOfRangeException(
nameof(count),
"Argument count must be greater than or equal to 0.");
}
if (count > buffer.Length - offset)
{
throw new ArgumentOutOfRangeException(
nameof(count),
"Argument count cannot be greater than the length of buffer minus offset.");
}
_udpClient.Client.Send(buffer.Skip(offset).Take(count).ToArray());
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
DisposableUtility.Dispose(ref _udpClient);
}
}
}
}

View File

@ -0,0 +1,95 @@
namespace Modbus
{
using System;
#if NET46
using System.Runtime.Serialization;
#endif
/// <summary>
/// An exception that provides the exception code that will be sent in response to an invalid Modbus request.
/// </summary>
#if NET46
[Serializable]
#endif
public class InvalidModbusRequestException : Exception
{
private readonly byte _exceptionCode;
/// <summary>
/// Initializes a new instance of the <see cref="InvalidModbusRequestException" /> class with a specified Modbus exception code.
/// </summary>
/// <param name="exceptionCode">The Modbus exception code to provide to the slave.</param>
public InvalidModbusRequestException(byte exceptionCode)
: this(GetMessage(exceptionCode), exceptionCode)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InvalidModbusRequestException" /> class with a specified error message and Modbus exception code.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="exceptionCode">The Modbus exception code to provide to the slave.</param>
public InvalidModbusRequestException(string message, byte exceptionCode)
: this(message, exceptionCode, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InvalidModbusRequestException" /> class with a specified Modbus exception code and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="exceptionCode">The Modbus exception code to provide to the slave.</param>
/// <param name="innerException">The exception that is the cause of the current exception. If the <paramref name="innerException" /> parameter is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
public InvalidModbusRequestException(byte exceptionCode, Exception innerException)
: this(GetMessage(exceptionCode), exceptionCode, innerException)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InvalidModbusRequestException" /> class with a specified Modbus exception code and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="exceptionCode">The Modbus exception code to provide to the slave.</param>
/// <param name="innerException">The exception that is the cause of the current exception. If the <paramref name="innerException" /> parameter is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
public InvalidModbusRequestException(string message, byte exceptionCode, Exception innerException)
: base(message, innerException)
{
_exceptionCode = exceptionCode;
}
#if NET46
/// <summary>
/// Initializes a new instance of the <see cref="InvalidModbusRequestException" /> class with serialized data.
/// </summary>
/// <param name="info">The object that holds the serialized object data.</param>
/// <param name="context">The contextual information about the source or destination.</param>
protected InvalidModbusRequestException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
_exceptionCode = info.GetByte(nameof(ExceptionCode));
}
#endif
/// <summary>
/// Gets the Modbus exception code to provide to the slave.
/// </summary>
public byte ExceptionCode
{
get { return _exceptionCode; }
}
#if NET46
/// <summary>Sets the <see cref="SerializationInfo" /> object with the Modbus exception code and additional exception information.</summary>
/// <param name="info">The <see cref="SerializationInfo" /> that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The <see cref="StreamingContext" /> that contains contextual information about the source or destination.</param>
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("ExceptionCode", this._exceptionCode, typeof(byte));
}
#endif
private static string GetMessage(byte exceptionCode)
{
return $"Modbus exception code {exceptionCode}.";
}
}
}

View File

@ -0,0 +1,77 @@
namespace Modbus.Message
{
using System;
/// <summary>
/// Abstract Modbus message.
/// </summary>
public abstract class AbstractModbusMessage
{
private readonly ModbusMessageImpl _messageImpl;
/// <summary>
/// Abstract Modbus message.
/// </summary>
internal AbstractModbusMessage()
{
_messageImpl = new ModbusMessageImpl();
}
/// <summary>
/// Abstract Modbus message.
/// </summary>
internal AbstractModbusMessage(byte slaveAddress, byte functionCode)
{
_messageImpl = new ModbusMessageImpl(slaveAddress, functionCode);
}
public ushort TransactionId
{
get { return _messageImpl.TransactionId; }
set { _messageImpl.TransactionId = value; }
}
public byte FunctionCode
{
get { return _messageImpl.FunctionCode; }
set { _messageImpl.FunctionCode = value; }
}
public byte SlaveAddress
{
get { return _messageImpl.SlaveAddress; }
set { _messageImpl.SlaveAddress = value; }
}
public byte[] MessageFrame
{
get { return _messageImpl.MessageFrame; }
}
public virtual byte[] ProtocolDataUnit
{
get { return _messageImpl.ProtocolDataUnit; }
}
public abstract int MinimumFrameSize { get; }
internal ModbusMessageImpl MessageImpl
{
get { return _messageImpl; }
}
public void Initialize(byte[] frame)
{
if (frame.Length < MinimumFrameSize)
{
string msg = $"Message frame must contain at least {MinimumFrameSize} bytes of data.";
throw new FormatException(msg);
}
_messageImpl.Initialize(frame);
InitializeUnique(frame);
}
protected abstract void InitializeUnique(byte[] frame);
}
}

View File

@ -0,0 +1,23 @@
namespace Modbus.Message
{
using Data;
public abstract class AbstractModbusMessageWithData<TData> : AbstractModbusMessage
where TData : IModbusMessageDataCollection
{
internal AbstractModbusMessageWithData()
{
}
internal AbstractModbusMessageWithData(byte slaveAddress, byte functionCode)
: base(slaveAddress, functionCode)
{
}
public TData Data
{
get { return (TData)MessageImpl.Data; }
set { MessageImpl.Data = value; }
}
}
}

View File

@ -0,0 +1,53 @@
namespace Modbus.Message
{
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using Data;
using Unme.Common;
internal class DiagnosticsRequestResponse : AbstractModbusMessageWithData<RegisterCollection>, IModbusMessage
{
public DiagnosticsRequestResponse()
{
}
public DiagnosticsRequestResponse(ushort subFunctionCode, byte slaveAddress, RegisterCollection data)
: base(slaveAddress, Modbus.Diagnostics)
{
SubFunctionCode = subFunctionCode;
Data = data;
}
public override int MinimumFrameSize
{
get { return 6; }
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "May implement addtional sub function codes in the future.")]
public ushort SubFunctionCode
{
get { return MessageImpl.SubFunctionCode.Value; }
set { MessageImpl.SubFunctionCode = value; }
}
public override string ToString()
{
Debug.Assert(
SubFunctionCode == Modbus.DiagnosticsReturnQueryData,
"Need to add support for additional sub-function.");
return $"Diagnostics message, sub-function return query data - {Data}.";
}
protected override void InitializeUnique(byte[] frame)
{
SubFunctionCode = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2));
Data = new RegisterCollection(frame.Slice(4, 2).ToArray());
}
}
}

View File

@ -0,0 +1,43 @@
namespace Modbus.Message
{
using System.Diagnostics.CodeAnalysis;
/// <summary>
/// A message built by the master (client) that initiates a Modbus transaction.
/// </summary>
public interface IModbusMessage
{
/// <summary>
/// The function code tells the server what kind of action to perform.
/// </summary>
byte FunctionCode { get; set; }
/// <summary>
/// Address of the slave (server).
/// </summary>
byte SlaveAddress { get; set; }
/// <summary>
/// Composition of the slave address and protocol data unit.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
byte[] MessageFrame { get; }
/// <summary>
/// Composition of the function code and message data.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
byte[] ProtocolDataUnit { get; }
/// <summary>
/// A unique identifier assigned to a message when using the IP protocol.
/// </summary>
ushort TransactionId { get; set; }
/// <summary>
/// Initializes a modbus message from the specified message frame.
/// </summary>
/// <param name="frame">Bytes of Modbus frame.</param>
void Initialize(byte[] frame);
}
}

View File

@ -0,0 +1,13 @@
namespace Modbus.Message
{
/// <summary>
/// Methods specific to a modbus request message.
/// </summary>
public interface IModbusRequest : IModbusMessage
{
/// <summary>
/// Validate the specified response against the current request.
/// </summary>
void ValidateResponse(IModbusMessage response);
}
}

View File

@ -0,0 +1,82 @@
namespace Modbus.Message
{
using System;
/// <summary>
/// Modbus message factory.
/// </summary>
public static class ModbusMessageFactory
{
/// <summary>
/// Minimum request frame length.
/// </summary>
private const int MinRequestFrameLength = 3;
/// <summary>
/// Create a Modbus message.
/// </summary>
/// <typeparam name="T">Modbus message type.</typeparam>
/// <param name="frame">Bytes of Modbus frame.</param>
/// <returns>New Modbus message based on type and frame bytes.</returns>
public static T CreateModbusMessage<T>(byte[] frame)
where T : IModbusMessage, new()
{
IModbusMessage message = new T();
message.Initialize(frame);
return (T)message;
}
/// <summary>
/// Create a Modbus request.
/// </summary>
/// <param name="frame">Bytes of Modbus frame.</param>
/// <returns>Modbus request.</returns>
public static IModbusMessage CreateModbusRequest(byte[] frame)
{
if (frame.Length < MinRequestFrameLength)
{
string msg = $"Argument 'frame' must have a length of at least {MinRequestFrameLength} bytes.";
throw new FormatException(msg);
}
IModbusMessage request;
byte functionCode = frame[1];
switch (functionCode)
{
case Modbus.ReadCoils:
case Modbus.ReadInputs:
request = CreateModbusMessage<ReadCoilsInputsRequest>(frame);
break;
case Modbus.ReadHoldingRegisters:
case Modbus.ReadInputRegisters:
request = CreateModbusMessage<ReadHoldingInputRegistersRequest>(frame);
break;
case Modbus.WriteSingleCoil:
request = CreateModbusMessage<WriteSingleCoilRequestResponse>(frame);
break;
case Modbus.WriteSingleRegister:
request = CreateModbusMessage<WriteSingleRegisterRequestResponse>(frame);
break;
case Modbus.Diagnostics:
request = CreateModbusMessage<DiagnosticsRequestResponse>(frame);
break;
case Modbus.WriteMultipleCoils:
request = CreateModbusMessage<WriteMultipleCoilsRequest>(frame);
break;
case Modbus.WriteMultipleRegisters:
request = CreateModbusMessage<WriteMultipleRegistersRequest>(frame);
break;
case Modbus.ReadWriteMultipleRegisters:
request = CreateModbusMessage<ReadWriteMultipleRegistersRequest>(frame);
break;
default:
string msg = $"Unsupported function code {functionCode}";
throw new ArgumentException(msg, nameof(frame));
}
return request;
}
}
}

View File

@ -0,0 +1,117 @@
namespace Modbus.Message
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using Data;
/// <summary>
/// Class holding all implementation shared between two or more message types.
/// Interfaces expose subsets of type specific implementations.
/// </summary>
internal class ModbusMessageImpl
{
public ModbusMessageImpl()
{
}
public ModbusMessageImpl(byte slaveAddress, byte functionCode)
{
SlaveAddress = slaveAddress;
FunctionCode = functionCode;
}
public byte? ByteCount { get; set; }
public byte? ExceptionCode { get; set; }
public ushort TransactionId { get; set; }
public byte FunctionCode { get; set; }
public ushort? NumberOfPoints { get; set; }
public byte SlaveAddress { get; set; }
public ushort? StartAddress { get; set; }
public ushort? SubFunctionCode { get; set; }
public IModbusMessageDataCollection Data { get; set; }
public byte[] MessageFrame
{
get
{
var pdu = ProtocolDataUnit;
var frame = new MemoryStream(1 + pdu.Length);
frame.WriteByte(SlaveAddress);
frame.Write(pdu, 0, pdu.Length);
return frame.ToArray();
}
}
public byte[] ProtocolDataUnit
{
get
{
List<byte> pdu = new List<byte>();
pdu.Add(FunctionCode);
if (ExceptionCode.HasValue)
{
pdu.Add(ExceptionCode.Value);
}
if (SubFunctionCode.HasValue)
{
pdu.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)SubFunctionCode.Value)));
}
if (StartAddress.HasValue)
{
pdu.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)StartAddress.Value)));
}
if (NumberOfPoints.HasValue)
{
pdu.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)NumberOfPoints.Value)));
}
if (ByteCount.HasValue)
{
pdu.Add(ByteCount.Value);
}
if (Data != null)
{
pdu.AddRange(Data.NetworkBytes);
}
return pdu.ToArray();
}
}
public void Initialize(byte[] frame)
{
if (frame == null)
{
throw new ArgumentNullException(nameof(frame), "Argument frame cannot be null.");
}
if (frame.Length < Modbus.MinimumFrameSize)
{
string msg = $"Message frame must contain at least {Modbus.MinimumFrameSize} bytes of data.";
throw new FormatException(msg);
}
SlaveAddress = frame[0];
FunctionCode = frame[1];
}
}
}

View File

@ -0,0 +1,76 @@
namespace Modbus.Message
{
using System;
using System.IO;
using System.Net;
public class ReadCoilsInputsRequest : AbstractModbusMessage, IModbusRequest
{
public ReadCoilsInputsRequest()
{
}
public ReadCoilsInputsRequest(byte functionCode, byte slaveAddress, ushort startAddress, ushort numberOfPoints)
: base(slaveAddress, functionCode)
{
StartAddress = startAddress;
NumberOfPoints = numberOfPoints;
}
public ushort StartAddress
{
get { return MessageImpl.StartAddress.Value; }
set { MessageImpl.StartAddress = value; }
}
public override int MinimumFrameSize
{
get { return 6; }
}
public ushort NumberOfPoints
{
get
{
return MessageImpl.NumberOfPoints.Value;
}
set
{
if (value > Modbus.MaximumDiscreteRequestResponseSize)
{
string msg = $"Maximum amount of data {Modbus.MaximumDiscreteRequestResponseSize} coils.";
throw new ArgumentOutOfRangeException(nameof(NumberOfPoints), msg);
}
MessageImpl.NumberOfPoints = value;
}
}
public override string ToString()
{
string msg = $"Read {NumberOfPoints} {(FunctionCode == Modbus.ReadCoils ? "coils" : "inputs")} starting at address {StartAddress}.";
return msg;
}
public void ValidateResponse(IModbusMessage response)
{
var typedResponse = (ReadCoilsInputsResponse)response;
// best effort validation - the same response for a request for 1 vs 6 coils (same byte count) will pass validation.
var expectedByteCount = (NumberOfPoints + 7) / 8;
if (expectedByteCount != typedResponse.ByteCount)
{
string msg = $"Unexpected byte count. Expected {expectedByteCount}, received {typedResponse.ByteCount}.";
throw new IOException(msg);
}
}
protected override void InitializeUnique(byte[] frame)
{
StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2));
NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4));
}
}
}

View File

@ -0,0 +1,50 @@
namespace Modbus.Message
{
using System;
using System.Linq;
using Data;
using Unme.Common;
public class ReadCoilsInputsResponse : AbstractModbusMessageWithData<DiscreteCollection>, IModbusMessage
{
public ReadCoilsInputsResponse()
{
}
public ReadCoilsInputsResponse(byte functionCode, byte slaveAddress, byte byteCount, DiscreteCollection data)
: base(slaveAddress, functionCode)
{
ByteCount = byteCount;
Data = data;
}
public byte ByteCount
{
get { return MessageImpl.ByteCount.Value; }
set { MessageImpl.ByteCount = value; }
}
public override int MinimumFrameSize
{
get { return 3; }
}
public override string ToString()
{
string msg = $"Read {Data.Count()} {(FunctionCode == Modbus.ReadInputs ? "inputs" : "coils")} - {Data}.";
return msg;
}
protected override void InitializeUnique(byte[] frame)
{
if (frame.Length < 3 + frame[2])
{
throw new FormatException("Message frame data segment does not contain enough bytes.");
}
ByteCount = frame[2];
Data = new DiscreteCollection(frame.Slice(3, ByteCount).ToArray());
}
}
}

View File

@ -0,0 +1,76 @@
namespace Modbus.Message
{
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
public class ReadHoldingInputRegistersRequest : AbstractModbusMessage, IModbusRequest
{
public ReadHoldingInputRegistersRequest()
{
}
public ReadHoldingInputRegistersRequest(byte functionCode, byte slaveAddress, ushort startAddress, ushort numberOfPoints)
: base(slaveAddress, functionCode)
{
StartAddress = startAddress;
NumberOfPoints = numberOfPoints;
}
public ushort StartAddress
{
get { return MessageImpl.StartAddress.Value; }
set { MessageImpl.StartAddress = value; }
}
public override int MinimumFrameSize
{
get { return 6; }
}
public ushort NumberOfPoints
{
get
{
return MessageImpl.NumberOfPoints.Value;
}
set
{
if (value > Modbus.MaximumRegisterRequestResponseSize)
{
string msg = $"Maximum amount of data {Modbus.MaximumRegisterRequestResponseSize} registers.";
throw new ArgumentOutOfRangeException(nameof(NumberOfPoints), msg);
}
MessageImpl.NumberOfPoints = value;
}
}
public override string ToString()
{
string msg = $"Read {NumberOfPoints} {(FunctionCode == Modbus.ReadHoldingRegisters ? "holding" : "input")} registers starting at address {StartAddress}.";
return msg;
}
public void ValidateResponse(IModbusMessage response)
{
var typedResponse = response as ReadHoldingInputRegistersResponse;
Debug.Assert(typedResponse != null, "Argument response should be of type ReadHoldingInputRegistersResponse.");
var expectedByteCount = NumberOfPoints * 2;
if (expectedByteCount != typedResponse.ByteCount)
{
string msg = $"Unexpected byte count. Expected {expectedByteCount}, received {typedResponse.ByteCount}.";
throw new IOException(msg);
}
}
protected override void InitializeUnique(byte[] frame)
{
StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2));
NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4));
}
}
}

View File

@ -0,0 +1,56 @@
namespace Modbus.Message
{
using System;
using System.Linq;
using Data;
using Unme.Common;
public class ReadHoldingInputRegistersResponse : AbstractModbusMessageWithData<RegisterCollection>, IModbusMessage
{
public ReadHoldingInputRegistersResponse()
{
}
public ReadHoldingInputRegistersResponse(byte functionCode, byte slaveAddress, RegisterCollection data)
: base(slaveAddress, functionCode)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
ByteCount = data.ByteCount;
Data = data;
}
public byte ByteCount
{
get { return MessageImpl.ByteCount.Value; }
set { MessageImpl.ByteCount = value; }
}
public override int MinimumFrameSize
{
get { return 3; }
}
public override string ToString()
{
string msg = $"Read {Data.Count} {(FunctionCode == Modbus.ReadHoldingRegisters ? "holding" : "input")} registers.";
return msg;
}
protected override void InitializeUnique(byte[] frame)
{
if (frame.Length < MinimumFrameSize + frame[2])
{
throw new FormatException("Message frame does not contain enough bytes.");
}
ByteCount = frame[2];
Data = new RegisterCollection(frame.Slice(3, ByteCount).ToArray());
}
}
}

View File

@ -0,0 +1,108 @@
namespace Modbus.Message
{
using System;
using System.IO;
using Data;
public class ReadWriteMultipleRegistersRequest : AbstractModbusMessage, IModbusRequest
{
private ReadHoldingInputRegistersRequest _readRequest;
private WriteMultipleRegistersRequest _writeRequest;
public ReadWriteMultipleRegistersRequest()
{
}
public ReadWriteMultipleRegistersRequest(
byte slaveAddress,
ushort startReadAddress,
ushort numberOfPointsToRead,
ushort startWriteAddress,
RegisterCollection writeData)
: base(slaveAddress, Modbus.ReadWriteMultipleRegisters)
{
_readRequest = new ReadHoldingInputRegistersRequest(
Modbus.ReadHoldingRegisters,
slaveAddress,
startReadAddress,
numberOfPointsToRead);
_writeRequest = new WriteMultipleRegistersRequest(
slaveAddress,
startWriteAddress,
writeData);
}
public override byte[] ProtocolDataUnit
{
get
{
byte[] readPdu = _readRequest.ProtocolDataUnit;
byte[] writePdu = _writeRequest.ProtocolDataUnit;
var stream = new MemoryStream(readPdu.Length + writePdu.Length);
stream.WriteByte(FunctionCode);
// read and write PDUs without function codes
stream.Write(readPdu, 1, readPdu.Length - 1);
stream.Write(writePdu, 1, writePdu.Length - 1);
return stream.ToArray();
}
}
public ReadHoldingInputRegistersRequest ReadRequest
{
get { return _readRequest; }
}
public WriteMultipleRegistersRequest WriteRequest
{
get { return _writeRequest; }
}
public override int MinimumFrameSize
{
get { return 11; }
}
public override string ToString()
{
string msg = $"Write {_writeRequest.NumberOfPoints} holding registers starting at address {_writeRequest.StartAddress}, and read {_readRequest.NumberOfPoints} registers starting at address {_readRequest.StartAddress}.";
return msg;
}
public void ValidateResponse(IModbusMessage response)
{
var typedResponse = (ReadHoldingInputRegistersResponse)response;
var expectedByteCount = ReadRequest.NumberOfPoints * 2;
if (expectedByteCount != typedResponse.ByteCount)
{
string msg = $"Unexpected byte count in response. Expected {expectedByteCount}, received {typedResponse.ByteCount}.";
throw new IOException(msg);
}
}
protected override void InitializeUnique(byte[] frame)
{
if (frame.Length < MinimumFrameSize + frame[10])
{
throw new FormatException("Message frame does not contain enough bytes.");
}
byte[] readFrame = new byte[2 + 4];
byte[] writeFrame = new byte[frame.Length - 6 + 2];
readFrame[0] = writeFrame[0] = SlaveAddress;
readFrame[1] = writeFrame[1] = FunctionCode;
Buffer.BlockCopy(frame, 2, readFrame, 2, 4);
Buffer.BlockCopy(frame, 6, writeFrame, 2, frame.Length - 6);
_readRequest = ModbusMessageFactory.CreateModbusMessage<ReadHoldingInputRegistersRequest>(readFrame);
_writeRequest = ModbusMessageFactory.CreateModbusMessage<WriteMultipleRegistersRequest>(writeFrame);
}
}
}

View File

@ -0,0 +1,80 @@
namespace Modbus.Message
{
using System;
using System.Collections.Generic;
using System.Globalization;
public class SlaveExceptionResponse : AbstractModbusMessage, IModbusMessage
{
private static readonly Dictionary<byte, string> _exceptionMessages = CreateExceptionMessages();
public SlaveExceptionResponse()
{
}
public SlaveExceptionResponse(byte slaveAddress, byte functionCode, byte exceptionCode)
: base(slaveAddress, functionCode)
{
SlaveExceptionCode = exceptionCode;
}
public override int MinimumFrameSize
{
get { return 3; }
}
public byte SlaveExceptionCode
{
get { return MessageImpl.ExceptionCode.Value; }
set { MessageImpl.ExceptionCode = value; }
}
/// <summary>
/// Returns a <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
/// </summary>
/// <returns>
/// A <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
/// </returns>
public override string ToString()
{
string msg = _exceptionMessages.ContainsKey(SlaveExceptionCode)
? _exceptionMessages[SlaveExceptionCode]
: Resources.Unknown;
return string.Format(
CultureInfo.InvariantCulture,
Resources.SlaveExceptionResponseFormat,
Environment.NewLine,
FunctionCode,
SlaveExceptionCode,
msg);
}
internal static Dictionary<byte, string> CreateExceptionMessages()
{
Dictionary<byte, string> messages = new Dictionary<byte, string>(9);
messages.Add(1, Resources.IllegalFunction);
messages.Add(2, Resources.IllegalDataAddress);
messages.Add(3, Resources.IllegalDataValue);
messages.Add(4, Resources.SlaveDeviceFailure);
messages.Add(5, Resources.Acknowlege);
messages.Add(6, Resources.SlaveDeviceBusy);
messages.Add(8, Resources.MemoryParityError);
messages.Add(10, Resources.GatewayPathUnavailable);
messages.Add(11, Resources.GatewayTargetDeviceFailedToRespond);
return messages;
}
protected override void InitializeUnique(byte[] frame)
{
if (FunctionCode <= Modbus.ExceptionOffset)
{
throw new FormatException(Resources.SlaveExceptionResponseInvalidFunctionCode);
}
SlaveExceptionCode = frame[2];
}
}
}

View File

@ -0,0 +1,108 @@
namespace Modbus.Message
{
using System;
using System.IO;
using System.Linq;
using System.Net;
using Data;
using Unme.Common;
/// <summary>
/// Write Multiple Coils request.
/// </summary>
public class WriteMultipleCoilsRequest : AbstractModbusMessageWithData<DiscreteCollection>, IModbusRequest
{
/// <summary>
/// Write Multiple Coils request.
/// </summary>
public WriteMultipleCoilsRequest()
{
}
/// <summary>
/// Write Multiple Coils request.
/// </summary>
public WriteMultipleCoilsRequest(byte slaveAddress, ushort startAddress, DiscreteCollection data)
: base(slaveAddress, Modbus.WriteMultipleCoils)
{
StartAddress = startAddress;
NumberOfPoints = (ushort)data.Count;
ByteCount = (byte)((data.Count + 7) / 8);
Data = data;
}
public byte ByteCount
{
get { return MessageImpl.ByteCount.Value; }
set { MessageImpl.ByteCount = value; }
}
public ushort NumberOfPoints
{
get
{
return MessageImpl.NumberOfPoints.Value;
}
set
{
if (value > Modbus.MaximumDiscreteRequestResponseSize)
{
string msg = $"Maximum amount of data {Modbus.MaximumDiscreteRequestResponseSize} coils.";
throw new ArgumentOutOfRangeException("NumberOfPoints", msg);
}
MessageImpl.NumberOfPoints = value;
}
}
public ushort StartAddress
{
get { return MessageImpl.StartAddress.Value; }
set { MessageImpl.StartAddress = value; }
}
public override int MinimumFrameSize
{
get { return 7; }
}
public override string ToString()
{
string msg = $"Write {NumberOfPoints} coils starting at address {StartAddress}.";
return msg;
}
public void ValidateResponse(IModbusMessage response)
{
var typedResponse = (WriteMultipleCoilsResponse)response;
if (StartAddress != typedResponse.StartAddress)
{
string msg = $"Unexpected start address in response. Expected {StartAddress}, received {typedResponse.StartAddress}.";
throw new IOException(msg);
}
if (NumberOfPoints != typedResponse.NumberOfPoints)
{
string msg = $"Unexpected number of points in response. Expected {NumberOfPoints}, received {typedResponse.NumberOfPoints}.";
throw new IOException(msg);
}
}
protected override void InitializeUnique(byte[] frame)
{
if (frame.Length < MinimumFrameSize + frame[6])
{
throw new FormatException("Message frame does not contain enough bytes.");
}
StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2));
NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4));
ByteCount = frame[6];
Data = new DiscreteCollection(frame.Slice(7, ByteCount).ToArray());
}
}
}

View File

@ -0,0 +1,61 @@
namespace Modbus.Message
{
using System;
using System.Net;
public class WriteMultipleCoilsResponse : AbstractModbusMessage, IModbusMessage
{
public WriteMultipleCoilsResponse()
{
}
public WriteMultipleCoilsResponse(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
: base(slaveAddress, Modbus.WriteMultipleCoils)
{
StartAddress = startAddress;
NumberOfPoints = numberOfPoints;
}
public ushort NumberOfPoints
{
get
{
return MessageImpl.NumberOfPoints.Value;
}
set
{
if (value > Modbus.MaximumDiscreteRequestResponseSize)
{
string msg = $"Maximum amount of data {Modbus.MaximumDiscreteRequestResponseSize} coils.";
throw new ArgumentOutOfRangeException("NumberOfPoints", msg);
}
MessageImpl.NumberOfPoints = value;
}
}
public ushort StartAddress
{
get { return MessageImpl.StartAddress.Value; }
set { MessageImpl.StartAddress = value; }
}
public override int MinimumFrameSize
{
get { return 6; }
}
public override string ToString()
{
string msg = $"Wrote {NumberOfPoints} coils starting at address {StartAddress}.";
return msg;
}
protected override void InitializeUnique(byte[] frame)
{
StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2));
NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4));
}
}
}

View File

@ -0,0 +1,99 @@
namespace Modbus.Message
{
using System;
using System.IO;
using System.Linq;
using System.Net;
using Data;
using Unme.Common;
public class WriteMultipleRegistersRequest : AbstractModbusMessageWithData<RegisterCollection>, IModbusRequest
{
public WriteMultipleRegistersRequest()
{
}
public WriteMultipleRegistersRequest(byte slaveAddress, ushort startAddress, RegisterCollection data)
: base(slaveAddress, Modbus.WriteMultipleRegisters)
{
StartAddress = startAddress;
NumberOfPoints = (ushort)data.Count;
ByteCount = (byte)(data.Count * 2);
Data = data;
}
public byte ByteCount
{
get { return MessageImpl.ByteCount.Value; }
set { MessageImpl.ByteCount = value; }
}
public ushort NumberOfPoints
{
get
{
return MessageImpl.NumberOfPoints.Value;
}
set
{
if (value > Modbus.MaximumRegisterRequestResponseSize)
{
string msg = $"Maximum amount of data {Modbus.MaximumRegisterRequestResponseSize} registers.";
throw new ArgumentOutOfRangeException(nameof(NumberOfPoints), msg);
}
MessageImpl.NumberOfPoints = value;
}
}
public ushort StartAddress
{
get { return MessageImpl.StartAddress.Value; }
set { MessageImpl.StartAddress = value; }
}
public override int MinimumFrameSize
{
get { return 7; }
}
public override string ToString()
{
string msg = $"Write {NumberOfPoints} holding registers starting at address {StartAddress}.";
return msg;
}
public void ValidateResponse(IModbusMessage response)
{
var typedResponse = (WriteMultipleRegistersResponse)response;
if (StartAddress != typedResponse.StartAddress)
{
string msg = $"Unexpected start address in response. Expected {StartAddress}, received {typedResponse.StartAddress}.";
throw new IOException(msg);
}
if (NumberOfPoints != typedResponse.NumberOfPoints)
{
string msg = $"Unexpected number of points in response. Expected {NumberOfPoints}, received {typedResponse.NumberOfPoints}.";
throw new IOException(msg);
}
}
protected override void InitializeUnique(byte[] frame)
{
if (frame.Length < MinimumFrameSize + frame[6])
{
throw new FormatException("Message frame does not contain enough bytes.");
}
StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2));
NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4));
ByteCount = frame[6];
Data = new RegisterCollection(frame.Slice(7, ByteCount).ToArray());
}
}
}

View File

@ -0,0 +1,61 @@
namespace Modbus.Message
{
using System;
using System.Net;
public class WriteMultipleRegistersResponse : AbstractModbusMessage, IModbusMessage
{
public WriteMultipleRegistersResponse()
{
}
public WriteMultipleRegistersResponse(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
: base(slaveAddress, Modbus.WriteMultipleRegisters)
{
StartAddress = startAddress;
NumberOfPoints = numberOfPoints;
}
public ushort NumberOfPoints
{
get
{
return MessageImpl.NumberOfPoints.Value;
}
set
{
if (value > Modbus.MaximumRegisterRequestResponseSize)
{
string msg = $"Maximum amount of data {Modbus.MaximumRegisterRequestResponseSize} registers.";
throw new ArgumentOutOfRangeException(nameof(NumberOfPoints), msg);
}
MessageImpl.NumberOfPoints = value;
}
}
public ushort StartAddress
{
get { return MessageImpl.StartAddress.Value; }
set { MessageImpl.StartAddress = value; }
}
public override int MinimumFrameSize
{
get { return 6; }
}
public override string ToString()
{
string msg = $"Wrote {NumberOfPoints} holding registers starting at address {StartAddress}.";
return msg;
}
protected override void InitializeUnique(byte[] frame)
{
StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2));
NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4));
}
}
}

View File

@ -0,0 +1,69 @@
namespace Modbus.Message
{
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using Data;
using Unme.Common;
public class WriteSingleCoilRequestResponse : AbstractModbusMessageWithData<RegisterCollection>, IModbusRequest
{
public WriteSingleCoilRequestResponse()
{
}
public WriteSingleCoilRequestResponse(byte slaveAddress, ushort startAddress, bool coilState)
: base(slaveAddress, Modbus.WriteSingleCoil)
{
StartAddress = startAddress;
Data = new RegisterCollection(coilState ? Modbus.CoilOn : Modbus.CoilOff);
}
public override int MinimumFrameSize
{
get { return 6; }
}
public ushort StartAddress
{
get { return MessageImpl.StartAddress.Value; }
set { MessageImpl.StartAddress = value; }
}
public override string ToString()
{
Debug.Assert(Data != null, "Argument Data cannot be null.");
Debug.Assert(Data.Count() == 1, "Data should have a count of 1.");
string msg = $"Write single coil {(Data.First() == Modbus.CoilOn ? 1 : 0)} at address {StartAddress}.";
return msg;
}
public void ValidateResponse(IModbusMessage response)
{
var typedResponse = (WriteSingleCoilRequestResponse)response;
if (StartAddress != typedResponse.StartAddress)
{
string msg = $"Unexpected start address in response. Expected {StartAddress}, received {typedResponse.StartAddress}.";
throw new IOException(msg);
}
if (Data.First() != typedResponse.Data.First())
{
string msg = $"Unexpected data in response. Expected {Data.First()}, received {typedResponse.Data.First()}.";
throw new IOException(msg);
}
}
protected override void InitializeUnique(byte[] frame)
{
StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2));
Data = new RegisterCollection(frame.Slice(4, 2).ToArray());
}
}
}

View File

@ -0,0 +1,67 @@
namespace Modbus.Message
{
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using Data;
public class WriteSingleRegisterRequestResponse : AbstractModbusMessageWithData<RegisterCollection>, IModbusRequest
{
public WriteSingleRegisterRequestResponse()
{
}
public WriteSingleRegisterRequestResponse(byte slaveAddress, ushort startAddress, ushort registerValue)
: base(slaveAddress, Modbus.WriteSingleRegister)
{
StartAddress = startAddress;
Data = new RegisterCollection(registerValue);
}
public override int MinimumFrameSize
{
get { return 6; }
}
public ushort StartAddress
{
get { return MessageImpl.StartAddress.Value; }
set { MessageImpl.StartAddress = value; }
}
public override string ToString()
{
Debug.Assert(Data != null, "Argument Data cannot be null.");
Debug.Assert(Data.Count() == 1, "Data should have a count of 1.");
string msg = $"Write single holding register {Data[0]} at address {StartAddress}.";
return msg;
}
public void ValidateResponse(IModbusMessage response)
{
var typedResponse = (WriteSingleRegisterRequestResponse)response;
if (StartAddress != typedResponse.StartAddress)
{
string msg = $"Unexpected start address in response. Expected {StartAddress}, received {typedResponse.StartAddress}.";
throw new IOException(msg);
}
if (Data.First() != typedResponse.Data.First())
{
string msg = $"Unexpected data in response. Expected {Data.First()}, received {typedResponse.Data.First()}.";
throw new IOException(msg);
}
}
protected override void InitializeUnique(byte[] frame)
{
StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2));
Data = new RegisterCollection((ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4)));
}
}
}

View File

@ -0,0 +1,60 @@
namespace Modbus
{
/// <summary>
/// Defines constants related to the Modbus protocol.
/// </summary>
internal static class Modbus
{
// supported function codes
public const byte ReadCoils = 1;
public const byte ReadInputs = 2;
public const byte ReadHoldingRegisters = 3;
public const byte ReadInputRegisters = 4;
public const byte WriteSingleCoil = 5;
public const byte WriteSingleRegister = 6;
public const byte Diagnostics = 8;
public const ushort DiagnosticsReturnQueryData = 0;
public const byte WriteMultipleCoils = 15;
public const byte WriteMultipleRegisters = 16;
public const byte ReadWriteMultipleRegisters = 23;
public const int MaximumDiscreteRequestResponseSize = 2040;
public const int MaximumRegisterRequestResponseSize = 127;
// modbus slave exception offset that is added to the function code, to flag an exception
public const byte ExceptionOffset = 128;
// modbus slave exception codes
public const byte IllegalFunction = 1;
public const byte IllegalDataAddress = 2;
public const byte Acknowledge = 5;
public const byte SlaveDeviceBusy = 6;
// default setting for number of retries for IO operations
public const int DefaultRetries = 3;
// default number of milliseconds to wait after encountering an ACKNOWLEGE or SLAVE DEVIC BUSY slave exception response.
public const int DefaultWaitToRetryMilliseconds = 250;
// default setting for IO timeouts in milliseconds
public const int DefaultTimeout = 1000;
// smallest supported message frame size (sans checksum)
public const int MinimumFrameSize = 2;
public const ushort CoilOn = 0xFF00;
public const ushort CoilOff = 0x0000;
// IP slaves should be addressed by IP
public const byte DefaultIpSlaveUnitId = 0;
// An existing connection was forcibly closed by the remote host
public const int ConnectionResetByPeer = 10054;
// Existing socket connection is being closed
public const int WSACancelBlockingCall = 10004;
// used by the ASCII tranport to indicate end of message
public const string NewLine = "\r\n";
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("NModbus4")]
[assembly: AssemblyProduct("NModbus4")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyCopyright("Copyright © 2006 Scott Alexander, 2015 Dmitry Turin")]
[assembly: AssemblyDescription("NModbus4 is a C# implementation of the Modbus protocol. " +
"Provides connectivity to Modbus slave compatible devices and applications. " +
"Supports ASCII, RTU, TCP, and UDP protocols. " +
"NModbus4 it's a fork of NModbus(https://code.google.com/p/nmodbus)")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: CLSCompliant(false)]
[assembly: NeutralResourcesLanguage("en-US")]
[assembly: ComVisible(false)]
[assembly: AssemblyVersion("3.0.0.0")]
[assembly: AssemblyFileVersion("3.0.0.0")]
[assembly: AssemblyInformationalVersion("3.0.0-dev")]
#if !SIGNED
[assembly: InternalsVisibleTo("NModbus4.UnitTests")]
[assembly: InternalsVisibleTo("NModbus4.IntegrationTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
#endif

View File

@ -0,0 +1,41 @@
namespace Modbus
{
internal static class Resources
{
public const string Acknowlege = "Specialized use in conjunction with programming commands.The server (or slave) has accepted the request and is processing it, but a long duration of time will be required to do so.This response is returned to prevent a timeout error from occurring in the client(or master). The client(or master) can next issue a Poll Program Complete message to determine if processing is completed.";
public const string EmptyEndPoint = "Argument endPoint cannot be empty.";
public const string GatewayPathUnavailable = "Specialized use in conjunction with gateways, indicates that the gateway was unable to allocate an internal communication path from the input port to the output port for processing the request.Usually means that the gateway is misconfigured or overloaded.";
public const string GatewayTargetDeviceFailedToRespond = "Specialized use in conjunction with gateways, indicates that no response was obtained from the target device.Usually means that the device is not present on the network.";
public const string HexCharacterCountNotEven = "Hex string must have even number of characters.";
public const string IllegalDataAddress = "The data address received in the query is not an allowable address for the server (or slave). More specifically, the combination of reference number and transfer length is invalid.For a controller with 100 registers, the PDU addresses the first register as 0, and the last one as 99. If a request is submitted with a starting register address of 96 and a quantity of registers of 4, then this request will successfully operate(address-wise at least) on registers 96, 97, 98, 99. If a request is submitted with a starting register address of 96 and a quantity of registers of 5, then this request will fail with Exception Code 0x02 “Illegal Data Address” since it attempts to operate on registers 96, 97, 98, 99 and 100, and there is no register with address 100.";
public const string IllegalDataValue = "A value contained in the query data field is not an allowable value for server(or slave). This indicates a fault in the structure of the remainder of a complex request, such as that the implied length is incorrect.It specifically does NOT mean that a data item submitted for storage in a register has a value outside the expectation of the application program, since the MODBUS protocol is unaware of the significance of any particular value of any particular register.";
public const string IllegalFunction = "The function code received in the query is not an allowable action for the server (or slave). This may be because the function code is only applicable to newer devices, and was not implemented in the unit selected.It could also indicate that the server(or slave) is in the wrong state to process a request of this type, for example because it is unconfigured and is being asked to return register values.";
public const string MemoryParityError = "Specialized use in conjunction with function codes 20 and 21 and reference type 6, to indicate that the extended file area failed to pass a consistency check.";
public const string NetworkBytesNotEven = "Array networkBytes must contain an even number of bytes.";
public const string SlaveDeviceBusy = "Specialized use in conjunction with programming commands. The server (or slave) is engaged in processing a longduration program command.The client(or master) should retransmit the message later when the server(or slave) is free.";
public const string SlaveDeviceFailure = "An unrecoverable error occurred while the server(or slave) was attempting to perform the requested action.";
public const string SlaveExceptionResponseFormat = "Function Code: {1}{0}Exception Code: {2} - {3}";
public const string SlaveExceptionResponseInvalidFunctionCode = "Invalid function code value for SlaveExceptionResponse.";
public const string TimeoutNotSupported = "The compact framework UDP client does not support timeouts.";
public const string UdpClientNotConnected = "UdpClient must be bound to a default remote host. Call the Connect method.";
public const string Unknown = "Unknown slave exception code.";
public const string WaitRetryGreaterThanZero = "WaitToRetryMilliseconds must be greater than 0.";
}
}

View File

@ -0,0 +1,71 @@
namespace Modbus.Serial
{
using System;
using System.Diagnostics;
using System.IO.Ports;
using global::Modbus.IO;
/// <summary>
/// Concrete Implementor - http://en.wikipedia.org/wiki/Bridge_Pattern
/// </summary>
public class SerialPortAdapter : IStreamResource
{
private const string NewLine = "\r\n";
private SerialPort _serialPort;
public SerialPortAdapter(SerialPort serialPort)
{
Debug.Assert(serialPort != null, "Argument serialPort cannot be null.");
_serialPort = serialPort;
_serialPort.NewLine = NewLine;
}
public int InfiniteTimeout
{
get { return SerialPort.InfiniteTimeout; }
}
public int ReadTimeout
{
get { return _serialPort.ReadTimeout; }
set { _serialPort.ReadTimeout = value; }
}
public int WriteTimeout
{
get { return _serialPort.WriteTimeout; }
set { _serialPort.WriteTimeout = value; }
}
public void DiscardInBuffer()
{
_serialPort.DiscardInBuffer();
}
public int Read(byte[] buffer, int offset, int count)
{
return _serialPort.Read(buffer, offset, count);
}
public void Write(byte[] buffer, int offset, int count)
{
_serialPort.Write(buffer, offset, count);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_serialPort?.Dispose();
_serialPort = null;
}
}
}
}

View File

@ -0,0 +1,173 @@
namespace Modbus
{
using System;
using System.Diagnostics.CodeAnalysis;
#if NET46
using System.Runtime.Serialization;
using System.Security.Permissions;
#endif
using Message;
/// <summary>
/// Represents slave errors that occur during communication.
/// </summary>
#if NET46
[Serializable]
#endif
public class SlaveException : Exception
{
private const string SlaveAddressPropertyName = "SlaveAdress";
private const string FunctionCodePropertyName = "FunctionCode";
private const string SlaveExceptionCodePropertyName = "SlaveExceptionCode";
private readonly SlaveExceptionResponse _slaveExceptionResponse;
/// <summary>
/// Initializes a new instance of the <see cref="SlaveException" /> class.
/// </summary>
public SlaveException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SlaveException" /> class.
/// </summary>
/// <param name="message">The message.</param>
public SlaveException(string message)
: base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SlaveException" /> class.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="innerException">The inner exception.</param>
public SlaveException(string message, Exception innerException)
: base(message, innerException)
{
}
internal SlaveException(SlaveExceptionResponse slaveExceptionResponse)
{
_slaveExceptionResponse = slaveExceptionResponse;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Used by test code.")]
internal SlaveException(string message, SlaveExceptionResponse slaveExceptionResponse)
: base(message)
{
_slaveExceptionResponse = slaveExceptionResponse;
}
#if NET46
/// <summary>
/// Initializes a new instance of the <see cref="SlaveException" /> class.
/// </summary>
/// <param name="info">
/// The <see cref="T:System.Runtime.Serialization.SerializationInfo"></see> that holds the serialized
/// object data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="T:System.Runtime.Serialization.StreamingContext"></see> that contains contextual
/// information about the source or destination.
/// </param>
/// <exception cref="T:System.Runtime.Serialization.SerializationException">
/// The class name is null or
/// <see cref="P:System.Exception.HResult"></see> is zero (0).
/// </exception>
/// <exception cref="T:System.ArgumentNullException">The info parameter is null. </exception>
protected SlaveException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
if (info != null)
{
_slaveExceptionResponse = new SlaveExceptionResponse(
info.GetByte(SlaveAddressPropertyName),
info.GetByte(FunctionCodePropertyName),
info.GetByte(SlaveExceptionCodePropertyName));
}
}
#endif
/// <summary>
/// Gets a message that describes the current exception.
/// </summary>
/// <value>
/// The error message that explains the reason for the exception, or an empty string.
/// </value>
public override string Message
{
get
{
string responseString;
responseString = _slaveExceptionResponse != null ? string.Concat(Environment.NewLine, _slaveExceptionResponse) : string.Empty;
return string.Concat(base.Message, responseString);
}
}
/// <summary>
/// Gets the response function code that caused the exception to occur, or 0.
/// </summary>
/// <value>The function code.</value>
public byte FunctionCode
{
get { return _slaveExceptionResponse != null ? _slaveExceptionResponse.FunctionCode : (byte)0; }
}
/// <summary>
/// Gets the slave exception code, or 0.
/// </summary>
/// <value>The slave exception code.</value>
public byte SlaveExceptionCode
{
get { return _slaveExceptionResponse != null ? _slaveExceptionResponse.SlaveExceptionCode : (byte)0; }
}
/// <summary>
/// Gets the slave address, or 0.
/// </summary>
/// <value>The slave address.</value>
public byte SlaveAddress
{
get { return _slaveExceptionResponse != null ? _slaveExceptionResponse.SlaveAddress : (byte)0; }
}
#if NET46
/// <summary>
/// When overridden in a derived class, sets the <see cref="T:System.Runtime.Serialization.SerializationInfo"></see>
/// with information about the exception.
/// </summary>
/// <param name="info">
/// The <see cref="T:System.Runtime.Serialization.SerializationInfo"></see> that holds the serialized
/// object data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="T:System.Runtime.Serialization.StreamingContext"></see> that contains contextual
/// information about the source or destination.
/// </param>
/// <exception cref="T:System.ArgumentNullException">The info parameter is a null reference (Nothing in Visual Basic). </exception>
/// <PermissionSet>
/// <IPermission
/// class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
/// version="1" Read="*AllFiles*" PathDiscovery="*AllFiles*" />
/// <IPermission
/// class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
/// version="1" Flags="SerializationFormatter" />
/// </PermissionSet>
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
[SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods", Justification = "Argument info is validated, rule does not understand AND condition.")]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
if (info != null && _slaveExceptionResponse != null)
{
info.AddValue(SlaveAddressPropertyName, _slaveExceptionResponse.SlaveAddress);
info.AddValue(FunctionCodePropertyName, _slaveExceptionResponse.FunctionCode);
info.AddValue(SlaveExceptionCodePropertyName, _slaveExceptionResponse.SlaveExceptionCode);
}
}
#endif
}
}

View File

@ -0,0 +1,19 @@
namespace Modbus.Unme.Common
{
using System;
internal static class DisposableUtility
{
public static void Dispose<T>(ref T item)
where T : class, IDisposable
{
if (item == null)
{
return;
}
item.Dispose();
item = default(T);
}
}
}

View File

@ -0,0 +1,32 @@
namespace Modbus.Unme.Common
{
using System;
using System.Collections.Generic;
using System.Linq;
internal static class SequenceUtility
{
public static IEnumerable<T> Slice<T>(this IEnumerable<T> source, int startIndex, int size)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
var enumerable = source as T[] ?? source.ToArray();
int num = enumerable.Count();
if (startIndex < 0 || num < startIndex)
{
throw new ArgumentOutOfRangeException(nameof(startIndex));
}
if (size < 0 || startIndex + size > num)
{
throw new ArgumentOutOfRangeException(nameof(size));
}
return enumerable.Skip(startIndex).Take(size);
}
}
}

View File

@ -0,0 +1,122 @@
namespace Modbus.Utility
{
using System;
using System.Diagnostics.CodeAnalysis;
/// <summary>
/// Possible options for DiscriminatedUnion type.
/// </summary>
public enum DiscriminatedUnionOption
{
/// <summary>
/// Option A.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "A")]
A,
/// <summary>
/// Option B.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "B")]
B
}
/// <summary>
/// A data type that can store one of two possible strongly typed options.
/// </summary>
/// <typeparam name="TA">The type of option A.</typeparam>
/// <typeparam name="TB">The type of option B.</typeparam>
public class DiscriminatedUnion<TA, TB>
{
private TA optionA;
private TB optionB;
private DiscriminatedUnionOption option;
/// <summary>
/// Gets the value of option A.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "A")]
public TA A
{
get
{
if (this.Option != DiscriminatedUnionOption.A)
{
string msg = $"{DiscriminatedUnionOption.A} is not a valid option for this discriminated union instance.";
throw new InvalidOperationException(msg);
}
return this.optionA;
}
}
/// <summary>
/// Gets the value of option B.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "B")]
public TB B
{
get
{
if (this.Option != DiscriminatedUnionOption.B)
{
string msg = $"{DiscriminatedUnionOption.B} is not a valid option for this discriminated union instance.";
throw new InvalidOperationException(msg);
}
return this.optionB;
}
}
/// <summary>
/// Gets the discriminated value option set for this instance.
/// </summary>
public DiscriminatedUnionOption Option
{
get { return this.option; }
}
/// <summary>
/// Factory method for creating DiscriminatedUnion with option A set.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "Factory method.")]
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "0#a")]
public static DiscriminatedUnion<TA, TB> CreateA(TA a)
{
return new DiscriminatedUnion<TA, TB>() { option = DiscriminatedUnionOption.A, optionA = a };
}
/// <summary>
/// Factory method for creating DiscriminatedUnion with option B set.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "Factory method.")]
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "0#b")]
public static DiscriminatedUnion<TA, TB> CreateB(TB b)
{
return new DiscriminatedUnion<TA, TB>() { option = DiscriminatedUnionOption.B, optionB = b };
}
/// <summary>
/// Returns a <see cref="T:System.String" /> that represents the current <see cref="T:System.Object" />.
/// </summary>
/// <returns>
/// A <see cref="T:System.String" /> that represents the current <see cref="T:System.Object" />.
/// </returns>
public override string ToString()
{
string value = null;
switch (Option)
{
case DiscriminatedUnionOption.A:
value = A.ToString();
break;
case DiscriminatedUnionOption.B:
value = B.ToString();
break;
}
return value;
}
}
}

View File

@ -0,0 +1,217 @@
namespace Modbus.Utility
{
using System;
using System.Linq;
using System.Net;
using System.Text;
/// <summary>
/// Modbus utility methods.
/// </summary>
public static class ModbusUtility
{
private static readonly ushort[] CrcTable =
{
0X0000, 0XC0C1, 0XC181, 0X0140, 0XC301, 0X03C0, 0X0280, 0XC241,
0XC601, 0X06C0, 0X0780, 0XC741, 0X0500, 0XC5C1, 0XC481, 0X0440,
0XCC01, 0X0CC0, 0X0D80, 0XCD41, 0X0F00, 0XCFC1, 0XCE81, 0X0E40,
0X0A00, 0XCAC1, 0XCB81, 0X0B40, 0XC901, 0X09C0, 0X0880, 0XC841,
0XD801, 0X18C0, 0X1980, 0XD941, 0X1B00, 0XDBC1, 0XDA81, 0X1A40,
0X1E00, 0XDEC1, 0XDF81, 0X1F40, 0XDD01, 0X1DC0, 0X1C80, 0XDC41,
0X1400, 0XD4C1, 0XD581, 0X1540, 0XD701, 0X17C0, 0X1680, 0XD641,
0XD201, 0X12C0, 0X1380, 0XD341, 0X1100, 0XD1C1, 0XD081, 0X1040,
0XF001, 0X30C0, 0X3180, 0XF141, 0X3300, 0XF3C1, 0XF281, 0X3240,
0X3600, 0XF6C1, 0XF781, 0X3740, 0XF501, 0X35C0, 0X3480, 0XF441,
0X3C00, 0XFCC1, 0XFD81, 0X3D40, 0XFF01, 0X3FC0, 0X3E80, 0XFE41,
0XFA01, 0X3AC0, 0X3B80, 0XFB41, 0X3900, 0XF9C1, 0XF881, 0X3840,
0X2800, 0XE8C1, 0XE981, 0X2940, 0XEB01, 0X2BC0, 0X2A80, 0XEA41,
0XEE01, 0X2EC0, 0X2F80, 0XEF41, 0X2D00, 0XEDC1, 0XEC81, 0X2C40,
0XE401, 0X24C0, 0X2580, 0XE541, 0X2700, 0XE7C1, 0XE681, 0X2640,
0X2200, 0XE2C1, 0XE381, 0X2340, 0XE101, 0X21C0, 0X2080, 0XE041,
0XA001, 0X60C0, 0X6180, 0XA141, 0X6300, 0XA3C1, 0XA281, 0X6240,
0X6600, 0XA6C1, 0XA781, 0X6740, 0XA501, 0X65C0, 0X6480, 0XA441,
0X6C00, 0XACC1, 0XAD81, 0X6D40, 0XAF01, 0X6FC0, 0X6E80, 0XAE41,
0XAA01, 0X6AC0, 0X6B80, 0XAB41, 0X6900, 0XA9C1, 0XA881, 0X6840,
0X7800, 0XB8C1, 0XB981, 0X7940, 0XBB01, 0X7BC0, 0X7A80, 0XBA41,
0XBE01, 0X7EC0, 0X7F80, 0XBF41, 0X7D00, 0XBDC1, 0XBC81, 0X7C40,
0XB401, 0X74C0, 0X7580, 0XB541, 0X7700, 0XB7C1, 0XB681, 0X7640,
0X7200, 0XB2C1, 0XB381, 0X7340, 0XB101, 0X71C0, 0X7080, 0XB041,
0X5000, 0X90C1, 0X9181, 0X5140, 0X9301, 0X53C0, 0X5280, 0X9241,
0X9601, 0X56C0, 0X5780, 0X9741, 0X5500, 0X95C1, 0X9481, 0X5440,
0X9C01, 0X5CC0, 0X5D80, 0X9D41, 0X5F00, 0X9FC1, 0X9E81, 0X5E40,
0X5A00, 0X9AC1, 0X9B81, 0X5B40, 0X9901, 0X59C0, 0X5880, 0X9841,
0X8801, 0X48C0, 0X4980, 0X8941, 0X4B00, 0X8BC1, 0X8A81, 0X4A40,
0X4E00, 0X8EC1, 0X8F81, 0X4F40, 0X8D01, 0X4DC0, 0X4C80, 0X8C41,
0X4400, 0X84C1, 0X8581, 0X4540, 0X8701, 0X47C0, 0X4680, 0X8641,
0X8201, 0X42C0, 0X4380, 0X8341, 0X4100, 0X81C1, 0X8081, 0X4040
};
/// <summary>
/// Converts four UInt16 values into a IEEE 64 floating point format.
/// </summary>
/// <param name="b3">Highest-order ushort value.</param>
/// <param name="b2">Second-to-highest-order ushort value.</param>
/// <param name="b1">Second-to-lowest-order ushort value.</param>
/// <param name="b0">Lowest-order ushort value.</param>
/// <returns>IEEE 64 floating point value.</returns>
public static double GetDouble(ushort b3, ushort b2, ushort b1, ushort b0)
{
byte[] value = BitConverter.GetBytes(b0)
.Concat(BitConverter.GetBytes(b1))
.Concat(BitConverter.GetBytes(b2))
.Concat(BitConverter.GetBytes(b3))
.ToArray();
return BitConverter.ToDouble(value, 0);
}
/// <summary>
/// Converts two UInt16 values into a IEEE 32 floating point format.
/// </summary>
/// <param name="highOrderValue">High order ushort value.</param>
/// <param name="lowOrderValue">Low order ushort value.</param>
/// <returns>IEEE 32 floating point value.</returns>
public static float GetSingle(ushort highOrderValue, ushort lowOrderValue)
{
byte[] value = BitConverter.GetBytes(lowOrderValue)
.Concat(BitConverter.GetBytes(highOrderValue))
.ToArray();
return BitConverter.ToSingle(value, 0);
}
/// <summary>
/// Converts two UInt16 values into a UInt32.
/// </summary>
public static uint GetUInt32(ushort highOrderValue, ushort lowOrderValue)
{
byte[] value = BitConverter.GetBytes(lowOrderValue)
.Concat(BitConverter.GetBytes(highOrderValue))
.ToArray();
return BitConverter.ToUInt32(value, 0);
}
/// <summary>
/// Converts an array of bytes to an ASCII byte array.
/// </summary>
/// <param name="numbers">The byte array.</param>
/// <returns>An array of ASCII byte values.</returns>
public static byte[] GetAsciiBytes(params byte[] numbers)
{
return Encoding.UTF8.GetBytes(numbers.SelectMany(n => n.ToString("X2")).ToArray());
}
/// <summary>
/// Converts an array of UInt16 to an ASCII byte array.
/// </summary>
/// <param name="numbers">The ushort array.</param>
/// <returns>An array of ASCII byte values.</returns>
public static byte[] GetAsciiBytes(params ushort[] numbers)
{
return Encoding.UTF8.GetBytes(numbers.SelectMany(n => n.ToString("X4")).ToArray());
}
/// <summary>
/// Converts a network order byte array to an array of UInt16 values in host order.
/// </summary>
/// <param name="networkBytes">The network order byte array.</param>
/// <returns>The host order ushort array.</returns>
public static ushort[] NetworkBytesToHostUInt16(byte[] networkBytes)
{
if (networkBytes == null)
{
throw new ArgumentNullException(nameof(networkBytes));
}
if (networkBytes.Length % 2 != 0)
{
throw new FormatException(Resources.NetworkBytesNotEven);
}
ushort[] result = new ushort[networkBytes.Length / 2];
for (int i = 0; i < result.Length; i++)
{
result[i] = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(networkBytes, i * 2));
}
return result;
}
/// <summary>
/// Converts a hex string to a byte array.
/// </summary>
/// <param name="hex">The hex string.</param>
/// <returns>Array of bytes.</returns>
public static byte[] HexToBytes(string hex)
{
if (hex == null)
{
throw new ArgumentNullException(nameof(hex));
}
if (hex.Length % 2 != 0)
{
throw new FormatException(Resources.HexCharacterCountNotEven);
}
byte[] bytes = new byte[hex.Length / 2];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return bytes;
}
/// <summary>
/// Calculate Longitudinal Redundancy Check.
/// </summary>
/// <param name="data">The data used in LRC.</param>
/// <returns>LRC value.</returns>
public static byte CalculateLrc(byte[] data)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
byte lrc = 0;
foreach (byte b in data)
{
lrc += b;
}
lrc = (byte)((lrc ^ 0xFF) + 1);
return lrc;
}
/// <summary>
/// Calculate Cyclical Redundancy Check.
/// </summary>
/// <param name="data">The data used in CRC.</param>
/// <returns>CRC value.</returns>
public static byte[] CalculateCrc(byte[] data)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
ushort crc = ushort.MaxValue;
foreach (byte b in data)
{
byte tableIndex = (byte)(crc ^ b);
crc >>= 8;
crc ^= CrcTable[tableIndex];
}
return BitConverter.GetBytes(crc);
}
}
}

Binary file not shown.

View File

@ -126,9 +126,9 @@ namespace Plugin
} }
} }
} }
catch (Exception) catch (Exception ex)
{ {
Console.WriteLine("驱动加载失败一般是驱动项目引用的nuget或dll没有复制到驱动文件夹");
} }
} }

View File

@ -1,6 +1,6 @@
# iotgateway # iotgateway
# github地址:[iotgateway](https://github.com/iioter/iotgateway/) https://github.com/iioter/iotgateway ## github地址:[iotgateway](https://github.com/iioter/iotgateway/) https://github.com/iioter/iotgateway
# gitee地址:[iotgateway](https://gitee.com/wang_haidong/iotgateway/) https://gitee.com/wang_haidong/iotgateway ## gitee地址:[iotgateway](https://gitee.com/wang_haidong/iotgateway/) https://gitee.com/wang_haidong/iotgateway
基于.net5的跨平台物联网网关。通过可视化配置轻松的连接到你的任何设备和系统(如PLC、扫码枪、CNC、数据库、串口设备、上位机、OPC Server、OPC UA Server、Mqtt Server等),从而与 Thingsboard、IoTSharp或您自己的物联网平台进行双向数据通讯。提供简单的驱动开发接口当然也可以进行边缘计算。 基于.net5的跨平台物联网网关。通过可视化配置轻松的连接到你的任何设备和系统(如PLC、扫码枪、CNC、数据库、串口设备、上位机、OPC Server、OPC UA Server、Mqtt Server等),从而与 Thingsboard、IoTSharp或您自己的物联网平台进行双向数据通讯。提供简单的驱动开发接口当然也可以进行边缘计算。
* 物联网网关mqtt输出支持thingsboard * 物联网网关mqtt输出支持thingsboard
* 抛砖引玉,共同进步 * 抛砖引玉,共同进步
@ -53,3 +53,12 @@
![6 gateway 修改设备为自启动](https://user-images.githubusercontent.com/29589505/145705269-c816789c-cd67-4c01-973f-ae4f10eb41d9.png) ![6 gateway 修改设备为自启动](https://user-images.githubusercontent.com/29589505/145705269-c816789c-cd67-4c01-973f-ae4f10eb41d9.png)
![7 thingsboard 查看到设备和数据](https://user-images.githubusercontent.com/29589505/145705270-31d8884f-7f6f-4ff5-a6bb-1d57a97012f4.png) ![7 thingsboard 查看到设备和数据](https://user-images.githubusercontent.com/29589505/145705270-31d8884f-7f6f-4ff5-a6bb-1d57a97012f4.png)
![8 gateway 查看到数据](https://user-images.githubusercontent.com/29589505/145705271-cb80b80e-006e-4312-8843-6d0ae9457cb1.png) ![8 gateway 查看到数据](https://user-images.githubusercontent.com/29589505/145705271-cb80b80e-006e-4312-8843-6d0ae9457cb1.png)
# 声明
## 君子性非异也,善假于物也
1. [WTM(MIT)](https://github.com/dotnetcore/WTM)
2. [NModbus4(MIT)](https://github.com/NModbus4/NModbus4)
3. [EFCore(MIT)](https://github.com/dotnet/efcore)
4. [LayUI(MIT)](https://github.com/sentsin/layui)
5. [SQLite](https://github.com/sqlite/sqlite)