Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 885079ae70 | |||
| 17fc216658 | |||
| 885231466b | |||
| 5b8a2a8af1 | |||
| 980dab22f3 | |||
| 9270f49519 | |||
| 241a9d114c | |||
| 9283b04971 | |||
| 8b3441f6dd | |||
| 63c88f5da7 | |||
| e7300bfbde | |||
| 38dd94471d | |||
| 56664cdac5 | |||
| 4ef7500c3b | |||
| 05759f8e12 | |||
| 6fc7cfda9a | |||
| fb67e0b77e | |||
| ce3d873cd0 | |||
| 1cf49f74ea | |||
| 39863880d5 | |||
| ec0ba31b86 | |||
| 96b5ee21c8 | |||
| 6a231e02cb | |||
| c1a70de6bb | |||
| 6bf011d53f | |||
| 0c81ab6b44 | |||
| 3e8f2cd73b | |||
| e830e43c36 | |||
| 6a63dbb739 | |||
| 1536c60336 |
@@ -20,17 +20,12 @@ build-debug:
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG == null
|
||||
script:
|
||||
- shopt -s globstar
|
||||
- mkdir ./artifacts
|
||||
- dotnet restore --no-cache --force
|
||||
- dotnet build -c Debug --nologo --no-restore --no-incremental
|
||||
- mkdir ./artifacts
|
||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.snupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Debug/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Debug/*.snupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.snupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.snupkg ./artifacts/
|
||||
- mv ./**/*.nupkg ./artifacts/
|
||||
- mv ./**/*.snupkg ./artifacts/
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/*.nupkg
|
||||
@@ -47,10 +42,20 @@ test-debug:
|
||||
- 64bit
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG == null
|
||||
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
||||
coverage: /Branch coverage[\s\S].+%/
|
||||
before_script:
|
||||
- dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
|
||||
script:
|
||||
- dotnet restore --no-cache --force
|
||||
- dotnet test -c Debug --nologo --no-restore
|
||||
- dotnet test -c Debug --nologo /p:CoverletOutputFormat=Cobertura
|
||||
- /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary
|
||||
after_script:
|
||||
- cat /reports/Summary.txt
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: ./**/coverage.cobertura.xml
|
||||
|
||||
deploy-debug:
|
||||
stage: deploy
|
||||
@@ -77,22 +82,17 @@ build-release:
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG != null
|
||||
script:
|
||||
- shopt -s globstar
|
||||
- mkdir ./artifacts
|
||||
- dotnet restore --no-cache --force
|
||||
- dotnet build -c Release --nologo --no-restore --no-incremental
|
||||
- mkdir ./artifacts
|
||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.snupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Release/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Release/*.snupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.snupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.nupkg ./artifacts/
|
||||
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.snupkg ./artifacts/
|
||||
- mv ./**/*.nupkg ./artifacts/
|
||||
- mv ./**/*.snupkg ./artifacts/
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/*.nupkg
|
||||
- artifacts/*.snupkg
|
||||
expire_in: 1 days
|
||||
expire_in: 7 days
|
||||
|
||||
test-release:
|
||||
stage: test
|
||||
@@ -104,10 +104,20 @@ test-release:
|
||||
- amd64
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG != null
|
||||
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
||||
coverage: /Branch coverage[\s\S].+%/
|
||||
before_script:
|
||||
- dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
|
||||
script:
|
||||
- dotnet restore --no-cache --force
|
||||
- dotnet test -c Release --nologo --no-restore
|
||||
- dotnet test -c Release --nologo /p:CoverletOutputFormat=Cobertura
|
||||
- /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary
|
||||
after_script:
|
||||
- cat /reports/Summary.txt
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: ./**/coverage.cobertura.xml
|
||||
|
||||
deploy-release:
|
||||
stage: deploy
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
|
||||
<PackageId>AMWD.Protocols.Modbus.Common</PackageId>
|
||||
<AssemblyName>amwd-modbus-common</AssemblyName>
|
||||
|
||||
24
AMWD.Protocols.Modbus.Common/Contracts/IModbusProxy.cs
Normal file
24
AMWD.Protocols.Modbus.Common/Contracts/IModbusProxy.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Modbus proxy.
|
||||
/// </summary>
|
||||
public interface IModbusProxy : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts the proxy.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
Task StartAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Stops the proxy.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
Task StopAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Base implementation of a Modbus client.
|
||||
/// </summary>
|
||||
public abstract class ModbusClientBase : IDisposable
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
||||
/// </remarks>
|
||||
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
|
||||
/// <param name="disposeConnection">
|
||||
/// <see langword="true"/> if the connection should be disposed of by Dispose(),
|
||||
/// <see langword="false"/> otherwise if you inted to reuse the connection.
|
||||
/// </param>
|
||||
public abstract class ModbusClientBase(IModbusConnection connection, bool disposeConnection) : IDisposable
|
||||
{
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the connection should be disposed of by <see cref="Dispose()"/>.
|
||||
/// </summary>
|
||||
protected readonly bool disposeConnection;
|
||||
protected readonly bool disposeConnection = disposeConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IModbusConnection"/> responsible for invoking the requests.
|
||||
/// </summary>
|
||||
protected readonly IModbusConnection connection;
|
||||
protected readonly IModbusConnection connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
||||
@@ -32,20 +40,6 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
: this(connection, true)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="IModbusConnection"/> responsible for invoking the requests.</param>
|
||||
/// <param name="disposeConnection">
|
||||
/// <see langword="true"/> if the connection should be disposed of by Dispose(),
|
||||
/// <see langword="false"/> otherwise if you inted to reuse the connection.
|
||||
/// </param>
|
||||
public ModbusClientBase(IModbusConnection connection, bool disposeConnection)
|
||||
{
|
||||
this.connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
this.disposeConnection = disposeConnection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the protocol type to use.
|
||||
/// </summary>
|
||||
@@ -67,7 +61,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadCoils(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
// The protocol processes complete bytes from the response.
|
||||
@@ -92,7 +86,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadDiscreteInputs(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
// The protocol processes complete bytes from the response.
|
||||
@@ -117,7 +111,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadHoldingRegisters(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList();
|
||||
@@ -140,7 +134,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeReadInputRegisters(unitId, startAddress, count);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList();
|
||||
@@ -184,7 +178,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
do
|
||||
{
|
||||
var request = Protocol.SerializeReadDeviceIdentification(unitId, category, requestObjectId);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
result = Protocol.DeserializeReadDeviceIdentification(response);
|
||||
@@ -247,7 +241,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteSingleCoil(unitId, coil);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var result = Protocol.DeserializeWriteSingleCoil(response);
|
||||
@@ -268,7 +262,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteSingleHoldingRegister(unitId, register);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var result = Protocol.DeserializeWriteSingleHoldingRegister(response);
|
||||
@@ -289,7 +283,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteMultipleCoils(unitId, coils);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response);
|
||||
@@ -309,7 +303,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||
Assertions();
|
||||
|
||||
var request = Protocol.SerializeWriteMultipleHoldingRegisters(unitId, registers);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken);
|
||||
var response = await connection.InvokeAsync(request, Protocol.CheckResponseComplete, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
Protocol.ValidateResponse(request, response);
|
||||
|
||||
var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response);
|
||||
|
||||
@@ -8,19 +8,26 @@ namespace AMWD.Protocols.Modbus.Common.Events
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class CoilWrittenEventArgs : EventArgs
|
||||
{
|
||||
internal CoilWrittenEventArgs(byte unitId, ushort address, bool value)
|
||||
{
|
||||
UnitId = unitId;
|
||||
Address = address;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unit id.
|
||||
/// </summary>
|
||||
public byte UnitId { get; set; }
|
||||
public byte UnitId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the coil address.
|
||||
/// </summary>
|
||||
public ushort Address { get; set; }
|
||||
public ushort Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the coil value.
|
||||
/// </summary>
|
||||
public bool Value { get; set; }
|
||||
public bool Value { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,29 +8,39 @@ namespace AMWD.Protocols.Modbus.Common.Events
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class RegisterWrittenEventArgs : EventArgs
|
||||
{
|
||||
internal RegisterWrittenEventArgs(byte unitId, ushort address, byte highByte, byte lowByte)
|
||||
{
|
||||
UnitId = unitId;
|
||||
Address = address;
|
||||
HighByte = highByte;
|
||||
LowByte = lowByte;
|
||||
|
||||
Value = new[] { highByte, lowByte }.GetBigEndianUInt16();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unit id.
|
||||
/// </summary>
|
||||
public byte UnitId { get; set; }
|
||||
public byte UnitId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address of the register.
|
||||
/// </summary>
|
||||
public ushort Address { get; set; }
|
||||
public ushort Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the register.
|
||||
/// </summary>
|
||||
public ushort Value { get; set; }
|
||||
public ushort Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the high byte of the register.
|
||||
/// </summary>
|
||||
public byte HighByte { get; set; }
|
||||
public byte HighByte { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the low byte of the register.
|
||||
/// </summary>
|
||||
public byte LowByte { get; set; }
|
||||
public byte LowByte { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
#if !NET8_0_OR_GREATER
|
||||
using System.Runtime.Serialization;
|
||||
#endif
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
@@ -12,14 +13,14 @@ namespace AMWD.Protocols.Modbus.Common
|
||||
Array.Reverse(bytes);
|
||||
}
|
||||
|
||||
public static ushort GetBigEndianUInt16(this byte[] bytes, int offset = 0)
|
||||
public static ushort GetBigEndianUInt16(this IReadOnlyList<byte> bytes, int offset = 0)
|
||||
{
|
||||
byte[] b = bytes.Skip(offset).Take(2).ToArray();
|
||||
b.SwapBigEndian();
|
||||
return BitConverter.ToUInt16(b, 0);
|
||||
}
|
||||
|
||||
public static byte[] ToBigEndianBytes(this ushort value)
|
||||
public static IReadOnlyList<byte> ToBigEndianBytes(this ushort value)
|
||||
{
|
||||
byte[] b = BitConverter.GetBytes(value);
|
||||
b.SwapBigEndian();
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
@@ -91,5 +92,23 @@ namespace AMWD.Protocols.Modbus.Common
|
||||
/// Gets or sets a value indicating whether individual access (<see cref="ModbusDeviceIdentificationCategory.Individual"/>) is allowed.
|
||||
/// </summary>
|
||||
public bool IsIndividualAccessAllowed { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine(nameof(DeviceIdentification));
|
||||
sb.AppendLine($" {nameof(VendorName)}: {VendorName}");
|
||||
sb.AppendLine($" {nameof(ProductCode)}: {ProductCode}");
|
||||
sb.AppendLine($" {nameof(MajorMinorRevision)}: {MajorMinorRevision}");
|
||||
sb.AppendLine($" {nameof(VendorUrl)}: {VendorUrl}");
|
||||
sb.AppendLine($" {nameof(ProductName)}: {ProductName}");
|
||||
sb.AppendLine($" {nameof(ModelName)}: {ModelName}");
|
||||
sb.AppendLine($" {nameof(UserApplicationName)}: {UserApplicationName}");
|
||||
sb.AppendLine($" {nameof(IsIndividualAccessAllowed)}: {IsIndividualAccessAllowed}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,11 @@ namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
get
|
||||
{
|
||||
byte[] blob = [HighByte, LowByte];
|
||||
blob.SwapBigEndian();
|
||||
return BitConverter.ToUInt16(blob, 0);
|
||||
return new[] { HighByte, LowByte }.GetBigEndianUInt16();
|
||||
}
|
||||
set
|
||||
{
|
||||
byte[] blob = BitConverter.GetBytes(value);
|
||||
blob.SwapBigEndian();
|
||||
|
||||
var blob = value.ToBigEndianBytes();
|
||||
HighByte = blob[0];
|
||||
LowByte = blob[1];
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
get
|
||||
{
|
||||
byte[] blob = [HighByte, LowByte];
|
||||
blob.SwapBigEndian();
|
||||
return BitConverter.ToUInt16(blob, 0);
|
||||
return new[] { HighByte, LowByte }.GetBigEndianUInt16();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace AMWD.Protocols.Modbus.Common.Models
|
||||
/// 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
|
||||
internal class ModbusDevice(byte id) : IDisposable
|
||||
{
|
||||
private readonly ReaderWriterLockSlim _rwLockCoils = new();
|
||||
private readonly ReaderWriterLockSlim _rwLockDiscreteInputs = new();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
#if NET6_0_OR_GREATER
|
||||
using System;
|
||||
#endif
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common
|
||||
{
|
||||
|
||||
@@ -92,11 +92,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadCoils:X2}";
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||
|
||||
// LRC
|
||||
@@ -151,11 +151,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadDiscreteInputs:X2}";
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||
|
||||
// LRC
|
||||
@@ -209,11 +209,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadHoldingRegisters:X2}";
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||
|
||||
// LRC
|
||||
@@ -264,11 +264,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadInputRegisters:X2}";
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||
|
||||
// LRC
|
||||
@@ -383,7 +383,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleCoil:X2}";
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
||||
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||
|
||||
// Value
|
||||
@@ -426,7 +426,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleRegister:X2}";
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
||||
var addrBytes = register.Address.ToBigEndianBytes();
|
||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||
|
||||
// Value
|
||||
@@ -497,11 +497,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleCoils:X2}";
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
||||
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||
|
||||
// Byte count
|
||||
@@ -567,11 +567,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleRegisters:X2}";
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
||||
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||
|
||||
// Byte count
|
||||
@@ -675,6 +675,10 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
/// <summary>
|
||||
/// Calculate LRC for Modbus ASCII.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The LRC calculation algorithm is defined in the Modbus serial line specification.
|
||||
/// See <see href="https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf">Modbus over Serial Line v1.02</see>, Appendix B, page 38.
|
||||
/// </remarks>
|
||||
/// <param name="message">The message chars.</param>
|
||||
/// <param name="start">The start index.</param>
|
||||
/// <param name="length">The number of bytes to calculate.</param>
|
||||
|
||||
@@ -6,11 +6,15 @@ using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of the Modbus RTU over TCP protocol.
|
||||
/// Implementation of the Modbus RTU over Modbus TCP protocol.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Modbus RTU over Modbus TCP is rarely used.
|
||||
/// It is a non-standard variant of Modbus TCP that includes the Modbus RTU CRC at the end of the message.
|
||||
/// It is a non-standard variant:
|
||||
/// You can define it as RTU message with an additional TCP header
|
||||
/// or as TCP message with an additional CRC16 checksum at the end (header not included!).
|
||||
/// <br/>
|
||||
/// Definition found on <see href="https://www.fernhillsoftware.com/help/drivers/modbus/modbus-protocol.html">Fernhill Software</see>.
|
||||
/// </remarks>
|
||||
public class RtuOverTcpProtocol : IModbusProtocol
|
||||
{
|
||||
@@ -115,12 +119,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.ReadCoils;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -178,12 +182,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -241,12 +245,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -301,12 +305,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -428,7 +432,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
||||
|
||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
||||
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
@@ -475,7 +479,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||
|
||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
||||
var addrBytes = register.Address.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
@@ -538,12 +542,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
||||
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -618,12 +622,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
||||
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -743,7 +747,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
|
||||
// Transaction id
|
||||
ushort txId = GetNextTransacitonId();
|
||||
byte[] txBytes = txId.ToBigEndianBytes();
|
||||
var txBytes = txId.ToBigEndianBytes();
|
||||
header[0] = txBytes[0];
|
||||
header[1] = txBytes[1];
|
||||
|
||||
@@ -752,7 +756,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
header[3] = 0x00;
|
||||
|
||||
// Number of following bytes
|
||||
byte[] countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
||||
var countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
||||
header[4] = countBytes[0];
|
||||
header[5] = countBytes[1];
|
||||
|
||||
|
||||
@@ -10,6 +10,22 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
/// </summary>
|
||||
public class RtuProtocol : IModbusProtocol
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private static readonly byte[] _readFunctionCodes = [
|
||||
(byte)ModbusFunctionCode.ReadCoils,
|
||||
(byte)ModbusFunctionCode.ReadDiscreteInputs,
|
||||
(byte)ModbusFunctionCode.ReadHoldingRegisters,
|
||||
(byte)ModbusFunctionCode.ReadInputRegisters];
|
||||
|
||||
private static readonly byte[] _writeFunctionCodes = [
|
||||
(byte)ModbusFunctionCode.WriteSingleCoil,
|
||||
(byte)ModbusFunctionCode.WriteSingleRegister,
|
||||
(byte)ModbusFunctionCode.WriteMultipleCoils,
|
||||
(byte)ModbusFunctionCode.WriteMultipleRegisters];
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constants
|
||||
|
||||
/// <summary>
|
||||
@@ -96,12 +112,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[1] = (byte)ModbusFunctionCode.ReadCoils;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[2] = addrBytes[0];
|
||||
request[3] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[4] = countBytes[0];
|
||||
request[5] = countBytes[1];
|
||||
|
||||
@@ -156,12 +172,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[1] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[2] = addrBytes[0];
|
||||
request[3] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[4] = countBytes[0];
|
||||
request[5] = countBytes[1];
|
||||
|
||||
@@ -216,12 +232,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[1] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[2] = addrBytes[0];
|
||||
request[3] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[4] = countBytes[0];
|
||||
request[5] = countBytes[1];
|
||||
|
||||
@@ -273,12 +289,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[1] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[2] = addrBytes[0];
|
||||
request[3] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[4] = countBytes[0];
|
||||
request[5] = countBytes[1];
|
||||
|
||||
@@ -394,7 +410,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
// Function code
|
||||
request[1] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
||||
|
||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
||||
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||
request[2] = addrBytes[0];
|
||||
request[3] = addrBytes[1];
|
||||
|
||||
@@ -438,7 +454,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
// Function code
|
||||
request[1] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||
|
||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
||||
var addrBytes = register.Address.ToBigEndianBytes();
|
||||
request[2] = addrBytes[0];
|
||||
request[3] = addrBytes[1];
|
||||
|
||||
@@ -495,11 +511,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
|
||||
request[1] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
||||
|
||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
||||
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||
request[2] = addrBytes[0];
|
||||
request[3] = addrBytes[1];
|
||||
|
||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
request[4] = countBytes[0];
|
||||
request[5] = countBytes[1];
|
||||
|
||||
@@ -565,11 +581,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[0] = unitId;
|
||||
request[1] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
||||
|
||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
||||
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||
request[2] = addrBytes[0];
|
||||
request[3] = addrBytes[1];
|
||||
|
||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
request[4] = countBytes[0];
|
||||
request[5] = countBytes[1];
|
||||
|
||||
@@ -627,7 +643,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
// - 0x03 Read Holding Registers
|
||||
// - 0x04 Read Input Registers
|
||||
// do have a "following bytes" at position 3
|
||||
if (new[] { 0x01, 0x02, 0x03, 0x04 }.Contains(responseBytes[1]))
|
||||
if (_readFunctionCodes.Contains(responseBytes[1]))
|
||||
{
|
||||
// Unit ID, Function Code, ByteCount, 2x CRC and length of ByteCount
|
||||
if (responseBytes.Count < 5 + responseBytes[2])
|
||||
@@ -638,7 +654,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
// - 0x06 Write Single Register
|
||||
// - 0x0F Write Multiple Coils
|
||||
// - 0x10 Write Multiple Registers
|
||||
if (new[] { 0x05, 0x06, 0x0F, 0x10 }.Contains(responseBytes[1]))
|
||||
if (_writeFunctionCodes.Contains(responseBytes[1]))
|
||||
{
|
||||
// Write Single => Unit ID, Function code, 2x Address, 2x Value, 2x CRC
|
||||
// Write Multi => Unit ID, Function code, 2x Address, 2x QuantityWritten, 2x CRC
|
||||
@@ -715,13 +731,13 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
if (isError)
|
||||
throw new ModbusException("Remote Error") { ErrorCode = (ModbusErrorCode)response[2] };
|
||||
|
||||
if (new[] { 0x01, 0x02, 0x03, 0x04 }.Contains(fnCode))
|
||||
if (_readFunctionCodes.Contains(fnCode))
|
||||
{
|
||||
if (response.Count != 5 + response[2])
|
||||
throw new ModbusException("Number of following bytes does not match.");
|
||||
}
|
||||
|
||||
if (new[] { 0x05, 0x06, 0x0F, 0x10 }.Contains(fnCode))
|
||||
if (_writeFunctionCodes.Contains(fnCode))
|
||||
{
|
||||
if (response.Count != 8)
|
||||
throw new ModbusException("Number of bytes does not match.");
|
||||
@@ -733,6 +749,10 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
/// <summary>
|
||||
/// Calculate CRC16 for Modbus RTU.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The CRC 16 calculation algorithm is defined in the Modbus serial line specification.
|
||||
/// See <see href="https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf">Modbus over Serial Line v1.02</see>, Appendix B, page 40.
|
||||
/// </remarks>
|
||||
/// <param name="bytes">The message bytes.</param>
|
||||
/// <param name="start">The start index.</param>
|
||||
/// <param name="length">The number of bytes to calculate.</param>
|
||||
|
||||
@@ -101,12 +101,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.ReadCoils;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -159,12 +159,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -217,12 +217,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -272,12 +272,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
||||
var addrBytes = startAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = count.ToBigEndianBytes();
|
||||
var countBytes = count.ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -389,7 +389,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
||||
|
||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
||||
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
@@ -431,7 +431,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
// Function code
|
||||
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||
|
||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
||||
var addrBytes = register.Address.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
@@ -489,12 +489,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
||||
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -564,12 +564,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
||||
|
||||
// Starting address
|
||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
||||
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||
request[8] = addrBytes[0];
|
||||
request[9] = addrBytes[1];
|
||||
|
||||
// Quantity
|
||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||
request[10] = countBytes[0];
|
||||
request[11] = countBytes[1];
|
||||
|
||||
@@ -678,7 +678,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
|
||||
// Transaction id
|
||||
ushort txId = GetNextTransacitonId();
|
||||
byte[] txBytes = txId.ToBigEndianBytes();
|
||||
var txBytes = txId.ToBigEndianBytes();
|
||||
header[0] = txBytes[0];
|
||||
header[1] = txBytes[1];
|
||||
|
||||
@@ -687,7 +687,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
header[3] = 0x00;
|
||||
|
||||
// Number of following bytes
|
||||
byte[] countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
||||
var countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
||||
header[4] = countBytes[0];
|
||||
header[5] = countBytes[1];
|
||||
|
||||
|
||||
478
AMWD.Protocols.Modbus.Common/Protocols/VirtualProtocol.cs
Normal file
478
AMWD.Protocols.Modbus.Common/Protocols/VirtualProtocol.cs
Normal file
@@ -0,0 +1,478 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
using AMWD.Protocols.Modbus.Common.Events;
|
||||
using AMWD.Protocols.Modbus.Common.Models;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class VirtualProtocol : IModbusProtocol, IDisposable
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
private readonly ReaderWriterLockSlim _deviceListLock = new();
|
||||
private readonly Dictionary<byte, ModbusDevice> _devices = [];
|
||||
|
||||
#endregion Fields
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
_deviceListLock.Dispose();
|
||||
|
||||
foreach (var device in _devices.Values)
|
||||
device.Dispose();
|
||||
|
||||
_devices.Clear();
|
||||
}
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler<CoilWrittenEventArgs> CoilWritten;
|
||||
|
||||
public event EventHandler<RegisterWrittenEventArgs> RegisterWritten;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Properties
|
||||
|
||||
public string Name => nameof(VirtualProtocol);
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Protocol
|
||||
|
||||
public bool CheckResponseComplete(IReadOnlyList<byte> responseBytes) => true;
|
||||
|
||||
public IReadOnlyList<Coil> DeserializeReadCoils(IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!_devices.TryGetValue(response[0], out var device))
|
||||
throw new TimeoutException("Device not found.");
|
||||
|
||||
ushort start = response.GetBigEndianUInt16(1);
|
||||
ushort count = response.GetBigEndianUInt16(3);
|
||||
|
||||
return Enumerable.Range(0, count)
|
||||
.Select(i => device.GetCoil((ushort)(start + i)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public DeviceIdentificationRaw DeserializeReadDeviceIdentification(IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!_devices.TryGetValue(response[0], out var _))
|
||||
throw new TimeoutException("Device not found.");
|
||||
|
||||
var result = new DeviceIdentificationRaw
|
||||
{
|
||||
AllowsIndividualAccess = false,
|
||||
MoreRequestsNeeded = false,
|
||||
Objects = []
|
||||
};
|
||||
|
||||
if (response[1] >= 1)
|
||||
{
|
||||
string version = GetType().Assembly
|
||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
.InformationalVersion;
|
||||
|
||||
result.Objects.Add(0, Encoding.UTF8.GetBytes("AM.WD"));
|
||||
result.Objects.Add(1, Encoding.UTF8.GetBytes("AMWD.Protocols.Modbus"));
|
||||
result.Objects.Add(2, Encoding.UTF8.GetBytes(version));
|
||||
}
|
||||
|
||||
if (response[1] >= 2)
|
||||
{
|
||||
result.Objects.Add(3, Encoding.UTF8.GetBytes("https://github.com/AM-WD/AMWD.Protocols.Modbus"));
|
||||
result.Objects.Add(4, Encoding.UTF8.GetBytes("Modbus Protocol for .NET"));
|
||||
result.Objects.Add(5, Encoding.UTF8.GetBytes("Virtual Device"));
|
||||
result.Objects.Add(6, Encoding.UTF8.GetBytes("Virtual Modbus Client"));
|
||||
}
|
||||
|
||||
if (response[1] >= 3)
|
||||
{
|
||||
for (int i = 128; i < 256; i++)
|
||||
result.Objects.Add((byte)i, []);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public IReadOnlyList<DiscreteInput> DeserializeReadDiscreteInputs(IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!_devices.TryGetValue(response[0], out var device))
|
||||
throw new TimeoutException("Device not found.");
|
||||
|
||||
ushort start = response.GetBigEndianUInt16(1);
|
||||
ushort count = response.GetBigEndianUInt16(3);
|
||||
|
||||
return Enumerable.Range(0, count)
|
||||
.Select(i => device.GetDiscreteInput((ushort)(start + i)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public IReadOnlyList<HoldingRegister> DeserializeReadHoldingRegisters(IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!_devices.TryGetValue(response[0], out var device))
|
||||
throw new TimeoutException("Device not found.");
|
||||
|
||||
ushort start = response.GetBigEndianUInt16(1);
|
||||
ushort count = response.GetBigEndianUInt16(3);
|
||||
|
||||
return Enumerable.Range(0, count)
|
||||
.Select(i => device.GetHoldingRegister((ushort)(start + i)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public IReadOnlyList<InputRegister> DeserializeReadInputRegisters(IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!_devices.TryGetValue(response[0], out var device))
|
||||
throw new TimeoutException("Device not found.");
|
||||
|
||||
ushort start = response.GetBigEndianUInt16(1);
|
||||
ushort count = response.GetBigEndianUInt16(3);
|
||||
|
||||
return Enumerable.Range(0, count)
|
||||
.Select(i => device.GetInputRegister((ushort)(start + i)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public (ushort FirstAddress, ushort NumberOfCoils) DeserializeWriteMultipleCoils(IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!_devices.TryGetValue(response[0], out var device))
|
||||
throw new TimeoutException("Device not found.");
|
||||
|
||||
ushort start = response.GetBigEndianUInt16(1);
|
||||
ushort count = response.GetBigEndianUInt16(3);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var coil = new Coil
|
||||
{
|
||||
Address = (ushort)(start + i),
|
||||
HighByte = response[5 + i]
|
||||
};
|
||||
device.SetCoil(coil);
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
CoilWritten?.Invoke(this, new CoilWrittenEventArgs(
|
||||
unitId: response[0],
|
||||
address: coil.Address,
|
||||
value: coil.Value));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (start, count);
|
||||
}
|
||||
|
||||
public (ushort FirstAddress, ushort NumberOfRegisters) DeserializeWriteMultipleHoldingRegisters(IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!_devices.TryGetValue(response[0], out var device))
|
||||
throw new TimeoutException("Device not found.");
|
||||
|
||||
ushort start = response.GetBigEndianUInt16(1);
|
||||
ushort count = response.GetBigEndianUInt16(3);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var register = new HoldingRegister
|
||||
{
|
||||
Address = (ushort)(start + i),
|
||||
HighByte = response[5 + i * 2],
|
||||
LowByte = response[5 + i * 2 + 1]
|
||||
};
|
||||
device.SetHoldingRegister(register);
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs(
|
||||
unitId: response[0],
|
||||
address: register.Address,
|
||||
highByte: register.HighByte,
|
||||
lowByte: register.LowByte));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (start, count);
|
||||
}
|
||||
|
||||
public Coil DeserializeWriteSingleCoil(IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!_devices.TryGetValue(response[0], out var device))
|
||||
throw new TimeoutException("Device not found.");
|
||||
|
||||
var coil = new Coil
|
||||
{
|
||||
Address = response.GetBigEndianUInt16(1),
|
||||
HighByte = response[3]
|
||||
};
|
||||
device.SetCoil(coil);
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
CoilWritten?.Invoke(this, new CoilWrittenEventArgs(
|
||||
unitId: response[0],
|
||||
address: coil.Address,
|
||||
value: coil.Value));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
return coil;
|
||||
}
|
||||
|
||||
public HoldingRegister DeserializeWriteSingleHoldingRegister(IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!_devices.TryGetValue(response[0], out var device))
|
||||
throw new TimeoutException("Device not found.");
|
||||
|
||||
var register = new HoldingRegister
|
||||
{
|
||||
Address = response.GetBigEndianUInt16(1),
|
||||
HighByte = response[3],
|
||||
LowByte = response[4]
|
||||
};
|
||||
device.SetHoldingRegister(register);
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
RegisterWritten?.Invoke(this, new RegisterWrittenEventArgs(
|
||||
unitId: response[0],
|
||||
address: register.Address,
|
||||
highByte: register.HighByte,
|
||||
lowByte: register.LowByte));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
return register;
|
||||
}
|
||||
|
||||
public IReadOnlyList<byte> SerializeReadCoils(byte unitId, ushort startAddress, ushort count)
|
||||
{
|
||||
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
|
||||
}
|
||||
|
||||
public IReadOnlyList<byte> SerializeReadDeviceIdentification(byte unitId, ModbusDeviceIdentificationCategory category, ModbusDeviceIdentificationObject objectId)
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
|
||||
throw new ArgumentOutOfRangeException(nameof(category));
|
||||
|
||||
return [unitId, (byte)category];
|
||||
}
|
||||
|
||||
public IReadOnlyList<byte> SerializeReadDiscreteInputs(byte unitId, ushort startAddress, ushort count)
|
||||
{
|
||||
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
|
||||
}
|
||||
|
||||
public IReadOnlyList<byte> SerializeReadHoldingRegisters(byte unitId, ushort startAddress, ushort count)
|
||||
{
|
||||
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
|
||||
}
|
||||
|
||||
public IReadOnlyList<byte> SerializeReadInputRegisters(byte unitId, ushort startAddress, ushort count)
|
||||
{
|
||||
return [unitId, .. startAddress.ToBigEndianBytes(), .. count.ToBigEndianBytes()];
|
||||
}
|
||||
|
||||
public IReadOnlyList<byte> SerializeWriteMultipleCoils(byte unitId, IReadOnlyList<Coil> coils)
|
||||
{
|
||||
ushort start = coils.OrderBy(c => c.Address).First().Address;
|
||||
ushort count = (ushort)coils.Count;
|
||||
byte[] values = coils.Select(c => c.HighByte).ToArray();
|
||||
|
||||
return [unitId, .. start.ToBigEndianBytes(), .. count.ToBigEndianBytes(), .. values];
|
||||
}
|
||||
|
||||
public IReadOnlyList<byte> SerializeWriteMultipleHoldingRegisters(byte unitId, IReadOnlyList<HoldingRegister> registers)
|
||||
{
|
||||
ushort start = registers.OrderBy(c => c.Address).First().Address;
|
||||
ushort count = (ushort)registers.Count;
|
||||
byte[] values = registers.SelectMany(r => new[] { r.HighByte, r.LowByte }).ToArray();
|
||||
|
||||
return [unitId, .. start.ToBigEndianBytes(), .. count.ToBigEndianBytes(), .. values];
|
||||
}
|
||||
|
||||
public IReadOnlyList<byte> SerializeWriteSingleCoil(byte unitId, Coil coil)
|
||||
{
|
||||
return [unitId, .. coil.Address.ToBigEndianBytes(), coil.HighByte];
|
||||
}
|
||||
|
||||
public IReadOnlyList<byte> SerializeWriteSingleHoldingRegister(byte unitId, HoldingRegister register)
|
||||
{
|
||||
return [unitId, .. register.Address.ToBigEndianBytes(), register.HighByte, register.LowByte];
|
||||
}
|
||||
|
||||
public void ValidateResponse(IReadOnlyList<byte> request, IReadOnlyList<byte> response)
|
||||
{
|
||||
if (!request.SequenceEqual(response))
|
||||
throw new InvalidOperationException("Request and response have to be the same on virtual protocol.");
|
||||
}
|
||||
|
||||
#endregion Protocol
|
||||
|
||||
#region Device Handling
|
||||
|
||||
public bool AddDevice(byte unitId)
|
||||
{
|
||||
Assertions();
|
||||
using (_deviceListLock.GetWriteLock())
|
||||
{
|
||||
if (_devices.ContainsKey(unitId))
|
||||
return false;
|
||||
|
||||
_devices.Add(unitId, new ModbusDevice(unitId));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveDevice(byte unitId)
|
||||
{
|
||||
Assertions();
|
||||
using (_deviceListLock.GetWriteLock())
|
||||
{
|
||||
if (_devices.ContainsKey(unitId))
|
||||
return false;
|
||||
|
||||
return _devices.Remove(unitId);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Device Handling
|
||||
|
||||
#region Entity Handling
|
||||
|
||||
public Coil GetCoil(byte unitId, ushort address)
|
||||
{
|
||||
Assertions();
|
||||
using (_deviceListLock.GetReadLock())
|
||||
{
|
||||
return _devices.TryGetValue(unitId, out var device)
|
||||
? device.GetCoil(address)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCoil(byte unitId, Coil coil)
|
||||
{
|
||||
Assertions();
|
||||
using (_deviceListLock.GetWriteLock())
|
||||
{
|
||||
if (_devices.TryGetValue(unitId, out var device))
|
||||
device.SetCoil(coil);
|
||||
}
|
||||
}
|
||||
|
||||
public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
|
||||
{
|
||||
Assertions();
|
||||
using (_deviceListLock.GetReadLock())
|
||||
{
|
||||
return _devices.TryGetValue(unitId, out var device)
|
||||
? device.GetDiscreteInput(address)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
|
||||
{
|
||||
Assertions();
|
||||
using (_deviceListLock.GetWriteLock())
|
||||
{
|
||||
if (_devices.TryGetValue(unitId, out var device))
|
||||
device.SetDiscreteInput(discreteInput);
|
||||
}
|
||||
}
|
||||
|
||||
public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
|
||||
{
|
||||
Assertions();
|
||||
using (_deviceListLock.GetReadLock())
|
||||
{
|
||||
return _devices.TryGetValue(unitId, out var device)
|
||||
? device.GetHoldingRegister(address)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
|
||||
{
|
||||
Assertions();
|
||||
using (_deviceListLock.GetWriteLock())
|
||||
{
|
||||
if (_devices.TryGetValue(unitId, out var device))
|
||||
device.SetHoldingRegister(holdingRegister);
|
||||
}
|
||||
}
|
||||
|
||||
public InputRegister GetInputRegister(byte unitId, ushort address)
|
||||
{
|
||||
Assertions();
|
||||
using (_deviceListLock.GetReadLock())
|
||||
{
|
||||
return _devices.TryGetValue(unitId, out var device)
|
||||
? device.GetInputRegister(address)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetInputRegister(byte unitId, InputRegister inputRegister)
|
||||
{
|
||||
Assertions();
|
||||
using (_deviceListLock.GetWriteLock())
|
||||
{
|
||||
if (_devices.TryGetValue(unitId, out var device))
|
||||
device.SetInputRegister(inputRegister);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Entity Handling
|
||||
|
||||
private void Assertions()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ObjectDisposedException.ThrowIf(_isDisposed, this);
|
||||
#else
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,8 @@ The different types handled by the Modbus Protocol.
|
||||
In addition, you'll find the `DeviceIdentification` there.
|
||||
It is used for a "special" function called _Read Device Identification_ (0x2B / 43), not supported on all devices.
|
||||
|
||||
The `ModbusDevice` is used for the server implementations in the derived packages.
|
||||
The `ModbusDevice` is used for the `VirtualModbusClient`.
|
||||
In combination with the *Proxy implementations (in the derived packages) it can be used as server.
|
||||
|
||||
|
||||
### Protocols
|
||||
@@ -59,8 +60,8 @@ Here you have the specific default implementations for the Modbus Protocol.
|
||||
|
||||
- ASCII
|
||||
- RTU
|
||||
- RTU over TCP
|
||||
- TCP
|
||||
- [RTU over TCP]
|
||||
|
||||
**NOTE:**
|
||||
The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) and maximum unit ID of 247 referring to the specification.
|
||||
@@ -68,4 +69,10 @@ This validation is _not_ implemented here due to real world experience, that som
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
|
||||
Published under MIT License (see [choose a license])
|
||||
|
||||
|
||||
|
||||
[RTU over TCP]: https://www.fernhillsoftware.com/help/drivers/modbus/modbus-protocol.html
|
||||
[choose a license]: https://choosealicense.com/licenses/mit/
|
||||
|
||||
|
||||
180
AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs
Normal file
180
AMWD.Protocols.Modbus.Common/Utils/VirtualModbusClient.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
using AMWD.Protocols.Modbus.Common.Events;
|
||||
using AMWD.Protocols.Modbus.Common.Models;
|
||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a virtual Modbus client.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class VirtualModbusClient : ModbusClientBase
|
||||
{
|
||||
#region Constructor
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VirtualModbusClient"/> class.
|
||||
/// </summary>
|
||||
/// <remarks><strong>DO NOT MODIFY</strong> connection or protocol.</remarks>
|
||||
public VirtualModbusClient()
|
||||
: base(new VirtualConnection())
|
||||
{
|
||||
Protocol = new VirtualProtocol();
|
||||
|
||||
TypedProtocol.CoilWritten += (sender, e) => CoilWritten?.Invoke(this, e);
|
||||
TypedProtocol.RegisterWritten += (sender, e) => RegisterWritten?.Invoke(this, e);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a <see cref="Coil"/>-value received through a remote client has been written.
|
||||
/// </summary>
|
||||
public event EventHandler<CoilWrittenEventArgs> CoilWritten;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a <see cref="HoldingRegister"/>-value received from a remote client has been written.
|
||||
/// </summary>
|
||||
public event EventHandler<RegisterWrittenEventArgs> RegisterWritten;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Properties
|
||||
|
||||
internal VirtualProtocol TypedProtocol
|
||||
=> Protocol as VirtualProtocol;
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Device Handling
|
||||
|
||||
/// <summary>
|
||||
/// Adds a device to the virtual client.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id of the device.</param>
|
||||
/// <returns><see langword="true"/> if the device was added successfully, <see langword="false"/> otherwise.</returns>
|
||||
public bool AddDevice(byte unitId)
|
||||
=> TypedProtocol.AddDevice(unitId);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a device from the virtual client.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit id of the device.</param>
|
||||
/// <returns><see langword="true"/> if the device was removed successfully, <see langword="false"/> otherwise.</returns>
|
||||
public bool RemoveDevice(byte unitId)
|
||||
=> TypedProtocol.RemoveDevice(unitId);
|
||||
|
||||
#endregion Device Handling
|
||||
|
||||
#region Entity Handling
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Coil"/> from the specified <see cref="ModbusDevice"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit ID of the device.</param>
|
||||
/// <param name="address">The address of the <see cref="Coil"/>.</param>
|
||||
public Coil GetCoil(byte unitId, ushort address)
|
||||
=> TypedProtocol.GetCoil(unitId, address);
|
||||
|
||||
/// <summary>
|
||||
/// Sets a <see cref="Coil"/> to the specified <see cref="ModbusDevice"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit ID of the device.</param>
|
||||
/// <param name="coil">The <see cref="Coil"/> to set.</param>
|
||||
public void SetCoil(byte unitId, Coil coil)
|
||||
=> TypedProtocol.SetCoil(unitId, coil);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="DiscreteInput"/> from the specified <see cref="ModbusDevice"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit ID of the device.</param>
|
||||
/// <param name="address">The address of the <see cref="DiscreteInput"/>.</param>
|
||||
public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
|
||||
=> TypedProtocol.GetDiscreteInput(unitId, address);
|
||||
|
||||
/// <summary>
|
||||
/// Sets a <see cref="DiscreteInput"/> to the specified <see cref="ModbusDevice"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit ID of the device.</param>
|
||||
/// <param name="discreteInput">The <see cref="DiscreteInput"/> to set.</param>
|
||||
public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
|
||||
=> TypedProtocol.SetDiscreteInput(unitId, discreteInput);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="HoldingRegister"/> from the specified <see cref="ModbusDevice"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit ID of the device.</param>
|
||||
/// <param name="address">The address of the <see cref="HoldingRegister"/>.</param>
|
||||
public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
|
||||
=> TypedProtocol.GetHoldingRegister(unitId, address);
|
||||
|
||||
/// <summary>
|
||||
/// Sets a <see cref="HoldingRegister"/> to the specified <see cref="ModbusDevice"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit ID of the device.</param>
|
||||
/// <param name="holdingRegister">The <see cref="HoldingRegister"/> to set.</param>
|
||||
public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
|
||||
=> TypedProtocol.SetHoldingRegister(unitId, holdingRegister);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="InputRegister"/> from the specified <see cref="ModbusDevice"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit ID of the device.</param>
|
||||
/// <param name="address">The address of the <see cref="InputRegister"/>.</param>
|
||||
public InputRegister GetInputRegister(byte unitId, ushort address)
|
||||
=> TypedProtocol.GetInputRegister(unitId, address);
|
||||
|
||||
/// <summary>
|
||||
/// Sets a <see cref="InputRegister"/> to the specified <see cref="ModbusDevice"/>.
|
||||
/// </summary>
|
||||
/// <param name="unitId">The unit ID of the device.</param>
|
||||
/// <param name="inputRegister">The <see cref="InputRegister"/> to set.</param>
|
||||
public void SetInputRegister(byte unitId, InputRegister inputRegister)
|
||||
=> TypedProtocol.SetInputRegister(unitId, inputRegister);
|
||||
|
||||
#endregion Entity Handling
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
TypedProtocol.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
|
||||
#region Connection
|
||||
|
||||
internal class VirtualConnection : IModbusConnection
|
||||
{
|
||||
public string Name => nameof(VirtualConnection);
|
||||
|
||||
public TimeSpan IdleTimeout { get; set; }
|
||||
|
||||
public TimeSpan ConnectTimeout { get; set; }
|
||||
|
||||
public TimeSpan ReadTimeout { get; set; }
|
||||
|
||||
public TimeSpan WriteTimeout { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{ /* nothing to do */ }
|
||||
|
||||
public Task<IReadOnlyList<byte>> InvokeAsync(
|
||||
IReadOnlyList<byte> request,
|
||||
Func<IReadOnlyList<byte>, bool> validateResponseComplete,
|
||||
CancellationToken cancellationToken = default) => Task.FromResult(request);
|
||||
}
|
||||
|
||||
#endregion Connection
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
|
||||
<PackageId>AMWD.Protocols.Modbus.Proxy</PackageId>
|
||||
<AssemblyName>amwd-modbus-proxy</AssemblyName>
|
||||
<RootNamespace>AMWD.Protocols.Modbus.Proxy</RootNamespace>
|
||||
|
||||
<Product>Modbus Proxy Clients</Product>
|
||||
<Description>Plugging Modbus Servers and Clients together to create Modbus Proxies.</Description>
|
||||
<PackageTags>Modbus Protocol Proxy</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs" Link="Extensions/StreamExtensions.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="/" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="System.IO.Ports" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
|
||||
<PackageReference Include="System.IO.Ports" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,10 +0,0 @@
|
||||
# Modbus Protocol for .NET | Proxy
|
||||
|
||||
With this package the server and client implementations will be combined as proxy.
|
||||
|
||||
You can use any `ModbusBasClient` implementation as target client and plug it into the implemented `ModbusTcpProxy` or `ModbusRtuProxy`, which implement the server side.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
|
||||
<PackageId>AMWD.Protocols.Modbus.Serial</PackageId>
|
||||
<AssemblyName>amwd-modbus-serial</AssemblyName>
|
||||
@@ -14,11 +13,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs" Link="InternalsVisibleTo.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
|
||||
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
||||
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
|
||||
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
|
||||
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -38,11 +36,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Extensions\" />
|
||||
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,20 +8,20 @@ using System.Threading.Tasks;
|
||||
using AMWD.Protocols.Modbus.Common;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||
using AMWD.Protocols.Modbus.Serial;
|
||||
using AMWD.Protocols.Modbus.Serial.Utils;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Proxy
|
||||
namespace AMWD.Protocols.Modbus.Serial
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a Modbus serial line RTU server proxying all requests to a Modbus client of choice.
|
||||
/// </summary>
|
||||
public class ModbusRtuProxy : IDisposable
|
||||
public class ModbusRtuProxy : IModbusProxy
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
private readonly SerialPort _serialPort;
|
||||
private readonly SerialPortWrapper _serialPort;
|
||||
private CancellationTokenSource _stopCts;
|
||||
|
||||
#endregion Fields
|
||||
@@ -33,31 +33,25 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
|
||||
/// <param name="portName">The name of the serial port to use.</param>
|
||||
/// <param name="baudRate">The baud rate of the serial port (Default: 19.200).</param>
|
||||
public ModbusRtuProxy(ModbusClientBase client, string portName, BaudRate baudRate = BaudRate.Baud19200)
|
||||
public ModbusRtuProxy(ModbusClientBase client, string portName)
|
||||
{
|
||||
Client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(portName))
|
||||
throw new ArgumentNullException(nameof(portName));
|
||||
|
||||
if (!Enum.IsDefined(typeof(BaudRate), baudRate))
|
||||
throw new ArgumentOutOfRangeException(nameof(baudRate));
|
||||
|
||||
if (!ModbusSerialClient.AvailablePortNames.Contains(portName))
|
||||
throw new ArgumentException($"The serial port ({portName}) is not available.", nameof(portName));
|
||||
|
||||
_serialPort = new SerialPort
|
||||
_serialPort = new SerialPortWrapper
|
||||
{
|
||||
PortName = portName,
|
||||
BaudRate = (int)baudRate,
|
||||
Handshake = Handshake.None,
|
||||
|
||||
BaudRate = (int)BaudRate.Baud19200,
|
||||
DataBits = 8,
|
||||
ReadTimeout = 1000,
|
||||
RtsEnable = false,
|
||||
StopBits = StopBits.One,
|
||||
Parity = Parity.Even,
|
||||
Handshake = Handshake.None,
|
||||
ReadTimeout = 1000,
|
||||
WriteTimeout = 1000,
|
||||
Parity = Parity.Even
|
||||
RtsEnable = false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,80 +64,108 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
/// </summary>
|
||||
public ModbusClientBase Client { get; }
|
||||
|
||||
/// <inheritdoc cref="SerialPort.PortName"/>
|
||||
public string PortName => _serialPort.PortName;
|
||||
#region SerialPort Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the baud rate of the serial port.
|
||||
/// </summary>
|
||||
public BaudRate BaudRate
|
||||
/// <inheritdoc cref="SerialPort.PortName" />
|
||||
public virtual string PortName
|
||||
{
|
||||
get => _serialPort.PortName;
|
||||
set => _serialPort.PortName = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.BaudRate" />
|
||||
public virtual BaudRate BaudRate
|
||||
{
|
||||
get => (BaudRate)_serialPort.BaudRate;
|
||||
set => _serialPort.BaudRate = (int)value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Handshake"/>
|
||||
public Handshake Handshake
|
||||
{
|
||||
get => _serialPort.Handshake;
|
||||
set => _serialPort.Handshake = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.DataBits" />
|
||||
public int DataBits
|
||||
/// <remarks>
|
||||
/// From the Specs:
|
||||
/// <br/>
|
||||
/// On <see cref="AsciiProtocol"/> it can be 7 or 8.
|
||||
/// <br/>
|
||||
/// On <see cref="RtuProtocol"/> it has to be 8.
|
||||
/// </remarks>
|
||||
public virtual int DataBits
|
||||
{
|
||||
get => _serialPort.DataBits;
|
||||
set => _serialPort.DataBits = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.IsOpen"/>
|
||||
public bool IsOpen => _serialPort.IsOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a read operation does not finish.
|
||||
/// </summary>
|
||||
public TimeSpan ReadTimeout
|
||||
/// <inheritdoc cref="SerialPort.Handshake" />
|
||||
public virtual Handshake Handshake
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
|
||||
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
|
||||
get => _serialPort.Handshake;
|
||||
set => _serialPort.Handshake = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Parity" />
|
||||
/// <remarks>
|
||||
/// From the Specs:
|
||||
/// <br/>
|
||||
/// <see cref="Parity.Even"/> is recommended and therefore the default value.
|
||||
/// <br/>
|
||||
/// If you use <see cref="Parity.None"/>, <see cref="StopBits.Two"/> is required,
|
||||
/// otherwise <see cref="StopBits.One"/> should work fine.
|
||||
/// </remarks>
|
||||
public virtual Parity Parity
|
||||
{
|
||||
get => _serialPort.Parity;
|
||||
set => _serialPort.Parity = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.RtsEnable" />
|
||||
public bool RtsEnable
|
||||
public virtual bool RtsEnable
|
||||
{
|
||||
get => _serialPort.RtsEnable;
|
||||
set => _serialPort.RtsEnable = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.StopBits" />
|
||||
public StopBits StopBits
|
||||
/// <remarks>
|
||||
/// From the Specs:
|
||||
/// <br/>
|
||||
/// Should be <see cref="StopBits.One"/> for <see cref="Parity.Even"/> or <see cref="Parity.Odd"/>.
|
||||
/// <br/>
|
||||
/// Should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
|
||||
/// </remarks>
|
||||
public virtual StopBits StopBits
|
||||
{
|
||||
get => _serialPort.StopBits;
|
||||
set => _serialPort.StopBits = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPortWrapper.IsOpen"/>
|
||||
public bool IsOpen => _serialPort.IsOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a write operation does not finish.
|
||||
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a read/receive operation does not finish.
|
||||
/// </summary>
|
||||
public TimeSpan WriteTimeout
|
||||
public virtual TimeSpan ReadTimeout
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
|
||||
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a write/send operation does not finish.
|
||||
/// </summary>
|
||||
public virtual TimeSpan WriteTimeout
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
|
||||
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Parity"/>
|
||||
public Parity Parity
|
||||
{
|
||||
get => _serialPort.Parity;
|
||||
set => _serialPort.Parity = value;
|
||||
}
|
||||
#endregion SerialPort Properties
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Control Methods
|
||||
|
||||
/// <summary>
|
||||
/// Starts the server.
|
||||
/// Starts the proxy.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||
@@ -164,23 +186,22 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the server.
|
||||
/// Stops the proxy.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
public Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assertions();
|
||||
return StopAsyncInternal(cancellationToken);
|
||||
StopAsyncInternal();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task StopAsyncInternal(CancellationToken cancellationToken)
|
||||
private void StopAsyncInternal()
|
||||
{
|
||||
_stopCts.Cancel();
|
||||
_stopCts?.Cancel();
|
||||
|
||||
_serialPort.Close();
|
||||
_serialPort.DataReceived -= OnDataReceived;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -193,7 +214,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
StopAsyncInternal(CancellationToken.None).Wait();
|
||||
StopAsyncInternal();
|
||||
|
||||
_serialPort.Dispose();
|
||||
_stopCts?.Dispose();
|
||||
@@ -207,13 +228,16 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException(GetType().FullName);
|
||||
#endif
|
||||
|
||||
if (string.IsNullOrWhiteSpace(PortName))
|
||||
throw new ArgumentNullException(nameof(PortName), "The serial port name cannot be empty.");
|
||||
}
|
||||
|
||||
#endregion Control Methods
|
||||
|
||||
#region Client Handling
|
||||
|
||||
private void OnDataReceived(object _, SerialDataReceivedEventArgs evArgs)
|
||||
private void OnDataReceived(object _, SerialDataReceivedEventArgs __)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -282,16 +306,14 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
default: // unknown function
|
||||
{
|
||||
byte[] responseBytes = new byte[5];
|
||||
Array.Copy(requestBytes, 0, responseBytes, 0, 2);
|
||||
var responseBytes = new List<byte>();
|
||||
responseBytes.AddRange(requestBytes.Take(2));
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||
|
||||
// Mark as error
|
||||
responseBytes[1] |= 0x80;
|
||||
|
||||
responseBytes[2] = (byte)ModbusErrorCode.IllegalFunction;
|
||||
|
||||
SetCrc(responseBytes);
|
||||
return responseBytes;
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,7 +331,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.AddRange(requestBytes.Take(2));
|
||||
try
|
||||
{
|
||||
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
|
||||
for (int i = 0; i < coils.Count; i++)
|
||||
@@ -332,8 +354,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -349,7 +370,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.AddRange(requestBytes.Take(2));
|
||||
try
|
||||
{
|
||||
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
|
||||
for (int i = 0; i < discreteInputs.Count; i++)
|
||||
@@ -372,8 +393,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -389,7 +409,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.AddRange(requestBytes.Take(2));
|
||||
try
|
||||
{
|
||||
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[holdingRegisters.Count * 2];
|
||||
for (int i = 0; i < holdingRegisters.Count; i++)
|
||||
@@ -407,8 +427,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -424,7 +443,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.AddRange(requestBytes.Take(2));
|
||||
try
|
||||
{
|
||||
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[count * 2];
|
||||
for (int i = 0; i < count; i++)
|
||||
@@ -442,8 +461,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -461,8 +479,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes[1] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -474,7 +491,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
LowByte = requestBytes[5],
|
||||
};
|
||||
|
||||
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken);
|
||||
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -492,8 +509,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -514,7 +530,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
LowByte = requestBytes[5]
|
||||
};
|
||||
|
||||
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken);
|
||||
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -532,8 +548,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -553,8 +568,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes[1] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -576,7 +590,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
});
|
||||
}
|
||||
|
||||
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken);
|
||||
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -594,8 +608,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -604,7 +617,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
return null;
|
||||
|
||||
var responseBytes = new List<byte>();
|
||||
responseBytes.AddRange(requestBytes.Take(8));
|
||||
responseBytes.AddRange(requestBytes.Take(2));
|
||||
|
||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||
@@ -615,8 +628,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes[1] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -633,8 +645,9 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
HighByte = requestBytes[baseOffset + i * 2],
|
||||
LowByte = requestBytes[baseOffset + i * 2 + 1]
|
||||
});
|
||||
}
|
||||
|
||||
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken);
|
||||
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -646,19 +659,20 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
responseBytes[1] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
{
|
||||
if (requestBytes.Length < 7)
|
||||
return null;
|
||||
|
||||
var responseBytes = new List<byte>();
|
||||
responseBytes.AddRange(requestBytes.Take(2));
|
||||
|
||||
@@ -667,8 +681,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes[1] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[4];
|
||||
@@ -677,8 +690,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes[1] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
var category = (ModbusDeviceIdentificationCategory)requestBytes[3];
|
||||
@@ -687,13 +699,12 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes[1] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken);
|
||||
var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[0], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
var bodyBytes = new List<byte>();
|
||||
|
||||
@@ -702,31 +713,20 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
// Conformity
|
||||
bodyBytes.Add((byte)category);
|
||||
if (res.IsIndividualAccessAllowed)
|
||||
if (deviceInfo.IsIndividualAccessAllowed)
|
||||
bodyBytes[2] |= 0x80;
|
||||
|
||||
// More, NextId, NumberOfObjects
|
||||
bodyBytes.AddRange(new byte[3]);
|
||||
|
||||
int maxObjectId;
|
||||
switch (category)
|
||||
int maxObjectId = category switch
|
||||
{
|
||||
case ModbusDeviceIdentificationCategory.Basic:
|
||||
maxObjectId = 0x02;
|
||||
break;
|
||||
|
||||
case ModbusDeviceIdentificationCategory.Regular:
|
||||
maxObjectId = 0x06;
|
||||
break;
|
||||
|
||||
case ModbusDeviceIdentificationCategory.Extended:
|
||||
maxObjectId = 0xFF;
|
||||
break;
|
||||
|
||||
default: // Individual
|
||||
maxObjectId = requestBytes[4];
|
||||
break;
|
||||
}
|
||||
ModbusDeviceIdentificationCategory.Basic => 0x02,
|
||||
ModbusDeviceIdentificationCategory.Regular => 0x06,
|
||||
ModbusDeviceIdentificationCategory.Extended => 0xFF,
|
||||
// Individual
|
||||
_ => requestBytes[4],
|
||||
};
|
||||
|
||||
byte numberOfObjects = 0;
|
||||
for (int i = requestBytes[4]; i <= maxObjectId; i++)
|
||||
@@ -735,17 +735,19 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
if (0x07 <= i && i <= 0x7F)
|
||||
continue;
|
||||
|
||||
byte[] objBytes = GetDeviceObject((byte)i, res);
|
||||
byte[] objBytes = GetDeviceObject((byte)i, deviceInfo);
|
||||
|
||||
// We need to split the response if it would exceed the max ADU size
|
||||
if (responseBytes.Count + bodyBytes.Count + objBytes.Length > RtuProtocol.MAX_ADU_LENGTH)
|
||||
// We need to split the response if it would exceed the max ADU size.
|
||||
// 2 bytes of CRC have to be added.
|
||||
if (responseBytes.Count + bodyBytes.Count + objBytes.Length + 2 > RtuProtocol.MAX_ADU_LENGTH)
|
||||
{
|
||||
bodyBytes[3] = 0xFF;
|
||||
bodyBytes[4] = (byte)i;
|
||||
|
||||
bodyBytes[5] = numberOfObjects;
|
||||
responseBytes.AddRange(bodyBytes);
|
||||
return [.. responseBytes];
|
||||
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
bodyBytes.AddRange(objBytes);
|
||||
@@ -755,16 +757,14 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
bodyBytes[5] = numberOfObjects;
|
||||
responseBytes.AddRange(bodyBytes);
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
responseBytes[1] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
|
||||
AddCrc(responseBytes);
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,7 +775,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
{
|
||||
case ModbusDeviceIdentificationObject.VendorName:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -783,7 +783,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.ProductCode:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -791,7 +791,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.MajorMinorRevision:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -799,7 +799,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.VendorUrl:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -807,7 +807,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.ProductName:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -815,7 +815,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.ModelName:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -823,7 +823,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.UserApplicationName:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -831,9 +831,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
default:
|
||||
{
|
||||
if (deviceIdentification.ExtendedObjects.ContainsKey(objectId))
|
||||
if (deviceIdentification.ExtendedObjects.TryGetValue(objectId, out byte[] bytes))
|
||||
{
|
||||
byte[] bytes = deviceIdentification.ExtendedObjects[objectId];
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -848,20 +847,28 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
return [.. result];
|
||||
}
|
||||
|
||||
private static void SetCrc(byte[] bytes)
|
||||
private static byte[] ReturnResponse(List<byte> response)
|
||||
{
|
||||
byte[] crc = RtuProtocol.CRC16(bytes, 0, bytes.Length - 2);
|
||||
bytes[bytes.Length - 2] = crc[0];
|
||||
bytes[bytes.Length - 1] = crc[1];
|
||||
}
|
||||
|
||||
private static void AddCrc(List<byte> bytes)
|
||||
{
|
||||
byte[] crc = RtuProtocol.CRC16(bytes);
|
||||
bytes.Add(crc[0]);
|
||||
bytes.Add(crc[1]);
|
||||
response.AddRange(RtuProtocol.CRC16(response));
|
||||
return [.. response];
|
||||
}
|
||||
|
||||
#endregion Request Handling
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"RTU Proxy");
|
||||
sb.AppendLine($" {nameof(PortName)}: {PortName}");
|
||||
sb.AppendLine($" {nameof(BaudRate)}: {(int)BaudRate}");
|
||||
sb.AppendLine($" {nameof(DataBits)}: {DataBits}");
|
||||
sb.AppendLine($" {nameof(StopBits)}: {StopBits}");
|
||||
sb.AppendLine($" {nameof(Parity)}: {Parity}");
|
||||
sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO.Ports;
|
||||
using System.Text;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||
|
||||
@@ -15,7 +16,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
/// </summary>
|
||||
/// <param name="portName">The name of the serial port to use.</param>
|
||||
public ModbusSerialClient(string portName)
|
||||
: this(new ModbusSerialConnection { PortName = portName })
|
||||
: this(new ModbusSerialConnection(portName))
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
@@ -40,8 +41,8 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
Protocol = new RtuProtocol();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.GetPortNames" />
|
||||
public static string[] AvailablePortNames => SerialPort.GetPortNames();
|
||||
/// <inheritdoc cref="ModbusSerialConnection.AvailablePortNames" />
|
||||
public static string[] AvailablePortNames => ModbusSerialConnection.AvailablePortNames;
|
||||
|
||||
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
||||
public TimeSpan IdleTimeout
|
||||
@@ -223,5 +224,22 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
serialConnection.StopBits = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"Serial Client {PortName}");
|
||||
sb.AppendLine($" {nameof(BaudRate)}: {(int)BaudRate}");
|
||||
sb.AppendLine($" {nameof(DataBits)}: {DataBits}");
|
||||
sb.AppendLine($" {nameof(StopBits)}: {(StopBits == StopBits.OnePointFive ? "1.5" : ((int)StopBits).ToString())}");
|
||||
sb.AppendLine($" {nameof(Parity)}: {Parity.ToString().ToLower()}");
|
||||
sb.AppendLine($" {nameof(Handshake)}: {Handshake.ToString().ToLower()}");
|
||||
sb.AppendLine($" {nameof(RtsEnable)}: {RtsEnable.ToString().ToLower()}");
|
||||
sb.AppendLine($" {nameof(DriverEnabledRS485)}: {DriverEnabledRS485.ToString().ToLower()}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,18 +31,24 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
private readonly Task _processingTask;
|
||||
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
||||
|
||||
// Only required to cover all logic branches on unit tests.
|
||||
private bool _isUnitTest = false;
|
||||
private readonly bool _isLinux;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusSerialConnection"/> class.
|
||||
/// </summary>
|
||||
public ModbusSerialConnection()
|
||||
public ModbusSerialConnection(string portName)
|
||||
{
|
||||
_isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(portName))
|
||||
throw new ArgumentNullException(nameof(portName));
|
||||
|
||||
_serialPort = new SerialPortWrapper
|
||||
{
|
||||
PortName = portName,
|
||||
|
||||
BaudRate = (int)BaudRate.Baud19200,
|
||||
DataBits = 8,
|
||||
Handshake = Handshake.None,
|
||||
@@ -59,6 +65,9 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <inheritdoc cref="SerialPort.GetPortNames" />
|
||||
public static string[] AvailablePortNames => SerialPort.GetPortNames();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "Serial";
|
||||
|
||||
@@ -68,20 +77,6 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
/// <inheritdoc/>
|
||||
public virtual TimeSpan ConnectTimeout { get; set; } = TimeSpan.MaxValue;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual TimeSpan ReadTimeout
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
|
||||
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual TimeSpan WriteTimeout
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
|
||||
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the RS485 driver has to be enabled via software switch.
|
||||
/// </summary>
|
||||
@@ -107,9 +102,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
set => _serialPort.PortName = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the serial baud rate.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="SerialPort.BaudRate" />
|
||||
public virtual BaudRate BaudRate
|
||||
{
|
||||
get => (BaudRate)_serialPort.BaudRate;
|
||||
@@ -118,7 +111,11 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
|
||||
/// <inheritdoc cref="SerialPort.DataBits" />
|
||||
/// <remarks>
|
||||
/// Should be 7 for ASCII mode and 8 for RTU mode.
|
||||
/// From the Specs:
|
||||
/// <br/>
|
||||
/// On <see cref="AsciiProtocol"/> it can be 7 or 8.
|
||||
/// <br/>
|
||||
/// On <see cref="RtuProtocol"/> it has to be 8.
|
||||
/// </remarks>
|
||||
public virtual int DataBits
|
||||
{
|
||||
@@ -159,9 +156,9 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
/// <remarks>
|
||||
/// From the Specs:
|
||||
/// <br/>
|
||||
/// Should be <see cref="StopBits.One"/> for <see cref="Parity.Even"/> or <see cref="Parity.Odd"/> and
|
||||
/// Should be <see cref="StopBits.One"/> for <see cref="Parity.Even"/> or <see cref="Parity.Odd"/>.
|
||||
/// <br/>
|
||||
/// should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
|
||||
/// Should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
|
||||
/// </remarks>
|
||||
public virtual StopBits StopBits
|
||||
{
|
||||
@@ -169,6 +166,20 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
set => _serialPort.StopBits = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual TimeSpan ReadTimeout
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
|
||||
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual TimeSpan WriteTimeout
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
|
||||
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
|
||||
}
|
||||
|
||||
#endregion SerialPort Properties
|
||||
|
||||
#endregion Properties
|
||||
@@ -188,7 +199,6 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
|
||||
try
|
||||
{
|
||||
_processingTask.Wait();
|
||||
_processingTask.Dispose();
|
||||
}
|
||||
catch
|
||||
@@ -259,7 +269,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
try
|
||||
{
|
||||
// Get next request to process
|
||||
var item = await _requestQueue.DequeueAsync(cancellationToken);
|
||||
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
// Remove registration => already removed from queue
|
||||
item.CancellationTokenRegistration.Dispose();
|
||||
@@ -267,13 +277,13 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
// Build combined cancellation token
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
||||
// Wait for exclusive access
|
||||
await _portLock.WaitAsync(linkedCts.Token);
|
||||
await _portLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
try
|
||||
{
|
||||
// Ensure connection is up
|
||||
await AssertConnection(linkedCts.Token);
|
||||
|
||||
await _serialPort.WriteAsync(item.Request, linkedCts.Token);
|
||||
await _serialPort.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
linkedCts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -282,7 +292,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
|
||||
do
|
||||
{
|
||||
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token);
|
||||
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (readCount < 1)
|
||||
throw new EndOfStreamException();
|
||||
|
||||
@@ -313,7 +323,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
_portLock.Release();
|
||||
_idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan);
|
||||
|
||||
await Task.Delay(InterRequestDelay, cancellationToken);
|
||||
await Task.Delay(InterRequestDelay, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
@@ -344,7 +354,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
_serialPort.Close();
|
||||
_serialPort.ResetRS485DriverStateFlags();
|
||||
|
||||
if (DriverEnabledRS485 && (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || _isUnitTest))
|
||||
if (DriverEnabledRS485 && _isLinux)
|
||||
{
|
||||
var flags = _serialPort.GetRS485DriverStateFlags();
|
||||
flags |= RS485Flags.Enabled;
|
||||
@@ -352,7 +362,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
_serialPort.ChangeRS485DriverStateFlags(flags);
|
||||
}
|
||||
|
||||
using var connectTask = Task.Run(_serialPort.Open);
|
||||
using var connectTask = Task.Run(_serialPort.Open, cancellationToken);
|
||||
if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask)
|
||||
{
|
||||
await connectTask;
|
||||
@@ -370,7 +380,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
catch
|
||||
{ /* keep it quiet */ }
|
||||
|
||||
@@ -44,10 +44,10 @@ using var client = new ModbusSerialClient(serialPort)
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [**tl;dr**Legal])
|
||||
Published under MIT License (see [choose a license])
|
||||
|
||||
|
||||
|
||||
[v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
|
||||
[v1.02]: https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf
|
||||
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
|
||||
[choose a license]: https://choosealicense.com/licenses/mit/
|
||||
|
||||
@@ -20,6 +20,30 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructor
|
||||
|
||||
public SerialPortWrapper()
|
||||
{
|
||||
_serialPort.DataReceived += (sender, e) => DataReceived?.Invoke(this, e);
|
||||
_serialPort.PinChanged += (sender, e) => PinChanged?.Invoke(this, e);
|
||||
_serialPort.ErrorReceived += (sender, e) => ErrorReceived?.Invoke(this, e);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Events
|
||||
|
||||
/// <inheritdoc cref="SerialPort.DataReceived"/>
|
||||
public virtual event SerialDataReceivedEventHandler DataReceived;
|
||||
|
||||
/// <inheritdoc cref="SerialPort.PinChanged"/>
|
||||
public virtual event SerialPinChangedEventHandler PinChanged;
|
||||
|
||||
/// <inheritdoc cref="SerialPort.ErrorReceived"/>
|
||||
public virtual event SerialErrorReceivedEventHandler ErrorReceived;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Handshake"/>
|
||||
@@ -82,6 +106,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
||||
set => _serialPort.Parity = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.BytesToWrite"/>
|
||||
public virtual int BytesToWrite
|
||||
=> _serialPort.BytesToWrite;
|
||||
|
||||
/// <inheritdoc cref="SerialPort.BaudRate"/>
|
||||
public virtual int BaudRate
|
||||
{
|
||||
@@ -89,6 +117,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
||||
set => _serialPort.BaudRate = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SerialPort.BytesToRead"/>
|
||||
public virtual int BytesToRead
|
||||
=> _serialPort.BytesToRead;
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
@@ -101,6 +133,14 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
||||
public virtual void Open()
|
||||
=> _serialPort.Open();
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Read(byte[], int, int)"/>
|
||||
public virtual int Read(byte[] buffer, int offset, int count)
|
||||
=> _serialPort.Read(buffer, offset, count);
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Write(byte[], int, int)"/>
|
||||
public virtual void Write(byte[] buffer, int offset, int count)
|
||||
=> _serialPort.Write(buffer, offset, count);
|
||||
|
||||
/// <inheritdoc cref="SerialPort.Dispose"/>
|
||||
public virtual void Dispose()
|
||||
=> _serialPort.Dispose();
|
||||
@@ -117,7 +157,7 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
||||
/// <remarks>
|
||||
/// There seems to be a bug with the async stream implementation on Windows.
|
||||
/// <br/>
|
||||
/// See this StackOverflow answer: <see href="https://stackoverflow.com/a/54610437/11906695" />
|
||||
/// See this StackOverflow answer: <see href="https://stackoverflow.com/a/54610437/11906695" />.
|
||||
/// </remarks>
|
||||
/// <param name="buffer">The buffer to write the data into.</param>
|
||||
/// <param name="offset">The byte offset in buffer at which to begin writing data from the serial port.</param>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
|
||||
<PackageId>AMWD.Protocols.Modbus.Tcp</PackageId>
|
||||
<AssemblyName>amwd-modbus-tcp</AssemblyName>
|
||||
@@ -14,11 +13,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs" Link="InternalsVisibleTo.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
|
||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
|
||||
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
||||
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Extensions/ReaderWriterLockSlimExtensions.cs" Link="Extensions/ReaderWriterLockSlimExtensions.cs" />
|
||||
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Utils/AsyncQueue.cs" Link="Utils/AsyncQueue.cs" />
|
||||
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -26,7 +24,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Protocols.Modbus.Tcp.Utils;
|
||||
|
||||
namespace System.IO
|
||||
{
|
||||
@@ -11,7 +12,25 @@ namespace System.IO
|
||||
int offset = 0;
|
||||
do
|
||||
{
|
||||
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken);
|
||||
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (count < 1)
|
||||
throw new EndOfStreamException();
|
||||
|
||||
offset += count;
|
||||
}
|
||||
while (offset < expectedBytes && !cancellationToken.IsCancellationRequested);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public static async Task<byte[]> ReadExpectedBytesAsync(this NetworkStreamWrapper stream, int expectedBytes, CancellationToken cancellationToken = default)
|
||||
{
|
||||
byte[] buffer = new byte[expectedBytes];
|
||||
int offset = 0;
|
||||
do
|
||||
{
|
||||
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (count < 1)
|
||||
throw new EndOfStreamException();
|
||||
|
||||
|
||||
17
AMWD.Protocols.Modbus.Tcp/Extensions/TaskExtensions.cs
Normal file
17
AMWD.Protocols.Modbus.Tcp/Extensions/TaskExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Extensions
|
||||
{
|
||||
internal static class TaskExtensions
|
||||
{
|
||||
public static async void Forget(this Task task)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
}
|
||||
catch
|
||||
{ /* keep it quiet */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||
|
||||
@@ -101,5 +102,16 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
tcpConnection.Port = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"TCP Client {Hostname}");
|
||||
sb.AppendLine($" {nameof(Port)}: {Port}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
private readonly Task _processingTask;
|
||||
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
||||
|
||||
private TimeSpan _readTimeout = TimeSpan.FromMilliseconds(1);
|
||||
private TimeSpan _writeTimeout = TimeSpan.FromMilliseconds(1);
|
||||
private TimeSpan _readTimeout = TimeSpan.FromSeconds(1);
|
||||
private TimeSpan _writeTimeout = TimeSpan.FromSeconds(1);
|
||||
|
||||
#endregion Fields
|
||||
|
||||
@@ -65,8 +65,12 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
get => _readTimeout;
|
||||
set
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
|
||||
#else
|
||||
if (value < TimeSpan.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
#endif
|
||||
|
||||
_readTimeout = value;
|
||||
|
||||
@@ -81,8 +85,12 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
get => _writeTimeout;
|
||||
set
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
|
||||
#else
|
||||
if (value < TimeSpan.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
#endif
|
||||
|
||||
_writeTimeout = value;
|
||||
|
||||
@@ -208,7 +216,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
try
|
||||
{
|
||||
// Get next request to process
|
||||
var item = await _requestQueue.DequeueAsync(cancellationToken);
|
||||
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
// Remove registration => already removed from queue
|
||||
item.CancellationTokenRegistration.Dispose();
|
||||
@@ -216,19 +224,19 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
// Build combined cancellation token
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
||||
// Wait for exclusive access
|
||||
await _clientLock.WaitAsync(linkedCts.Token);
|
||||
await _clientLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
try
|
||||
{
|
||||
// Ensure connection is up
|
||||
await AssertConnection(linkedCts.Token);
|
||||
await AssertConnection(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
var stream = _tcpClient.GetStream();
|
||||
await stream.FlushAsync(linkedCts.Token);
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
await stream.WriteAsync(item.Request, linkedCts.Token);
|
||||
await stream.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
#else
|
||||
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token);
|
||||
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
#endif
|
||||
|
||||
linkedCts.Token.ThrowIfCancellationRequested();
|
||||
@@ -239,9 +247,9 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
do
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
int readCount = await stream.ReadAsync(buffer, linkedCts.Token);
|
||||
int readCount = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
#else
|
||||
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token);
|
||||
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
#endif
|
||||
if (readCount < 1)
|
||||
throw new EndOfStreamException();
|
||||
@@ -332,7 +340,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
catch
|
||||
{ /* keep it quiet */ }
|
||||
@@ -376,10 +384,9 @@ namespace AMWD.Protocols.Modbus.Tcp
|
||||
|
||||
try
|
||||
{
|
||||
return Dns.GetHostAddresses(hostname)
|
||||
return [.. Dns.GetHostAddresses(hostname)
|
||||
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
.OrderBy(a => a.AddressFamily) // prefer IPv4
|
||||
.ToArray();
|
||||
.OrderBy(a => a.AddressFamily)]; // prefer IPv4
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -10,62 +10,38 @@ using System.Threading.Tasks;
|
||||
using AMWD.Protocols.Modbus.Common;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||
using AMWD.Protocols.Modbus.Tcp.Extensions;
|
||||
using AMWD.Protocols.Modbus.Tcp.Utils;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Proxy
|
||||
namespace AMWD.Protocols.Modbus.Tcp
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a Modbus TCP server proxying all requests to a Modbus client of choice.
|
||||
/// </summary>
|
||||
public class ModbusTcpProxy : IDisposable
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="ModbusTcpProxy"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
|
||||
/// <param name="listenAddress">An <see cref="IPAddress"/> to listen on.</param>
|
||||
public class ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress) : IModbusProxy
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
private TcpListener _listener;
|
||||
private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(100);
|
||||
|
||||
private readonly TcpListenerWrapper _tcpListener = new(listenAddress, 502);
|
||||
private CancellationTokenSource _stopCts;
|
||||
private Task _clientConnectTask = Task.CompletedTask;
|
||||
|
||||
private readonly SemaphoreSlim _clientListLock = new(1, 1);
|
||||
private readonly List<TcpClient> _clients = [];
|
||||
private readonly List<Task> _clientTasks = [];
|
||||
private readonly List<TcpClientWrapper> _clients = [];
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModbusTcpProxy"/> class.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
|
||||
/// <param name="listenAddress">An <see cref="IPAddress"/> to listen on (Default: <see cref="IPAddress.Loopback"/>).</param>
|
||||
/// <param name="listenPort">A port to listen on (Default: 502).</param>
|
||||
public ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress = null, int listenPort = 502)
|
||||
{
|
||||
Client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
|
||||
ListenAddress = listenAddress ?? IPAddress.Loopback;
|
||||
|
||||
if (ushort.MinValue < listenPort || listenPort < ushort.MaxValue)
|
||||
throw new ArgumentOutOfRangeException(nameof(listenPort));
|
||||
|
||||
try
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
using var testListener = new TcpListener(ListenAddress, listenPort);
|
||||
#else
|
||||
var testListener = new TcpListener(ListenAddress, listenPort);
|
||||
#endif
|
||||
testListener.Start(1);
|
||||
ListenPort = (testListener.LocalEndpoint as IPEndPoint).Port;
|
||||
testListener.Stop();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ArgumentException($"{nameof(ListenPort)} ({listenPort}) is already in use.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
@@ -73,27 +49,46 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
/// <summary>
|
||||
/// Gets the Modbus client used to request the remote device, that should be proxied.
|
||||
/// </summary>
|
||||
public ModbusClientBase Client { get; }
|
||||
public ModbusClientBase Client { get; } = client ?? throw new ArgumentNullException(nameof(client));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IPAddress"/> to listen on.
|
||||
/// </summary>
|
||||
public IPAddress ListenAddress { get; }
|
||||
public IPAddress ListenAddress
|
||||
{
|
||||
get => _tcpListener.LocalIPEndPoint.Address;
|
||||
set => _tcpListener.LocalIPEndPoint.Address = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the port to listen on.
|
||||
/// </summary>
|
||||
public int ListenPort { get; }
|
||||
public int ListenPort
|
||||
{
|
||||
get => _tcpListener.LocalIPEndPoint.Port;
|
||||
set => _tcpListener.LocalIPEndPoint.Port = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the server is running.
|
||||
/// </summary>
|
||||
public bool IsRunning => _listener?.Server.IsBound ?? false;
|
||||
public bool IsRunning => _tcpListener.Socket.IsBound;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the read/write timeout for the incoming connections (not the <see cref="Client"/>!).
|
||||
/// Default: 100 seconds.
|
||||
/// </summary>
|
||||
public TimeSpan ReadWriteTimeout { get; set; }
|
||||
public TimeSpan ReadWriteTimeout
|
||||
{
|
||||
get => _readWriteTimeout;
|
||||
set
|
||||
{
|
||||
if (value != Timeout.InfiniteTimeSpan && value < TimeSpan.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
_readWriteTimeout = value;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
@@ -108,20 +103,17 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
Assertions();
|
||||
|
||||
_stopCts?.Cancel();
|
||||
|
||||
_listener?.Stop();
|
||||
#if NET8_0_OR_GREATER
|
||||
_listener?.Dispose();
|
||||
#endif
|
||||
_tcpListener.Stop();
|
||||
|
||||
_stopCts?.Dispose();
|
||||
_stopCts = new CancellationTokenSource();
|
||||
|
||||
_listener = new TcpListener(ListenAddress, ListenPort);
|
||||
// Only allowed to set, if the socket is in the InterNetworkV6 address family.
|
||||
// See: https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.dualmode?view=netstandard-2.0#exceptions
|
||||
if (ListenAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
_listener.Server.DualMode = true;
|
||||
_tcpListener.Socket.DualMode = true;
|
||||
|
||||
_listener.Start();
|
||||
_tcpListener.Start();
|
||||
_clientConnectTask = WaitForClientAsync(_stopCts.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
@@ -139,24 +131,12 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_stopCts.Cancel();
|
||||
|
||||
_listener.Stop();
|
||||
#if NET8_0_OR_GREATER
|
||||
_listener.Dispose();
|
||||
#endif
|
||||
try
|
||||
{
|
||||
await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Terminated
|
||||
}
|
||||
_stopCts?.Cancel();
|
||||
_tcpListener.Stop();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAny(Task.WhenAll(_clientTasks), Task.Delay(Timeout.Infinite, cancellationToken));
|
||||
await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -178,8 +158,10 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
_clientListLock.Dispose();
|
||||
_clients.Clear();
|
||||
_tcpListener.Dispose();
|
||||
|
||||
_stopCts?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Assertions()
|
||||
@@ -202,16 +184,13 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
{
|
||||
try
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
var client = await _listener.AcceptTcpClientAsync(cancellationToken);
|
||||
#else
|
||||
var client = await _listener.AcceptTcpClientAsync();
|
||||
#endif
|
||||
await _clientListLock.WaitAsync(cancellationToken);
|
||||
var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
try
|
||||
{
|
||||
_clients.Add(client);
|
||||
_clientTasks.Add(HandleClientAsync(client, cancellationToken));
|
||||
// Can be ignored as it will terminate by itself on cancellation
|
||||
HandleClientAsync(client, cancellationToken).Forget();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -225,7 +204,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
|
||||
private async Task HandleClientAsync(TcpClientWrapper client, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -234,23 +213,32 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
{
|
||||
var requestBytes = new List<byte>();
|
||||
|
||||
// Waiting for next request
|
||||
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
requestBytes.AddRange(headerBytes);
|
||||
|
||||
ushort length = headerBytes
|
||||
.Skip(4).Take(2).ToArray()
|
||||
.GetBigEndianUInt16();
|
||||
|
||||
// Waiting for the remaining required data
|
||||
using (var cts = new CancellationTokenSource(ReadWriteTimeout))
|
||||
using (cancellationToken.Register(cts.Cancel))
|
||||
{
|
||||
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token);
|
||||
requestBytes.AddRange(headerBytes);
|
||||
|
||||
byte[] followingCountBytes = headerBytes.Skip(4).Take(2).ToArray();
|
||||
followingCountBytes.SwapBigEndian();
|
||||
int followingCount = BitConverter.ToUInt16(followingCountBytes, 0);
|
||||
|
||||
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token);
|
||||
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(length, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
requestBytes.AddRange(bodyBytes);
|
||||
}
|
||||
|
||||
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken);
|
||||
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (responseBytes != null)
|
||||
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken);
|
||||
{
|
||||
// Write response when available
|
||||
using (var cts = new CancellationTokenSource(ReadWriteTimeout))
|
||||
using (cancellationToken.Register(cts.Cancel))
|
||||
{
|
||||
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -259,7 +247,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _clientListLock.WaitAsync(cancellationToken);
|
||||
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
try
|
||||
{
|
||||
_clients.Remove(client);
|
||||
@@ -309,14 +297,14 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
default: // unknown function
|
||||
{
|
||||
byte[] responseBytes = new byte[9];
|
||||
Array.Copy(requestBytes, 0, responseBytes, 0, 8);
|
||||
var responseBytes = new List<byte>();
|
||||
responseBytes.AddRange(requestBytes.Take(8));
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||
|
||||
// Mark as error
|
||||
responseBytes[7] |= 0x80;
|
||||
|
||||
responseBytes[8] = (byte)ModbusErrorCode.IllegalFunction;
|
||||
return Task.FromResult(responseBytes);
|
||||
return Task.FromResult(ReturnResponse(responseBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,7 +322,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.AddRange(requestBytes.Take(8));
|
||||
try
|
||||
{
|
||||
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
|
||||
for (int i = 0; i < coils.Count; i++)
|
||||
@@ -357,7 +345,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -373,7 +361,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.AddRange(requestBytes.Take(8));
|
||||
try
|
||||
{
|
||||
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
|
||||
for (int i = 0; i < discreteInputs.Count; i++)
|
||||
@@ -396,7 +384,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -412,7 +400,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.AddRange(requestBytes.Take(8));
|
||||
try
|
||||
{
|
||||
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[holdingRegisters.Count * 2];
|
||||
for (int i = 0; i < holdingRegisters.Count; i++)
|
||||
@@ -430,7 +418,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -446,7 +434,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.AddRange(requestBytes.Take(8));
|
||||
try
|
||||
{
|
||||
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken);
|
||||
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
byte[] values = new byte[count * 2];
|
||||
for (int i = 0; i < count; i++)
|
||||
@@ -464,7 +452,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -481,7 +469,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
{
|
||||
responseBytes[7] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||
return [.. responseBytes];
|
||||
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -493,7 +482,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
LowByte = requestBytes[11],
|
||||
};
|
||||
|
||||
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken);
|
||||
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -511,7 +500,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -533,7 +522,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
LowByte = requestBytes[11]
|
||||
};
|
||||
|
||||
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken);
|
||||
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -551,7 +540,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -570,7 +559,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
{
|
||||
responseBytes[7] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||
return [.. responseBytes];
|
||||
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -592,7 +582,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
});
|
||||
}
|
||||
|
||||
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken);
|
||||
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -610,7 +600,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
@@ -629,7 +619,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
{
|
||||
responseBytes[7] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||
return [.. responseBytes];
|
||||
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -646,8 +637,9 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
HighByte = requestBytes[baseOffset + i * 2],
|
||||
LowByte = requestBytes[baseOffset + i * 2 + 1]
|
||||
});
|
||||
}
|
||||
|
||||
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken);
|
||||
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
if (isSuccess)
|
||||
{
|
||||
// Response is an echo of the request
|
||||
@@ -659,18 +651,20 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
responseBytes[7] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
}
|
||||
|
||||
return [.. responseBytes];
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||
{
|
||||
if (requestBytes.Length < 11)
|
||||
return null;
|
||||
|
||||
var responseBytes = new List<byte>();
|
||||
responseBytes.AddRange(requestBytes.Take(8));
|
||||
|
||||
@@ -678,7 +672,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
{
|
||||
responseBytes[7] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||
return [.. responseBytes];
|
||||
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[10];
|
||||
@@ -686,7 +681,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
{
|
||||
responseBytes[7] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
|
||||
return [.. responseBytes];
|
||||
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
var category = (ModbusDeviceIdentificationCategory)requestBytes[9];
|
||||
@@ -694,12 +690,13 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
{
|
||||
responseBytes[7] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||
return [.. responseBytes];
|
||||
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken);
|
||||
var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
|
||||
var bodyBytes = new List<byte>();
|
||||
|
||||
@@ -708,31 +705,20 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
// Conformity
|
||||
bodyBytes.Add((byte)category);
|
||||
if (res.IsIndividualAccessAllowed)
|
||||
if (deviceInfo.IsIndividualAccessAllowed)
|
||||
bodyBytes[2] |= 0x80;
|
||||
|
||||
// More, NextId, NumberOfObjects
|
||||
bodyBytes.AddRange(new byte[3]);
|
||||
|
||||
int maxObjectId;
|
||||
switch (category)
|
||||
int maxObjectId = category switch
|
||||
{
|
||||
case ModbusDeviceIdentificationCategory.Basic:
|
||||
maxObjectId = 0x02;
|
||||
break;
|
||||
|
||||
case ModbusDeviceIdentificationCategory.Regular:
|
||||
maxObjectId = 0x06;
|
||||
break;
|
||||
|
||||
case ModbusDeviceIdentificationCategory.Extended:
|
||||
maxObjectId = 0xFF;
|
||||
break;
|
||||
|
||||
default: // Individual
|
||||
maxObjectId = requestBytes[10];
|
||||
break;
|
||||
}
|
||||
ModbusDeviceIdentificationCategory.Basic => 0x02,
|
||||
ModbusDeviceIdentificationCategory.Regular => 0x06,
|
||||
ModbusDeviceIdentificationCategory.Extended => 0xFF,
|
||||
// Individual
|
||||
_ => requestBytes[10],
|
||||
};
|
||||
|
||||
byte numberOfObjects = 0;
|
||||
for (int i = requestBytes[10]; i <= maxObjectId; i++)
|
||||
@@ -741,7 +727,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
if (0x07 <= i && i <= 0x7F)
|
||||
continue;
|
||||
|
||||
byte[] objBytes = GetDeviceObject((byte)i, res);
|
||||
byte[] objBytes = GetDeviceObject((byte)i, deviceInfo);
|
||||
|
||||
// We need to split the response if it would exceed the max ADU size
|
||||
if (responseBytes.Count + bodyBytes.Count + objBytes.Length > TcpProtocol.MAX_ADU_LENGTH)
|
||||
@@ -751,7 +737,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
bodyBytes[5] = numberOfObjects;
|
||||
responseBytes.AddRange(bodyBytes);
|
||||
return [.. responseBytes];
|
||||
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
|
||||
bodyBytes.AddRange(objBytes);
|
||||
@@ -760,24 +747,26 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
bodyBytes[5] = numberOfObjects;
|
||||
responseBytes.AddRange(bodyBytes);
|
||||
return [.. responseBytes];
|
||||
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
responseBytes[7] |= 0x80;
|
||||
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||
return [.. responseBytes];
|
||||
|
||||
return ReturnResponse(responseBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification)
|
||||
private static byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification)
|
||||
{
|
||||
var result = new List<byte> { objectId };
|
||||
switch ((ModbusDeviceIdentificationObject)objectId)
|
||||
{
|
||||
case ModbusDeviceIdentificationObject.VendorName:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -785,7 +774,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.ProductCode:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -793,7 +782,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.MajorMinorRevision:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -801,7 +790,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.VendorUrl:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -809,7 +798,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.ProductName:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -817,7 +806,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.ModelName:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -825,7 +814,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
case ModbusDeviceIdentificationObject.UserApplicationName:
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? "");
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -833,9 +822,8 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
|
||||
default:
|
||||
{
|
||||
if (deviceIdentification.ExtendedObjects.ContainsKey(objectId))
|
||||
if (deviceIdentification.ExtendedObjects.TryGetValue(objectId, out byte[] bytes))
|
||||
{
|
||||
byte[] bytes = deviceIdentification.ExtendedObjects[objectId];
|
||||
result.Add((byte)bytes.Length);
|
||||
result.AddRange(bytes);
|
||||
}
|
||||
@@ -850,6 +838,29 @@ namespace AMWD.Protocols.Modbus.Proxy
|
||||
return [.. result];
|
||||
}
|
||||
|
||||
private static byte[] ReturnResponse(List<byte> response)
|
||||
{
|
||||
ushort followingBytes = (ushort)(response.Count - 6);
|
||||
var bytes = followingBytes.ToBigEndianBytes();
|
||||
response[4] = bytes[0];
|
||||
response[5] = bytes[1];
|
||||
|
||||
return [.. response];
|
||||
}
|
||||
|
||||
#endregion Request Handling
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"TCP Proxy");
|
||||
sb.AppendLine($" {nameof(ListenAddress)}: {ListenAddress}");
|
||||
sb.AppendLine($" {nameof(ListenPort)}: {ListenPort}");
|
||||
sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,14 +23,14 @@ float voltage = registers.GetSingle();
|
||||
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
||||
```
|
||||
|
||||
If you want to use the `RTU over TCP` protocol instead, you can do this on initialization:
|
||||
If you have a device speaking `RTU` connected over `TCP`, you can use it as followed:
|
||||
|
||||
```csharp
|
||||
// [...]
|
||||
|
||||
using var client = new ModbusTcpClient(host, port)
|
||||
{
|
||||
Protocol = new RtuOverTcpProtocol();
|
||||
Protocol = new RtuProtocol()
|
||||
};
|
||||
|
||||
// [...]
|
||||
@@ -44,10 +44,10 @@ using var client = new ModbusTcpClient(host, port)
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [**tl;dr**Legal])
|
||||
Published under MIT License (see [choose a license])
|
||||
|
||||
|
||||
|
||||
[v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
|
||||
[v1.0b]: https://modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf
|
||||
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
|
||||
[choose a license]: https://choosealicense.com/licenses/mit/
|
||||
|
||||
30
AMWD.Protocols.Modbus.Tcp/Utils/IPEndPointWrapper.cs
Normal file
30
AMWD.Protocols.Modbus.Tcp/Utils/IPEndPointWrapper.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
{
|
||||
/// <inheritdoc cref="IPEndPoint" />
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class IPEndPointWrapper(EndPoint endPoint)
|
||||
{
|
||||
private readonly IPEndPoint _ipEndPoint = (IPEndPoint)endPoint;
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <inheritdoc cref="IPEndPoint.Address"/>
|
||||
public virtual IPAddress Address
|
||||
{
|
||||
get => _ipEndPoint.Address;
|
||||
set => _ipEndPoint.Address = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IPEndPoint.Port"/>
|
||||
public virtual int Port
|
||||
{
|
||||
get => _ipEndPoint.Port;
|
||||
set => _ipEndPoint.Port = value;
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,9 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
{
|
||||
/// <inheritdoc cref="NetworkStream" />
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class NetworkStreamWrapper : IDisposable
|
||||
internal class NetworkStreamWrapper(NetworkStream stream) : IDisposable
|
||||
{
|
||||
private readonly NetworkStream _stream;
|
||||
|
||||
[Obsolete("Constructor only for mocking on UnitTests!", error: true)]
|
||||
public NetworkStreamWrapper()
|
||||
{ }
|
||||
|
||||
public NetworkStreamWrapper(NetworkStream stream)
|
||||
{
|
||||
_stream = stream;
|
||||
}
|
||||
private readonly NetworkStream _stream = stream;
|
||||
|
||||
/// <inheritdoc cref="NetworkStream.Dispose" />
|
||||
public virtual void Dispose()
|
||||
|
||||
26
AMWD.Protocols.Modbus.Tcp/Utils/SocketWrapper.cs
Normal file
26
AMWD.Protocols.Modbus.Tcp/Utils/SocketWrapper.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
{
|
||||
/// <inheritdoc cref="Socket" />
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class SocketWrapper(Socket socket) : IDisposable
|
||||
{
|
||||
private readonly Socket _socket = socket;
|
||||
|
||||
/// <inheritdoc cref="Socket.DualMode" />
|
||||
public virtual bool DualMode
|
||||
{
|
||||
get => _socket.DualMode;
|
||||
set => _socket.DualMode = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Socket.IsBound" />
|
||||
public virtual bool IsBound
|
||||
=> _socket.IsBound;
|
||||
|
||||
public virtual void Dispose()
|
||||
=> _socket.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,30 @@ using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Transactions;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
{
|
||||
/// <inheritdoc cref="TcpClient" />
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class TcpClientWrapper(AddressFamily addressFamily) : IDisposable
|
||||
internal class TcpClientWrapper : IDisposable
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly TcpClient _client = new(addressFamily);
|
||||
private readonly TcpClient _client;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
public TcpClientWrapper(AddressFamily addressFamily)
|
||||
{
|
||||
_client = new TcpClient(addressFamily);
|
||||
}
|
||||
|
||||
public TcpClientWrapper(TcpClient client)
|
||||
{
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <inheritdoc cref="TcpClient.Connected" />
|
||||
|
||||
@@ -3,6 +3,9 @@ using System.Net.Sockets;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for creating <see cref="TcpClientWrapper"/> instances.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class TcpClientWrapperFactory
|
||||
{
|
||||
|
||||
87
AMWD.Protocols.Modbus.Tcp/Utils/TcpListenerWrapper.cs
Normal file
87
AMWD.Protocols.Modbus.Tcp/Utils/TcpListenerWrapper.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||
{
|
||||
/// <inheritdoc cref="TcpListener" />
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class TcpListenerWrapper(IPAddress localaddr, int port) : IDisposable
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly TcpListener _tcpListener = new(localaddr, port);
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructor
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <inheritdoc cref="TcpListener.LocalEndpoint"/>
|
||||
public virtual IPEndPointWrapper LocalIPEndPoint
|
||||
=> new(_tcpListener.LocalEndpoint);
|
||||
|
||||
public virtual SocketWrapper Socket
|
||||
=> new(_tcpListener.Server);
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Accepts a pending connection request as a cancellable asynchronous operation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This operation will not block. The returned <see cref="Task{TResult}"/> object will complete after the TCP connection has been accepted.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Use the <see cref="TcpClientWrapper.GetStream"/> method to obtain the underlying <see cref="NetworkStreamWrapper"/> of the returned <see cref="TcpClientWrapper"/> in the <see cref="Task{TResult}"/>.
|
||||
/// The <see cref="NetworkStreamWrapper"/> will provide you with methods for sending and receiving with the remote host.
|
||||
/// When you are through with the <see cref="TcpClientWrapper"/>, be sure to call its <see cref="TcpClientWrapper.Close"/> method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation</param>
|
||||
/// <returns>
|
||||
/// The task object representing the asynchronous operation.
|
||||
/// The <see cref="Task{TResult}.Result"/> property on the task object returns a <see cref="TcpClientWrapper"/> used to send and receive data.
|
||||
/// </returns>
|
||||
/// <exception cref="InvalidOperationException">The listener has not been started with a call to <see cref="Start"/>.</exception>
|
||||
/// <exception cref="SocketException">
|
||||
/// Use the <see cref="SocketException.ErrorCode"/> property to obtain the specific error code.
|
||||
/// When you have obtained this code, you can refer to the
|
||||
/// <see href="https://learn.microsoft.com/en-us/windows/desktop/winsock/windows-sockets-error-codes-2">Windows Sockets version 2 API error code</see>
|
||||
/// documentation for a detailed description of the error.
|
||||
/// </exception>
|
||||
/// <exception cref="OperationCanceledException">The cancellation token was canceled. This exception is stored into the returned task.</exception>
|
||||
public virtual async Task<TcpClientWrapper> AcceptTcpClientAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
var tcpClient = await _tcpListener.AcceptTcpClientAsync(cancellationToken);
|
||||
#else
|
||||
var tcpClient = await _tcpListener.AcceptTcpClientAsync();
|
||||
#endif
|
||||
return new TcpClientWrapper(tcpClient);
|
||||
}
|
||||
|
||||
public virtual void Start()
|
||||
=> _tcpListener.Start();
|
||||
|
||||
public virtual void Stop()
|
||||
=> _tcpListener.Stop();
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
_tcpListener.Dispose();
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -2,35 +2,31 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<CollectCoverage>true</CollectCoverage>
|
||||
<CoverletOutputFormat>Cobertura</CoverletOutputFormat>
|
||||
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.7.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.7.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
|
||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
|
||||
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
|
||||
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -105,16 +105,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowExceptionOnNullConnection()
|
||||
{
|
||||
// Arrange
|
||||
IModbusConnection connection = null;
|
||||
|
||||
// Act
|
||||
new ModbusClientBaseWrapper(connection);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new ModbusClientBaseWrapper(connection));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -155,31 +152,25 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ObjectDisposedException))]
|
||||
public async Task ShouldAssertDisposed()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
client.Dispose();
|
||||
|
||||
// Act
|
||||
await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT);
|
||||
|
||||
// Assert - ObjectDisposedException
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<ObjectDisposedException>(() => client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public async Task ShouldAssertProtocolSet()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
client.Protocol = null;
|
||||
|
||||
// Act
|
||||
await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT));
|
||||
}
|
||||
|
||||
#endregion Common/Connection/Assertions
|
||||
|
||||
@@ -41,20 +41,16 @@
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetSingle()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetSingle(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => registers.GetSingle(0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetSingleForLength()
|
||||
{
|
||||
// Arrange
|
||||
@@ -63,16 +59,13 @@
|
||||
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetSingle(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetSingle(0));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetSingle(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
@@ -82,14 +75,11 @@
|
||||
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetSingle(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetSingle(startIndex));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetSingleForType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -99,10 +89,8 @@
|
||||
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetSingle(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetSingle(0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -145,20 +133,16 @@
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetDouble()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetDouble(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => registers.GetDouble(0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetDoubleForLength()
|
||||
{
|
||||
// Arrange
|
||||
@@ -169,16 +153,13 @@
|
||||
new() { Address = 102, HighByte = 0x7A, LowByte = 0xE1 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetDouble(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetDouble(0));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetDouble(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
@@ -190,14 +171,11 @@
|
||||
new() { Address = 103, HighByte = 0x47, LowByte = 0xAE }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetDouble(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetDouble(startIndex));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetDoubleForType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -209,10 +187,8 @@
|
||||
new InputRegister { Address = 103, HighByte = 0x47, LowByte = 0xAE }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetDouble(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetDouble(0));
|
||||
}
|
||||
|
||||
#endregion Modbus to value
|
||||
|
||||
@@ -30,16 +30,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetBoolean()
|
||||
{
|
||||
// Arrange
|
||||
Coil coil = null;
|
||||
|
||||
// Act
|
||||
coil.GetBoolean();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => coil.GetBoolean());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -95,35 +92,28 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnString()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] list = null;
|
||||
|
||||
// Act
|
||||
list.GetString(2);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => list.GetString(2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnStringForEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var registers = Array.Empty<HoldingRegister>();
|
||||
|
||||
// Act
|
||||
registers.GetString(2);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetString(2));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnString(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
@@ -133,14 +123,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
||||
new() { Address = 2, HighByte = 67, LowByte = 0 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetString(2, startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetString(2, startIndex));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnStringForMixedTypes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -150,10 +137,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
||||
new InputRegister { Address = 2, HighByte = 67, LowByte = 0 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetString(2);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetString(2));
|
||||
}
|
||||
|
||||
#endregion Modbus to value
|
||||
@@ -272,16 +257,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetString()
|
||||
{
|
||||
// Arrange
|
||||
string str = null;
|
||||
|
||||
// Act
|
||||
_ = str.ToRegisters(100).ToArray();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => str.ToRegisters(100).ToArray());
|
||||
}
|
||||
|
||||
#endregion Value to Modbus
|
||||
|
||||
@@ -32,31 +32,23 @@
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullForGetSByte()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister register = null;
|
||||
|
||||
// Act
|
||||
register.GetSByte();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => register.GetSByte());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentForGetSByte()
|
||||
{
|
||||
// Arrange
|
||||
var obj = new Coil();
|
||||
|
||||
// Act
|
||||
obj.GetSByte();
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => obj.GetSByte());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -86,31 +78,23 @@
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullForGetInt16()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister register = null;
|
||||
|
||||
// Act
|
||||
register.GetInt16();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => register.GetInt16());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentForGetInt16()
|
||||
{
|
||||
// Arrange
|
||||
var obj = new Coil();
|
||||
|
||||
// Act
|
||||
obj.GetInt16();
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => obj.GetInt16());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -149,21 +133,16 @@
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetInt32()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetInt32(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => registers.GetInt32(0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetInt32ForLength()
|
||||
{
|
||||
// Arrange
|
||||
@@ -172,17 +151,13 @@
|
||||
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt32(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetInt32(0));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetInt32(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
@@ -192,15 +167,11 @@
|
||||
new HoldingRegister { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt32(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetInt32(startIndex));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetInt32ForType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -210,11 +181,8 @@
|
||||
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt32(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetInt32(0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -257,21 +225,16 @@
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetInt64()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetInt64(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => registers.GetInt64(0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetInt64ForLength()
|
||||
{
|
||||
// Arrange
|
||||
@@ -282,17 +245,13 @@
|
||||
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt64(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetInt64(0));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetInt64(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
@@ -304,15 +263,11 @@
|
||||
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt64(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetInt64(startIndex));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetInt64ForType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -324,11 +279,8 @@
|
||||
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetInt64(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetInt64(0));
|
||||
}
|
||||
|
||||
#endregion Modbus to value
|
||||
|
||||
@@ -32,29 +32,23 @@
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullForGetByte()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister register = null;
|
||||
|
||||
// Act
|
||||
register.GetByte();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => register.GetByte());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentForGetByte()
|
||||
{
|
||||
// Arrange
|
||||
var obj = new Coil();
|
||||
|
||||
// Act
|
||||
obj.GetByte();
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => obj.GetByte());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -84,29 +78,23 @@
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullForGetUInt16()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister register = null;
|
||||
|
||||
// Act
|
||||
register.GetUInt16();
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => register.GetUInt16());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentForGetUInt16()
|
||||
{
|
||||
// Arrange
|
||||
var obj = new Coil();
|
||||
|
||||
// Act
|
||||
obj.GetUInt16();
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => obj.GetUInt16());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -145,21 +133,16 @@
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetUInt32()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetUInt32(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => registers.GetUInt32(0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetUInt32ForLength()
|
||||
{
|
||||
// Arrange
|
||||
@@ -168,16 +151,13 @@
|
||||
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt32(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetUInt32(1));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetUInt32(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
@@ -187,14 +167,11 @@
|
||||
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt32(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetUInt32(startIndex));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetUInt32ForType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -204,10 +181,8 @@
|
||||
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt32(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetUInt32(0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -250,21 +225,16 @@
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowNullOnGetUInt64()
|
||||
{
|
||||
// Arrange
|
||||
HoldingRegister[] registers = null;
|
||||
|
||||
// Act
|
||||
registers.GetUInt64(0);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
Assert.Fail();
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => registers.GetUInt64(0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetUInt64ForLength()
|
||||
{
|
||||
// Arrange
|
||||
@@ -275,16 +245,13 @@
|
||||
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt64(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetUInt64(0));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(-1)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeOnGetUInt64(int startIndex)
|
||||
{
|
||||
// Arrange
|
||||
@@ -296,14 +263,11 @@
|
||||
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt64(startIndex);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetUInt64(startIndex));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentOnGetUInt64ForType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -315,10 +279,8 @@
|
||||
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||
};
|
||||
|
||||
// Act
|
||||
registers.GetUInt64(0);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => registers.GetUInt64(0));
|
||||
}
|
||||
|
||||
#endregion Modbus to value
|
||||
|
||||
@@ -32,29 +32,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(2001)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -88,7 +82,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
||||
{
|
||||
// Arrange
|
||||
@@ -98,10 +91,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
_ = protocol.DeserializeReadCoils(responseBytes);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadCoils(responseBytes));
|
||||
}
|
||||
|
||||
#endregion Read Coils
|
||||
@@ -129,29 +120,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(2001)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -185,7 +170,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
||||
{
|
||||
// Arrange
|
||||
@@ -195,10 +179,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadDiscreteInputs(responseBytes);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDiscreteInputs(responseBytes));
|
||||
}
|
||||
|
||||
#endregion Read Discrete Inputs
|
||||
@@ -226,29 +208,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(2001)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -276,7 +252,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -286,10 +261,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadHoldingRegisters(responseBytes);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadHoldingRegisters(responseBytes));
|
||||
}
|
||||
|
||||
#endregion Read Holding Registers
|
||||
@@ -317,29 +290,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(2001)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -367,7 +334,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -377,10 +343,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadInputRegisters(responseBytes);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadInputRegisters(responseBytes));
|
||||
}
|
||||
|
||||
#endregion Read Input Registers
|
||||
@@ -410,16 +374,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -449,7 +410,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -459,12 +419,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadDeviceIdentification(responseBytes);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(responseBytes));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
||||
{
|
||||
// Arrange
|
||||
@@ -474,8 +433,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadDeviceIdentification(responseBytes);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(responseBytes));
|
||||
}
|
||||
|
||||
#endregion Read Device Identification
|
||||
@@ -502,16 +461,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteSingleCoil(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleCoil(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -557,16 +513,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -619,22 +572,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(1969)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
||||
{
|
||||
// Arrange
|
||||
@@ -644,14 +593,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
@@ -662,14 +608,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
@@ -680,10 +623,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -732,22 +673,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(124)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
@@ -757,14 +694,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -775,14 +709,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -793,10 +724,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -898,7 +827,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForMissingHeaderOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -907,12 +835,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
AddTrailer(ref response);
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForMissingTrailerOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -920,12 +847,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
string response = $":{UNIT_ID:X2}010100";
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForUnitIdOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -934,12 +860,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
AddTrailer(ref response);
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForLrcOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -947,12 +872,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
string response = $":{UNIT_ID:X2}010001FF00XX\r\n";
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -961,12 +885,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
AddTrailer(ref response);
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForErrorOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -975,8 +898,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
AddTrailer(ref response);
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -984,7 +907,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataRow(0x02)]
|
||||
[DataRow(0x03)]
|
||||
[DataRow(0x04)]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForReadLengthOnValidateResponse(int fn)
|
||||
{
|
||||
// Arrange
|
||||
@@ -993,8 +915,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
AddTrailer(ref response);
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -1002,7 +924,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataRow(0x06)]
|
||||
[DataRow(0x0F)]
|
||||
[DataRow(0x10)]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForWriteLengthOnValidateResponse(int fn)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1011,8 +932,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
AddTrailer(ref response);
|
||||
var protocol = new AsciiProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -1033,58 +954,46 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataRow("")]
|
||||
[DataRow(" ")]
|
||||
[DataRow("\t")]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullExceptionForMessageOnLrc(string msg)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
AsciiProtocol.LRC(msg);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => AsciiProtocol.LRC(msg));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(-1)]
|
||||
[DataRow(4)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeExceptionForStartOnLrc(int start)
|
||||
{
|
||||
// Arrange
|
||||
string msg = "0207";
|
||||
|
||||
// Act
|
||||
AsciiProtocol.LRC(msg, start);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => AsciiProtocol.LRC(msg, start));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(5)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeExceptionForLengthOnLrc(int length)
|
||||
{
|
||||
// Arrange
|
||||
string msg = "0207";
|
||||
|
||||
// Act
|
||||
AsciiProtocol.LRC(msg, 0, length);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => AsciiProtocol.LRC(msg, 0, length));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForMessageLengthOnLrc()
|
||||
{
|
||||
// Arrange
|
||||
string msg = "0207";
|
||||
|
||||
// Act
|
||||
AsciiProtocol.LRC(msg);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => AsciiProtocol.LRC(msg));
|
||||
}
|
||||
|
||||
#endregion Validation
|
||||
|
||||
@@ -55,29 +55,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(2001)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -106,16 +100,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadCoils([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x01, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadCoils([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x01, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]));
|
||||
}
|
||||
|
||||
#endregion Read Coils
|
||||
@@ -166,29 +157,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(2001)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -217,16 +202,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadDiscreteInputs([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x02, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDiscreteInputs([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x02, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]));
|
||||
}
|
||||
|
||||
#endregion Read Discrete Inputs
|
||||
@@ -277,29 +259,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(126)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -323,16 +299,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadHoldingRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x07, UNIT_ID, 0x03, 0x04, 0x02, 0x2B, 0x00, 0x00]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadHoldingRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x03, 0x04, 0x02, 0x2B, 0x00, 0x00]));
|
||||
}
|
||||
|
||||
#endregion Read Holding Registers
|
||||
@@ -383,29 +356,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(126)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -429,16 +396,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadInputRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x07, UNIT_ID, 0x04, 0x04, 0x02, 0x2B, 0x00, 0x00]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadInputRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x04, 0x04, 0x02, 0x2B, 0x00, 0x00]));
|
||||
}
|
||||
|
||||
#endregion Read Input Registers
|
||||
@@ -493,16 +457,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -529,27 +490,25 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
||||
{
|
||||
// Arrange
|
||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x05, UNIT_ID, 0x2B, 0x0D, 0x00, 0x00];
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadDeviceIdentification(response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
||||
{
|
||||
// Arrange
|
||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, UNIT_ID, 0x2B, 0x0E, 0x08, 0x00, 0x00];
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadDeviceIdentification(response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||
}
|
||||
|
||||
#endregion Read Device Identification
|
||||
@@ -600,16 +559,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteSingleCoil(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleCoil(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -676,16 +632,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -765,22 +718,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(1969)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
||||
{
|
||||
// Arrange
|
||||
@@ -790,14 +739,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
@@ -808,14 +754,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
@@ -826,10 +769,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -908,22 +849,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(124)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
@@ -933,14 +870,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -951,14 +885,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -969,10 +900,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -1065,7 +994,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0x00, 0x00)]
|
||||
[DataRow(0x01, 0x01)]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForTransactionIdOnValidateResponse(int hi, int lo)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1074,14 +1002,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
SetCrc(response);
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0x00, 0x01)]
|
||||
[DataRow(0x01, 0x00)]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForProtocolIdOnValidateResponse(int hi, int lo)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1090,12 +1017,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
SetCrc(response);
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForFollowingBytesOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -1104,12 +1030,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
SetCrc(response);
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForUnitIdOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -1118,12 +1043,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
SetCrc(response);
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -1132,12 +1056,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
SetCrc(response);
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForModbusErrorOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -1146,14 +1069,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
SetCrc(response);
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0x59, 0x6C)]
|
||||
[DataRow(0x58, 0x6B)]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForCrcOnValidateResponse(int hi, int lo)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1161,8 +1083,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, UNIT_ID, 0x01, 0x01, 0x00, (byte)hi, (byte)lo];
|
||||
var protocol = new RtuOverTcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
#endregion Validation
|
||||
|
||||
@@ -43,29 +43,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(2001)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -94,16 +88,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
_ = protocol.DeserializeReadCoils([UNIT_ID, 0x01, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadCoils([UNIT_ID, 0x01, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]));
|
||||
}
|
||||
|
||||
#endregion Read Coils
|
||||
@@ -142,29 +133,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(2001)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -193,16 +178,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
_ = protocol.DeserializeReadDiscreteInputs([UNIT_ID, 0x02, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDiscreteInputs([UNIT_ID, 0x02, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]));
|
||||
}
|
||||
|
||||
#endregion Read Discrete Inputs
|
||||
@@ -241,29 +223,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(126)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -287,16 +263,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadHoldingRegisters([UNIT_ID, 0x03, 0x04, 0x02, 0x2B, 0x00, 0x00]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadHoldingRegisters([UNIT_ID, 0x03, 0x04, 0x02, 0x2B, 0x00, 0x00]));
|
||||
}
|
||||
|
||||
#endregion Read Holding Registers
|
||||
@@ -335,29 +308,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(126)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -381,16 +348,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadInputRegisters([UNIT_ID, 0x04, 0x04, 0x02, 0x2B, 0x00, 0x00]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadInputRegisters([UNIT_ID, 0x04, 0x04, 0x02, 0x2B, 0x00, 0x00]));
|
||||
}
|
||||
|
||||
#endregion Read Input Registers
|
||||
@@ -433,16 +397,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -469,27 +430,25 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
||||
{
|
||||
// Arrange
|
||||
byte[] response = [UNIT_ID, 0x2B, 0x0D, 0x00, 0x00];
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadDeviceIdentification(response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
||||
{
|
||||
// Arrange
|
||||
byte[] response = [UNIT_ID, 0x2B, 0x0E, 0x08, 0x00, 0x00];
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadDeviceIdentification(response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||
}
|
||||
|
||||
#endregion Read Device Identification
|
||||
@@ -528,16 +487,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteSingleCoil(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleCoil(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -592,16 +548,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -669,22 +622,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(1969)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
||||
{
|
||||
// Arrange
|
||||
@@ -694,14 +643,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
@@ -712,14 +658,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
@@ -730,10 +673,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -800,22 +741,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(124)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
@@ -825,14 +762,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -843,14 +777,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -861,10 +792,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -1105,7 +1034,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForUnitIdOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -1114,14 +1042,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
SetCrc(response);
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0x57, 0x6C)]
|
||||
[DataRow(0x58, 0x6B)]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForCrcOnValidateResponse(int hi, int lo)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1129,12 +1056,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
byte[] response = [UNIT_ID, 0x01, 0x01, 0x00, (byte)hi, (byte)lo];
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -1143,12 +1069,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
SetCrc(response);
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForErrorOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -1157,8 +1082,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
SetCrc(response);
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -1166,7 +1091,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataRow(0x02)]
|
||||
[DataRow(0x03)]
|
||||
[DataRow(0x04)]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForReadLengthOnValidateResponse(int fn)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1175,8 +1099,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
SetCrc(response);
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -1184,7 +1108,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataRow(0x06)]
|
||||
[DataRow(0x0F)]
|
||||
[DataRow(0x10)]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForWriteLengthOnValidateResponse(int fn)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1193,8 +1116,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
SetCrc(response);
|
||||
var protocol = new RtuProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -1217,43 +1140,36 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow(new byte[0])]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShuldThrowArgumentNullExceptionForBytesOnCrc16(byte[] bytes)
|
||||
{
|
||||
// Act
|
||||
_ = RtuProtocol.CRC16(bytes);
|
||||
// Arrange
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => RtuProtocol.CRC16(bytes));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(-1)]
|
||||
[DataRow(10)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeForStartOnCrc16(int start)
|
||||
{
|
||||
// Arrange
|
||||
byte[] bytes = Encoding.UTF8.GetBytes("0123456789");
|
||||
|
||||
// Act
|
||||
_ = RtuProtocol.CRC16(bytes, start);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => RtuProtocol.CRC16(bytes, start));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(11)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeForLengthOnCrc16(int length)
|
||||
{
|
||||
// Arrange
|
||||
byte[] bytes = Encoding.UTF8.GetBytes("0123456789");
|
||||
|
||||
// Act
|
||||
_ = RtuProtocol.CRC16(bytes, 0, length);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => RtuProtocol.CRC16(bytes, 0, length));
|
||||
}
|
||||
|
||||
#endregion Validation
|
||||
|
||||
@@ -53,29 +53,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(2001)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -104,16 +98,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
var coils = protocol.DeserializeReadCoils([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x02, 0xCD, 0x6B, 0x05]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadCoils([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x02, 0xCD, 0x6B, 0x05]));
|
||||
}
|
||||
|
||||
#endregion Read Coils
|
||||
@@ -162,29 +153,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(2001)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -213,16 +198,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadDiscreteInputs([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x02, 0x03, 0xCD, 0x6B]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDiscreteInputs([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x02, 0x03, 0xCD, 0x6B]));
|
||||
}
|
||||
|
||||
#endregion Read Discrete Inputs
|
||||
@@ -271,29 +253,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(126)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -317,16 +293,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadHoldingRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x03, 0x04, 0x02, 0x2B]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadHoldingRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x03, 0x04, 0x02, 0x2B]));
|
||||
}
|
||||
|
||||
#endregion Read Holding Registers
|
||||
@@ -375,29 +348,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(126)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -421,16 +388,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadInputRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x04, 0x04, 0x02, 0x2B]);
|
||||
|
||||
// Assert - ModbusException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadInputRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x04, 0x04, 0x02, 0x2B]));
|
||||
}
|
||||
|
||||
#endregion Read Input Registers
|
||||
@@ -483,16 +447,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeExceptionOnSerializeReadDeviceIdentification()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
@@ -519,27 +480,25 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
||||
{
|
||||
// Arrange
|
||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x2A, 0x2B, 0x0D];
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadDeviceIdentification(response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
||||
{
|
||||
// Arrange
|
||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x2A, 0x2B, 0x0E, 0x08];
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.DeserializeReadDeviceIdentification(response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||
}
|
||||
|
||||
#endregion Read Device Identification
|
||||
@@ -588,16 +547,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteSingleCoil(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleCoil(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -662,16 +618,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -749,22 +702,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(1969)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
||||
{
|
||||
// Arrange
|
||||
@@ -774,14 +723,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
@@ -792,14 +738,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
||||
{
|
||||
// Arrange
|
||||
@@ -810,10 +753,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -890,22 +831,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(124)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
||||
{
|
||||
// Arrange
|
||||
@@ -915,14 +852,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -933,14 +867,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -951,10 +882,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
};
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
||||
|
||||
// Assert - ArgumentException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -1045,7 +974,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
[DataTestMethod]
|
||||
[DataRow(0x00, 0x00)]
|
||||
[DataRow(0x01, 0x01)]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForTransactionIdOnValidateResponse(int hi, int lo)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1053,14 +981,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x01, 0x01, 0x00];
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0x00, 0x01)]
|
||||
[DataRow(0x01, 0x00)]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForProtocolIdOnValidateResponse(int hi, int lo)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1068,12 +995,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x01, 0x01, 0x00];
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForFollowingBytesOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -1081,12 +1007,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x2A, 0x01, 0x01, 0x00];
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForUnitIdOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -1094,12 +1019,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2B, 0x01, 0x01, 0x00];
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -1107,12 +1031,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x02, 0x01, 0x00];
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ModbusException))]
|
||||
public void ShouldThrowForModbusErrorOnValidateResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -1120,8 +1043,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x2A, 0x81, 0x01];
|
||||
var protocol = new TcpProtocol();
|
||||
|
||||
// Act
|
||||
protocol.ValidateResponse(request, response);
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||
}
|
||||
|
||||
#endregion Validation
|
||||
|
||||
23
AMWD.Protocols.Modbus.Tests/Helper.cs
Normal file
23
AMWD.Protocols.Modbus.Tests/Helper.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tests
|
||||
{
|
||||
internal static class Helper
|
||||
{
|
||||
public static T CreateInstance<T>(params object[] args)
|
||||
{
|
||||
var type = typeof(T);
|
||||
|
||||
object instance = type.Assembly.CreateInstance(
|
||||
typeName: type.FullName,
|
||||
ignoreCase: false,
|
||||
bindingAttr: BindingFlags.Instance | BindingFlags.NonPublic,
|
||||
binder: null,
|
||||
args: args,
|
||||
culture: null,
|
||||
activationAttributes: null);
|
||||
|
||||
return (T)instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
2004
AMWD.Protocols.Modbus.Tests/Serial/ModbusRtuProxyTest.cs
Normal file
2004
AMWD.Protocols.Modbus.Tests/Serial/ModbusRtuProxyTest.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,13 +13,15 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
string portName = "COM-42";
|
||||
|
||||
_genericConnectionMock = new Mock<IModbusConnection>();
|
||||
_genericConnectionMock.Setup(c => c.IdleTimeout).Returns(TimeSpan.FromSeconds(40));
|
||||
_genericConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(30));
|
||||
_genericConnectionMock.Setup(c => c.ReadTimeout).Returns(TimeSpan.FromSeconds(20));
|
||||
_genericConnectionMock.Setup(c => c.WriteTimeout).Returns(TimeSpan.FromSeconds(10));
|
||||
|
||||
_serialConnectionMock = new Mock<ModbusSerialConnection>();
|
||||
_serialConnectionMock = new Mock<ModbusSerialConnection>(portName);
|
||||
|
||||
_serialConnectionMock.Setup(c => c.IdleTimeout).Returns(TimeSpan.FromSeconds(10));
|
||||
_serialConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(20));
|
||||
@@ -28,7 +30,7 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
|
||||
_serialConnectionMock.Setup(c => c.DriverEnabledRS485).Returns(true);
|
||||
_serialConnectionMock.Setup(c => c.InterRequestDelay).Returns(TimeSpan.FromSeconds(50));
|
||||
_serialConnectionMock.Setup(c => c.PortName).Returns("COM-42");
|
||||
_serialConnectionMock.Setup(c => c.PortName).Returns(portName);
|
||||
_serialConnectionMock.Setup(c => c.BaudRate).Returns(BaudRate.Baud2400);
|
||||
_serialConnectionMock.Setup(c => c.DataBits).Returns(7);
|
||||
_serialConnectionMock.Setup(c => c.Handshake).Returns(Handshake.XOnXOff);
|
||||
@@ -231,5 +233,18 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
|
||||
_serialConnectionMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldPrintCleanString()
|
||||
{
|
||||
// Arrange
|
||||
using var client = new ModbusSerialClient(_serialConnectionMock.Object);
|
||||
|
||||
// Act
|
||||
string str = client.ToString();
|
||||
|
||||
// Assert
|
||||
SnapshotAssert.AreEqual(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,47 +90,50 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
connection.Dispose();
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow("")]
|
||||
[DataRow(" ")]
|
||||
public void ShouldThrowArgumentNullExceptionOnCreate(string portName)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new ModbusSerialClient(portName));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ObjectDisposedException))]
|
||||
public async Task ShouldThrowDisposedExceptionOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
var connection = GetConnection();
|
||||
connection.Dispose();
|
||||
|
||||
// Act
|
||||
await connection.InvokeAsync(null, null);
|
||||
|
||||
// Assert - OjbectDisposedException
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<ObjectDisposedException>(() => connection.InvokeAsync(null, null));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow(new byte[0])]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public async Task ShouldThrowArgumentNullExceptionForMissingRequestOnInvokeAsync(byte[] request)
|
||||
{
|
||||
// Arrange
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
await connection.InvokeAsync(request, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => connection.InvokeAsync(request, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public async Task ShouldThrowArgumentNullExceptionForMissingValidationOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] request = new byte[1];
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
await connection.InvokeAsync(request, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => connection.InvokeAsync(request, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -161,10 +164,8 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
_serialPortMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(false)]
|
||||
[DataRow(true)]
|
||||
public async Task ShouldOpenAndCloseOnInvokeAsync(bool modifyDriver)
|
||||
[TestMethod]
|
||||
public async Task ShouldOpenAndCloseOnInvokeAsyncOnLinuxNotModifyingDriver()
|
||||
{
|
||||
// Arrange
|
||||
_alwaysOpen = false;
|
||||
@@ -178,8 +179,9 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
_serialLineResponseQueue.Enqueue(expectedResponse);
|
||||
|
||||
var connection = GetSerialConnection();
|
||||
connection.GetType().GetField("_isLinux", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(connection, true);
|
||||
connection.IdleTimeout = TimeSpan.FromMilliseconds(200);
|
||||
connection.DriverEnabledRS485 = modifyDriver;
|
||||
connection.DriverEnabledRS485 = false;
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
@@ -198,11 +200,50 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
_serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2));
|
||||
_serialPortMock.Verify(c => c.Open(), Times.Once);
|
||||
|
||||
if (modifyDriver)
|
||||
_serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_serialPortMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldOpenAndCloseOnInvokeAsyncOnLinuxModifyingDriver()
|
||||
{
|
||||
// Arrange
|
||||
_alwaysOpen = false;
|
||||
_isOpenQueue.Enqueue(false);
|
||||
_isOpenQueue.Enqueue(true);
|
||||
_isOpenQueue.Enqueue(true);
|
||||
|
||||
byte[] request = [1, 2, 3];
|
||||
byte[] expectedResponse = [9, 8, 7];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
_serialLineResponseQueue.Enqueue(expectedResponse);
|
||||
|
||||
var connection = GetSerialConnection();
|
||||
connection.GetType().GetField("_isLinux", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(connection, true);
|
||||
connection.IdleTimeout = TimeSpan.FromMilliseconds(200);
|
||||
connection.DriverEnabledRS485 = true;
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
await Task.Delay(500);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(response);
|
||||
|
||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||
CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First());
|
||||
|
||||
_serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Once);
|
||||
|
||||
_serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3));
|
||||
_serialPortMock.Verify(c => c.Close(), Times.Exactly(2));
|
||||
_serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2));
|
||||
_serialPortMock.Verify(c => c.Open(), Times.Once);
|
||||
|
||||
_serialPortMock.Verify(c => c.GetRS485DriverStateFlags(), Times.Once);
|
||||
_serialPortMock.Verify(c => c.ChangeRS485DriverStateFlags(It.IsAny<RS485Flags>()), Times.Once);
|
||||
}
|
||||
|
||||
_serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
@@ -211,7 +252,90 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(EndOfStreamException))]
|
||||
public async Task ShouldOpenAndCloseOnInvokeAsyncOnOtherOsNotModifyingDriver()
|
||||
{
|
||||
// Arrange
|
||||
_alwaysOpen = false;
|
||||
_isOpenQueue.Enqueue(false);
|
||||
_isOpenQueue.Enqueue(true);
|
||||
_isOpenQueue.Enqueue(true);
|
||||
|
||||
byte[] request = [1, 2, 3];
|
||||
byte[] expectedResponse = [9, 8, 7];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
_serialLineResponseQueue.Enqueue(expectedResponse);
|
||||
|
||||
var connection = GetSerialConnection();
|
||||
connection.GetType().GetField("_isLinux", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(connection, false);
|
||||
connection.IdleTimeout = TimeSpan.FromMilliseconds(200);
|
||||
connection.DriverEnabledRS485 = false;
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
await Task.Delay(500);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(response);
|
||||
|
||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||
CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First());
|
||||
|
||||
_serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Once);
|
||||
|
||||
_serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3));
|
||||
_serialPortMock.Verify(c => c.Close(), Times.Exactly(2));
|
||||
_serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2));
|
||||
_serialPortMock.Verify(c => c.Open(), Times.Once);
|
||||
|
||||
_serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_serialPortMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldOpenAndCloseOnInvokeAsyncOnOtherOsModifyingDriver()
|
||||
{
|
||||
// Arrange
|
||||
_alwaysOpen = false;
|
||||
_isOpenQueue.Enqueue(false);
|
||||
_isOpenQueue.Enqueue(true);
|
||||
_isOpenQueue.Enqueue(true);
|
||||
|
||||
byte[] request = [1, 2, 3];
|
||||
byte[] expectedResponse = [9, 8, 7];
|
||||
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||
_serialLineResponseQueue.Enqueue(expectedResponse);
|
||||
|
||||
var connection = GetSerialConnection();
|
||||
connection.GetType().GetField("_isLinux", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(connection, false);
|
||||
connection.IdleTimeout = TimeSpan.FromMilliseconds(200);
|
||||
connection.DriverEnabledRS485 = true;
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
await Task.Delay(500);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(response);
|
||||
|
||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||
CollectionAssert.AreEqual(request, _serialLineRequestCallbacks.First());
|
||||
|
||||
_serialPortMock.VerifyGet(c => c.ReadTimeout, Times.Once);
|
||||
|
||||
_serialPortMock.Verify(c => c.IsOpen, Times.Exactly(3));
|
||||
_serialPortMock.Verify(c => c.Close(), Times.Exactly(2));
|
||||
_serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2));
|
||||
_serialPortMock.Verify(c => c.Open(), Times.Once);
|
||||
|
||||
_serialPortMock.Verify(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_serialPortMock.Verify(ns => ns.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_serialPortMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldThrowEndOfStreamExceptionOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -220,10 +344,8 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
|
||||
// Assert - EndOfStreamException
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<EndOfStreamException>(() => connection.InvokeAsync(request, validation));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -305,7 +427,6 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(TaskCanceledException))]
|
||||
public async Task ShouldThrowTaskCancelledExceptionForDisposeOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -317,16 +438,16 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.Delay(100));
|
||||
|
||||
// Act
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<TaskCanceledException>(async () =>
|
||||
{
|
||||
var task = connection.InvokeAsync(request, validation);
|
||||
connection.Dispose();
|
||||
await task;
|
||||
|
||||
// Assert - TaskCancelledException
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(TaskCanceledException))]
|
||||
public async Task ShouldThrowTaskCancelledExceptionForCancelOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -339,12 +460,13 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.Delay(100));
|
||||
|
||||
// Act
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<TaskCanceledException>(async () =>
|
||||
{
|
||||
var task = connection.InvokeAsync(request, validation, cts.Token);
|
||||
cts.Cancel();
|
||||
await task;
|
||||
|
||||
// Assert - TaskCancelledException
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -360,7 +482,7 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
var connection = GetConnection();
|
||||
_serialPortMock
|
||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<byte[], CancellationToken>((req, _) => _serialLineRequestCallbacks.Add(req.ToArray()))
|
||||
.Callback<byte[], CancellationToken>((req, _) => _serialLineRequestCallbacks.Add([.. req]))
|
||||
.Returns(Task.Delay(100));
|
||||
|
||||
// Act
|
||||
@@ -403,7 +525,7 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
var connection = GetConnection();
|
||||
_serialPortMock
|
||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<byte[], CancellationToken>((req, _) => _serialLineRequestCallbacks.Add(req.ToArray()))
|
||||
.Callback<byte[], CancellationToken>((req, _) => _serialLineRequestCallbacks.Add([.. req]))
|
||||
.Returns(Task.Delay(100));
|
||||
|
||||
// Act
|
||||
@@ -467,16 +589,13 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
|
||||
var connection = new ModbusSerialConnection();
|
||||
var connection = new ModbusSerialConnection("some-port");
|
||||
|
||||
// Replace real connection with mock
|
||||
var connectionField = connection.GetType().GetField("_serialPort", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
(connectionField.GetValue(connection) as SerialPortWrapper)?.Dispose();
|
||||
connectionField.SetValue(connection, _serialPortMock.Object);
|
||||
|
||||
// Set unit test mode
|
||||
connection.GetType().GetField("_isUnitTest", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(connection, true);
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
DeviceIdentification
|
||||
VendorName: VendorName
|
||||
ProductCode: ProductCode
|
||||
MajorMinorRevision: MajorMinorRevision
|
||||
VendorUrl:
|
||||
ProductName:
|
||||
ModelName:
|
||||
UserApplicationName:
|
||||
IsIndividualAccessAllowed: False
|
||||
@@ -0,0 +1,8 @@
|
||||
Serial Client COM-42
|
||||
BaudRate: 2400
|
||||
DataBits: 7
|
||||
StopBits: 1.5
|
||||
Parity: space
|
||||
Handshake: xonxoff
|
||||
RtsEnable: true
|
||||
DriverEnabledRS485: true
|
||||
83
AMWD.Protocols.Modbus.Tests/SnapshotAssert.cs
Normal file
83
AMWD.Protocols.Modbus.Tests/SnapshotAssert.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.Tests
|
||||
{
|
||||
// ================================================================================================================================ //
|
||||
// Source: https://git.am-wd.de/am-wd/common/-/blob/fb26e441a48214aaae72003c4a5ac33d5c7b929a/src/AMWD.Common.Test/SnapshotAssert.cs //
|
||||
// ================================================================================================================================ //
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal sealed class SnapshotAssert
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests whether the specified string is equal to the saved snapshot.
|
||||
/// </summary>
|
||||
/// <param name="actual">The current aggregated content string.</param>
|
||||
/// <param name="message">An optional message to display if the assertion fails.</param>
|
||||
/// <param name="callerFilePath">The absolute file path of the calling file (filled automatically on compile time).</param>
|
||||
/// <param name="callerMemberName">The name of the calling method (filled automatically on compile time).</param>
|
||||
public static void AreEqual(string actual, string message = null, [CallerFilePath] string callerFilePath = null, [CallerMemberName] string callerMemberName = null)
|
||||
{
|
||||
string cleanLineEnding = actual
|
||||
.Replace("\r\n", "\n") // Windows
|
||||
.Replace("\r", "\n"); // MacOS
|
||||
AreEqual(Encoding.UTF8.GetBytes(cleanLineEnding), message, callerFilePath, callerMemberName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the specified byte array is equal to the saved snapshot.
|
||||
/// </summary>
|
||||
/// <param name="actual">The current aggregated content bytes.</param>
|
||||
/// <param name="message">An optional message to display if the assertion fails.</param>
|
||||
/// <param name="callerFilePath">The absolute file path of the calling file (filled automatically on compile time).</param>
|
||||
/// <param name="callerMemberName">The name of the calling method (filled automatically on compile time).</param>
|
||||
public static void AreEqual(byte[] actual, string message = null, [CallerFilePath] string callerFilePath = null, [CallerMemberName] string callerMemberName = null)
|
||||
=> AreEqual(actual, null, message, callerFilePath, callerMemberName);
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the specified byte array is equal to the saved snapshot.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The past has shown, that e.g. wkhtmltopdf prints the current timestamp at the beginning of the PDF file.
|
||||
/// Therefore you can specify which sequences of bytes should be excluded from the comparison.
|
||||
/// </remarks>
|
||||
/// <param name="actual">The current aggregated content bytes.</param>
|
||||
/// <param name="excludedSequences">The excluded sequences.</param>
|
||||
/// <param name="message">An optional message to display if the assertion fails.</param>
|
||||
/// <param name="callerFilePath">The absolute file path of the calling file (filled automatically on compile time).</param>
|
||||
/// <param name="callerMemberName">The name of the calling method (filled automatically on compile time).</param>
|
||||
public static void AreEqual(byte[] actual, List<(int Start, int Length)> excludedSequences = null, string message = null, [CallerFilePath] string callerFilePath = null, [CallerMemberName] string callerMemberName = null)
|
||||
{
|
||||
string callerDirectory = Path.GetDirectoryName(callerFilePath);
|
||||
string callerFileName = Path.GetFileNameWithoutExtension(callerFilePath);
|
||||
|
||||
string snapshotDirectory = Path.Combine(callerDirectory, "Snapshots", callerFileName);
|
||||
string snapshotFilePath = Path.Combine(snapshotDirectory, $"{callerMemberName}.snap.bin");
|
||||
|
||||
if (File.Exists(snapshotFilePath))
|
||||
{
|
||||
byte[] expected = File.ReadAllBytes(snapshotFilePath);
|
||||
if (actual.Length != expected.Length)
|
||||
Assert.Fail(message);
|
||||
|
||||
for (int i = 0; i < actual.Length; i++)
|
||||
{
|
||||
if (excludedSequences?.Any(s => s.Start <= i && i < s.Start + s.Length) == true)
|
||||
continue;
|
||||
|
||||
if (actual[i] != expected[i])
|
||||
Assert.Fail(message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Directory.Exists(snapshotDirectory))
|
||||
Directory.CreateDirectory(snapshotDirectory);
|
||||
|
||||
File.WriteAllBytes(snapshotFilePath, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,5 +162,18 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
||||
|
||||
_tcpConnectionMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldPrintCleanString()
|
||||
{
|
||||
// Arrange
|
||||
using var client = new ModbusTcpClient(_tcpConnectionMock.Object);
|
||||
|
||||
// Act
|
||||
string str = client.ToString();
|
||||
|
||||
// Assert
|
||||
SnapshotAssert.AreEqual(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,31 +80,25 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
||||
[DataRow(null)]
|
||||
[DataRow("")]
|
||||
[DataRow(" ")]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void ShouldThrowArgumentNullExceptionForInvalidHostname(string hostname)
|
||||
{
|
||||
// Arrange
|
||||
var connection = GetTcpConnection();
|
||||
|
||||
// Act
|
||||
connection.Hostname = hostname;
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => connection.Hostname = hostname);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(65536)]
|
||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ShouldThrowArgumentOutOfRangeExceptionForInvalidPort(int port)
|
||||
{
|
||||
// Arrange
|
||||
var connection = GetTcpConnection();
|
||||
|
||||
// Act
|
||||
connection.Port = port;
|
||||
|
||||
// Assert - ArgumentOutOfRangeException
|
||||
// Act + Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => connection.Port = port);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -119,46 +113,37 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ObjectDisposedException))]
|
||||
public async Task ShouldThrowDisposedExceptionOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
var connection = GetConnection();
|
||||
connection.Dispose();
|
||||
|
||||
// Act
|
||||
await connection.InvokeAsync(null, null);
|
||||
|
||||
// Assert - OjbectDisposedException
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<ObjectDisposedException>(() => connection.InvokeAsync(null, null));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow(new byte[0])]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public async Task ShouldThrowArgumentNullExceptionForMissingRequestOnInvokeAsync(byte[] request)
|
||||
{
|
||||
// Arrange
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
await connection.InvokeAsync(request, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => connection.InvokeAsync(request, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public async Task ShouldThrowArgumentNullExceptionForMissingValidationOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] request = new byte[1];
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
await connection.InvokeAsync(request, null);
|
||||
|
||||
// Assert - ArgumentNullException
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => connection.InvokeAsync(request, null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -235,7 +220,6 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(EndOfStreamException))]
|
||||
public async Task ShouldThrowEndOfStreamExceptionOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -244,14 +228,11 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
||||
|
||||
var connection = GetConnection();
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
|
||||
// Assert - EndOfStreamException
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<EndOfStreamException>(() => connection.InvokeAsync(request, validation));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ApplicationException))]
|
||||
public async Task ShouldThrowApplicationExceptionWhenHostNotResolvableOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -264,10 +245,8 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
||||
var connection = GetConnection();
|
||||
connection.GetType().GetField("_hostname", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(connection, "");
|
||||
|
||||
// Act
|
||||
var response = await connection.InvokeAsync(request, validation);
|
||||
|
||||
// Assert - ApplicationException
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<ApplicationException>(() => connection.InvokeAsync(request, validation));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -351,8 +330,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(TaskCanceledException))]
|
||||
public async Task ShouldThrowTaskCancelledExceptionForDisposeOnInvokeAsync()
|
||||
public async Task ShouldThrowTaskCanceledExceptionForDisposeOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] request = [1, 2, 3];
|
||||
@@ -363,17 +341,17 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
||||
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(new ValueTask(Task.Delay(100)));
|
||||
|
||||
// Act
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<TaskCanceledException>(async () =>
|
||||
{
|
||||
var task = connection.InvokeAsync(request, validation);
|
||||
connection.Dispose();
|
||||
await task;
|
||||
|
||||
// Assert - TaskCancelledException
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(TaskCanceledException))]
|
||||
public async Task ShouldThrowTaskCancelledExceptionForCancelOnInvokeAsync()
|
||||
public async Task ShouldThrowTaskCanceledExceptionForCancelOnInvokeAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] request = [1, 2, 3];
|
||||
@@ -385,12 +363,13 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
||||
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(new ValueTask(Task.Delay(100)));
|
||||
|
||||
// Act
|
||||
// Act + Assert
|
||||
await Assert.ThrowsExceptionAsync<TaskCanceledException>(async () =>
|
||||
{
|
||||
var task = connection.InvokeAsync(request, validation, cts.Token);
|
||||
cts.Cancel();
|
||||
await task;
|
||||
|
||||
// Assert - TaskCancelledException
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -498,7 +477,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
||||
|
||||
private ModbusTcpConnection GetTcpConnection()
|
||||
{
|
||||
_networkStreamMock = new Mock<NetworkStreamWrapper>();
|
||||
_networkStreamMock = new Mock<NetworkStreamWrapper>(null);
|
||||
_networkStreamMock
|
||||
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<ReadOnlyMemory<byte>, CancellationToken>((req, _) => _networkRequestCallbacks.Add(req.ToArray()))
|
||||
|
||||
2149
AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpProxyTest.cs
Normal file
2149
AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpProxyTest.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
TCP Client 127.0.0.1
|
||||
Port: 502
|
||||
@@ -0,0 +1,9 @@
|
||||
DeviceIdentification
|
||||
VendorName: VendorName
|
||||
ProductCode: ProductCode
|
||||
MajorMinorRevision: MajorMinorRevision
|
||||
VendorUrl:
|
||||
ProductName:
|
||||
ModelName:
|
||||
UserApplicationName:
|
||||
IsIndividualAccessAllowed: False
|
||||
@@ -35,7 +35,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tcp",
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Serial", "AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj", "{D966826F-EE6C-4BC0-9185-C2A9A50FD586}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Proxy", "AMWD.Protocols.Modbus.Proxy\AMWD.Protocols.Modbus.Proxy.csproj", "{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliClient", "CliClient\CliClient.csproj", "{B0E53462-B0ED-4685-8AA5-948DC160EE27}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliProxy", "CliProxy\CliProxy.csproj", "{AC922E80-E9B6-493D-B1D1-752527E883ED}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -59,10 +61,14 @@ Global
|
||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AC922E80-E9B6-493D-B1D1-752527E883ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AC922E80-E9B6-493D-B1D1-752527E883ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AC922E80-E9B6-493D-B1D1-752527E883ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AC922E80-E9B6-493D-B1D1-752527E883ED}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
BIN
AMWD.Protocols.Modbus.snk
Normal file
BIN
AMWD.Protocols.Modbus.snk
Normal file
Binary file not shown.
78
CHANGELOG.md
78
CHANGELOG.md
@@ -2,33 +2,90 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
_no changes_
|
||||
_nothing changed yet_
|
||||
|
||||
|
||||
## [v0.4.2] (2025-02-07)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixing issue with R/W timeouts while processing client requests on the `ModbusTcpProxy`.
|
||||
|
||||
|
||||
## [v0.4.1] (2025-02-06)
|
||||
|
||||
### Changed
|
||||
|
||||
- Async methods do not return on captured context anymore (`Task.ConfigureAwait(false)`).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Set `Socket.DualMode` on IPv4 network address is not allowed (`ModbusTcpProxy`).
|
||||
|
||||
|
||||
## [v0.4.0] (2025-01-29)
|
||||
|
||||
### Added
|
||||
|
||||
- Small CLI client for Modbus communication.
|
||||
- Small CLI proxy to forward messages.
|
||||
- `VirtualModbusClient` added to `AMWD.Protocols.Modbus.Common`.
|
||||
|
||||
### Changed
|
||||
|
||||
- The `ModbusTcpProxy.ReadWriteTimeout` has a default value of 100 seconds (same default as a `HttpClient` has).
|
||||
- The `ModbusRtuProxy` moved from `AMWD.Protocols.Modbus.Proxy` to `AMWD.Protocols.Modbus.Serial`.
|
||||
- The `ModbusTcpProxy` moved from `AMWD.Protocols.Modbus.Proxy` to `AMWD.Protocols.Modbus.Tcp`.
|
||||
- Server implementations are proxies with a virtual Modbus client.
|
||||
|
||||
### Removed
|
||||
|
||||
- Discontinue the `AMWD.Protocols.Modbus.Proxy` package (introduced in [v0.3.0]).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Wrong _following bytes_ calculation in `ModbusTcpProxy`.
|
||||
- Wrong processing of `WriteMultipleHoldingRegisters` for proxies.
|
||||
|
||||
|
||||
## [v0.3.2] (2024-09-04)
|
||||
|
||||
### Added
|
||||
|
||||
- Build configuration for strong named assemblies.
|
||||
|
||||
|
||||
## [v0.3.1] (2024-06-28)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Issues with range validation on several lines of code in server implementations.
|
||||
|
||||
|
||||
## [v0.3.0] (2024-05-31)
|
||||
|
||||
### Added
|
||||
|
||||
- New `AMWD.Protocols.Modbus.Proxy` package, that contains the server implementations as proxies
|
||||
- New `AMWD.Protocols.Modbus.Proxy` package, that contains the server implementations as proxies.
|
||||
|
||||
### Changed
|
||||
|
||||
- Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used
|
||||
- Made `Protocol` property of `ModbusClientBase` non-abstract
|
||||
- Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used.
|
||||
- Made `Protocol` property of `ModbusClientBase` non-abstract.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Issue with missing client on TCP connection when using default constructor (seems that `AddressFamily.Unknown` caused the problem)
|
||||
- Issue with missing client on TCP connection when using default constructor (seems that `AddressFamily.Unknown` caused the problem).
|
||||
|
||||
|
||||
## [v0.2.0] (2024-04-02)
|
||||
|
||||
First "final" re-implementation
|
||||
First "final" re-implementation.
|
||||
|
||||
|
||||
## v0.1.0 (2022-08-28)
|
||||
@@ -38,6 +95,11 @@ So this tag is only here for documentation purposes of the NuGet Gallery.
|
||||
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...HEAD
|
||||
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.4.2...HEAD
|
||||
[v0.4.2]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.4.1...v0.4.2
|
||||
[v0.4.1]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.4.0...v0.4.1
|
||||
[v0.4.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.2...v0.4.0
|
||||
[v0.3.2]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.1...v0.3.2
|
||||
[v0.3.1]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...v0.3.1
|
||||
[v0.3.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.2.0...v0.3.0
|
||||
[v0.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0
|
||||
|
||||
35
CliClient/Cli/Argument.cs
Normal file
35
CliClient/Cli/Argument.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace AMWD.Common.Cli
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a logical argument in the command line. Options with their additional
|
||||
/// parameters are combined in one argument.
|
||||
/// </summary>
|
||||
internal class Argument
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the <see cref="Argument"/> class.
|
||||
/// </summary>
|
||||
/// <param name="option">The <see cref="Option"/> that is set in this argument; or null.</param>
|
||||
/// <param name="values">The additional parameter values for the option; or the argument value.</param>
|
||||
internal Argument(Option option, string[] values)
|
||||
{
|
||||
Option = option;
|
||||
Values = values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Option"/> that is set in this argument; or null.
|
||||
/// </summary>
|
||||
public Option Option { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the additional parameter values for the option; or the argument value.
|
||||
/// </summary>
|
||||
public string[] Values { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first item of <see cref="Values"/>; or null.
|
||||
/// </summary>
|
||||
public string Value => Values.Length > 0 ? Values[0] : null;
|
||||
}
|
||||
}
|
||||
366
CliClient/Cli/CommandLineParser.cs
Normal file
366
CliClient/Cli/CommandLineParser.cs
Normal file
@@ -0,0 +1,366 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AMWD.Common.Cli
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides options and arguments parsing from command line arguments or a single string.
|
||||
/// </summary>
|
||||
internal class CommandLineParser
|
||||
{
|
||||
#region Private data
|
||||
|
||||
private string[] _args;
|
||||
private List<Argument> _parsedArguments;
|
||||
private readonly List<Option> _options = [];
|
||||
|
||||
#endregion Private data
|
||||
|
||||
#region Configuration properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the option names are case-sensitive.
|
||||
/// (Default: false)
|
||||
/// </summary>
|
||||
public bool IsCaseSensitive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether incomplete options can be automatically
|
||||
/// completed if there is only a single matching option.
|
||||
/// (Default: true)
|
||||
/// </summary>
|
||||
public bool AutoCompleteOptions { get; set; } = true;
|
||||
|
||||
#endregion Configuration properties
|
||||
|
||||
#region Custom arguments line parsing
|
||||
|
||||
// Source: http://stackoverflow.com/a/23961658/143684
|
||||
/// <summary>
|
||||
/// Parses a single string into an arguments array.
|
||||
/// </summary>
|
||||
/// <param name="argsString">The string that contains the entire command line.</param>
|
||||
public static string[] ParseArgsString(string argsString)
|
||||
{
|
||||
// Collects the split argument strings
|
||||
var args = new List<string>();
|
||||
|
||||
// Builds the current argument
|
||||
var currentArg = new StringBuilder();
|
||||
|
||||
// Indicates whether the last character was a backslash escape character
|
||||
bool escape = false;
|
||||
|
||||
// Indicates whether we're in a quoted range
|
||||
bool inQuote = false;
|
||||
|
||||
// Indicates whether there were quotes in the current arguments
|
||||
bool hadQuote = false;
|
||||
|
||||
// Remembers the previous character
|
||||
char prevCh = '\0';
|
||||
|
||||
// Iterate all characters from the input string
|
||||
for (int i = 0; i < argsString.Length; i++)
|
||||
{
|
||||
char ch = argsString[i];
|
||||
if (ch == '\\' && !escape)
|
||||
{
|
||||
// Beginning of a backslash-escape sequence
|
||||
escape = true;
|
||||
}
|
||||
else if (ch == '\\' && escape)
|
||||
{
|
||||
// Double backslash, keep one
|
||||
currentArg.Append(ch);
|
||||
escape = false;
|
||||
}
|
||||
else if (ch == '"' && !escape)
|
||||
{
|
||||
// Toggle quoted range
|
||||
inQuote = !inQuote;
|
||||
hadQuote = true;
|
||||
if (inQuote && prevCh == '"')
|
||||
{
|
||||
// Doubled quote within a quoted range is like escaping
|
||||
currentArg.Append(ch);
|
||||
}
|
||||
}
|
||||
else if (ch == '"' && escape)
|
||||
{
|
||||
// Backslash-escaped quote, keep it
|
||||
currentArg.Append(ch);
|
||||
escape = false;
|
||||
}
|
||||
else if (char.IsWhiteSpace(ch) && !inQuote)
|
||||
{
|
||||
if (escape)
|
||||
{
|
||||
// Add pending escape char
|
||||
currentArg.Append('\\');
|
||||
escape = false;
|
||||
}
|
||||
// Accept empty arguments only if they are quoted
|
||||
if (currentArg.Length > 0 || hadQuote)
|
||||
{
|
||||
args.Add(currentArg.ToString());
|
||||
}
|
||||
// Reset for next argument
|
||||
currentArg.Clear();
|
||||
hadQuote = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (escape)
|
||||
{
|
||||
// Add pending escape char
|
||||
currentArg.Append('\\');
|
||||
escape = false;
|
||||
}
|
||||
// Copy character from input, no special meaning
|
||||
currentArg.Append(ch);
|
||||
}
|
||||
prevCh = ch;
|
||||
}
|
||||
// Save last argument
|
||||
if (currentArg.Length > 0 || hadQuote)
|
||||
{
|
||||
args.Add(currentArg.ToString());
|
||||
}
|
||||
return [.. args];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the command line arguments from a single string.
|
||||
/// </summary>
|
||||
/// <param name="argsString">The string that contains the entire command line.</param>
|
||||
public void ReadArgs(string argsString)
|
||||
{
|
||||
_args = ParseArgsString(argsString);
|
||||
}
|
||||
|
||||
#endregion Custom arguments line parsing
|
||||
|
||||
#region Options management
|
||||
|
||||
/// <summary>
|
||||
/// Registers a named option without additional parameters.
|
||||
/// </summary>
|
||||
/// <param name="name">The option name.</param>
|
||||
/// <returns>The option instance.</returns>
|
||||
public Option RegisterOption(string name)
|
||||
{
|
||||
return RegisterOption(name, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a named option.
|
||||
/// </summary>
|
||||
/// <param name="name">The option name.</param>
|
||||
/// <param name="parameterCount">The number of additional parameters for this option.</param>
|
||||
/// <returns>The option instance.</returns>
|
||||
public Option RegisterOption(string name, int parameterCount)
|
||||
{
|
||||
var option = new Option(name, parameterCount);
|
||||
_options.Add(option);
|
||||
return option;
|
||||
}
|
||||
|
||||
#endregion Options management
|
||||
|
||||
#region Parsing method
|
||||
|
||||
/// <summary>
|
||||
/// Parses all command line arguments.
|
||||
/// </summary>
|
||||
/// <param name="args">The command line arguments.</param>
|
||||
public void Parse(string[] args)
|
||||
{
|
||||
_args = args ?? throw new ArgumentNullException(nameof(args));
|
||||
Parse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses all command line arguments.
|
||||
/// </summary>
|
||||
public void Parse()
|
||||
{
|
||||
// Use args of the current process if no other source was given
|
||||
if (_args == null)
|
||||
{
|
||||
_args = Environment.GetCommandLineArgs();
|
||||
if (_args.Length > 0)
|
||||
{
|
||||
// Skip myself (args[0])
|
||||
_args = _args.Skip(1).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear/reset data
|
||||
_parsedArguments = [];
|
||||
foreach (var option in _options)
|
||||
{
|
||||
option.IsSet = false;
|
||||
option.SetCount = 0;
|
||||
option.Argument = null;
|
||||
}
|
||||
|
||||
var comparison = IsCaseSensitive
|
||||
? StringComparison.Ordinal
|
||||
: StringComparison.OrdinalIgnoreCase;
|
||||
var argumentWalker = new EnumerableWalker<string>(_args);
|
||||
bool optMode = true;
|
||||
foreach (string arg in argumentWalker.Cast<string>())
|
||||
{
|
||||
if (arg == "--")
|
||||
{
|
||||
optMode = false;
|
||||
}
|
||||
else if (optMode && (arg.StartsWith("/") || arg.StartsWith("-")))
|
||||
{
|
||||
string optName = arg.Substring(arg.StartsWith("--") ? 2 : 1);
|
||||
|
||||
// Split option value if separated with : or = instead of whitespace
|
||||
int separatorIndex = optName.IndexOfAny([':', '=']);
|
||||
string optValue = null;
|
||||
if (separatorIndex != -1)
|
||||
{
|
||||
optValue = optName.Substring(separatorIndex + 1);
|
||||
optName = optName.Substring(0, separatorIndex);
|
||||
}
|
||||
|
||||
// Find the option with complete name match
|
||||
var option = _options.FirstOrDefault(o => o.Names.Any(n => n.Equals(optName, comparison)));
|
||||
if (option == null)
|
||||
{
|
||||
// Try to complete the name to a unique registered option
|
||||
var matchingOptions = _options.Where(o => o.Names.Any(n => n.StartsWith(optName, comparison))).ToList();
|
||||
if (AutoCompleteOptions && matchingOptions.Count > 1)
|
||||
throw new Exception("Invalid option, completion is not unique: " + arg);
|
||||
|
||||
if (!AutoCompleteOptions || matchingOptions.Count == 0)
|
||||
throw new Exception("Unknown option: " + arg);
|
||||
|
||||
// Accept the single auto-completed option
|
||||
option = matchingOptions[0];
|
||||
}
|
||||
|
||||
// Check for single usage
|
||||
if (option.IsSingle && option.IsSet)
|
||||
throw new Exception("Option cannot be set multiple times: " + arg);
|
||||
|
||||
// Collect option values from next argument strings
|
||||
string[] values = new string[option.ParameterCount];
|
||||
for (int i = 0; i < option.ParameterCount; i++)
|
||||
{
|
||||
if (optValue != null)
|
||||
{
|
||||
// The first value was included in this argument string
|
||||
values[i] = optValue;
|
||||
optValue = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fetch another argument string
|
||||
values[i] = argumentWalker.GetNext();
|
||||
}
|
||||
|
||||
if (values[i] == null)
|
||||
throw new Exception("Missing argument " + (i + 1) + " for option: " + arg);
|
||||
}
|
||||
var argument = new Argument(option, values);
|
||||
|
||||
// Set usage data on the option instance for quick access
|
||||
option.IsSet = true;
|
||||
option.SetCount++;
|
||||
option.Argument = argument;
|
||||
|
||||
if (option.Action != null)
|
||||
{
|
||||
option.Action(argument);
|
||||
}
|
||||
else
|
||||
{
|
||||
_parsedArguments.Add(argument);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_parsedArguments.Add(new Argument(null, [arg]));
|
||||
}
|
||||
}
|
||||
|
||||
var missingOption = _options.FirstOrDefault(o => o.IsRequired && !o.IsSet);
|
||||
if (missingOption != null)
|
||||
throw new Exception("Missing required option: /" + missingOption.Names[0]);
|
||||
}
|
||||
|
||||
#endregion Parsing method
|
||||
|
||||
#region Parsed data properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parsed arguments.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||
/// exception handling.
|
||||
/// </remarks>
|
||||
public Argument[] Arguments
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_parsedArguments == null)
|
||||
Parse();
|
||||
|
||||
return [.. _parsedArguments];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the options that are set in the command line, including their value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||
/// exception handling.
|
||||
/// </remarks>
|
||||
public Option[] SetOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_parsedArguments == null)
|
||||
Parse();
|
||||
|
||||
return _parsedArguments
|
||||
.Where(a => a.Option != null)
|
||||
.Select(a => a.Option)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the free arguments that are set in the command line and don't belong to an option.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||
/// exception handling.
|
||||
/// </remarks>
|
||||
public string[] FreeArguments
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_parsedArguments == null)
|
||||
Parse();
|
||||
|
||||
return _parsedArguments
|
||||
.Where(a => a.Option == null)
|
||||
.Select(a => a.Value)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Parsed data properties
|
||||
}
|
||||
}
|
||||
53
CliClient/Cli/EnumerableWalker.cs
Normal file
53
CliClient/Cli/EnumerableWalker.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AMWD.Common.Cli
|
||||
{
|
||||
/// <summary>
|
||||
/// Walks through an <see cref="IEnumerable{T}"/> and allows retrieving additional items.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <remarks>
|
||||
/// Initialises a new instance of the <see cref="EnumerableWalker{T}"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="array">The array to walk though.</param>
|
||||
internal class EnumerableWalker<T>(IEnumerable<T> array)
|
||||
: IEnumerable<T> where T : class
|
||||
{
|
||||
private readonly IEnumerable<T> _array = array ?? throw new ArgumentNullException(nameof(array));
|
||||
private IEnumerator<T> _enumerator;
|
||||
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator()
|
||||
{
|
||||
_enumerator = _array.GetEnumerator();
|
||||
return _enumerator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the enumerator.
|
||||
/// </summary>
|
||||
/// <returns>The enumerator.</returns>
|
||||
public IEnumerator GetEnumerator()
|
||||
{
|
||||
_enumerator = _array.GetEnumerator();
|
||||
return _enumerator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next item.
|
||||
/// </summary>
|
||||
/// <returns>The next item.</returns>
|
||||
public T GetNext()
|
||||
{
|
||||
if (_enumerator.MoveNext())
|
||||
{
|
||||
return _enumerator.Current;
|
||||
}
|
||||
else
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
CliClient/Cli/Option.cs
Normal file
112
CliClient/Cli/Option.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AMWD.Common.Cli
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a named option.
|
||||
/// </summary>
|
||||
internal class Option
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the <see cref="Option"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The primary name of the option.</param>
|
||||
/// <param name="parameterCount">The number of additional parameters for this option.</param>
|
||||
internal Option(string name, int parameterCount)
|
||||
{
|
||||
Names = [name];
|
||||
ParameterCount = parameterCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the names of this option.
|
||||
/// </summary>
|
||||
public List<string> Names { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of additional parameters for this option.
|
||||
/// </summary>
|
||||
public int ParameterCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this option is required.
|
||||
/// </summary>
|
||||
public bool IsRequired { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this option can only be specified once.
|
||||
/// </summary>
|
||||
public bool IsSingle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the action to invoke when the option is set.
|
||||
/// </summary>
|
||||
public Action<Argument> Action { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this option is set in the command line.
|
||||
/// </summary>
|
||||
public bool IsSet { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of times that this option is set in the command line.
|
||||
/// </summary>
|
||||
public int SetCount { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Argument"/> instance that contains additional parameters set
|
||||
/// for this option.
|
||||
/// </summary>
|
||||
public Argument Argument { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the <see cref="Argument"/> instance for this option.
|
||||
/// </summary>
|
||||
public string Value => Argument?.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Sets alias names for this option.
|
||||
/// </summary>
|
||||
/// <param name="names">The alias names for this option.</param>
|
||||
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||
public Option Alias(params string[] names)
|
||||
{
|
||||
Names.AddRange(names);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks this option as required. If a required option is not set in the command line,
|
||||
/// an exception is thrown on parsing.
|
||||
/// </summary>
|
||||
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||
public Option Required()
|
||||
{
|
||||
IsRequired = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks this option as single. If a single option is set multiple times in the
|
||||
/// command line, an exception is thrown on parsing.
|
||||
/// </summary>
|
||||
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||
public Option Single()
|
||||
{
|
||||
IsSingle = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the action to invoke when the option is set.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to invoke when the option is set.</param>
|
||||
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||
public Option Do(Action<Argument> action)
|
||||
{
|
||||
Action = action;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
CliClient/CliClient.csproj
Normal file
34
CliClient/CliClient.csproj
Normal file
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<AssemblyName>modbus-client</AssemblyName>
|
||||
<RootNamespace>AMWD.Protocols.Modbus.CliClient</RootNamespace>
|
||||
|
||||
<Product>Modbus CLI client</Product>
|
||||
<Description>Small CLI client for Modbus communication.</Description>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="$(SolutionDir)/package-icon.png" />
|
||||
<None Remove="$(SolutionDir)/LICENSE.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
|
||||
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
628
CliClient/Program.cs
Normal file
628
CliClient/Program.cs
Normal file
@@ -0,0 +1,628 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Ports;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Common.Cli;
|
||||
using AMWD.Protocols.Modbus.Common;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||
using AMWD.Protocols.Modbus.Serial;
|
||||
using AMWD.Protocols.Modbus.Tcp;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.CliClient
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
// General
|
||||
private static string _target;
|
||||
private static Option _helpOption;
|
||||
private static Option _debugOption;
|
||||
|
||||
private static Option _protocolOption;
|
||||
private static Option _addressOption;
|
||||
private static Option _referenceOption;
|
||||
private static Option _countOption;
|
||||
private static Option _typeOption;
|
||||
private static Option _intervalOption;
|
||||
private static Option _timeoutOption;
|
||||
private static Option _onceOption;
|
||||
|
||||
// Serial
|
||||
private static Option _baudOption;
|
||||
private static Option _dataBitsOption;
|
||||
private static Option _stopBitsOption;
|
||||
private static Option _parityOption;
|
||||
private static Option _softSwitchOption;
|
||||
|
||||
// TCP
|
||||
private static Option _portOption;
|
||||
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
if (!ParseArguments(args))
|
||||
{
|
||||
Console.Error.WriteLine("Could not parse arguments.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (_helpOption.IsSet)
|
||||
{
|
||||
PrintHelp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_target))
|
||||
{
|
||||
Console.Error.WriteLine("No serial port or tcp host specified.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!_typeOption.IsSet)
|
||||
{
|
||||
Console.Error.WriteLine("No type specified.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (_, e) =>
|
||||
{
|
||||
cts.Cancel();
|
||||
e.Cancel = true;
|
||||
};
|
||||
|
||||
if (_debugOption.IsSet)
|
||||
{
|
||||
Console.Error.Write("Waiting for debugger ");
|
||||
while (!Debugger.IsAttached)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.Error.Write(".");
|
||||
await Task.Delay(1000, cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Console.Error.WriteLine();
|
||||
}
|
||||
|
||||
using var client = CreateClient();
|
||||
|
||||
if (_protocolOption.IsSet)
|
||||
{
|
||||
switch (_protocolOption.Value.ToLower())
|
||||
{
|
||||
case "ascii": client.Protocol = new AsciiProtocol(); break;
|
||||
case "rtu": client.Protocol = new RtuProtocol(); break;
|
||||
case "tcp": client.Protocol = new TcpProtocol(); break;
|
||||
}
|
||||
}
|
||||
|
||||
byte deviceAddress = 1;
|
||||
if (_addressOption.IsSet && byte.TryParse(_addressOption.Value, out byte addressValue))
|
||||
deviceAddress = addressValue;
|
||||
|
||||
ushort reference = 0;
|
||||
if (_referenceOption.IsSet && ushort.TryParse(_referenceOption.Value, out ushort referenceValue))
|
||||
reference = referenceValue;
|
||||
|
||||
ushort count = 1;
|
||||
if (_countOption.IsSet && ushort.TryParse(_countOption.Value, out ushort countValue))
|
||||
count = countValue;
|
||||
|
||||
int interval = 1000;
|
||||
if (_intervalOption.IsSet && int.TryParse(_intervalOption.Value, out int intervalValue))
|
||||
interval = intervalValue;
|
||||
|
||||
bool runOnce = _onceOption.IsSet;
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_typeOption.Value.ToLower() == "id")
|
||||
{
|
||||
runOnce = true;
|
||||
|
||||
var deviceIdentification = await client.ReadDeviceIdentificationAsync(deviceAddress, ModbusDeviceIdentificationCategory.Regular, cancellationToken: cts.Token);
|
||||
Console.WriteLine(deviceIdentification);
|
||||
}
|
||||
else if (_typeOption.Value.ToLower() == "coil")
|
||||
{
|
||||
var coils = await client.ReadCoilsAsync(deviceAddress, reference, count, cts.Token);
|
||||
foreach (var coil in coils)
|
||||
Console.WriteLine($" Coil {coil.Address}: {coil.Value}");
|
||||
}
|
||||
else if (_typeOption.Value.ToLower() == "discrete")
|
||||
{
|
||||
var discreteInputs = await client.ReadDiscreteInputsAsync(deviceAddress, reference, count, cts.Token);
|
||||
foreach (var discreteInput in discreteInputs)
|
||||
Console.WriteLine($" Discrete Input {discreteInput.Address}: {discreteInput.Value}");
|
||||
}
|
||||
else if (_typeOption.Value.StartsWith("input", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string type = _typeOption.Value.ToLower().Split(':').Last();
|
||||
switch (type)
|
||||
{
|
||||
case "hex":
|
||||
{
|
||||
var registers = await client.ReadInputRegistersAsync(deviceAddress, reference, count, cts.Token);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var register = registers[i];
|
||||
Console.WriteLine($" Input Register {register.Address}: {register.HighByte:X2} {register.LowByte:X2}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "i8":
|
||||
{
|
||||
var registers = await client.ReadInputRegistersAsync(deviceAddress, reference, count, cts.Token);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var register = registers[i];
|
||||
Console.WriteLine($" Input Register {register.Address}: {register.GetSByte()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "i16":
|
||||
{
|
||||
var registers = await client.ReadInputRegistersAsync(deviceAddress, reference, count, cts.Token);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var register = registers[i];
|
||||
Console.WriteLine($" Input Register {register.Address}: {register.GetInt16()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "i32":
|
||||
{
|
||||
int cnt = count * 2;
|
||||
var registers = await client.ReadInputRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 2)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(2);
|
||||
Console.WriteLine($" Input Register {subRegisters.First().Address}: {subRegisters.GetInt32()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "i64":
|
||||
{
|
||||
int cnt = count * 4;
|
||||
var registers = await client.ReadInputRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 4)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(4);
|
||||
Console.WriteLine($" Input Register {subRegisters.First().Address}: {subRegisters.GetInt64()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "u8":
|
||||
{
|
||||
var registers = await client.ReadInputRegistersAsync(deviceAddress, reference, count, cts.Token);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var register = registers[i];
|
||||
Console.WriteLine($" Input Register {register.Address}: {register.GetByte()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "u16":
|
||||
{
|
||||
var registers = await client.ReadInputRegistersAsync(deviceAddress, reference, count, cts.Token);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var register = registers[i];
|
||||
Console.WriteLine($" Input Register {register.Address}: {register.GetUInt16()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "u32":
|
||||
{
|
||||
int cnt = count * 2;
|
||||
var registers = await client.ReadInputRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 2)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(2);
|
||||
Console.WriteLine($" Input Register {subRegisters.First().Address}: {subRegisters.GetUInt32()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "u64":
|
||||
{
|
||||
int cnt = count * 4;
|
||||
var registers = await client.ReadInputRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 4)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(4);
|
||||
Console.WriteLine($" Input Register {subRegisters.First().Address}: {subRegisters.GetUInt64()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "f32":
|
||||
{
|
||||
int cnt = count * 2;
|
||||
var registers = await client.ReadInputRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 2)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(2);
|
||||
Console.WriteLine($" Input Register {subRegisters.First().Address}: {subRegisters.GetSingle()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "f64":
|
||||
{
|
||||
int cnt = count * 4;
|
||||
var registers = await client.ReadInputRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 4)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(4);
|
||||
Console.WriteLine($" Input Register {subRegisters.First().Address}: {subRegisters.GetDouble()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (_typeOption.Value.StartsWith("holding", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string type = _typeOption.Value.ToLower().Split(':').Last();
|
||||
switch (type)
|
||||
{
|
||||
case "hex":
|
||||
{
|
||||
var registers = await client.ReadHoldingRegistersAsync(deviceAddress, reference, count, cts.Token);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var register = registers[i];
|
||||
Console.WriteLine($" Holding Register {register.Address}: {register.HighByte:X2} {register.LowByte:X2}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "i8":
|
||||
{
|
||||
var registers = await client.ReadHoldingRegistersAsync(deviceAddress, reference, count, cts.Token);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var register = registers[i];
|
||||
Console.WriteLine($" Holding Register {register.Address}: {register.GetSByte()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "i16":
|
||||
{
|
||||
var registers = await client.ReadHoldingRegistersAsync(deviceAddress, reference, count, cts.Token);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var register = registers[i];
|
||||
Console.WriteLine($" Holding Register {register.Address}: {register.GetInt16()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "i32":
|
||||
{
|
||||
int cnt = count * 2;
|
||||
var registers = await client.ReadHoldingRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 2)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(2);
|
||||
Console.WriteLine($" Holding Register {subRegisters.First().Address}: {subRegisters.GetInt32()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "i64":
|
||||
{
|
||||
int cnt = count * 4;
|
||||
var registers = await client.ReadHoldingRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 4)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(4);
|
||||
Console.WriteLine($" Holding Register {subRegisters.First().Address}: {subRegisters.GetInt64()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "u8":
|
||||
{
|
||||
var registers = await client.ReadHoldingRegistersAsync(deviceAddress, reference, count, cts.Token);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var register = registers[i];
|
||||
Console.WriteLine($" Holding Register {register.Address}: {register.GetByte()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "u16":
|
||||
{
|
||||
var registers = await client.ReadHoldingRegistersAsync(deviceAddress, reference, count, cts.Token);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var register = registers[i];
|
||||
Console.WriteLine($" Holding Register {register.Address}: {register.GetUInt16()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "u32":
|
||||
{
|
||||
int cnt = count * 2;
|
||||
var registers = await client.ReadHoldingRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 2)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(2);
|
||||
Console.WriteLine($" Holding Register {subRegisters.First().Address}: {subRegisters.GetUInt32()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "u64":
|
||||
{
|
||||
int cnt = count * 4;
|
||||
var registers = await client.ReadHoldingRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 4)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(4);
|
||||
Console.WriteLine($" Holding Register {subRegisters.First().Address}: {subRegisters.GetUInt64()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "f32":
|
||||
{
|
||||
int cnt = count * 2;
|
||||
var registers = await client.ReadHoldingRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 2)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(2);
|
||||
Console.WriteLine($" Holding Register {subRegisters.First().Address}: {subRegisters.GetSingle()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "f64":
|
||||
{
|
||||
int cnt = count * 4;
|
||||
var registers = await client.ReadHoldingRegistersAsync(deviceAddress, reference, (ushort)cnt, cts.Token);
|
||||
for (int i = 0; i < cnt; i += 4)
|
||||
{
|
||||
var subRegisters = registers.Skip(i).Take(4);
|
||||
Console.WriteLine($" Holding Register {subRegisters.First().Address}: {subRegisters.GetDouble()}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine($"Unknown type: {_typeOption.Value}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
await Task.Delay(interval, cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
while (!runOnce && !cts.Token.IsCancellationRequested);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool ParseArguments(string[] args)
|
||||
{
|
||||
var cmdLine = new CommandLineParser();
|
||||
|
||||
_helpOption = cmdLine.RegisterOption("help").Alias("h");
|
||||
_debugOption = cmdLine.RegisterOption("debug");
|
||||
|
||||
// General Options
|
||||
_protocolOption = cmdLine.RegisterOption("protocol", 1).Alias("m");
|
||||
_addressOption = cmdLine.RegisterOption("address", 1).Alias("a");
|
||||
_referenceOption = cmdLine.RegisterOption("reference", 1).Alias("r");
|
||||
_countOption = cmdLine.RegisterOption("count", 1).Alias("c");
|
||||
_typeOption = cmdLine.RegisterOption("type", 1).Alias("t");
|
||||
_intervalOption = cmdLine.RegisterOption("interval", 1).Alias("i");
|
||||
_timeoutOption = cmdLine.RegisterOption("timeout", 1).Alias("o");
|
||||
_onceOption = cmdLine.RegisterOption("once").Alias("1");
|
||||
|
||||
// Serial Options
|
||||
_baudOption = cmdLine.RegisterOption("baud", 1).Alias("b");
|
||||
_dataBitsOption = cmdLine.RegisterOption("data-bits", 1).Alias("d");
|
||||
_stopBitsOption = cmdLine.RegisterOption("stop-bits", 1).Alias("s");
|
||||
_parityOption = cmdLine.RegisterOption("parity", 1).Alias("p");
|
||||
_softSwitchOption = cmdLine.RegisterOption("enable-rs485");
|
||||
|
||||
// TCP Options
|
||||
_portOption = cmdLine.RegisterOption("port", 1).Alias("p");
|
||||
|
||||
try
|
||||
{
|
||||
cmdLine.Parse(args);
|
||||
_target = cmdLine.FreeArguments.FirstOrDefault();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
Console.WriteLine($"Usage: {typeof(Program).Assembly.GetName().Name} [OPTIONS] <serial-port>|<tcp-host>");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Serial Port:");
|
||||
Console.WriteLine(" COM1, COM2, ... on Windows");
|
||||
Console.WriteLine(" /dev/ttyS0, /dev/ttyUSB0, ... on Linux");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("TCP Host:");
|
||||
Console.WriteLine(" 192.168.x.y as IPv4");
|
||||
Console.WriteLine(" fd00:1234:x:y::z as IPv6");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("General Options:");
|
||||
Console.WriteLine(" -h, --help");
|
||||
Console.WriteLine(" Shows this help message.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --debug");
|
||||
Console.WriteLine(" Waits for a debugger to attach before starting.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -m, --protocol <ascii|rtu|tcp>");
|
||||
Console.WriteLine(" Select which protocol to use.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -a, --address #");
|
||||
Console.WriteLine(" The slave/device address. 1-247 for serial, 0-255 for TCP. Default: 1");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -r, --reference #");
|
||||
Console.WriteLine(" The start reference to read from. 0-65535. Default: 0");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -c, --count #");
|
||||
Console.WriteLine(" The number of values to read. Default: 1");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -t, --type <coil|discrete>");
|
||||
Console.WriteLine(" Reads a discrete value (bool): Coil or Discrete Input.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -t, --type input:<kind>");
|
||||
Console.WriteLine(" Reads an input register. Kind: (e.g. i32)");
|
||||
Console.WriteLine(" hex = print as HEX representation");
|
||||
Console.WriteLine(" i = signed integer (8, 16, 32, 64)");
|
||||
Console.WriteLine(" u = unsigned integer (8, 16, 32, 64)");
|
||||
Console.WriteLine(" f = floating point (32, 64)");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -t, --type holding:<kind>");
|
||||
Console.WriteLine(" Reads a holding register. Kind: (e.g. i32)");
|
||||
Console.WriteLine(" hex = print as HEX representation");
|
||||
Console.WriteLine(" i = signed integer (8, 16, 32, 64)");
|
||||
Console.WriteLine(" u = unsigned integer (8, 16, 32, 64)");
|
||||
Console.WriteLine(" f = floating point (32, 64)");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -t, --type id");
|
||||
Console.WriteLine(" Tries to read the device identification (Fn 43, Regular).");
|
||||
Console.WriteLine(" This option implies --once.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -i, --interval #");
|
||||
Console.WriteLine(" The polling interval in milliseconds. Default: 1000");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -o, --timeout #");
|
||||
Console.WriteLine(" The timeout in milliseconds. Default: 1000");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -1, --once");
|
||||
Console.WriteLine(" Just query once, no interval polling.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Serial Options:");
|
||||
Console.WriteLine(" -b, --baud #");
|
||||
Console.WriteLine(" The baud rate (e.g. 9600). Default: 19200");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -d, --databits #");
|
||||
Console.WriteLine(" The number of data bits (7/8 for ASCII, otherwise 8). Default: 8");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -s, --stopbits #");
|
||||
Console.WriteLine(" The number of stop bits (1/2). Default: 1");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" -p, --parity <none|odd|even>");
|
||||
Console.WriteLine(" The kind of parity. Default: even");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --enable-rs485");
|
||||
Console.WriteLine(" Enables the RS485 software switch for serial adapters capable of RS232 and RS485.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("TCP Options:");
|
||||
Console.WriteLine(" -p, --port #");
|
||||
Console.WriteLine(" The TCP port of the remote device. Default: 502");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
private static bool IsSerialTarget()
|
||||
{
|
||||
return OperatingSystem.IsWindows()
|
||||
? _target.StartsWith("COM", StringComparison.OrdinalIgnoreCase)
|
||||
: _target.StartsWith("/dev/", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static ModbusClientBase CreateClient()
|
||||
{
|
||||
int timeout = 1000;
|
||||
if (_timeoutOption.IsSet && int.TryParse(_timeoutOption.Value, out int timeoutValue))
|
||||
timeout = timeoutValue;
|
||||
|
||||
if (IsSerialTarget())
|
||||
{
|
||||
BaudRate baudRate = BaudRate.Baud19200;
|
||||
if (_baudOption.IsSet && int.TryParse(_baudOption.Value, out int baudRateValue))
|
||||
baudRate = (BaudRate)baudRateValue;
|
||||
|
||||
int dataBits = 8;
|
||||
if (_dataBitsOption.IsSet && int.TryParse(_dataBitsOption.Value, out int dataBitsValue))
|
||||
dataBits = dataBitsValue;
|
||||
|
||||
StopBits stopBits = StopBits.One;
|
||||
if (_stopBitsOption.IsSet && float.TryParse(_stopBitsOption.Value, out float stopBitsValue))
|
||||
{
|
||||
switch (stopBitsValue)
|
||||
{
|
||||
case 1.0f: stopBits = StopBits.One; break;
|
||||
case 1.5f: stopBits = StopBits.OnePointFive; break;
|
||||
case 2.0f: stopBits = StopBits.Two; break;
|
||||
}
|
||||
}
|
||||
|
||||
Parity parity = Parity.Even;
|
||||
if (_parityOption.IsSet)
|
||||
{
|
||||
switch (_parityOption.Value.ToLower())
|
||||
{
|
||||
case "none": parity = Parity.None; break;
|
||||
case "odd": parity = Parity.Odd; break;
|
||||
case "even": parity = Parity.Even; break;
|
||||
}
|
||||
}
|
||||
|
||||
bool enableRs485 = _softSwitchOption.IsSet;
|
||||
|
||||
var client = new ModbusSerialClient(_target)
|
||||
{
|
||||
BaudRate = baudRate,
|
||||
DataBits = dataBits,
|
||||
StopBits = stopBits,
|
||||
Parity = parity,
|
||||
|
||||
ReadTimeout = TimeSpan.FromMilliseconds(timeout),
|
||||
WriteTimeout = TimeSpan.FromMilliseconds(timeout),
|
||||
|
||||
DriverEnabledRS485 = enableRs485
|
||||
};
|
||||
|
||||
Console.WriteLine(client);
|
||||
return client;
|
||||
}
|
||||
else
|
||||
{
|
||||
int port = 502;
|
||||
if (_portOption.IsSet && int.TryParse(_portOption.Value, out int portValue))
|
||||
port = portValue;
|
||||
|
||||
var client = new ModbusTcpClient(_target)
|
||||
{
|
||||
Port = port,
|
||||
|
||||
ReadTimeout = TimeSpan.FromMilliseconds(timeout),
|
||||
WriteTimeout = TimeSpan.FromMilliseconds(timeout),
|
||||
};
|
||||
|
||||
Console.WriteLine(client);
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
CliClient/Properties/launchSettings.json
Normal file
9
CliClient/Properties/launchSettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"profiles": {
|
||||
"ConsoleApp": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "--debug COM1",
|
||||
"remoteDebugEnabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
95
CliClient/README.md
Normal file
95
CliClient/README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Modbus CLI client
|
||||
|
||||
This project contains a small CLI tool to test Modbus connections.
|
||||
|
||||
```
|
||||
Usage: modbus-client [OPTIONS] <serial-port>|<tcp-host>
|
||||
|
||||
Serial Port:
|
||||
COM1, COM2, ... on Windows
|
||||
/dev/ttyS0, /dev/ttyUSB0, ... on Linux
|
||||
|
||||
TCP Host:
|
||||
192.168.x.y as IPv4
|
||||
fd00:1234:x:y::z as IPv6
|
||||
|
||||
General Options:
|
||||
-h, --help
|
||||
Shows this help message.
|
||||
|
||||
--debug
|
||||
Waits for a debugger to attach before starting.
|
||||
|
||||
-m, --protocol <ascii|rtu|tcp>
|
||||
Select which protocol to use.
|
||||
|
||||
-a, --address #
|
||||
The slave/device address. 1-247 for serial, 0-255 for TCP. Default: 1
|
||||
|
||||
-r, --reference #
|
||||
The start reference to read from. 0-65535. Default: 0
|
||||
|
||||
-c, --count #
|
||||
The number of values to read. Default: 1
|
||||
|
||||
-t, --type <coil|discrete>
|
||||
Reads a discrete value (bool): Coil or Discrete Input.
|
||||
|
||||
-t, --type input:<kind>
|
||||
Reads an input register. Kind: (e.g. i32)
|
||||
hex = print as HEX representation
|
||||
i = signed integer (8, 16, 32, 64)
|
||||
u = unsigned integer (8, 16, 32, 64)
|
||||
f = floating point (32, 64)
|
||||
|
||||
-t, --type holding:<kind>
|
||||
Reads a holding register. Kind: (e.g. i32)
|
||||
hex = print as HEX representation
|
||||
i = signed integer (8, 16, 32, 64)
|
||||
u = unsigned integer (8, 16, 32, 64)
|
||||
f = floating point (32, 64)
|
||||
|
||||
-t, --type id
|
||||
Tries to read the device identification (Fn 43, Regular).
|
||||
This option implies --once.
|
||||
|
||||
-i, --interval #
|
||||
The polling interval in milliseconds. Default: 1000
|
||||
|
||||
-o, --timeout #
|
||||
The timeout in milliseconds. Default: 1000
|
||||
|
||||
-1, --once
|
||||
Just query once, no interval polling.
|
||||
|
||||
|
||||
Serial Options:
|
||||
-b, --baud #
|
||||
The baud rate (e.g. 9600). Default: 19200
|
||||
|
||||
-d, --databits #
|
||||
The number of data bits (7/8 for ASCII, otherwise 8). Default: 8
|
||||
|
||||
-s, --stopbits #
|
||||
The number of stop bits (1/2). Default: 1
|
||||
|
||||
-p, --parity <none|odd|even>
|
||||
The kind of parity. Default: even
|
||||
|
||||
--enable-rs485
|
||||
Enables the RS485 software switch for serial adapters capable of RS232 and RS485.
|
||||
|
||||
|
||||
TCP Options:
|
||||
-p, --port #
|
||||
The TCP port of the remote device. Default: 502
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [choose a license])
|
||||
|
||||
|
||||
|
||||
[choose a license]: https://choosealicense.com/licenses/mit/
|
||||
35
CliProxy/Cli/Argument.cs
Normal file
35
CliProxy/Cli/Argument.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace AMWD.Common.Cli
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a logical argument in the command line. Options with their additional
|
||||
/// parameters are combined in one argument.
|
||||
/// </summary>
|
||||
internal class Argument
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the <see cref="Argument"/> class.
|
||||
/// </summary>
|
||||
/// <param name="option">The <see cref="Option"/> that is set in this argument; or null.</param>
|
||||
/// <param name="values">The additional parameter values for the option; or the argument value.</param>
|
||||
internal Argument(Option option, string[] values)
|
||||
{
|
||||
Option = option;
|
||||
Values = values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Option"/> that is set in this argument; or null.
|
||||
/// </summary>
|
||||
public Option Option { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the additional parameter values for the option; or the argument value.
|
||||
/// </summary>
|
||||
public string[] Values { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first item of <see cref="Values"/>; or null.
|
||||
/// </summary>
|
||||
public string Value => Values.Length > 0 ? Values[0] : null;
|
||||
}
|
||||
}
|
||||
366
CliProxy/Cli/CommandLineParser.cs
Normal file
366
CliProxy/Cli/CommandLineParser.cs
Normal file
@@ -0,0 +1,366 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AMWD.Common.Cli
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides options and arguments parsing from command line arguments or a single string.
|
||||
/// </summary>
|
||||
internal class CommandLineParser
|
||||
{
|
||||
#region Private data
|
||||
|
||||
private string[] _args;
|
||||
private List<Argument> _parsedArguments;
|
||||
private readonly List<Option> _options = [];
|
||||
|
||||
#endregion Private data
|
||||
|
||||
#region Configuration properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the option names are case-sensitive.
|
||||
/// (Default: false)
|
||||
/// </summary>
|
||||
public bool IsCaseSensitive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether incomplete options can be automatically
|
||||
/// completed if there is only a single matching option.
|
||||
/// (Default: true)
|
||||
/// </summary>
|
||||
public bool AutoCompleteOptions { get; set; } = true;
|
||||
|
||||
#endregion Configuration properties
|
||||
|
||||
#region Custom arguments line parsing
|
||||
|
||||
// Source: http://stackoverflow.com/a/23961658/143684
|
||||
/// <summary>
|
||||
/// Parses a single string into an arguments array.
|
||||
/// </summary>
|
||||
/// <param name="argsString">The string that contains the entire command line.</param>
|
||||
public static string[] ParseArgsString(string argsString)
|
||||
{
|
||||
// Collects the split argument strings
|
||||
var args = new List<string>();
|
||||
|
||||
// Builds the current argument
|
||||
var currentArg = new StringBuilder();
|
||||
|
||||
// Indicates whether the last character was a backslash escape character
|
||||
bool escape = false;
|
||||
|
||||
// Indicates whether we're in a quoted range
|
||||
bool inQuote = false;
|
||||
|
||||
// Indicates whether there were quotes in the current arguments
|
||||
bool hadQuote = false;
|
||||
|
||||
// Remembers the previous character
|
||||
char prevCh = '\0';
|
||||
|
||||
// Iterate all characters from the input string
|
||||
for (int i = 0; i < argsString.Length; i++)
|
||||
{
|
||||
char ch = argsString[i];
|
||||
if (ch == '\\' && !escape)
|
||||
{
|
||||
// Beginning of a backslash-escape sequence
|
||||
escape = true;
|
||||
}
|
||||
else if (ch == '\\' && escape)
|
||||
{
|
||||
// Double backslash, keep one
|
||||
currentArg.Append(ch);
|
||||
escape = false;
|
||||
}
|
||||
else if (ch == '"' && !escape)
|
||||
{
|
||||
// Toggle quoted range
|
||||
inQuote = !inQuote;
|
||||
hadQuote = true;
|
||||
if (inQuote && prevCh == '"')
|
||||
{
|
||||
// Doubled quote within a quoted range is like escaping
|
||||
currentArg.Append(ch);
|
||||
}
|
||||
}
|
||||
else if (ch == '"' && escape)
|
||||
{
|
||||
// Backslash-escaped quote, keep it
|
||||
currentArg.Append(ch);
|
||||
escape = false;
|
||||
}
|
||||
else if (char.IsWhiteSpace(ch) && !inQuote)
|
||||
{
|
||||
if (escape)
|
||||
{
|
||||
// Add pending escape char
|
||||
currentArg.Append('\\');
|
||||
escape = false;
|
||||
}
|
||||
// Accept empty arguments only if they are quoted
|
||||
if (currentArg.Length > 0 || hadQuote)
|
||||
{
|
||||
args.Add(currentArg.ToString());
|
||||
}
|
||||
// Reset for next argument
|
||||
currentArg.Clear();
|
||||
hadQuote = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (escape)
|
||||
{
|
||||
// Add pending escape char
|
||||
currentArg.Append('\\');
|
||||
escape = false;
|
||||
}
|
||||
// Copy character from input, no special meaning
|
||||
currentArg.Append(ch);
|
||||
}
|
||||
prevCh = ch;
|
||||
}
|
||||
// Save last argument
|
||||
if (currentArg.Length > 0 || hadQuote)
|
||||
{
|
||||
args.Add(currentArg.ToString());
|
||||
}
|
||||
return [.. args];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the command line arguments from a single string.
|
||||
/// </summary>
|
||||
/// <param name="argsString">The string that contains the entire command line.</param>
|
||||
public void ReadArgs(string argsString)
|
||||
{
|
||||
_args = ParseArgsString(argsString);
|
||||
}
|
||||
|
||||
#endregion Custom arguments line parsing
|
||||
|
||||
#region Options management
|
||||
|
||||
/// <summary>
|
||||
/// Registers a named option without additional parameters.
|
||||
/// </summary>
|
||||
/// <param name="name">The option name.</param>
|
||||
/// <returns>The option instance.</returns>
|
||||
public Option RegisterOption(string name)
|
||||
{
|
||||
return RegisterOption(name, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a named option.
|
||||
/// </summary>
|
||||
/// <param name="name">The option name.</param>
|
||||
/// <param name="parameterCount">The number of additional parameters for this option.</param>
|
||||
/// <returns>The option instance.</returns>
|
||||
public Option RegisterOption(string name, int parameterCount)
|
||||
{
|
||||
var option = new Option(name, parameterCount);
|
||||
_options.Add(option);
|
||||
return option;
|
||||
}
|
||||
|
||||
#endregion Options management
|
||||
|
||||
#region Parsing method
|
||||
|
||||
/// <summary>
|
||||
/// Parses all command line arguments.
|
||||
/// </summary>
|
||||
/// <param name="args">The command line arguments.</param>
|
||||
public void Parse(string[] args)
|
||||
{
|
||||
_args = args ?? throw new ArgumentNullException(nameof(args));
|
||||
Parse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses all command line arguments.
|
||||
/// </summary>
|
||||
public void Parse()
|
||||
{
|
||||
// Use args of the current process if no other source was given
|
||||
if (_args == null)
|
||||
{
|
||||
_args = Environment.GetCommandLineArgs();
|
||||
if (_args.Length > 0)
|
||||
{
|
||||
// Skip myself (args[0])
|
||||
_args = _args.Skip(1).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear/reset data
|
||||
_parsedArguments = [];
|
||||
foreach (var option in _options)
|
||||
{
|
||||
option.IsSet = false;
|
||||
option.SetCount = 0;
|
||||
option.Argument = null;
|
||||
}
|
||||
|
||||
var comparison = IsCaseSensitive
|
||||
? StringComparison.Ordinal
|
||||
: StringComparison.OrdinalIgnoreCase;
|
||||
var argumentWalker = new EnumerableWalker<string>(_args);
|
||||
bool optMode = true;
|
||||
foreach (string arg in argumentWalker.Cast<string>())
|
||||
{
|
||||
if (arg == "--")
|
||||
{
|
||||
optMode = false;
|
||||
}
|
||||
else if (optMode && (arg.StartsWith("/") || arg.StartsWith("-")))
|
||||
{
|
||||
string optName = arg.Substring(arg.StartsWith("--") ? 2 : 1);
|
||||
|
||||
// Split option value if separated with : or = instead of whitespace
|
||||
int separatorIndex = optName.IndexOfAny([':', '=']);
|
||||
string optValue = null;
|
||||
if (separatorIndex != -1)
|
||||
{
|
||||
optValue = optName.Substring(separatorIndex + 1);
|
||||
optName = optName.Substring(0, separatorIndex);
|
||||
}
|
||||
|
||||
// Find the option with complete name match
|
||||
var option = _options.FirstOrDefault(o => o.Names.Any(n => n.Equals(optName, comparison)));
|
||||
if (option == null)
|
||||
{
|
||||
// Try to complete the name to a unique registered option
|
||||
var matchingOptions = _options.Where(o => o.Names.Any(n => n.StartsWith(optName, comparison))).ToList();
|
||||
if (AutoCompleteOptions && matchingOptions.Count > 1)
|
||||
throw new Exception("Invalid option, completion is not unique: " + arg);
|
||||
|
||||
if (!AutoCompleteOptions || matchingOptions.Count == 0)
|
||||
throw new Exception("Unknown option: " + arg);
|
||||
|
||||
// Accept the single auto-completed option
|
||||
option = matchingOptions[0];
|
||||
}
|
||||
|
||||
// Check for single usage
|
||||
if (option.IsSingle && option.IsSet)
|
||||
throw new Exception("Option cannot be set multiple times: " + arg);
|
||||
|
||||
// Collect option values from next argument strings
|
||||
string[] values = new string[option.ParameterCount];
|
||||
for (int i = 0; i < option.ParameterCount; i++)
|
||||
{
|
||||
if (optValue != null)
|
||||
{
|
||||
// The first value was included in this argument string
|
||||
values[i] = optValue;
|
||||
optValue = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fetch another argument string
|
||||
values[i] = argumentWalker.GetNext();
|
||||
}
|
||||
|
||||
if (values[i] == null)
|
||||
throw new Exception("Missing argument " + (i + 1) + " for option: " + arg);
|
||||
}
|
||||
var argument = new Argument(option, values);
|
||||
|
||||
// Set usage data on the option instance for quick access
|
||||
option.IsSet = true;
|
||||
option.SetCount++;
|
||||
option.Argument = argument;
|
||||
|
||||
if (option.Action != null)
|
||||
{
|
||||
option.Action(argument);
|
||||
}
|
||||
else
|
||||
{
|
||||
_parsedArguments.Add(argument);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_parsedArguments.Add(new Argument(null, [arg]));
|
||||
}
|
||||
}
|
||||
|
||||
var missingOption = _options.FirstOrDefault(o => o.IsRequired && !o.IsSet);
|
||||
if (missingOption != null)
|
||||
throw new Exception("Missing required option: /" + missingOption.Names[0]);
|
||||
}
|
||||
|
||||
#endregion Parsing method
|
||||
|
||||
#region Parsed data properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parsed arguments.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||
/// exception handling.
|
||||
/// </remarks>
|
||||
public Argument[] Arguments
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_parsedArguments == null)
|
||||
Parse();
|
||||
|
||||
return [.. _parsedArguments];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the options that are set in the command line, including their value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||
/// exception handling.
|
||||
/// </remarks>
|
||||
public Option[] SetOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_parsedArguments == null)
|
||||
Parse();
|
||||
|
||||
return _parsedArguments
|
||||
.Where(a => a.Option != null)
|
||||
.Select(a => a.Option)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the free arguments that are set in the command line and don't belong to an option.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||
/// exception handling.
|
||||
/// </remarks>
|
||||
public string[] FreeArguments
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_parsedArguments == null)
|
||||
Parse();
|
||||
|
||||
return _parsedArguments
|
||||
.Where(a => a.Option == null)
|
||||
.Select(a => a.Value)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Parsed data properties
|
||||
}
|
||||
}
|
||||
53
CliProxy/Cli/EnumerableWalker.cs
Normal file
53
CliProxy/Cli/EnumerableWalker.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AMWD.Common.Cli
|
||||
{
|
||||
/// <summary>
|
||||
/// Walks through an <see cref="IEnumerable{T}"/> and allows retrieving additional items.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <remarks>
|
||||
/// Initialises a new instance of the <see cref="EnumerableWalker{T}"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="array">The array to walk though.</param>
|
||||
internal class EnumerableWalker<T>(IEnumerable<T> array)
|
||||
: IEnumerable<T> where T : class
|
||||
{
|
||||
private readonly IEnumerable<T> _array = array ?? throw new ArgumentNullException(nameof(array));
|
||||
private IEnumerator<T> _enumerator;
|
||||
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator()
|
||||
{
|
||||
_enumerator = _array.GetEnumerator();
|
||||
return _enumerator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the enumerator.
|
||||
/// </summary>
|
||||
/// <returns>The enumerator.</returns>
|
||||
public IEnumerator GetEnumerator()
|
||||
{
|
||||
_enumerator = _array.GetEnumerator();
|
||||
return _enumerator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next item.
|
||||
/// </summary>
|
||||
/// <returns>The next item.</returns>
|
||||
public T GetNext()
|
||||
{
|
||||
if (_enumerator.MoveNext())
|
||||
{
|
||||
return _enumerator.Current;
|
||||
}
|
||||
else
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
CliProxy/Cli/Option.cs
Normal file
112
CliProxy/Cli/Option.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AMWD.Common.Cli
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a named option.
|
||||
/// </summary>
|
||||
internal class Option
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the <see cref="Option"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The primary name of the option.</param>
|
||||
/// <param name="parameterCount">The number of additional parameters for this option.</param>
|
||||
internal Option(string name, int parameterCount)
|
||||
{
|
||||
Names = [name];
|
||||
ParameterCount = parameterCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the names of this option.
|
||||
/// </summary>
|
||||
public List<string> Names { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of additional parameters for this option.
|
||||
/// </summary>
|
||||
public int ParameterCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this option is required.
|
||||
/// </summary>
|
||||
public bool IsRequired { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this option can only be specified once.
|
||||
/// </summary>
|
||||
public bool IsSingle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the action to invoke when the option is set.
|
||||
/// </summary>
|
||||
public Action<Argument> Action { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this option is set in the command line.
|
||||
/// </summary>
|
||||
public bool IsSet { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of times that this option is set in the command line.
|
||||
/// </summary>
|
||||
public int SetCount { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Argument"/> instance that contains additional parameters set
|
||||
/// for this option.
|
||||
/// </summary>
|
||||
public Argument Argument { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the <see cref="Argument"/> instance for this option.
|
||||
/// </summary>
|
||||
public string Value => Argument?.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Sets alias names for this option.
|
||||
/// </summary>
|
||||
/// <param name="names">The alias names for this option.</param>
|
||||
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||
public Option Alias(params string[] names)
|
||||
{
|
||||
Names.AddRange(names);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks this option as required. If a required option is not set in the command line,
|
||||
/// an exception is thrown on parsing.
|
||||
/// </summary>
|
||||
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||
public Option Required()
|
||||
{
|
||||
IsRequired = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks this option as single. If a single option is set multiple times in the
|
||||
/// command line, an exception is thrown on parsing.
|
||||
/// </summary>
|
||||
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||
public Option Single()
|
||||
{
|
||||
IsSingle = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the action to invoke when the option is set.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to invoke when the option is set.</param>
|
||||
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||
public Option Do(Action<Argument> action)
|
||||
{
|
||||
Action = action;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
CliProxy/CliProxy.csproj
Normal file
34
CliProxy/CliProxy.csproj
Normal file
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<AssemblyName>modbus-proxy</AssemblyName>
|
||||
<RootNamespace>AMWD.Protocols.Modbus.CliProxy</RootNamespace>
|
||||
|
||||
<Product>Modbus CLI proxy</Product>
|
||||
<Description>Small CLI proxy to forward messages.</Description>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="$(SolutionDir)/package-icon.png" />
|
||||
<None Remove="$(SolutionDir)/LICENSE.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
|
||||
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
380
CliProxy/Program.cs
Normal file
380
CliProxy/Program.cs
Normal file
@@ -0,0 +1,380 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Ports;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Common.Cli;
|
||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||
using AMWD.Protocols.Modbus.Serial;
|
||||
using AMWD.Protocols.Modbus.Tcp;
|
||||
|
||||
namespace AMWD.Protocols.Modbus.CliProxy
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
#region General options
|
||||
|
||||
private static Option _helpOption;
|
||||
private static Option _debugOption;
|
||||
|
||||
private static Option _serverOption;
|
||||
private static Option _clientOption;
|
||||
|
||||
private static Option _clientProtocolOption;
|
||||
|
||||
#endregion General options
|
||||
|
||||
#region Server options
|
||||
|
||||
private static Option _serverSerialBaudOption;
|
||||
private static Option _serverSerialDataBitsOption;
|
||||
private static Option _serverSerialStopBitsOption;
|
||||
private static Option _serverSerialParityOption;
|
||||
private static Option _serverSerialDeviceOption;
|
||||
|
||||
private static Option _serverTcpHostOption;
|
||||
private static Option _serverTcpPortOption;
|
||||
|
||||
#endregion Server options
|
||||
|
||||
#region Client options
|
||||
|
||||
private static Option _clientSerialBaudOption;
|
||||
private static Option _clientSerialDataBitsOption;
|
||||
private static Option _clientSerialStopBitsOption;
|
||||
private static Option _clientSerialParityOption;
|
||||
private static Option _clientSerialDeviceOption;
|
||||
private static Option _clientSerialSoftEnableOption;
|
||||
|
||||
private static Option _clientTcpHostOption;
|
||||
private static Option _clientTcpPortOption;
|
||||
|
||||
#endregion Client options
|
||||
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
if (!ParseArguments(args))
|
||||
{
|
||||
Console.Error.WriteLine("Could not parse arguments.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (_helpOption.IsSet)
|
||||
{
|
||||
PrintHelp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (_, e) =>
|
||||
{
|
||||
cts.Cancel();
|
||||
e.Cancel = true;
|
||||
};
|
||||
|
||||
if (_debugOption.IsSet)
|
||||
{
|
||||
Console.Error.Write("Waiting for debugger ");
|
||||
while (!Debugger.IsAttached)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.Error.Write(".");
|
||||
await Task.Delay(1000, cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Console.Error.WriteLine();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var client = CreateClient();
|
||||
Console.WriteLine(client);
|
||||
Console.WriteLine();
|
||||
|
||||
if (_clientProtocolOption.IsSet)
|
||||
{
|
||||
switch (_clientProtocolOption.Value.ToLower())
|
||||
{
|
||||
case "ascii": client.Protocol = new AsciiProtocol(); break;
|
||||
case "rtu": client.Protocol = new RtuProtocol(); break;
|
||||
case "tcp": client.Protocol = new TcpProtocol(); break;
|
||||
}
|
||||
}
|
||||
using var proxy = CreateProxy(client);
|
||||
Console.WriteLine(proxy);
|
||||
Console.WriteLine();
|
||||
|
||||
await proxy.StartAsync(cts.Token);
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Running proxy. Press Ctrl+C to stop.");
|
||||
await Task.Delay(Timeout.Infinite, cts.Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await proxy.StopAsync();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"{ex.GetType().Name}: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ParseArguments(string[] args)
|
||||
{
|
||||
var cmdLine = new CommandLineParser();
|
||||
|
||||
#region General options
|
||||
|
||||
_helpOption = cmdLine.RegisterOption("help").Alias("h");
|
||||
_debugOption = cmdLine.RegisterOption("debug");
|
||||
|
||||
_serverOption = cmdLine.RegisterOption("server", 1); // TCP | RTU
|
||||
_clientOption = cmdLine.RegisterOption("client", 1); // TCP | RTU
|
||||
|
||||
_clientProtocolOption = cmdLine.RegisterOption("client-protocol", 1);
|
||||
|
||||
#endregion General options
|
||||
|
||||
#region Server options
|
||||
|
||||
_serverSerialBaudOption = cmdLine.RegisterOption("server-baud", 1);
|
||||
_serverSerialDataBitsOption = cmdLine.RegisterOption("server-databits", 1);
|
||||
_serverSerialDeviceOption = cmdLine.RegisterOption("server-device", 1);
|
||||
_serverSerialStopBitsOption = cmdLine.RegisterOption("server-stopbits", 1);
|
||||
_serverSerialParityOption = cmdLine.RegisterOption("server-parity", 1);
|
||||
|
||||
_serverTcpHostOption = cmdLine.RegisterOption("server-host", 1);
|
||||
_serverTcpPortOption = cmdLine.RegisterOption("server-port", 1);
|
||||
|
||||
#endregion Server options
|
||||
|
||||
#region Client options
|
||||
|
||||
_clientSerialBaudOption = cmdLine.RegisterOption("client-baud", 1);
|
||||
_clientSerialDataBitsOption = cmdLine.RegisterOption("client-databits", 1);
|
||||
_clientSerialDeviceOption = cmdLine.RegisterOption("client-device", 1);
|
||||
_clientSerialStopBitsOption = cmdLine.RegisterOption("client-stopbits", 1);
|
||||
_clientSerialParityOption = cmdLine.RegisterOption("client-parity", 1);
|
||||
_clientSerialSoftEnableOption = cmdLine.RegisterOption("client-enable-rs485");
|
||||
|
||||
_clientTcpHostOption = cmdLine.RegisterOption("client-host", 1);
|
||||
_clientTcpPortOption = cmdLine.RegisterOption("client-port", 1);
|
||||
|
||||
#endregion Client options
|
||||
|
||||
try
|
||||
{
|
||||
cmdLine.Parse(args);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
Console.WriteLine($"Usage: {typeof(Program).Assembly.GetName().Name} --server <rtu|tcp> --client <rtu|tcp> [options]");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("General options:");
|
||||
Console.WriteLine(" --help, -h");
|
||||
Console.WriteLine(" Shows this help message.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --debug");
|
||||
Console.WriteLine(" Waits for a debugger to be attached before starting.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Server options:");
|
||||
Console.WriteLine(" --server <rtu|tcp>");
|
||||
Console.WriteLine(" Defines whether to use an RTU or an TCP proxy.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --server-baud #");
|
||||
Console.WriteLine(" The baud rate (e.g. 9600) to use for the RTU proxy. Default: 19200.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --server-databits #");
|
||||
Console.WriteLine(" The number of data bits. Default: 8.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --server-device <device-port>");
|
||||
Console.WriteLine(" The serial port to use (e.g. COM1, /dev/ttyS0).");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --server-parity <none|odd|even>");
|
||||
Console.WriteLine(" The parity to use. Default: even.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --server-stopbits #");
|
||||
Console.WriteLine(" The number of stop bits. Default: 1.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --server-host <address>");
|
||||
Console.WriteLine(" The IP address to listen on. Default: 127.0.0.1.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --server-port #");
|
||||
Console.WriteLine(" The port to listen on. Default: 502.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Client options:");
|
||||
Console.WriteLine(" --client <rtu|tcp>");
|
||||
Console.WriteLine(" Defines whether to use an RTU or an TCP client.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --client-protocol <ascii|rtu|tcp>");
|
||||
Console.WriteLine(" Select which Modbus protocol to use.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --client-baud #");
|
||||
Console.WriteLine(" The baud rate (e.g. 9600) to use for the RTU client. Default: 19200.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --client-databits #");
|
||||
Console.WriteLine(" The number of data bits. Default: 8.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --client-device <device-port>");
|
||||
Console.WriteLine(" The serial port to use (e.g. COM1, /dev/ttyS0).");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --client-parity <none|odd|even>");
|
||||
Console.WriteLine(" The parity to use. Default: even.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --client-stopbits #");
|
||||
Console.WriteLine(" The number of stop bits. Default: 1.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --client-enable-rs485");
|
||||
Console.WriteLine(" Enables the RS485 software switch for serial adapters capable of RS232 and RS485.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --client-host <hostname>");
|
||||
Console.WriteLine(" The host to connect to.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" --client-port #");
|
||||
Console.WriteLine(" The port to connect to. Default: 502.");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
private static ModbusClientBase CreateClient()
|
||||
{
|
||||
if (!_clientOption.IsSet)
|
||||
throw new ApplicationException("No client type specified.");
|
||||
|
||||
BaudRate baudRate = BaudRate.Baud19200;
|
||||
if (_clientSerialBaudOption.IsSet && int.TryParse(_clientSerialBaudOption.Value, out int baudRateValue))
|
||||
baudRate = (BaudRate)baudRateValue;
|
||||
|
||||
int dataBits = 8;
|
||||
if (_clientSerialDataBitsOption.IsSet && int.TryParse(_clientSerialDataBitsOption.Value, out int dataBitsValue))
|
||||
dataBits = dataBitsValue;
|
||||
|
||||
StopBits stopBits = StopBits.One;
|
||||
if (_clientSerialStopBitsOption.IsSet && float.TryParse(_clientSerialStopBitsOption.Value, out float stopBitsValue))
|
||||
{
|
||||
switch (stopBitsValue)
|
||||
{
|
||||
case 1.0f: stopBits = StopBits.One; break;
|
||||
case 1.5f: stopBits = StopBits.OnePointFive; break;
|
||||
case 2.0f: stopBits = StopBits.Two; break;
|
||||
}
|
||||
}
|
||||
|
||||
Parity parity = Parity.Even;
|
||||
if (_clientSerialParityOption.IsSet)
|
||||
{
|
||||
switch (_clientSerialParityOption.Value.ToLower())
|
||||
{
|
||||
case "none": parity = Parity.None; break;
|
||||
case "odd": parity = Parity.Odd; break;
|
||||
case "even": parity = Parity.Even; break;
|
||||
}
|
||||
}
|
||||
|
||||
bool enableRs485 = _clientSerialSoftEnableOption.IsSet;
|
||||
|
||||
int port = 502;
|
||||
if (_clientTcpPortOption.IsSet && ushort.TryParse(_clientTcpPortOption.Value, out ushort portValue))
|
||||
port = portValue;
|
||||
|
||||
return _clientOption.Value.ToLower() switch
|
||||
{
|
||||
"rtu" => new ModbusSerialClient(_clientSerialDeviceOption.Value)
|
||||
{
|
||||
BaudRate = baudRate,
|
||||
DataBits = dataBits,
|
||||
StopBits = stopBits,
|
||||
Parity = parity,
|
||||
|
||||
DriverEnabledRS485 = enableRs485
|
||||
},
|
||||
"tcp" => new ModbusTcpClient(_clientTcpHostOption.Value)
|
||||
{
|
||||
Port = port
|
||||
},
|
||||
_ => throw new ApplicationException($"Unknown client type: '{_clientOption.Value}'"),
|
||||
};
|
||||
}
|
||||
|
||||
private static IModbusProxy CreateProxy(ModbusClientBase client)
|
||||
{
|
||||
if (!_serverOption.IsSet)
|
||||
throw new ApplicationException("No proxy type specified.");
|
||||
|
||||
BaudRate baudRate = BaudRate.Baud19200;
|
||||
if (_serverSerialBaudOption.IsSet && int.TryParse(_serverSerialBaudOption.Value, out int baudRateValue))
|
||||
baudRate = (BaudRate)baudRateValue;
|
||||
|
||||
int dataBits = 8;
|
||||
if (_serverSerialDataBitsOption.IsSet && int.TryParse(_serverSerialDataBitsOption.Value, out int dataBitsValue))
|
||||
dataBits = dataBitsValue;
|
||||
|
||||
StopBits stopBits = StopBits.One;
|
||||
if (_serverSerialStopBitsOption.IsSet && float.TryParse(_serverSerialStopBitsOption.Value, out float stopBitsValue))
|
||||
{
|
||||
switch (stopBitsValue)
|
||||
{
|
||||
case 1.0f: stopBits = StopBits.One; break;
|
||||
case 1.5f: stopBits = StopBits.OnePointFive; break;
|
||||
case 2.0f: stopBits = StopBits.Two; break;
|
||||
}
|
||||
}
|
||||
|
||||
Parity parity = Parity.Even;
|
||||
if (_serverSerialParityOption.IsSet)
|
||||
{
|
||||
switch (_serverSerialParityOption.Value.ToLower())
|
||||
{
|
||||
case "none": parity = Parity.None; break;
|
||||
case "odd": parity = Parity.Odd; break;
|
||||
case "even": parity = Parity.Even; break;
|
||||
}
|
||||
}
|
||||
|
||||
int port = 502;
|
||||
if (_serverTcpPortOption.IsSet && ushort.TryParse(_serverTcpPortOption.Value, out ushort portValue))
|
||||
port = portValue;
|
||||
|
||||
return _serverOption.Value.ToLower() switch
|
||||
{
|
||||
"rtu" => new ModbusRtuProxy(client, _serverSerialDeviceOption.Value)
|
||||
{
|
||||
BaudRate = baudRate,
|
||||
DataBits = dataBits,
|
||||
StopBits = stopBits,
|
||||
Parity = parity
|
||||
},
|
||||
"tcp" => new ModbusTcpProxy(client, IPAddress.Parse(_serverTcpHostOption.Value))
|
||||
{
|
||||
ListenPort = port
|
||||
},
|
||||
_ => throw new ApplicationException($"Unknown client type: '{_serverOption.Value}'"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
81
CliProxy/README.md
Normal file
81
CliProxy/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Modbus CLI proxy
|
||||
|
||||
This project contains a small CLI tool to proxy Modbus connections.
|
||||
|
||||
```
|
||||
Usage: modbus-proxy --server <rtu|tcp> --client <rtu|tcp> [options]
|
||||
|
||||
General options:
|
||||
--help, -h
|
||||
Shows this help message.
|
||||
|
||||
--debug
|
||||
Waits for a debugger to be attached before starting.
|
||||
|
||||
|
||||
Server options:
|
||||
--server <rtu|tcp>
|
||||
Defines whether to use an RTU or an TCP proxy.
|
||||
|
||||
--server-baud #
|
||||
The baud rate (e.g. 9600) to use for the RTU proxy. Default: 19200.
|
||||
|
||||
--server-databits #
|
||||
The number of data bits. Default: 8.
|
||||
|
||||
--server-device <device-port>
|
||||
The serial port to use (e.g. COM1, /dev/ttyS0).
|
||||
|
||||
--server-parity <none|odd|even>
|
||||
The parity to use. Default: even.
|
||||
|
||||
--server-stopbits #
|
||||
The number of stop bits. Default: 1.
|
||||
|
||||
--server-host <address>
|
||||
The IP address to listen on. Default: 127.0.0.1.
|
||||
|
||||
--server-port #
|
||||
The port to listen on. Default: 502.
|
||||
|
||||
|
||||
Client options:
|
||||
--client <rtu|tcp>
|
||||
Defines whether to use an RTU or an TCP client.
|
||||
|
||||
--client-protocol <ascii|rtu|tcp>
|
||||
Select which Modbus protocol to use.
|
||||
|
||||
--client-baud #
|
||||
The baud rate (e.g. 9600) to use for the RTU client. Default: 19200.
|
||||
|
||||
--client-databits #
|
||||
The number of data bits. Default: 8.
|
||||
|
||||
--client-device <device-port>
|
||||
The serial port to use (e.g. COM1, /dev/ttyS0).
|
||||
|
||||
--client-parity <none|odd|even>
|
||||
The parity to use. Default: even.
|
||||
|
||||
--client-stopbits #
|
||||
The number of stop bits. Default: 1.
|
||||
|
||||
--client-enable-rs485
|
||||
Enables the RS485 software switch for serial adapters capable of RS232 and RS485.
|
||||
|
||||
--client-host <hostname>
|
||||
The host to connect to.
|
||||
|
||||
--client-port #
|
||||
The port to connect to. Default: 502.
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [choose a license])
|
||||
|
||||
|
||||
|
||||
[choose a license]: https://choosealicense.com/licenses/mit/
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat>
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/AM-WD/AMWD.Protocols.Modbus.git</RepositoryUrl>
|
||||
@@ -18,19 +18,30 @@
|
||||
|
||||
<PackageIcon>package-icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageProjectUrl>https://wiki.am-wd.de/libs/modbus</PackageProjectUrl>
|
||||
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
|
||||
|
||||
<Title>Modbus Protocol for .NET</Title>
|
||||
<Company>AM.WD</Company>
|
||||
<Authors>Andreas Müller</Authors>
|
||||
<Copyright>© {copyright:2018-} AM.WD</Copyright>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>$(SolutionDir)/AMWD.Protocols.Modbus.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(SignAssembly)' != 'true'">
|
||||
<InternalsVisibleTo Include="AMWD.Protocols.Modbus.Tests" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(SignAssembly)' == 'true'">
|
||||
<InternalsVisibleTo Include="AMWD.Protocols.Modbus.Tests" PublicKey="0024000004800000940000000602000000240000525341310004000001000100adcc4f9f5bb3ac73cb30661f6f35772b8f90a74412925764a960af06ef125bdcec05ed1d139503d5203fb72aa3fa74bab58e82ac2a6cd4b650f8cbf7086a71bc2dfc67e95b8d26d776d60856acf3121f831529b1a4dee91b34ac84f95f71a1165b7783edb591929ba2a684100c92bbed8859c7266fb507f6f55bb6f7fcac80b4" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" PublicKey="0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(GITLAB_CI)' == 'true'">
|
||||
<SourceLinkGitLabHost Include="$(CI_SERVER_HOST)" Version="$(CI_SERVER_VERSION)" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0">
|
||||
@@ -40,11 +51,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../package-icon.png" Pack="true" PackagePath="/" />
|
||||
<None Include="$(SolutionDir)/package-icon.png" Pack="true" PackagePath="/" />
|
||||
<None Include="$(SolutionDir)/LICENSE.txt" Pack="true" PackagePath="/" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.3">
|
||||
<PackageReference Include="AMWD.NetRevisionTask" Version="1.2.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
The MIT License
|
||||
MIT License
|
||||
|
||||
Copyright (c) Andreas Müller
|
||||
|
||||
@@ -9,8 +9,9 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
17
README.md
17
README.md
@@ -2,9 +2,12 @@
|
||||
|
||||
Here you can find a basic implementation of the Modbus protocol.
|
||||
|
||||

|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
The project is divided into four parts.
|
||||
The project is divided into multiple parts.
|
||||
|
||||
To be mentioned at the beginning:
|
||||
Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]).
|
||||
@@ -20,11 +23,6 @@ For example the default protocol versions: `TCP`, `RTU` and `ASCII`.
|
||||
With this package you'll have anything you need to create your own client implementations.
|
||||
|
||||
|
||||
### [Proxy]
|
||||
|
||||
The package contains a TCP and a RTU server implementation as proxy which contains a client of your choice to connect to.
|
||||
|
||||
|
||||
### [Serial]
|
||||
|
||||
This package contains some wrappers and implementations for the serial protocol.
|
||||
@@ -39,16 +37,15 @@ It uses a specific TCP connection implementation and plugs all things from the C
|
||||
|
||||
---
|
||||
|
||||
Published under [MIT License] (see [**tl;dr**Legal])
|
||||
[](https://link.am-wd.de/donate)
|
||||
Published under [MIT License] (see [choose a license])
|
||||
[](https://link.am-wd.de/donate)
|
||||
[](https://link.am-wd.de/codeium)
|
||||
|
||||
|
||||
|
||||
[see here]: https://github.com/andreasAMmueller/Modbus
|
||||
[Common]: AMWD.Protocols.Modbus.Common/README.md
|
||||
[Proxy]: AMWD.Protocols.Modbus.Proxy/README.md
|
||||
[Serial]: AMWD.Protocols.Modbus.Serial/README.md
|
||||
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md
|
||||
[MIT License]: LICENSE.txt
|
||||
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
|
||||
[choose a license]: https://choosealicense.com/licenses/mit/
|
||||
|
||||
Reference in New Issue
Block a user