Add ModbusDevice as preparation for server implementations.

This commit is contained in:
2024-03-18 13:49:06 +01:00
parent 946614b86c
commit fbc9f9e429
9 changed files with 642 additions and 45 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
namespace AMWD.Protocols.Modbus.Common
{
@@ -10,5 +11,19 @@ namespace AMWD.Protocols.Modbus.Common
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);
}
public static ushort NetworkUInt16(this byte[] bytes, int offset = 0)
{
byte[] b = bytes.Skip(offset).Take(2).ToArray();
b.SwapNetworkOrder();
return BitConverter.ToUInt16(b, 0);
}
public static byte[] ToNetworkBytes(this ushort value)
{
byte[] b = BitConverter.GetBytes(value);
b.SwapNetworkOrder();
return b;
}
}
}

View File

@@ -5,7 +5,7 @@ using System.Linq;
namespace System
{
// ================================================================================================================================== //
// Source: https://git.am-wd.de/am.wd/common/-/blob/d4b390ad911ce302cc371bb2121fa9c31db1674a/AMWD.Common/Extensions/EnumExtensions.cs //
// Source: https://git.am-wd.de/am-wd/common/-/blob/d4b390ad911ce302cc371bb2121fa9c31db1674a/AMWD.Common/Extensions/EnumExtensions.cs //
// ================================================================================================================================== //
[Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static class EnumExtensions

View File

@@ -0,0 +1,92 @@
namespace System.Threading
{
// ================================================================================================================================================== //
// Source: https://git.am-wd.de/am-wd/common/-/blob/d4b390ad911ce302cc371bb2121fa9c31db1674a/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs //
// ================================================================================================================================================== //
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static class ReaderWriterLockSlimExtensions
{
/// <summary>
/// Acquires a read lock on a lock object that can be released with an
/// <see cref="IDisposable"/> instance.
/// </summary>
/// <param name="rwLock">The lock object.</param>
/// <param name="timeoutMilliseconds">The number of milliseconds to wait, or -1
/// (<see cref="Timeout.Infinite"/>) to wait indefinitely.</param>
/// <returns>An <see cref="IDisposable"/> instance to release the lock.</returns>
public static IDisposable GetReadLock(this ReaderWriterLockSlim rwLock, int timeoutMilliseconds = -1)
{
if (!rwLock.TryEnterReadLock(timeoutMilliseconds))
throw new TimeoutException("The read lock could not be acquired.");
return new DisposableReadWriteLock(rwLock, LockMode.Read);
}
/// <summary>
/// Acquires a upgradeable read lock on a lock object that can be released with an
/// <see cref="IDisposable"/> instance. The lock can be upgraded to a write lock temporarily
/// with <see cref="GetWriteLock"/> or until the lock is released with
/// <see cref="ReaderWriterLockSlim.EnterWriteLock"/> alone.
/// </summary>
/// <param name="rwLock">The lock object.</param>
/// <param name="timeoutMilliseconds">The number of milliseconds to wait, or -1
/// (<see cref="Timeout.Infinite"/>) to wait indefinitely.</param>
/// <returns>An <see cref="IDisposable"/> instance to release the lock. If the lock was
/// upgraded to a write lock, that will be released as well.</returns>
public static IDisposable GetUpgradeableReadLock(this ReaderWriterLockSlim rwLock, int timeoutMilliseconds = -1)
{
if (!rwLock.TryEnterUpgradeableReadLock(timeoutMilliseconds))
throw new TimeoutException("The upgradeable read lock could not be acquired.");
return new DisposableReadWriteLock(rwLock, LockMode.Upgradable);
}
/// <summary>
/// Acquires a write lock on a lock object that can be released with an
/// <see cref="IDisposable"/> instance.
/// </summary>
/// <param name="rwLock">The lock object.</param>
/// <param name="timeoutMilliseconds">The number of milliseconds to wait, or -1
/// (<see cref="Timeout.Infinite"/>) to wait indefinitely.</param>
/// <returns>An <see cref="IDisposable"/> instance to release the lock.</returns>
public static IDisposable GetWriteLock(this ReaderWriterLockSlim rwLock, int timeoutMilliseconds = -1)
{
if (!rwLock.TryEnterWriteLock(timeoutMilliseconds))
throw new TimeoutException("The write lock could not be acquired.");
return new DisposableReadWriteLock(rwLock, LockMode.Write);
}
private struct DisposableReadWriteLock(ReaderWriterLockSlim rwLock, LockMode lockMode)
: IDisposable
{
private readonly ReaderWriterLockSlim _rwLock = rwLock;
private LockMode _lockMode = lockMode;
public void Dispose()
{
if (_lockMode == LockMode.Read)
_rwLock.ExitReadLock();
if (_lockMode == LockMode.Upgradable && _rwLock.IsWriteLockHeld) // Upgraded with EnterWriteLock alone
_rwLock.ExitWriteLock();
if (_lockMode == LockMode.Upgradable)
_rwLock.ExitUpgradeableReadLock();
if (_lockMode == LockMode.Write)
_rwLock.ExitWriteLock();
_lockMode = LockMode.None;
}
}
private enum LockMode
{
None = 0,
Read = 1,
Upgradable = 2,
Write = 3,
}
}
}

View File

@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Threading;
namespace AMWD.Protocols.Modbus.Common.Models
{
/// <summary>
/// Represents a Modbus device used in a Modbus server implementation.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="ModbusDevice"/> class.
/// </remarks>
/// <param name="id">The <see cref="ModbusDevice"/> ID.</param>
public class ModbusDevice(byte id) : IDisposable
{
private readonly ReaderWriterLockSlim _rwLockCoils = new();
private readonly ReaderWriterLockSlim _rwLockDiscreteInputs = new();
private readonly ReaderWriterLockSlim _rwLockHoldingRegisters = new();
private readonly ReaderWriterLockSlim _rwLockInputRegisters = new();
private readonly HashSet<ushort> _coils = [];
private readonly HashSet<ushort> _discreteInputs = [];
private readonly Dictionary<ushort, ushort> _holdingRegisters = [];
private readonly Dictionary<ushort, ushort> _inputRegisters = [];
private bool _isDisposed;
/// <summary>
/// Gets the ID of the <see cref="ModbusDevice"/>.
/// </summary>
public byte Id { get; } = id;
/// <summary>
/// Releases the unmanaged resources used by the <see cref="ModbusDevice"/>
/// and optionally also discards the managed resources.
/// </summary>
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
_rwLockCoils.Dispose();
_rwLockDiscreteInputs.Dispose();
_rwLockHoldingRegisters.Dispose();
_rwLockInputRegisters.Dispose();
_coils.Clear();
_discreteInputs.Clear();
_holdingRegisters.Clear();
_inputRegisters.Clear();
}
/// <summary>
/// Gets a <see cref="Coil"/> from the <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="address">The address of the <see cref="Coil"/>.</param>
public Coil GetCoil(ushort address)
{
Assertions();
using (_rwLockCoils.GetReadLock())
{
return new Coil
{
Address = address,
HighByte = (byte)(_coils.Contains(address) ? 0xFF : 0x00)
};
}
}
/// <summary>
/// Sets a <see cref="Coil"/> to the <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="coil">The <see cref="Coil"/> to set.</param>
public void SetCoil(Coil coil)
{
Assertions();
using (_rwLockCoils.GetWriteLock())
{
if (coil.Value)
_coils.Add(coil.Address);
else
_coils.Remove(coil.Address);
}
}
/// <summary>
/// Gets a <see cref="DiscreteInput"/> from the <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="address">The address of the <see cref="DiscreteInput"/>.</param>
public DiscreteInput GetDiscreteInput(ushort address)
{
Assertions();
using (_rwLockDiscreteInputs.GetReadLock())
{
return new DiscreteInput
{
Address = address,
HighByte = (byte)(_discreteInputs.Contains(address) ? 0xFF : 0x00)
};
}
}
/// <summary>
/// Sets a <see cref="DiscreteInput"/> to the <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="input">The <see cref="DiscreteInput"/> to set.</param>
public void SetDiscreteInput(DiscreteInput input)
{
using (_rwLockDiscreteInputs.GetWriteLock())
{
if (input.Value)
_discreteInputs.Add(input.Address);
else
_discreteInputs.Remove(input.Address);
}
}
/// <summary>
/// Gets a <see cref="HoldingRegister"/> from the <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="address">The address of the <see cref="HoldingRegister"/>.</param>
public HoldingRegister GetHoldingRegister(ushort address)
{
Assertions();
using (_rwLockHoldingRegisters.GetReadLock())
{
if (!_holdingRegisters.TryGetValue(address, out ushort value))
value = 0x0000;
byte[] blob = BitConverter.GetBytes(value);
blob.SwapNetworkOrder();
return new HoldingRegister
{
Address = address,
HighByte = blob[0],
LowByte = blob[1]
};
}
}
/// <summary>
/// Sets a <see cref="HoldingRegister"/> to the <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="register">The <see cref="HoldingRegister"/> to set.</param>
public void SetHoldingRegister(HoldingRegister register)
{
Assertions();
using (_rwLockHoldingRegisters.GetWriteLock())
{
if (register.Value == 0)
{
_holdingRegisters.Remove(register.Address);
return;
}
byte[] blob = [register.HighByte, register.LowByte];
blob.SwapNetworkOrder();
_holdingRegisters[register.Address] = BitConverter.ToUInt16(blob, 0);
}
}
/// <summary>
/// Gets a <see cref="InputRegister"/> from the <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="address">The address of the <see cref="InputRegister"/>.</param>
public InputRegister GetInputRegister(ushort address)
{
Assertions();
using (_rwLockInputRegisters.GetReadLock())
{
if (!_inputRegisters.TryGetValue(address, out ushort value))
value = 0x0000;
byte[] blob = BitConverter.GetBytes(value);
blob.SwapNetworkOrder();
return new InputRegister
{
Address = address,
HighByte = blob[0],
LowByte = blob[1]
};
}
}
/// <summary>
/// Sets a <see cref="InputRegister"/> to the <see cref="ModbusDevice"/>.
/// </summary>
/// <param name="register">The <see cref="InputRegister"/> to set.</param>
public void SetInputRegister(InputRegister register)
{
Assertions();
using (_rwLockInputRegisters.GetWriteLock())
{
if (register.Value == 0)
{
_inputRegisters.Remove(register.Address);
return;
}
byte[] blob = [register.HighByte, register.LowByte];
blob.SwapNetworkOrder();
_inputRegisters[register.Address] = BitConverter.ToUInt16(blob, 0);
}
}
private void Assertions()
{
#if NET8_0_OR_GREATER
ObjectDisposedException.ThrowIf(_isDisposed, this);
#else
if (_isDisposed)
throw new ObjectDisposedException(GetType().FullName);
#endif
}
}
}

View File

@@ -89,12 +89,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadCoils;
// Starting address
byte[] addrBytes = ToNetworkBytes(startAddress);
byte[] addrBytes = startAddress.ToNetworkBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
// Quantity
byte[] countBytes = ToNetworkBytes(count);
byte[] countBytes = count.ToNetworkBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -144,12 +144,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
// Starting address
byte[] addrBytes = ToNetworkBytes(startAddress);
byte[] addrBytes = startAddress.ToNetworkBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
// Quantity
byte[] countBytes = ToNetworkBytes(count);
byte[] countBytes = count.ToNetworkBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -199,12 +199,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
// Starting address
byte[] addrBytes = ToNetworkBytes(startAddress);
byte[] addrBytes = startAddress.ToNetworkBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
// Quantity
byte[] countBytes = ToNetworkBytes(count);
byte[] countBytes = count.ToNetworkBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -251,12 +251,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
// Starting address
byte[] addrBytes = ToNetworkBytes(startAddress);
byte[] addrBytes = startAddress.ToNetworkBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
// Quantity
byte[] countBytes = ToNetworkBytes(count);
byte[] countBytes = count.ToNetworkBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -362,7 +362,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
byte[] addrBytes = ToNetworkBytes(coil.Address);
byte[] addrBytes = coil.Address.ToNetworkBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
@@ -377,7 +377,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
{
return new Coil
{
Address = ToNetworkUInt16(response.Skip(8).Take(2).ToArray()),
Address = response.ToArray().NetworkUInt16(8),
HighByte = response[10],
LowByte = response[11]
};
@@ -401,7 +401,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Function code
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
byte[] addrBytes = ToNetworkBytes(register.Address);
byte[] addrBytes = register.Address.ToNetworkBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
@@ -416,7 +416,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
{
return new HoldingRegister
{
Address = ToNetworkUInt16(response.Skip(8).Take(2).ToArray()),
Address = response.ToArray().NetworkUInt16(8),
HighByte = response[10],
LowByte = response[11]
};
@@ -454,11 +454,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
byte[] addrBytes = ToNetworkBytes(firstAddress);
byte[] addrBytes = firstAddress.ToNetworkBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
byte[] countBytes = ToNetworkBytes((ushort)orderedList.Count);
byte[] countBytes = ((ushort)orderedList.Count).ToNetworkBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -483,8 +483,8 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <inheritdoc/>
public (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList<byte> response)
{
ushort firstAddress = ToNetworkUInt16(response.Skip(8).Take(2).ToArray());
ushort numberOfCoils = ToNetworkUInt16(response.Skip(10).Take(2).ToArray());
ushort firstAddress = response.ToArray().NetworkUInt16(8);
ushort numberOfCoils = response.ToArray().NetworkUInt16(10);
return (firstAddress, numberOfCoils);
}
@@ -521,11 +521,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
byte[] addrBytes = ToNetworkBytes(firstAddress);
byte[] addrBytes = firstAddress.ToNetworkBytes();
request[8] = addrBytes[0];
request[9] = addrBytes[1];
byte[] countBytes = ToNetworkBytes((ushort)orderedList.Count);
byte[] countBytes = ((ushort)orderedList.Count).ToNetworkBytes();
request[10] = countBytes[0];
request[11] = countBytes[1];
@@ -544,8 +544,8 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
/// <inheritdoc/>
public (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList<byte> response)
{
ushort firstAddress = ToNetworkUInt16(response.Skip(8).Take(2).ToArray());
ushort numberOfRegisters = ToNetworkUInt16(response.Skip(10).Take(2).ToArray());
ushort firstAddress = response.ToArray().NetworkUInt16(8);
ushort numberOfRegisters = response.ToArray().NetworkUInt16(10);
return (firstAddress, numberOfRegisters);
}
@@ -563,7 +563,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
if (responseBytes.Count < 6)
return false;
ushort followingBytes = ToNetworkUInt16(responseBytes.Skip(4).Take(2).ToArray());
ushort followingBytes = responseBytes.ToArray().NetworkUInt16(4);
if (responseBytes.Count < followingBytes + 6)
return false;
@@ -582,7 +582,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
if (request[2] != response[2] || request[3] != response[3])
throw new ModbusException("Protocol Identifier does not match.");
ushort count = ToNetworkUInt16(response.Skip(4).Take(2).ToArray());
ushort count = response.ToArray().NetworkUInt16(4);
if (count != response.Count - 6)
throw new ModbusException("Number of following bytes does not match.");
@@ -636,7 +636,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
// Transaction id
ushort txId = GetNextTransacitonId();
byte[] txBytes = ToNetworkBytes(txId);
byte[] txBytes = txId.ToNetworkBytes();
header[0] = txBytes[0];
header[1] = txBytes[1];
@@ -645,7 +645,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
header[3] = 0x00;
// Number of following bytes
byte[] countBytes = ToNetworkBytes((ushort)followingBytes);
byte[] countBytes = ((ushort)followingBytes).ToNetworkBytes();
header[4] = countBytes[0];
header[5] = countBytes[1];
@@ -655,24 +655,6 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
return header;
}
private static byte[] ToNetworkBytes(ushort value)
{
byte[] bytes = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);
return bytes;
}
private static ushort ToNetworkUInt16(byte[] bytes)
{
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);
return BitConverter.ToUInt16(bytes, 0);
}
#endregion Private helpers
}
}