Compare commits
38 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 | |||
| 206c5420e1 | |||
| d027b6939a | |||
| 02a724521a | |||
| 2580554bb1 | |||
| bc3ca4fa52 | |||
| 54511c9366 | |||
| 763ca1da25 | |||
| a35c3491ae |
@@ -20,15 +20,12 @@ build-debug:
|
|||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_TAG == null
|
- if: $CI_COMMIT_TAG == null
|
||||||
script:
|
script:
|
||||||
|
- shopt -s globstar
|
||||||
|
- mkdir ./artifacts
|
||||||
- dotnet restore --no-cache --force
|
- dotnet restore --no-cache --force
|
||||||
- dotnet build -c Debug --nologo --no-restore --no-incremental
|
- dotnet build -c Debug --nologo --no-restore --no-incremental
|
||||||
- mkdir ./artifacts
|
- mv ./**/*.nupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Debug/*.nupkg ./artifacts/
|
- mv ./**/*.snupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/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/
|
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- artifacts/*.nupkg
|
- artifacts/*.nupkg
|
||||||
@@ -45,10 +42,20 @@ test-debug:
|
|||||||
- 64bit
|
- 64bit
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_TAG == null
|
- 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:
|
script:
|
||||||
- dotnet restore --no-cache --force
|
- dotnet test -c Debug --nologo /p:CoverletOutputFormat=Cobertura
|
||||||
- dotnet test -c Debug --nologo --no-restore
|
- /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:
|
deploy-debug:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
@@ -75,20 +82,17 @@ build-release:
|
|||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_TAG != null
|
- if: $CI_COMMIT_TAG != null
|
||||||
script:
|
script:
|
||||||
|
- shopt -s globstar
|
||||||
|
- mkdir ./artifacts
|
||||||
- dotnet restore --no-cache --force
|
- dotnet restore --no-cache --force
|
||||||
- dotnet build -c Release --nologo --no-restore --no-incremental
|
- dotnet build -c Release --nologo --no-restore --no-incremental
|
||||||
- mkdir ./artifacts
|
- mv ./**/*.nupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/bin/Release/*.nupkg ./artifacts/
|
- mv ./**/*.snupkg ./artifacts/
|
||||||
- mv ./AMWD.Protocols.Modbus.Common/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/
|
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- artifacts/*.nupkg
|
- artifacts/*.nupkg
|
||||||
- artifacts/*.snupkg
|
- artifacts/*.snupkg
|
||||||
expire_in: 1 days
|
expire_in: 7 days
|
||||||
|
|
||||||
test-release:
|
test-release:
|
||||||
stage: test
|
stage: test
|
||||||
@@ -100,10 +104,20 @@ test-release:
|
|||||||
- amd64
|
- amd64
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_TAG != null
|
- 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:
|
script:
|
||||||
- dotnet restore --no-cache --force
|
- dotnet test -c Release --nologo /p:CoverletOutputFormat=Cobertura
|
||||||
- dotnet test -c Release --nologo --no-restore
|
- /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:
|
deploy-release:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||||
<LangVersion>12.0</LangVersion>
|
|
||||||
|
|
||||||
<PackageId>AMWD.Protocols.Modbus.Common</PackageId>
|
<PackageId>AMWD.Protocols.Modbus.Common</PackageId>
|
||||||
<AssemblyName>amwd-modbus-common</AssemblyName>
|
<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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Common.Contracts
|
namespace AMWD.Protocols.Modbus.Common.Contracts
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base implementation of a Modbus client.
|
/// Base implementation of a Modbus client.
|
||||||
/// </summary>
|
/// </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;
|
private bool _isDisposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the connection should be disposed of by <see cref="Dispose()"/>.
|
/// Gets or sets a value indicating whether the connection should be disposed of by <see cref="Dispose()"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly bool disposeConnection;
|
protected readonly bool disposeConnection = disposeConnection;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="IModbusConnection"/> responsible for invoking the requests.
|
/// Gets or sets the <see cref="IModbusConnection"/> responsible for invoking the requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly IModbusConnection connection;
|
protected readonly IModbusConnection connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
/// Initializes a new instance of the <see cref="ModbusClientBase"/> class with a specific <see cref="IModbusConnection"/>.
|
||||||
@@ -32,27 +40,13 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
: this(connection, true)
|
: 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>
|
/// <summary>
|
||||||
/// Gets or sets the protocol type to use.
|
/// Gets or sets the protocol type to use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The default protocol used by the client should be initialized in the constructor.
|
/// The default protocol used by the client should be initialized in the constructor.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public abstract IModbusProtocol Protocol { get; set; }
|
public virtual IModbusProtocol Protocol { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads multiple <see cref="Coil"/>s.
|
/// Reads multiple <see cref="Coil"/>s.
|
||||||
@@ -67,7 +61,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeReadCoils(unitId, startAddress, count);
|
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);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
// The protocol processes complete bytes from the response.
|
// The protocol processes complete bytes from the response.
|
||||||
@@ -92,7 +86,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeReadDiscreteInputs(unitId, startAddress, count);
|
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);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
// The protocol processes complete bytes from the response.
|
// The protocol processes complete bytes from the response.
|
||||||
@@ -117,7 +111,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeReadHoldingRegisters(unitId, startAddress, count);
|
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);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList();
|
var holdingRegisters = Protocol.DeserializeReadHoldingRegisters(response).ToList();
|
||||||
@@ -140,7 +134,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeReadInputRegisters(unitId, startAddress, count);
|
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);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList();
|
var inputRegisters = Protocol.DeserializeReadInputRegisters(response).ToList();
|
||||||
@@ -184,7 +178,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
var request = Protocol.SerializeReadDeviceIdentification(unitId, category, requestObjectId);
|
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);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
result = Protocol.DeserializeReadDeviceIdentification(response);
|
result = Protocol.DeserializeReadDeviceIdentification(response);
|
||||||
@@ -247,7 +241,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeWriteSingleCoil(unitId, coil);
|
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);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var result = Protocol.DeserializeWriteSingleCoil(response);
|
var result = Protocol.DeserializeWriteSingleCoil(response);
|
||||||
@@ -268,7 +262,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeWriteSingleHoldingRegister(unitId, register);
|
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);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var result = Protocol.DeserializeWriteSingleHoldingRegister(response);
|
var result = Protocol.DeserializeWriteSingleHoldingRegister(response);
|
||||||
@@ -289,7 +283,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeWriteMultipleCoils(unitId, coils);
|
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);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response);
|
var (firstAddress, count) = Protocol.DeserializeWriteMultipleCoils(response);
|
||||||
@@ -309,7 +303,7 @@ namespace AMWD.Protocols.Modbus.Common.Contracts
|
|||||||
Assertions();
|
Assertions();
|
||||||
|
|
||||||
var request = Protocol.SerializeWriteMultipleHoldingRegisters(unitId, registers);
|
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);
|
Protocol.ValidateResponse(request, response);
|
||||||
|
|
||||||
var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response);
|
var (firstAddress, count) = Protocol.DeserializeWriteMultipleHoldingRegisters(response);
|
||||||
|
|||||||
@@ -8,19 +8,26 @@ namespace AMWD.Protocols.Modbus.Common.Events
|
|||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
public class CoilWrittenEventArgs : EventArgs
|
public class CoilWrittenEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
|
internal CoilWrittenEventArgs(byte unitId, ushort address, bool value)
|
||||||
|
{
|
||||||
|
UnitId = unitId;
|
||||||
|
Address = address;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unit id.
|
/// Gets or sets the unit id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte UnitId { get; set; }
|
public byte UnitId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the coil address.
|
/// Gets or sets the coil address.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort Address { get; set; }
|
public ushort Address { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the coil value.
|
/// Gets or sets the coil value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Value { get; set; }
|
public bool Value { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,29 +8,39 @@ namespace AMWD.Protocols.Modbus.Common.Events
|
|||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
public class RegisterWrittenEventArgs : EventArgs
|
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>
|
/// <summary>
|
||||||
/// Gets or sets the unit id.
|
/// Gets or sets the unit id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte UnitId { get; set; }
|
public byte UnitId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the address of the register.
|
/// Gets or sets the address of the register.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort Address { get; set; }
|
public ushort Address { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the value of the register.
|
/// Gets or sets the value of the register.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort Value { get; set; }
|
public ushort Value { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the high byte of the register.
|
/// Gets or sets the high byte of the register.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte HighByte { get; set; }
|
public byte HighByte { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the low byte of the register.
|
/// Gets or sets the low byte of the register.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte LowByte { get; set; }
|
public byte LowByte { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
#if !NET8_0_OR_GREATER
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Common
|
namespace AMWD.Protocols.Modbus.Common
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Common
|
namespace AMWD.Protocols.Modbus.Common
|
||||||
@@ -12,14 +13,14 @@ namespace AMWD.Protocols.Modbus.Common
|
|||||||
Array.Reverse(bytes);
|
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();
|
byte[] b = bytes.Skip(offset).Take(2).ToArray();
|
||||||
b.SwapBigEndian();
|
b.SwapBigEndian();
|
||||||
return BitConverter.ToUInt16(b, 0);
|
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);
|
byte[] b = BitConverter.GetBytes(value);
|
||||||
b.SwapBigEndian();
|
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.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Common
|
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.
|
/// Gets or sets a value indicating whether individual access (<see cref="ModbusDeviceIdentificationCategory.Individual"/>) is allowed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsIndividualAccessAllowed { get; set; }
|
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
|
get
|
||||||
{
|
{
|
||||||
byte[] blob = [HighByte, LowByte];
|
return new[] { HighByte, LowByte }.GetBigEndianUInt16();
|
||||||
blob.SwapBigEndian();
|
|
||||||
return BitConverter.ToUInt16(blob, 0);
|
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
byte[] blob = BitConverter.GetBytes(value);
|
var blob = value.ToBigEndianBytes();
|
||||||
blob.SwapBigEndian();
|
|
||||||
|
|
||||||
HighByte = blob[0];
|
HighByte = blob[0];
|
||||||
LowByte = blob[1];
|
LowByte = blob[1];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ namespace AMWD.Protocols.Modbus.Common
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
byte[] blob = [HighByte, LowByte];
|
return new[] { HighByte, LowByte }.GetBigEndianUInt16();
|
||||||
blob.SwapBigEndian();
|
|
||||||
return BitConverter.ToUInt16(blob, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace AMWD.Protocols.Modbus.Common.Models
|
|||||||
/// Initializes a new instance of the <see cref="ModbusDevice"/> class.
|
/// Initializes a new instance of the <see cref="ModbusDevice"/> class.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="id">The <see cref="ModbusDevice"/> ID.</param>
|
/// <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 _rwLockCoils = new();
|
||||||
private readonly ReaderWriterLockSlim _rwLockDiscreteInputs = 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
|
namespace AMWD.Protocols.Modbus.Common
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -92,11 +92,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadCoils:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadCoils:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// LRC
|
// LRC
|
||||||
@@ -151,11 +151,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadDiscreteInputs:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadDiscreteInputs:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// LRC
|
// LRC
|
||||||
@@ -209,11 +209,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadHoldingRegisters:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadHoldingRegisters:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// LRC
|
// LRC
|
||||||
@@ -264,11 +264,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadInputRegisters:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.ReadInputRegisters:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// LRC
|
// LRC
|
||||||
@@ -383,7 +383,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleCoil:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleCoil:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Value
|
// Value
|
||||||
@@ -426,7 +426,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleRegister:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteSingleRegister:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
var addrBytes = register.Address.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Value
|
// Value
|
||||||
@@ -497,11 +497,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleCoils:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleCoils:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// Byte count
|
// Byte count
|
||||||
@@ -567,11 +567,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleRegisters:X2}";
|
string request = $":{unitId:X2}{(byte)ModbusFunctionCode.WriteMultipleRegisters:X2}";
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
request += $"{addrBytes[0]:X2}{addrBytes[1]:X2}";
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
request += $"{countBytes[0]:X2}{countBytes[1]:X2}";
|
||||||
|
|
||||||
// Byte count
|
// Byte count
|
||||||
@@ -675,6 +675,10 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculate LRC for Modbus ASCII.
|
/// Calculate LRC for Modbus ASCII.
|
||||||
/// </summary>
|
/// </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="message">The message chars.</param>
|
||||||
/// <param name="start">The start index.</param>
|
/// <param name="start">The start index.</param>
|
||||||
/// <param name="length">The number of bytes to calculate.</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
|
namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implementation of the Modbus RTU over TCP protocol.
|
/// Implementation of the Modbus RTU over Modbus TCP protocol.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The Modbus RTU over Modbus TCP is rarely used.
|
/// 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>
|
/// </remarks>
|
||||||
public class RtuOverTcpProtocol : IModbusProtocol
|
public class RtuOverTcpProtocol : IModbusProtocol
|
||||||
{
|
{
|
||||||
@@ -115,12 +119,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadCoils;
|
request[7] = (byte)ModbusFunctionCode.ReadCoils;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -178,12 +182,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -241,12 +245,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -301,12 +305,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -428,7 +432,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
||||||
|
|
||||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
@@ -475,7 +479,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||||
|
|
||||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
var addrBytes = register.Address.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
@@ -538,12 +542,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -618,12 +622,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -743,7 +747,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
|
|
||||||
// Transaction id
|
// Transaction id
|
||||||
ushort txId = GetNextTransacitonId();
|
ushort txId = GetNextTransacitonId();
|
||||||
byte[] txBytes = txId.ToBigEndianBytes();
|
var txBytes = txId.ToBigEndianBytes();
|
||||||
header[0] = txBytes[0];
|
header[0] = txBytes[0];
|
||||||
header[1] = txBytes[1];
|
header[1] = txBytes[1];
|
||||||
|
|
||||||
@@ -752,7 +756,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
header[3] = 0x00;
|
header[3] = 0x00;
|
||||||
|
|
||||||
// Number of following bytes
|
// Number of following bytes
|
||||||
byte[] countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
var countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
||||||
header[4] = countBytes[0];
|
header[4] = countBytes[0];
|
||||||
header[5] = countBytes[1];
|
header[5] = countBytes[1];
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,22 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RtuProtocol : IModbusProtocol
|
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
|
#region Constants
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -96,12 +112,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[1] = (byte)ModbusFunctionCode.ReadCoils;
|
request[1] = (byte)ModbusFunctionCode.ReadCoils;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -156,12 +172,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[1] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
request[1] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -216,12 +232,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[1] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
request[1] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -273,12 +289,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[1] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
request[1] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -394,7 +410,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[1] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
request[1] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
||||||
|
|
||||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
@@ -438,7 +454,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[1] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
request[1] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||||
|
|
||||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
var addrBytes = register.Address.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
@@ -495,11 +511,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
|
|
||||||
request[1] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
request[1] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
||||||
|
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -565,11 +581,11 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[0] = unitId;
|
request[0] = unitId;
|
||||||
request[1] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
request[1] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
||||||
|
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[2] = addrBytes[0];
|
request[2] = addrBytes[0];
|
||||||
request[3] = addrBytes[1];
|
request[3] = addrBytes[1];
|
||||||
|
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[4] = countBytes[0];
|
request[4] = countBytes[0];
|
||||||
request[5] = countBytes[1];
|
request[5] = countBytes[1];
|
||||||
|
|
||||||
@@ -627,7 +643,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// - 0x03 Read Holding Registers
|
// - 0x03 Read Holding Registers
|
||||||
// - 0x04 Read Input Registers
|
// - 0x04 Read Input Registers
|
||||||
// do have a "following bytes" at position 3
|
// 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
|
// Unit ID, Function Code, ByteCount, 2x CRC and length of ByteCount
|
||||||
if (responseBytes.Count < 5 + responseBytes[2])
|
if (responseBytes.Count < 5 + responseBytes[2])
|
||||||
@@ -638,7 +654,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// - 0x06 Write Single Register
|
// - 0x06 Write Single Register
|
||||||
// - 0x0F Write Multiple Coils
|
// - 0x0F Write Multiple Coils
|
||||||
// - 0x10 Write Multiple Registers
|
// - 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 Single => Unit ID, Function code, 2x Address, 2x Value, 2x CRC
|
||||||
// Write Multi => Unit ID, Function code, 2x Address, 2x QuantityWritten, 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)
|
if (isError)
|
||||||
throw new ModbusException("Remote Error") { ErrorCode = (ModbusErrorCode)response[2] };
|
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])
|
if (response.Count != 5 + response[2])
|
||||||
throw new ModbusException("Number of following bytes does not match.");
|
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)
|
if (response.Count != 8)
|
||||||
throw new ModbusException("Number of bytes does not match.");
|
throw new ModbusException("Number of bytes does not match.");
|
||||||
@@ -733,6 +749,10 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculate CRC16 for Modbus RTU.
|
/// Calculate CRC16 for Modbus RTU.
|
||||||
/// </summary>
|
/// </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="bytes">The message bytes.</param>
|
||||||
/// <param name="start">The start index.</param>
|
/// <param name="start">The start index.</param>
|
||||||
/// <param name="length">The number of bytes to calculate.</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;
|
request[7] = (byte)ModbusFunctionCode.ReadCoils;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -159,12 +159,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
request[7] = (byte)ModbusFunctionCode.ReadDiscreteInputs;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -217,12 +217,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
request[7] = (byte)ModbusFunctionCode.ReadHoldingRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -272,12 +272,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
request[7] = (byte)ModbusFunctionCode.ReadInputRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = startAddress.ToBigEndianBytes();
|
var addrBytes = startAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = count.ToBigEndianBytes();
|
var countBytes = count.ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -389,7 +389,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
request[7] = (byte)ModbusFunctionCode.WriteSingleCoil;
|
||||||
|
|
||||||
byte[] addrBytes = coil.Address.ToBigEndianBytes();
|
var addrBytes = coil.Address.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
@@ -431,7 +431,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
// Function code
|
// Function code
|
||||||
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
request[7] = (byte)ModbusFunctionCode.WriteSingleRegister;
|
||||||
|
|
||||||
byte[] addrBytes = register.Address.ToBigEndianBytes();
|
var addrBytes = register.Address.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
@@ -489,12 +489,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
request[7] = (byte)ModbusFunctionCode.WriteMultipleCoils;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -564,12 +564,12 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
request[7] = (byte)ModbusFunctionCode.WriteMultipleRegisters;
|
||||||
|
|
||||||
// Starting address
|
// Starting address
|
||||||
byte[] addrBytes = firstAddress.ToBigEndianBytes();
|
var addrBytes = firstAddress.ToBigEndianBytes();
|
||||||
request[8] = addrBytes[0];
|
request[8] = addrBytes[0];
|
||||||
request[9] = addrBytes[1];
|
request[9] = addrBytes[1];
|
||||||
|
|
||||||
// Quantity
|
// Quantity
|
||||||
byte[] countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
var countBytes = ((ushort)orderedList.Count).ToBigEndianBytes();
|
||||||
request[10] = countBytes[0];
|
request[10] = countBytes[0];
|
||||||
request[11] = countBytes[1];
|
request[11] = countBytes[1];
|
||||||
|
|
||||||
@@ -678,7 +678,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
|
|
||||||
// Transaction id
|
// Transaction id
|
||||||
ushort txId = GetNextTransacitonId();
|
ushort txId = GetNextTransacitonId();
|
||||||
byte[] txBytes = txId.ToBigEndianBytes();
|
var txBytes = txId.ToBigEndianBytes();
|
||||||
header[0] = txBytes[0];
|
header[0] = txBytes[0];
|
||||||
header[1] = txBytes[1];
|
header[1] = txBytes[1];
|
||||||
|
|
||||||
@@ -687,7 +687,7 @@ namespace AMWD.Protocols.Modbus.Common.Protocols
|
|||||||
header[3] = 0x00;
|
header[3] = 0x00;
|
||||||
|
|
||||||
// Number of following bytes
|
// Number of following bytes
|
||||||
byte[] countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
var countBytes = ((ushort)followingBytes).ToBigEndianBytes();
|
||||||
header[4] = countBytes[0];
|
header[4] = countBytes[0];
|
||||||
header[5] = countBytes[1];
|
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.
|
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.
|
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
|
### Protocols
|
||||||
@@ -59,8 +60,8 @@ Here you have the specific default implementations for the Modbus Protocol.
|
|||||||
|
|
||||||
- ASCII
|
- ASCII
|
||||||
- RTU
|
- RTU
|
||||||
- RTU over TCP
|
|
||||||
- TCP
|
- TCP
|
||||||
|
- [RTU over TCP]
|
||||||
|
|
||||||
**NOTE:**
|
**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.
|
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/
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace System.Collections.Generic
|
|||||||
internalDequeueTcs = ResetToken(ref _dequeueTcs);
|
internalDequeueTcs = ResetToken(ref _dequeueTcs);
|
||||||
}
|
}
|
||||||
|
|
||||||
await WaitAsync(internalDequeueTcs, cancellationToken).ConfigureAwait(false);
|
await WaitAsync(internalDequeueTcs, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ namespace System.Collections.Generic
|
|||||||
{
|
{
|
||||||
if (await Task.WhenAny(tcs.Task, Task.Delay(-1, cancellationToken)) == tcs.Task)
|
if (await Task.WhenAny(tcs.Task, Task.Delay(-1, cancellationToken)) == tcs.Task)
|
||||||
{
|
{
|
||||||
await tcs.Task.ConfigureAwait(false);
|
await tcs.Task;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||||
<LangVersion>12.0</LangVersion>
|
|
||||||
|
|
||||||
<PackageId>AMWD.Protocols.Modbus.Serial</PackageId>
|
<PackageId>AMWD.Protocols.Modbus.Serial</PackageId>
|
||||||
<AssemblyName>amwd-modbus-serial</AssemblyName>
|
<AssemblyName>amwd-modbus-serial</AssemblyName>
|
||||||
@@ -14,11 +13,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs" Link="InternalsVisibleTo.cs" />
|
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
||||||
<Compile Include="../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="../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="../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" />
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -38,11 +36,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Extensions\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
874
AMWD.Protocols.Modbus.Serial/ModbusRtuProxy.cs
Normal file
874
AMWD.Protocols.Modbus.Serial/ModbusRtuProxy.cs
Normal file
@@ -0,0 +1,874 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
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.Utils;
|
||||||
|
|
||||||
|
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 : IModbusProxy
|
||||||
|
{
|
||||||
|
#region Fields
|
||||||
|
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
private readonly SerialPortWrapper _serialPort;
|
||||||
|
private CancellationTokenSource _stopCts;
|
||||||
|
|
||||||
|
#endregion Fields
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ModbusRtuProxy"/> class.
|
||||||
|
/// </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>
|
||||||
|
public ModbusRtuProxy(ModbusClientBase client, string portName)
|
||||||
|
{
|
||||||
|
Client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(portName))
|
||||||
|
throw new ArgumentNullException(nameof(portName));
|
||||||
|
|
||||||
|
_serialPort = new SerialPortWrapper
|
||||||
|
{
|
||||||
|
PortName = portName,
|
||||||
|
|
||||||
|
BaudRate = (int)BaudRate.Baud19200,
|
||||||
|
DataBits = 8,
|
||||||
|
StopBits = StopBits.One,
|
||||||
|
Parity = Parity.Even,
|
||||||
|
Handshake = Handshake.None,
|
||||||
|
ReadTimeout = 1000,
|
||||||
|
WriteTimeout = 1000,
|
||||||
|
RtsEnable = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Constructors
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Modbus client used to request the remote device, that should be proxied.
|
||||||
|
/// </summary>
|
||||||
|
public ModbusClientBase Client { get; }
|
||||||
|
|
||||||
|
#region SerialPort Properties
|
||||||
|
|
||||||
|
/// <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.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.Handshake" />
|
||||||
|
public virtual Handshake Handshake
|
||||||
|
{
|
||||||
|
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 virtual bool RtsEnable
|
||||||
|
{
|
||||||
|
get => _serialPort.RtsEnable;
|
||||||
|
set => _serialPort.RtsEnable = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.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 read/receive operation does not finish.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion SerialPort Properties
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
|
||||||
|
#region Control Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
|
||||||
|
_stopCts?.Cancel();
|
||||||
|
_serialPort.Close();
|
||||||
|
_serialPort.DataReceived -= OnDataReceived;
|
||||||
|
|
||||||
|
_stopCts?.Dispose();
|
||||||
|
_stopCts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
_serialPort.DataReceived += OnDataReceived;
|
||||||
|
_serialPort.Open();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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();
|
||||||
|
StopAsyncInternal();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopAsyncInternal()
|
||||||
|
{
|
||||||
|
_stopCts?.Cancel();
|
||||||
|
|
||||||
|
_serialPort.Close();
|
||||||
|
_serialPort.DataReceived -= OnDataReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases all managed and unmanaged resources used by the <see cref="ModbusRtuProxy"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
StopAsyncInternal();
|
||||||
|
|
||||||
|
_serialPort.Dispose();
|
||||||
|
_stopCts?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Assertions()
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
ObjectDisposedException.ThrowIf(_isDisposed, this);
|
||||||
|
#else
|
||||||
|
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 __)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var requestBytes = new List<byte>();
|
||||||
|
do
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[RtuProtocol.MAX_ADU_LENGTH];
|
||||||
|
int count = _serialPort.Read(buffer, 0, buffer.Length);
|
||||||
|
requestBytes.AddRange(buffer.Take(count));
|
||||||
|
|
||||||
|
_stopCts.Token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
while (_serialPort.BytesToRead > 0);
|
||||||
|
|
||||||
|
_stopCts.Token.ThrowIfCancellationRequested();
|
||||||
|
byte[] responseBytes = HandleRequest([.. requestBytes]);
|
||||||
|
if (responseBytes == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_stopCts.Token.ThrowIfCancellationRequested();
|
||||||
|
_serialPort.Write(responseBytes, 0, responseBytes.Length);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ /* keep it quiet */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Client Handling
|
||||||
|
|
||||||
|
#region Request Handling
|
||||||
|
|
||||||
|
private byte[] HandleRequest(byte[] requestBytes)
|
||||||
|
{
|
||||||
|
byte[] recvCrc = requestBytes.Skip(requestBytes.Length - 2).ToArray();
|
||||||
|
byte[] calcCrc = RtuProtocol.CRC16(requestBytes, 0, requestBytes.Length - 2);
|
||||||
|
if (!recvCrc.SequenceEqual(calcCrc))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
switch ((ModbusFunctionCode)requestBytes[1])
|
||||||
|
{
|
||||||
|
case ModbusFunctionCode.ReadCoils:
|
||||||
|
return HandleReadCoilsAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadDiscreteInputs:
|
||||||
|
return HandleReadDiscreteInputsAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadHoldingRegisters:
|
||||||
|
return HandleReadHoldingRegistersAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadInputRegisters:
|
||||||
|
return HandleReadInputRegistersAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteSingleCoil:
|
||||||
|
return HandleWriteSingleCoilAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteSingleRegister:
|
||||||
|
return HandleWriteSingleRegisterAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteMultipleCoils:
|
||||||
|
return HandleWriteMultipleCoilsAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteMultipleRegisters:
|
||||||
|
return HandleWriteMultipleRegistersAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
case ModbusFunctionCode.EncapsulatedInterface:
|
||||||
|
return HandleEncapsulatedInterfaceAsync(requestBytes, _stopCts.Token).Result;
|
||||||
|
|
||||||
|
default: // unknown function
|
||||||
|
{
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||||
|
|
||||||
|
// Mark as error
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[0];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
if (coils[i].Value)
|
||||||
|
{
|
||||||
|
int byteIndex = i / 8;
|
||||||
|
int bitIndex = i % 8;
|
||||||
|
|
||||||
|
values[byteIndex] |= (byte)(1 << bitIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[0];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
if (discreteInputs[i].Value)
|
||||||
|
{
|
||||||
|
int byteIndex = i / 8;
|
||||||
|
int bitIndex = i % 8;
|
||||||
|
|
||||||
|
values[byteIndex] |= (byte)(1 << bitIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[0];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
values[i * 2] = holdingRegisters[i].HighByte;
|
||||||
|
values[i * 2 + 1] = holdingRegisters[i].LowByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[0];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
values[i * 2] = inputRegisters[i].HighByte;
|
||||||
|
values[i * 2 + 1] = inputRegisters[i].LowByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
|
||||||
|
ushort address = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
|
||||||
|
if (requestBytes[4] != 0x00 && requestBytes[4] != 0xFF)
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var coil = new Coil
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[4],
|
||||||
|
LowByte = requestBytes[5],
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
|
||||||
|
ushort address = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var register = new HoldingRegister
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[4],
|
||||||
|
LowByte = requestBytes[5]
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 9)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
int byteCount = (int)Math.Ceiling(count / 8.0);
|
||||||
|
if (requestBytes.Length < 9 + byteCount)
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int baseOffset = 7;
|
||||||
|
var coils = new List<Coil>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
int bytePosition = i / 8;
|
||||||
|
int bitPosition = i % 8;
|
||||||
|
|
||||||
|
ushort address = (ushort)(firstAddress + i);
|
||||||
|
bool value = (requestBytes[baseOffset + bytePosition] & (1 << bitPosition)) > 0;
|
||||||
|
|
||||||
|
coils.Add(new Coil
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = value ? (byte)0xFF : (byte)0x00
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 9)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(2));
|
||||||
|
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
|
int byteCount = count * 2;
|
||||||
|
if (requestBytes.Length < 9 + byteCount)
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int baseOffset = 7;
|
||||||
|
var list = new List<HoldingRegister>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
ushort address = (ushort)(firstAddress + i);
|
||||||
|
|
||||||
|
list.Add(new HoldingRegister
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[baseOffset + i * 2],
|
||||||
|
LowByte = requestBytes[baseOffset + i * 2 + 1]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(2).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
if (requestBytes[2] != 0x0E)
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[4];
|
||||||
|
if (0x06 < requestBytes[4] && requestBytes[4] < 0x80)
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var category = (ModbusDeviceIdentificationCategory)requestBytes[3];
|
||||||
|
if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[0], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
|
var bodyBytes = new List<byte>();
|
||||||
|
|
||||||
|
// MEI, Category
|
||||||
|
bodyBytes.AddRange(requestBytes.Skip(2).Take(2));
|
||||||
|
|
||||||
|
// Conformity
|
||||||
|
bodyBytes.Add((byte)category);
|
||||||
|
if (deviceInfo.IsIndividualAccessAllowed)
|
||||||
|
bodyBytes[2] |= 0x80;
|
||||||
|
|
||||||
|
// More, NextId, NumberOfObjects
|
||||||
|
bodyBytes.AddRange(new byte[3]);
|
||||||
|
|
||||||
|
int maxObjectId = category switch
|
||||||
|
{
|
||||||
|
ModbusDeviceIdentificationCategory.Basic => 0x02,
|
||||||
|
ModbusDeviceIdentificationCategory.Regular => 0x06,
|
||||||
|
ModbusDeviceIdentificationCategory.Extended => 0xFF,
|
||||||
|
// Individual
|
||||||
|
_ => requestBytes[4],
|
||||||
|
};
|
||||||
|
|
||||||
|
byte numberOfObjects = 0;
|
||||||
|
for (int i = requestBytes[4]; i <= maxObjectId; i++)
|
||||||
|
{
|
||||||
|
// Reserved
|
||||||
|
if (0x07 <= i && i <= 0x7F)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] objBytes = GetDeviceObject((byte)i, deviceInfo);
|
||||||
|
|
||||||
|
// 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 ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes.AddRange(objBytes);
|
||||||
|
numberOfObjects++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes[5] = numberOfObjects;
|
||||||
|
responseBytes.AddRange(bodyBytes);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[1] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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 ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ProductCode:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.MajorMinorRevision:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.VendorUrl:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ProductName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ModelName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.UserApplicationName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
if (deviceIdentification.ExtendedObjects.TryGetValue(objectId, out byte[] bytes))
|
||||||
|
{
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Add(0x00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. result];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ReturnResponse(List<byte> response)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
AMWD.Protocols.Modbus.Serial/ModbusRtuServer.cs
Normal file
94
AMWD.Protocols.Modbus.Serial/ModbusRtuServer.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using AMWD.Protocols.Modbus.Common;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Events;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Utils;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Serial
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements a Modbus serial line RTU server proxying all requests to a virtual Modbus client.
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
public class ModbusRtuServer : ModbusRtuProxy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ModbusRtuServer"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="portName">The name of the serial port to use.</param>
|
||||||
|
public ModbusRtuServer(string portName)
|
||||||
|
: base(new VirtualModbusClient(), portName)
|
||||||
|
{
|
||||||
|
TypedClient.CoilWritten += (sender, e) => CoilWritten?.Invoke(this, e);
|
||||||
|
TypedClient.RegisterWritten += (sender, e) => RegisterWritten?.Invoke(this, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 VirtualModbusClient TypedClient
|
||||||
|
=> Client as VirtualModbusClient;
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
|
||||||
|
#region Device Handling
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.AddDevice(byte)"/>
|
||||||
|
public bool AddDevice(byte unitId)
|
||||||
|
=> TypedClient.AddDevice(unitId);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.RemoveDevice(byte)"/>
|
||||||
|
public bool RemoveDevice(byte unitId)
|
||||||
|
=> TypedClient.RemoveDevice(unitId);
|
||||||
|
|
||||||
|
#endregion Device Handling
|
||||||
|
|
||||||
|
#region Entity Handling
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetCoil(byte, ushort)"/>
|
||||||
|
public Coil GetCoil(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetCoil(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetCoil(byte, Coil)"/>
|
||||||
|
public void SetCoil(byte unitId, Coil coil)
|
||||||
|
=> TypedClient.SetCoil(unitId, coil);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetDiscreteInput(byte, ushort)"/>
|
||||||
|
public DiscreteInput GetDiscreteInput(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetDiscreteInput(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetDiscreteInput(byte, DiscreteInput)"/>
|
||||||
|
public void SetDiscreteInput(byte unitId, DiscreteInput discreteInput)
|
||||||
|
=> TypedClient.SetDiscreteInput(unitId, discreteInput);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetHoldingRegister(byte, ushort)"/>
|
||||||
|
public HoldingRegister GetHoldingRegister(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetHoldingRegister(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetHoldingRegister(byte, HoldingRegister)"/>
|
||||||
|
public void SetHoldingRegister(byte unitId, HoldingRegister holdingRegister)
|
||||||
|
=> TypedClient.SetHoldingRegister(unitId, holdingRegister);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.GetInputRegister(byte, ushort)"/>
|
||||||
|
public InputRegister GetInputRegister(byte unitId, ushort address)
|
||||||
|
=> TypedClient.GetInputRegister(unitId, address);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VirtualModbusClient.SetInputRegister(byte, InputRegister)"/>
|
||||||
|
public void SetInputRegister(byte unitId, InputRegister inputRegister)
|
||||||
|
=> TypedClient.SetInputRegister(unitId, inputRegister);
|
||||||
|
|
||||||
|
#endregion Entity Handling
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO.Ports;
|
using System.IO.Ports;
|
||||||
|
using System.Text;
|
||||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="portName">The name of the serial port to use.</param>
|
/// <param name="portName">The name of the serial port to use.</param>
|
||||||
public ModbusSerialClient(string portName)
|
public ModbusSerialClient(string portName)
|
||||||
: this(new ModbusSerialConnection { PortName = portName })
|
: this(new ModbusSerialConnection(portName))
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -40,11 +41,8 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
Protocol = new RtuProtocol();
|
Protocol = new RtuProtocol();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.GetPortNames" />
|
/// <inheritdoc cref="ModbusSerialConnection.AvailablePortNames" />
|
||||||
public static string[] AvailablePortNames => SerialPort.GetPortNames();
|
public static string[] AvailablePortNames => ModbusSerialConnection.AvailablePortNames;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IModbusProtocol Protocol { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
||||||
public TimeSpan IdleTimeout
|
public TimeSpan IdleTimeout
|
||||||
@@ -226,5 +224,22 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
serialConnection.StopBits = value;
|
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 Task _processingTask;
|
||||||
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
||||||
|
|
||||||
// Only required to cover all logic branches on unit tests.
|
private readonly bool _isLinux;
|
||||||
private bool _isUnitTest = false;
|
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ModbusSerialConnection"/> class.
|
/// Initializes a new instance of the <see cref="ModbusSerialConnection"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ModbusSerialConnection()
|
public ModbusSerialConnection(string portName)
|
||||||
{
|
{
|
||||||
|
_isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(portName))
|
||||||
|
throw new ArgumentNullException(nameof(portName));
|
||||||
|
|
||||||
_serialPort = new SerialPortWrapper
|
_serialPort = new SerialPortWrapper
|
||||||
{
|
{
|
||||||
|
PortName = portName,
|
||||||
|
|
||||||
BaudRate = (int)BaudRate.Baud19200,
|
BaudRate = (int)BaudRate.Baud19200,
|
||||||
DataBits = 8,
|
DataBits = 8,
|
||||||
Handshake = Handshake.None,
|
Handshake = Handshake.None,
|
||||||
@@ -59,6 +65,9 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.GetPortNames" />
|
||||||
|
public static string[] AvailablePortNames => SerialPort.GetPortNames();
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string Name => "Serial";
|
public string Name => "Serial";
|
||||||
|
|
||||||
@@ -68,20 +77,6 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual TimeSpan ConnectTimeout { get; set; } = TimeSpan.MaxValue;
|
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>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the RS485 driver has to be enabled via software switch.
|
/// Gets or sets a value indicating whether the RS485 driver has to be enabled via software switch.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -107,9 +102,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
set => _serialPort.PortName = value;
|
set => _serialPort.PortName = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc cref="SerialPort.BaudRate" />
|
||||||
/// Gets or sets the serial baud rate.
|
|
||||||
/// </summary>
|
|
||||||
public virtual BaudRate BaudRate
|
public virtual BaudRate BaudRate
|
||||||
{
|
{
|
||||||
get => (BaudRate)_serialPort.BaudRate;
|
get => (BaudRate)_serialPort.BaudRate;
|
||||||
@@ -118,7 +111,11 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.DataBits" />
|
/// <inheritdoc cref="SerialPort.DataBits" />
|
||||||
/// <remarks>
|
/// <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>
|
/// </remarks>
|
||||||
public virtual int DataBits
|
public virtual int DataBits
|
||||||
{
|
{
|
||||||
@@ -159,9 +156,9 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// From the Specs:
|
/// From the Specs:
|
||||||
/// <br/>
|
/// <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/>
|
/// <br/>
|
||||||
/// should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
|
/// Should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public virtual StopBits StopBits
|
public virtual StopBits StopBits
|
||||||
{
|
{
|
||||||
@@ -169,6 +166,20 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
set => _serialPort.StopBits = value;
|
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 SerialPort Properties
|
||||||
|
|
||||||
#endregion Properties
|
#endregion Properties
|
||||||
@@ -188,7 +199,6 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_processingTask.Wait();
|
|
||||||
_processingTask.Dispose();
|
_processingTask.Dispose();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -259,7 +269,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get next request to process
|
// Get next request to process
|
||||||
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(false);
|
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
// Remove registration => already removed from queue
|
// Remove registration => already removed from queue
|
||||||
item.CancellationTokenRegistration.Dispose();
|
item.CancellationTokenRegistration.Dispose();
|
||||||
@@ -267,13 +277,13 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
// Build combined cancellation token
|
// Build combined cancellation token
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
||||||
// Wait for exclusive access
|
// Wait for exclusive access
|
||||||
await _portLock.WaitAsync(linkedCts.Token).ConfigureAwait(false);
|
await _portLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Ensure connection is up
|
// Ensure connection is up
|
||||||
await AssertConnection(linkedCts.Token).ConfigureAwait(false);
|
await AssertConnection(linkedCts.Token);
|
||||||
|
|
||||||
await _serialPort.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(false);
|
await _serialPort.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
linkedCts.Token.ThrowIfCancellationRequested();
|
linkedCts.Token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
@@ -282,7 +292,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(false);
|
int readCount = await _serialPort.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
if (readCount < 1)
|
if (readCount < 1)
|
||||||
throw new EndOfStreamException();
|
throw new EndOfStreamException();
|
||||||
|
|
||||||
@@ -313,7 +323,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
_portLock.Release();
|
_portLock.Release();
|
||||||
_idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan);
|
_idleTimer.Change(IdleTimeout, Timeout.InfiniteTimeSpan);
|
||||||
|
|
||||||
await Task.Delay(InterRequestDelay, cancellationToken).ConfigureAwait(false);
|
await Task.Delay(InterRequestDelay, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
@@ -344,7 +354,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
_serialPort.Close();
|
_serialPort.Close();
|
||||||
_serialPort.ResetRS485DriverStateFlags();
|
_serialPort.ResetRS485DriverStateFlags();
|
||||||
|
|
||||||
if (DriverEnabledRS485 && (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || _isUnitTest))
|
if (DriverEnabledRS485 && _isLinux)
|
||||||
{
|
{
|
||||||
var flags = _serialPort.GetRS485DriverStateFlags();
|
var flags = _serialPort.GetRS485DriverStateFlags();
|
||||||
flags |= RS485Flags.Enabled;
|
flags |= RS485Flags.Enabled;
|
||||||
@@ -352,7 +362,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
_serialPort.ChangeRS485DriverStateFlags(flags);
|
_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)
|
if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask)
|
||||||
{
|
{
|
||||||
await connectTask;
|
await connectTask;
|
||||||
@@ -370,7 +380,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(false);
|
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{ /* keep it quiet */ }
|
{ /* keep it quiet */ }
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,19 @@ float voltage = registers.GetSingle();
|
|||||||
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to use the `ASCII` protocol instead, you can do this on initialization:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// [...]
|
||||||
|
|
||||||
|
using var client = new ModbusSerialClient(serialPort)
|
||||||
|
{
|
||||||
|
Protocol = new AsciiProtocol();
|
||||||
|
};
|
||||||
|
|
||||||
|
// [...]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
@@ -31,10 +44,10 @@ Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltag
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
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.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
|
||||||
[v1.02]: https://modbus.org/docs/Modbus_over_serial_line_V1_02.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
|
#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
|
#region Properties
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.Handshake"/>
|
/// <inheritdoc cref="SerialPort.Handshake"/>
|
||||||
@@ -82,6 +106,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
set => _serialPort.Parity = value;
|
set => _serialPort.Parity = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.BytesToWrite"/>
|
||||||
|
public virtual int BytesToWrite
|
||||||
|
=> _serialPort.BytesToWrite;
|
||||||
|
|
||||||
/// <inheritdoc cref="SerialPort.BaudRate"/>
|
/// <inheritdoc cref="SerialPort.BaudRate"/>
|
||||||
public virtual int BaudRate
|
public virtual int BaudRate
|
||||||
{
|
{
|
||||||
@@ -89,6 +117,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
set => _serialPort.BaudRate = value;
|
set => _serialPort.BaudRate = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SerialPort.BytesToRead"/>
|
||||||
|
public virtual int BytesToRead
|
||||||
|
=> _serialPort.BytesToRead;
|
||||||
|
|
||||||
#endregion Properties
|
#endregion Properties
|
||||||
|
|
||||||
#region Methods
|
#region Methods
|
||||||
@@ -101,6 +133,14 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
public virtual void Open()
|
public virtual void Open()
|
||||||
=> _serialPort.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"/>
|
/// <inheritdoc cref="SerialPort.Dispose"/>
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
=> _serialPort.Dispose();
|
=> _serialPort.Dispose();
|
||||||
@@ -117,7 +157,7 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// There seems to be a bug with the async stream implementation on Windows.
|
/// There seems to be a bug with the async stream implementation on Windows.
|
||||||
/// <br/>
|
/// <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>
|
/// </remarks>
|
||||||
/// <param name="buffer">The buffer to write the data into.</param>
|
/// <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>
|
/// <param name="offset">The byte offset in buffer at which to begin writing data from the serial port.</param>
|
||||||
@@ -145,7 +185,7 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await _serialPort.BaseStream.ReadAsync(buffer, offset, count, cts.Token).ConfigureAwait(false);
|
return await _serialPort.BaseStream.ReadAsync(buffer, offset, count, cts.Token);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -195,9 +235,9 @@ namespace AMWD.Protocols.Modbus.Serial.Utils
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
await _serialPort.BaseStream.WriteAsync(buffer, cts.Token).ConfigureAwait(false);
|
await _serialPort.BaseStream.WriteAsync(buffer, cts.Token);
|
||||||
#else
|
#else
|
||||||
await _serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length, cts.Token).ConfigureAwait(false);
|
await _serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length, cts.Token);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||||
<LangVersion>12.0</LangVersion>
|
|
||||||
|
|
||||||
<PackageId>AMWD.Protocols.Modbus.Tcp</PackageId>
|
<PackageId>AMWD.Protocols.Modbus.Tcp</PackageId>
|
||||||
<AssemblyName>amwd-modbus-tcp</AssemblyName>
|
<AssemblyName>amwd-modbus-tcp</AssemblyName>
|
||||||
@@ -14,11 +13,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/InternalsVisibleTo.cs" Link="InternalsVisibleTo.cs" />
|
<Compile Include="$(SolutionDir)/AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
|
||||||
<Compile Include="../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="../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="../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" />
|
||||||
<Compile Include="../AMWD.Protocols.Modbus.Common/Utils/RequestQueueItem.cs" Link="Utils/RequestQueueItem.cs" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -26,7 +24,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Protocols.Modbus.Tcp.Utils;
|
||||||
|
|
||||||
namespace System.IO
|
namespace System.IO
|
||||||
{
|
{
|
||||||
@@ -11,7 +12,25 @@ namespace System.IO
|
|||||||
int offset = 0;
|
int offset = 0;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken).ConfigureAwait(false);
|
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)
|
if (count < 1)
|
||||||
throw new EndOfStreamException();
|
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;
|
||||||
|
using System.Text;
|
||||||
using AMWD.Protocols.Modbus.Common.Contracts;
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
using AMWD.Protocols.Modbus.Common.Protocols;
|
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||||
|
|
||||||
@@ -40,9 +41,6 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
Protocol = new TcpProtocol();
|
Protocol = new TcpProtocol();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IModbusProtocol Protocol { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
/// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
|
||||||
public TimeSpan IdleTimeout
|
public TimeSpan IdleTimeout
|
||||||
{
|
{
|
||||||
@@ -104,5 +102,16 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
tcpConnection.Port = value;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,17 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
private bool _isDisposed;
|
private bool _isDisposed;
|
||||||
private readonly CancellationTokenSource _disposeCts = new();
|
private readonly CancellationTokenSource _disposeCts = new();
|
||||||
|
|
||||||
|
private readonly TcpClientWrapperFactory _tcpClientFactory = new();
|
||||||
private readonly SemaphoreSlim _clientLock = new(1, 1);
|
private readonly SemaphoreSlim _clientLock = new(1, 1);
|
||||||
private readonly TcpClientWrapper _tcpClient = new();
|
private TcpClientWrapper _tcpClient = null;
|
||||||
private readonly Timer _idleTimer;
|
private readonly Timer _idleTimer;
|
||||||
|
|
||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
||||||
|
|
||||||
|
private TimeSpan _readTimeout = TimeSpan.FromSeconds(1);
|
||||||
|
private TimeSpan _writeTimeout = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -58,15 +62,41 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual TimeSpan ReadTimeout
|
public virtual TimeSpan ReadTimeout
|
||||||
{
|
{
|
||||||
get => TimeSpan.FromMilliseconds(_tcpClient.ReceiveTimeout);
|
get => _readTimeout;
|
||||||
set => _tcpClient.ReceiveTimeout = (int)value.TotalMilliseconds;
|
set
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
|
||||||
|
#else
|
||||||
|
if (value < TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
_readTimeout = value;
|
||||||
|
|
||||||
|
if (_tcpClient != null)
|
||||||
|
_tcpClient.ReceiveTimeout = (int)value.TotalMilliseconds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual TimeSpan WriteTimeout
|
public virtual TimeSpan WriteTimeout
|
||||||
{
|
{
|
||||||
get => TimeSpan.FromMilliseconds(_tcpClient.SendTimeout);
|
get => _writeTimeout;
|
||||||
set => _tcpClient.SendTimeout = (int)value.TotalMilliseconds;
|
set
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
|
||||||
|
#else
|
||||||
|
if (value < TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
_writeTimeout = value;
|
||||||
|
|
||||||
|
if (_tcpClient != null)
|
||||||
|
_tcpClient.SendTimeout = (int)value.TotalMilliseconds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -116,7 +146,6 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_processingTask.Wait();
|
|
||||||
_processingTask.Dispose();
|
_processingTask.Dispose();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -124,7 +153,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
OnIdleTimer(null);
|
OnIdleTimer(null);
|
||||||
|
|
||||||
_tcpClient.Dispose();
|
_tcpClient?.Dispose();
|
||||||
_clientLock.Dispose();
|
_clientLock.Dispose();
|
||||||
|
|
||||||
while (_requestQueue.TryDequeue(out var item))
|
while (_requestQueue.TryDequeue(out var item))
|
||||||
@@ -164,7 +193,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
{
|
{
|
||||||
Request = [.. request],
|
Request = [.. request],
|
||||||
ValidateResponseComplete = validateResponseComplete,
|
ValidateResponseComplete = validateResponseComplete,
|
||||||
TaskCompletionSource = new(),
|
TaskCompletionSource = new TaskCompletionSource<IReadOnlyList<byte>>(),
|
||||||
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)
|
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -187,7 +216,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get next request to process
|
// Get next request to process
|
||||||
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(false);
|
var item = await _requestQueue.DequeueAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
// Remove registration => already removed from queue
|
// Remove registration => already removed from queue
|
||||||
item.CancellationTokenRegistration.Dispose();
|
item.CancellationTokenRegistration.Dispose();
|
||||||
@@ -195,19 +224,19 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
// Build combined cancellation token
|
// Build combined cancellation token
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, item.CancellationTokenSource.Token);
|
||||||
// Wait for exclusive access
|
// Wait for exclusive access
|
||||||
await _clientLock.WaitAsync(linkedCts.Token).ConfigureAwait(false);
|
await _clientLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Ensure connection is up
|
// Ensure connection is up
|
||||||
await AssertConnection(linkedCts.Token).ConfigureAwait(false);
|
await AssertConnection(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
var stream = _tcpClient.GetStream();
|
var stream = _tcpClient.GetStream();
|
||||||
await stream.FlushAsync(linkedCts.Token).ConfigureAwait(false);
|
await stream.FlushAsync(linkedCts.Token);
|
||||||
|
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
await stream.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(false);
|
await stream.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
#else
|
#else
|
||||||
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token).ConfigureAwait(false);
|
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
linkedCts.Token.ThrowIfCancellationRequested();
|
linkedCts.Token.ThrowIfCancellationRequested();
|
||||||
@@ -218,9 +247,9 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
int readCount = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(false);
|
int readCount = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
#else
|
#else
|
||||||
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(false);
|
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
#endif
|
#endif
|
||||||
if (readCount < 1)
|
if (readCount < 1)
|
||||||
throw new EndOfStreamException();
|
throw new EndOfStreamException();
|
||||||
@@ -267,7 +296,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
// Has to be called within _clientLock!
|
// Has to be called within _clientLock!
|
||||||
private async Task AssertConnection(CancellationToken cancellationToken)
|
private async Task AssertConnection(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_tcpClient.Connected)
|
if (_tcpClient?.Connected == true)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int delay = 1;
|
int delay = 1;
|
||||||
@@ -284,14 +313,16 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
{
|
{
|
||||||
foreach (var ipAddress in ipAddresses)
|
foreach (var ipAddress in ipAddresses)
|
||||||
{
|
{
|
||||||
_tcpClient.Close();
|
_tcpClient?.Close();
|
||||||
|
_tcpClient?.Dispose();
|
||||||
|
|
||||||
|
_tcpClient = _tcpClientFactory.Create(ipAddress.AddressFamily, _readTimeout, _writeTimeout);
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
using var connectTask = _tcpClient.ConnectAsync(ipAddress, Port, cancellationToken);
|
var connectTask = _tcpClient.ConnectAsync(ipAddress, Port, cancellationToken);
|
||||||
#else
|
#else
|
||||||
using var connectTask = _tcpClient.ConnectAsync(ipAddress, Port);
|
var connectTask = _tcpClient.ConnectAsync(ipAddress, Port);
|
||||||
#endif
|
#endif
|
||||||
if (await Task.WhenAny(connectTask, Task.Delay(ReadTimeout, cancellationToken)) == connectTask)
|
if (await Task.WhenAny(connectTask, Task.Delay(_readTimeout, cancellationToken)) == connectTask)
|
||||||
{
|
{
|
||||||
await connectTask;
|
await connectTask;
|
||||||
if (_tcpClient.Connected)
|
if (_tcpClient.Connected)
|
||||||
@@ -309,7 +340,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(false);
|
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{ /* keep it quiet */ }
|
{ /* keep it quiet */ }
|
||||||
@@ -353,10 +384,9 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Dns.GetHostAddresses(hostname)
|
return [.. Dns.GetHostAddresses(hostname)
|
||||||
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
|
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
.OrderBy(a => a.AddressFamily) // prefer IPv4
|
.OrderBy(a => a.AddressFamily)]; // prefer IPv4
|
||||||
.ToArray();
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
866
AMWD.Protocols.Modbus.Tcp/ModbusTcpProxy.cs
Normal file
866
AMWD.Protocols.Modbus.Tcp/ModbusTcpProxy.cs
Normal file
@@ -0,0 +1,866 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
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.Tcp
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements a Modbus TCP server proxying all requests to a Modbus client of choice.
|
||||||
|
/// </summary>
|
||||||
|
/// <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 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<TcpClientWrapper> _clients = [];
|
||||||
|
|
||||||
|
#endregion Fields
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
|
||||||
|
#endregion Constructors
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Modbus client used to request the remote device, that should be proxied.
|
||||||
|
/// </summary>
|
||||||
|
public ModbusClientBase Client { get; } = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IPAddress"/> to listen on.
|
||||||
|
/// </summary>
|
||||||
|
public IPAddress ListenAddress
|
||||||
|
{
|
||||||
|
get => _tcpListener.LocalIPEndPoint.Address;
|
||||||
|
set => _tcpListener.LocalIPEndPoint.Address = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the port to listen on.
|
||||||
|
/// </summary>
|
||||||
|
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 => _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 => _readWriteTimeout;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value != Timeout.InfiniteTimeSpan && value < TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
|
||||||
|
_readWriteTimeout = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
|
||||||
|
#region Control Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Assertions();
|
||||||
|
|
||||||
|
_stopCts?.Cancel();
|
||||||
|
_tcpListener.Stop();
|
||||||
|
|
||||||
|
_stopCts?.Dispose();
|
||||||
|
_stopCts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
_tcpListener.Socket.DualMode = true;
|
||||||
|
|
||||||
|
_tcpListener.Start();
|
||||||
|
_clientConnectTask = WaitForClientAsync(_stopCts.Token);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the server.
|
||||||
|
/// </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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_stopCts?.Cancel();
|
||||||
|
_tcpListener.Stop();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Terminated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases all managed and unmanaged resources used by the <see cref="ModbusTcpProxy"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
StopAsyncInternal(CancellationToken.None).Wait();
|
||||||
|
|
||||||
|
_clientListLock.Dispose();
|
||||||
|
_clients.Clear();
|
||||||
|
_tcpListener.Dispose();
|
||||||
|
|
||||||
|
_stopCts?.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Assertions()
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
ObjectDisposedException.ThrowIf(_isDisposed, this);
|
||||||
|
#else
|
||||||
|
if (_isDisposed)
|
||||||
|
throw new ObjectDisposedException(GetType().FullName);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Control Methods
|
||||||
|
|
||||||
|
#region Client Handling
|
||||||
|
|
||||||
|
private async Task WaitForClientAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_clients.Add(client);
|
||||||
|
// Can be ignored as it will terminate by itself on cancellation
|
||||||
|
HandleClientAsync(client, cancellationToken).Forget();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_clientListLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// There might be a failure here, that's ok, just keep it quiet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleClientAsync(TcpClientWrapper client, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var stream = client.GetStream();
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
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[] bodyBytes = await stream.ReadExpectedBytesAsync(length, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
requestBytes.AddRange(bodyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
if (responseBytes != null)
|
||||||
|
{
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
// Keep client processing quiet
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_clients.Remove(client);
|
||||||
|
client.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_clientListLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Client Handling
|
||||||
|
|
||||||
|
#region Request Handling
|
||||||
|
|
||||||
|
private Task<byte[]> HandleRequestAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
switch ((ModbusFunctionCode)requestBytes[7])
|
||||||
|
{
|
||||||
|
case ModbusFunctionCode.ReadCoils:
|
||||||
|
return HandleReadCoilsAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadDiscreteInputs:
|
||||||
|
return HandleReadDiscreteInputsAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadHoldingRegisters:
|
||||||
|
return HandleReadHoldingRegistersAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.ReadInputRegisters:
|
||||||
|
return HandleReadInputRegistersAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteSingleCoil:
|
||||||
|
return HandleWriteSingleCoilAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteSingleRegister:
|
||||||
|
return HandleWriteSingleRegisterAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteMultipleCoils:
|
||||||
|
return HandleWriteMultipleCoilsAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.WriteMultipleRegisters:
|
||||||
|
return HandleWriteMultipleRegistersAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
case ModbusFunctionCode.EncapsulatedInterface:
|
||||||
|
return HandleEncapsulatedInterfaceAsync(requestBytes, cancellationToken);
|
||||||
|
|
||||||
|
default: // unknown function
|
||||||
|
{
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||||
|
|
||||||
|
// Mark as error
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
|
||||||
|
return Task.FromResult(ReturnResponse(responseBytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[6];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
if (coils[i].Value)
|
||||||
|
{
|
||||||
|
int byteIndex = i / 8;
|
||||||
|
int bitIndex = i % 8;
|
||||||
|
|
||||||
|
values[byteIndex] |= (byte)(1 << bitIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[6];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
if (discreteInputs[i].Value)
|
||||||
|
{
|
||||||
|
int byteIndex = i / 8;
|
||||||
|
int bitIndex = i % 8;
|
||||||
|
|
||||||
|
values[byteIndex] |= (byte)(1 << bitIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[6];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
values[i * 2] = holdingRegisters[i].HighByte;
|
||||||
|
values[i * 2 + 1] = holdingRegisters[i].LowByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
byte unitId = requestBytes[6];
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
values[i * 2] = inputRegisters[i].HighByte;
|
||||||
|
values[i * 2 + 1] = inputRegisters[i].LowByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBytes.Add((byte)values.Length);
|
||||||
|
responseBytes.AddRange(values);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
|
||||||
|
ushort address = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
|
||||||
|
if (requestBytes[10] != 0x00 && requestBytes[10] != 0xFF)
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var coil = new Coil
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[10],
|
||||||
|
LowByte = requestBytes[11],
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(8).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 12)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
|
||||||
|
ushort address = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var register = new HoldingRegister
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[10],
|
||||||
|
LowByte = requestBytes[11]
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(8).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 13)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
int byteCount = (int)Math.Ceiling(count / 8.0);
|
||||||
|
if (requestBytes.Length < 13 + byteCount)
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int baseOffset = 13;
|
||||||
|
var coils = new List<Coil>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
int bytePosition = i / 8;
|
||||||
|
int bitPosition = i % 8;
|
||||||
|
|
||||||
|
ushort address = (ushort)(firstAddress + i);
|
||||||
|
bool value = (requestBytes[baseOffset + bytePosition] & (1 << bitPosition)) > 0;
|
||||||
|
|
||||||
|
coils.Add(new Coil
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = value ? (byte)0xFF : (byte)0x00
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(8).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (requestBytes.Length < 13)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var responseBytes = new List<byte>();
|
||||||
|
responseBytes.AddRange(requestBytes.Take(8));
|
||||||
|
|
||||||
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
|
int byteCount = count * 2;
|
||||||
|
if (requestBytes.Length < 13 + byteCount)
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int baseOffset = 13;
|
||||||
|
var list = new List<HoldingRegister>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
ushort address = (ushort)(firstAddress + i);
|
||||||
|
|
||||||
|
list.Add(new HoldingRegister
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
HighByte = requestBytes[baseOffset + i * 2],
|
||||||
|
LowByte = requestBytes[baseOffset + i * 2 + 1]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
// Response is an echo of the request
|
||||||
|
responseBytes.AddRange(requestBytes.Skip(8).Take(4));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
if (requestBytes[8] != 0x0E)
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[10];
|
||||||
|
if (0x06 < requestBytes[10] && requestBytes[10] < 0x80)
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var category = (ModbusDeviceIdentificationCategory)requestBytes[9];
|
||||||
|
if (!Enum.IsDefined(typeof(ModbusDeviceIdentificationCategory), category))
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
|
||||||
|
var bodyBytes = new List<byte>();
|
||||||
|
|
||||||
|
// MEI, Category
|
||||||
|
bodyBytes.AddRange(requestBytes.Skip(8).Take(2));
|
||||||
|
|
||||||
|
// Conformity
|
||||||
|
bodyBytes.Add((byte)category);
|
||||||
|
if (deviceInfo.IsIndividualAccessAllowed)
|
||||||
|
bodyBytes[2] |= 0x80;
|
||||||
|
|
||||||
|
// More, NextId, NumberOfObjects
|
||||||
|
bodyBytes.AddRange(new byte[3]);
|
||||||
|
|
||||||
|
int maxObjectId = category switch
|
||||||
|
{
|
||||||
|
ModbusDeviceIdentificationCategory.Basic => 0x02,
|
||||||
|
ModbusDeviceIdentificationCategory.Regular => 0x06,
|
||||||
|
ModbusDeviceIdentificationCategory.Extended => 0xFF,
|
||||||
|
// Individual
|
||||||
|
_ => requestBytes[10],
|
||||||
|
};
|
||||||
|
|
||||||
|
byte numberOfObjects = 0;
|
||||||
|
for (int i = requestBytes[10]; i <= maxObjectId; i++)
|
||||||
|
{
|
||||||
|
// Reserved
|
||||||
|
if (0x07 <= i && i <= 0x7F)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
bodyBytes[3] = 0xFF;
|
||||||
|
bodyBytes[4] = (byte)i;
|
||||||
|
|
||||||
|
bodyBytes[5] = numberOfObjects;
|
||||||
|
responseBytes.AddRange(bodyBytes);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes.AddRange(objBytes);
|
||||||
|
numberOfObjects++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes[5] = numberOfObjects;
|
||||||
|
responseBytes.AddRange(bodyBytes);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
responseBytes[7] |= 0x80;
|
||||||
|
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
|
||||||
|
|
||||||
|
return ReturnResponse(responseBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ProductCode:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.MajorMinorRevision:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.VendorUrl:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ProductName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.ModelName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ModbusDeviceIdentificationObject.UserApplicationName:
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? "");
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
if (deviceIdentification.ExtendedObjects.TryGetValue(objectId, out byte[] bytes))
|
||||||
|
{
|
||||||
|
result.Add((byte)bytes.Length);
|
||||||
|
result.AddRange(bytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Add(0x00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,6 +23,18 @@ float voltage = registers.GetSingle();
|
|||||||
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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 RtuProtocol()
|
||||||
|
};
|
||||||
|
|
||||||
|
// [...]
|
||||||
|
```
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
@@ -32,10 +44,10 @@ Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltag
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
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.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
|
||||||
[v1.0b]: https://modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.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" />
|
/// <inheritdoc cref="NetworkStream" />
|
||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
internal class NetworkStreamWrapper : IDisposable
|
internal class NetworkStreamWrapper(NetworkStream stream) : IDisposable
|
||||||
{
|
{
|
||||||
private readonly NetworkStream _stream;
|
private readonly NetworkStream _stream = stream;
|
||||||
|
|
||||||
[Obsolete("Constructor only for mocking on UnitTests!", error: true)]
|
|
||||||
public NetworkStreamWrapper()
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public NetworkStreamWrapper(NetworkStream stream)
|
|
||||||
{
|
|
||||||
_stream = stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="NetworkStream.Dispose" />
|
/// <inheritdoc cref="NetworkStream.Dispose" />
|
||||||
public virtual void 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,6 +3,7 @@ using System.Net;
|
|||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Transactions;
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||||
{
|
{
|
||||||
@@ -12,10 +13,20 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
|
|||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
private readonly TcpClient _client = new();
|
private readonly TcpClient _client;
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
|
public TcpClientWrapper(AddressFamily addressFamily)
|
||||||
|
{
|
||||||
|
_client = new TcpClient(addressFamily);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TcpClientWrapper(TcpClient client)
|
||||||
|
{
|
||||||
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
|
|
||||||
/// <inheritdoc cref="TcpClient.Connected" />
|
/// <inheritdoc cref="TcpClient.Connected" />
|
||||||
|
|||||||
29
AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapperFactory.cs
Normal file
29
AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapperFactory.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="TcpClientWrapper"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="addressFamily">The <see cref="AddressFamily"/> of the <see cref="TcpClient"/> to use.</param>
|
||||||
|
/// <param name="readTimeout">The read timeout.</param>
|
||||||
|
/// <param name="writeTimeout">The write timeout.</param>
|
||||||
|
/// <returns>A new <see cref="TcpClientWrapper"/> instance.</returns>
|
||||||
|
public virtual TcpClientWrapper Create(AddressFamily addressFamily, TimeSpan readTimeout, TimeSpan writeTimeout)
|
||||||
|
{
|
||||||
|
var client = new TcpClientWrapper(addressFamily)
|
||||||
|
{
|
||||||
|
ReceiveTimeout = (int)readTimeout.TotalMilliseconds,
|
||||||
|
SendTimeout = (int)writeTimeout.TotalMilliseconds
|
||||||
|
};
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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,31 +2,31 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<LangVersion>12.0</LangVersion>
|
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<IsTestProject>true</IsTestProject>
|
<IsTestProject>true</IsTestProject>
|
||||||
<CollectCoverage>true</CollectCoverage>
|
<CollectCoverage>true</CollectCoverage>
|
||||||
|
<CoverletOutputFormat>Cobertura</CoverletOutputFormat>
|
||||||
|
|
||||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.msbuild" Version="6.0.1">
|
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="Moq" Version="4.20.70" />
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
|
<PackageReference Include="MSTest.TestAdapter" Version="3.7.2" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
|
<PackageReference Include="MSTest.TestFramework" Version="3.7.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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" />
|
||||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
|
<ProjectReference Include="$(SolutionDir)\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.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -105,16 +105,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowExceptionOnNullConnection()
|
public void ShouldThrowExceptionOnNullConnection()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
IModbusConnection connection = null;
|
IModbusConnection connection = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
new ModbusClientBaseWrapper(connection);
|
Assert.ThrowsException<ArgumentNullException>(() => new ModbusClientBaseWrapper(connection));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
@@ -155,31 +152,25 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Contracts
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ObjectDisposedException))]
|
|
||||||
public async Task ShouldAssertDisposed()
|
public async Task ShouldAssertDisposed()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var client = GetClient();
|
var client = GetClient();
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT);
|
await Assert.ThrowsExceptionAsync<ObjectDisposedException>(() => client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT));
|
||||||
|
|
||||||
// Assert - ObjectDisposedException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public async Task ShouldAssertProtocolSet()
|
public async Task ShouldAssertProtocolSet()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var client = GetClient();
|
var client = GetClient();
|
||||||
client.Protocol = null;
|
client.Protocol = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
await client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT);
|
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => client.ReadCoilsAsync(UNIT_ID, START_ADDRESS, READ_COUNT));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Common/Connection/Assertions
|
#endregion Common/Connection/Assertions
|
||||||
|
|||||||
@@ -41,20 +41,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullOnGetSingle()
|
public void ShouldThrowNullOnGetSingle()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister[] registers = null;
|
HoldingRegister[] registers = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetSingle(0);
|
Assert.ThrowsException<ArgumentNullException>(() => registers.GetSingle(0));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetSingleForLength()
|
public void ShouldThrowArgumentOnGetSingleForLength()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -63,16 +59,13 @@
|
|||||||
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetSingle(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetSingle(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(1)]
|
[DataRow(1)]
|
||||||
[DataRow(-1)]
|
[DataRow(-1)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeOnGetSingle(int startIndex)
|
public void ShouldThrowArgumentOutOfRangeOnGetSingle(int startIndex)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -82,14 +75,11 @@
|
|||||||
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetSingle(startIndex);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetSingle(startIndex));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetSingleForType()
|
public void ShouldThrowArgumentOnGetSingleForType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -99,10 +89,8 @@
|
|||||||
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetSingle(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetSingle(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -145,20 +133,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullOnGetDouble()
|
public void ShouldThrowNullOnGetDouble()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister[] registers = null;
|
HoldingRegister[] registers = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetDouble(0);
|
Assert.ThrowsException<ArgumentNullException>(() => registers.GetDouble(0));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetDoubleForLength()
|
public void ShouldThrowArgumentOnGetDoubleForLength()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -169,16 +153,13 @@
|
|||||||
new() { Address = 102, HighByte = 0x7A, LowByte = 0xE1 }
|
new() { Address = 102, HighByte = 0x7A, LowByte = 0xE1 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetDouble(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetDouble(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(1)]
|
[DataRow(1)]
|
||||||
[DataRow(-1)]
|
[DataRow(-1)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeOnGetDouble(int startIndex)
|
public void ShouldThrowArgumentOutOfRangeOnGetDouble(int startIndex)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -190,14 +171,11 @@
|
|||||||
new() { Address = 103, HighByte = 0x47, LowByte = 0xAE }
|
new() { Address = 103, HighByte = 0x47, LowByte = 0xAE }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetDouble(startIndex);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetDouble(startIndex));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetDoubleForType()
|
public void ShouldThrowArgumentOnGetDoubleForType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -209,10 +187,8 @@
|
|||||||
new InputRegister { Address = 103, HighByte = 0x47, LowByte = 0xAE }
|
new InputRegister { Address = 103, HighByte = 0x47, LowByte = 0xAE }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetDouble(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetDouble(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Modbus to value
|
#endregion Modbus to value
|
||||||
|
|||||||
@@ -30,16 +30,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullOnGetBoolean()
|
public void ShouldThrowNullOnGetBoolean()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
Coil coil = null;
|
Coil coil = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
coil.GetBoolean();
|
Assert.ThrowsException<ArgumentNullException>(() => coil.GetBoolean());
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -95,35 +92,28 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullOnString()
|
public void ShouldThrowNullOnString()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister[] list = null;
|
HoldingRegister[] list = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
list.GetString(2);
|
Assert.ThrowsException<ArgumentNullException>(() => list.GetString(2));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnStringForEmptyList()
|
public void ShouldThrowArgumentOnStringForEmptyList()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var registers = Array.Empty<HoldingRegister>();
|
var registers = Array.Empty<HoldingRegister>();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetString(2);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetString(2));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(1)]
|
[DataRow(1)]
|
||||||
[DataRow(-1)]
|
[DataRow(-1)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeOnString(int startIndex)
|
public void ShouldThrowArgumentOutOfRangeOnString(int startIndex)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -133,14 +123,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
|||||||
new() { Address = 2, HighByte = 67, LowByte = 0 }
|
new() { Address = 2, HighByte = 67, LowByte = 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetString(2, startIndex);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetString(2, startIndex));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnStringForMixedTypes()
|
public void ShouldThrowArgumentOnStringForMixedTypes()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -150,10 +137,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
|||||||
new InputRegister { Address = 2, HighByte = 67, LowByte = 0 }
|
new InputRegister { Address = 2, HighByte = 67, LowByte = 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetString(2);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetString(2));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Modbus to value
|
#endregion Modbus to value
|
||||||
@@ -272,16 +257,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Extensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullOnGetString()
|
public void ShouldThrowNullOnGetString()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
string str = null;
|
string str = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
_ = str.ToRegisters(100).ToArray();
|
Assert.ThrowsException<ArgumentNullException>(() => str.ToRegisters(100).ToArray());
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Value to Modbus
|
#endregion Value to Modbus
|
||||||
|
|||||||
@@ -32,31 +32,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullForGetSByte()
|
public void ShouldThrowNullForGetSByte()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister register = null;
|
HoldingRegister register = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
register.GetSByte();
|
Assert.ThrowsException<ArgumentNullException>(() => register.GetSByte());
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentForGetSByte()
|
public void ShouldThrowArgumentForGetSByte()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var obj = new Coil();
|
var obj = new Coil();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
obj.GetSByte();
|
Assert.ThrowsException<ArgumentException>(() => obj.GetSByte());
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -86,31 +78,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullForGetInt16()
|
public void ShouldThrowNullForGetInt16()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister register = null;
|
HoldingRegister register = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
register.GetInt16();
|
Assert.ThrowsException<ArgumentNullException>(() => register.GetInt16());
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentForGetInt16()
|
public void ShouldThrowArgumentForGetInt16()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var obj = new Coil();
|
var obj = new Coil();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
obj.GetInt16();
|
Assert.ThrowsException<ArgumentException>(() => obj.GetInt16());
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -149,21 +133,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullOnGetInt32()
|
public void ShouldThrowNullOnGetInt32()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister[] registers = null;
|
HoldingRegister[] registers = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetInt32(0);
|
Assert.ThrowsException<ArgumentNullException>(() => registers.GetInt32(0));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetInt32ForLength()
|
public void ShouldThrowArgumentOnGetInt32ForLength()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -172,17 +151,13 @@
|
|||||||
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
new HoldingRegister { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetInt32(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetInt32(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(1)]
|
[DataRow(1)]
|
||||||
[DataRow(-1)]
|
[DataRow(-1)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeOnGetInt32(int startIndex)
|
public void ShouldThrowArgumentOutOfRangeOnGetInt32(int startIndex)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -192,15 +167,11 @@
|
|||||||
new HoldingRegister { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
new HoldingRegister { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetInt32(startIndex);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetInt32(startIndex));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetInt32ForType()
|
public void ShouldThrowArgumentOnGetInt32ForType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -210,11 +181,8 @@
|
|||||||
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetInt32(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetInt32(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -257,21 +225,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullOnGetInt64()
|
public void ShouldThrowNullOnGetInt64()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister[] registers = null;
|
HoldingRegister[] registers = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetInt64(0);
|
Assert.ThrowsException<ArgumentNullException>(() => registers.GetInt64(0));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetInt64ForLength()
|
public void ShouldThrowArgumentOnGetInt64ForLength()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -282,17 +245,13 @@
|
|||||||
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetInt64(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetInt64(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(1)]
|
[DataRow(1)]
|
||||||
[DataRow(-1)]
|
[DataRow(-1)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeOnGetInt64(int startIndex)
|
public void ShouldThrowArgumentOutOfRangeOnGetInt64(int startIndex)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -304,15 +263,11 @@
|
|||||||
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
new HoldingRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetInt64(startIndex);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetInt64(startIndex));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetInt64ForType()
|
public void ShouldThrowArgumentOnGetInt64ForType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -324,11 +279,8 @@
|
|||||||
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetInt64(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetInt64(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Modbus to value
|
#endregion Modbus to value
|
||||||
|
|||||||
@@ -32,29 +32,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullForGetByte()
|
public void ShouldThrowNullForGetByte()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister register = null;
|
HoldingRegister register = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
register.GetByte();
|
Assert.ThrowsException<ArgumentNullException>(() => register.GetByte());
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentForGetByte()
|
public void ShouldThrowArgumentForGetByte()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var obj = new Coil();
|
var obj = new Coil();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
obj.GetByte();
|
Assert.ThrowsException<ArgumentException>(() => obj.GetByte());
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -84,29 +78,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullForGetUInt16()
|
public void ShouldThrowNullForGetUInt16()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister register = null;
|
HoldingRegister register = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
register.GetUInt16();
|
Assert.ThrowsException<ArgumentNullException>(() => register.GetUInt16());
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentForGetUInt16()
|
public void ShouldThrowArgumentForGetUInt16()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var obj = new Coil();
|
var obj = new Coil();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
obj.GetUInt16();
|
Assert.ThrowsException<ArgumentException>(() => obj.GetUInt16());
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -145,21 +133,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullOnGetUInt32()
|
public void ShouldThrowNullOnGetUInt32()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister[] registers = null;
|
HoldingRegister[] registers = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt32(0);
|
Assert.ThrowsException<ArgumentNullException>(() => registers.GetUInt32(0));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetUInt32ForLength()
|
public void ShouldThrowArgumentOnGetUInt32ForLength()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -168,16 +151,13 @@
|
|||||||
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
new() { Address = 101, HighByte = 0x01, LowByte = 0x02 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt32(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetUInt32(1));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(1)]
|
[DataRow(1)]
|
||||||
[DataRow(-1)]
|
[DataRow(-1)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeOnGetUInt32(int startIndex)
|
public void ShouldThrowArgumentOutOfRangeOnGetUInt32(int startIndex)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -187,14 +167,11 @@
|
|||||||
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
new() { Address = 100, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt32(startIndex);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetUInt32(startIndex));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetUInt32ForType()
|
public void ShouldThrowArgumentOnGetUInt32ForType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -204,10 +181,8 @@
|
|||||||
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
new InputRegister { Address = 101, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt32(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetUInt32(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -250,21 +225,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowNullOnGetUInt64()
|
public void ShouldThrowNullOnGetUInt64()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
HoldingRegister[] registers = null;
|
HoldingRegister[] registers = null;
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt64(0);
|
Assert.ThrowsException<ArgumentNullException>(() => registers.GetUInt64(0));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetUInt64ForLength()
|
public void ShouldThrowArgumentOnGetUInt64ForLength()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -275,16 +245,13 @@
|
|||||||
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt64(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetUInt64(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(1)]
|
[DataRow(1)]
|
||||||
[DataRow(-1)]
|
[DataRow(-1)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeOnGetUInt64(int startIndex)
|
public void ShouldThrowArgumentOutOfRangeOnGetUInt64(int startIndex)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -296,14 +263,11 @@
|
|||||||
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
new() { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt64(startIndex);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => registers.GetUInt64(startIndex));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentOnGetUInt64ForType()
|
public void ShouldThrowArgumentOnGetUInt64ForType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -315,10 +279,8 @@
|
|||||||
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
new InputRegister { Address = 103, HighByte = 0x03, LowByte = 0x04 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
registers.GetUInt64(0);
|
Assert.ThrowsException<ArgumentException>(() => registers.GetUInt64(0));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Modbus to value
|
#endregion Modbus to value
|
||||||
|
|||||||
@@ -32,29 +32,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(2001)]
|
[DataRow(2001)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -88,7 +82,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -98,10 +91,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
_ = protocol.DeserializeReadCoils(responseBytes);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadCoils(responseBytes));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Coils
|
#endregion Read Coils
|
||||||
@@ -129,29 +120,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(2001)]
|
[DataRow(2001)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -185,7 +170,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -195,10 +179,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadDiscreteInputs(responseBytes);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDiscreteInputs(responseBytes));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Discrete Inputs
|
#endregion Read Discrete Inputs
|
||||||
@@ -226,29 +208,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(2001)]
|
[DataRow(2001)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -276,7 +252,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -286,10 +261,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadHoldingRegisters(responseBytes);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadHoldingRegisters(responseBytes));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Holding Registers
|
#endregion Read Holding Registers
|
||||||
@@ -317,29 +290,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(2001)]
|
[DataRow(2001)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -367,7 +334,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -377,10 +343,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadInputRegisters(responseBytes);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadInputRegisters(responseBytes));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Input Registers
|
#endregion Read Input Registers
|
||||||
@@ -410,16 +374,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification()
|
public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
@@ -449,7 +410,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -459,12 +419,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadDeviceIdentification(responseBytes);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(responseBytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -474,8 +433,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadDeviceIdentification(responseBytes);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(responseBytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Device Identification
|
#endregion Read Device Identification
|
||||||
@@ -502,16 +461,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteSingleCoil(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleCoil(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -557,16 +513,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -619,22 +572,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(1969)]
|
[DataRow(1969)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -644,14 +593,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -662,14 +608,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -680,10 +623,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -732,22 +673,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(124)]
|
[DataRow(124)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -757,14 +694,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -775,14 +709,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -793,10 +724,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -898,7 +827,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForMissingHeaderOnValidateResponse()
|
public void ShouldThrowForMissingHeaderOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -907,12 +835,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
AddTrailer(ref response);
|
AddTrailer(ref response);
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForMissingTrailerOnValidateResponse()
|
public void ShouldThrowForMissingTrailerOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -920,12 +847,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
string response = $":{UNIT_ID:X2}010100";
|
string response = $":{UNIT_ID:X2}010100";
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForUnitIdOnValidateResponse()
|
public void ShouldThrowForUnitIdOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -934,12 +860,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
AddTrailer(ref response);
|
AddTrailer(ref response);
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForLrcOnValidateResponse()
|
public void ShouldThrowForLrcOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -947,12 +872,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
string response = $":{UNIT_ID:X2}010001FF00XX\r\n";
|
string response = $":{UNIT_ID:X2}010001FF00XX\r\n";
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -961,12 +885,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
AddTrailer(ref response);
|
AddTrailer(ref response);
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForErrorOnValidateResponse()
|
public void ShouldThrowForErrorOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -975,8 +898,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
AddTrailer(ref response);
|
AddTrailer(ref response);
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
@@ -984,7 +907,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataRow(0x02)]
|
[DataRow(0x02)]
|
||||||
[DataRow(0x03)]
|
[DataRow(0x03)]
|
||||||
[DataRow(0x04)]
|
[DataRow(0x04)]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForReadLengthOnValidateResponse(int fn)
|
public void ShouldThrowForReadLengthOnValidateResponse(int fn)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -993,8 +915,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
AddTrailer(ref response);
|
AddTrailer(ref response);
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
@@ -1002,7 +924,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataRow(0x06)]
|
[DataRow(0x06)]
|
||||||
[DataRow(0x0F)]
|
[DataRow(0x0F)]
|
||||||
[DataRow(0x10)]
|
[DataRow(0x10)]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForWriteLengthOnValidateResponse(int fn)
|
public void ShouldThrowForWriteLengthOnValidateResponse(int fn)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1011,8 +932,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
AddTrailer(ref response);
|
AddTrailer(ref response);
|
||||||
var protocol = new AsciiProtocol();
|
var protocol = new AsciiProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response));
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(Encoding.ASCII.GetBytes(request), Encoding.ASCII.GetBytes(response)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -1033,58 +954,46 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataRow("")]
|
[DataRow("")]
|
||||||
[DataRow(" ")]
|
[DataRow(" ")]
|
||||||
[DataRow("\t")]
|
[DataRow("\t")]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullExceptionForMessageOnLrc(string msg)
|
public void ShouldThrowArgumentNullExceptionForMessageOnLrc(string msg)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
AsciiProtocol.LRC(msg);
|
Assert.ThrowsException<ArgumentNullException>(() => AsciiProtocol.LRC(msg));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(-1)]
|
[DataRow(-1)]
|
||||||
[DataRow(4)]
|
[DataRow(4)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeExceptionForStartOnLrc(int start)
|
public void ShouldThrowArgumentOutOfRangeExceptionForStartOnLrc(int start)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
string msg = "0207";
|
string msg = "0207";
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
AsciiProtocol.LRC(msg, start);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => AsciiProtocol.LRC(msg, start));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(5)]
|
[DataRow(5)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeExceptionForLengthOnLrc(int length)
|
public void ShouldThrowArgumentOutOfRangeExceptionForLengthOnLrc(int length)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
string msg = "0207";
|
string msg = "0207";
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
AsciiProtocol.LRC(msg, 0, length);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => AsciiProtocol.LRC(msg, 0, length));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForMessageLengthOnLrc()
|
public void ShouldThrowArgumentExceptionForMessageLengthOnLrc()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
string msg = "0207";
|
string msg = "0207";
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
AsciiProtocol.LRC(msg);
|
Assert.ThrowsException<ArgumentException>(() => AsciiProtocol.LRC(msg));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Validation
|
#endregion Validation
|
||||||
|
|||||||
@@ -55,29 +55,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(2001)]
|
[DataRow(2001)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -106,16 +100,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadCoils([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x01, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadCoils([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x01, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Coils
|
#endregion Read Coils
|
||||||
@@ -166,29 +157,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(2001)]
|
[DataRow(2001)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -217,16 +202,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadDiscreteInputs([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x02, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDiscreteInputs([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x02, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Discrete Inputs
|
#endregion Read Discrete Inputs
|
||||||
@@ -277,29 +259,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(126)]
|
[DataRow(126)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -323,16 +299,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadHoldingRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x07, UNIT_ID, 0x03, 0x04, 0x02, 0x2B, 0x00, 0x00]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadHoldingRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x03, 0x04, 0x02, 0x2B, 0x00, 0x00]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Holding Registers
|
#endregion Read Holding Registers
|
||||||
@@ -383,29 +356,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(126)]
|
[DataRow(126)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -429,16 +396,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadInputRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x07, UNIT_ID, 0x04, 0x04, 0x02, 0x2B, 0x00, 0x00]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadInputRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x08, UNIT_ID, 0x04, 0x04, 0x02, 0x2B, 0x00, 0x00]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Input Registers
|
#endregion Read Input Registers
|
||||||
@@ -493,16 +457,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification()
|
public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
@@ -529,27 +490,25 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x05, UNIT_ID, 0x2B, 0x0D, 0x00, 0x00];
|
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x05, UNIT_ID, 0x2B, 0x0D, 0x00, 0x00];
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadDeviceIdentification(response);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, UNIT_ID, 0x2B, 0x0E, 0x08, 0x00, 0x00];
|
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, UNIT_ID, 0x2B, 0x0E, 0x08, 0x00, 0x00];
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadDeviceIdentification(response);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Device Identification
|
#endregion Read Device Identification
|
||||||
@@ -600,16 +559,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteSingleCoil(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleCoil(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -676,16 +632,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -765,22 +718,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(1969)]
|
[DataRow(1969)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -790,14 +739,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -808,14 +754,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -826,10 +769,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -908,22 +849,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(124)]
|
[DataRow(124)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -933,14 +870,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -951,14 +885,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -969,10 +900,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -1065,7 +994,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0x00, 0x00)]
|
[DataRow(0x00, 0x00)]
|
||||||
[DataRow(0x01, 0x01)]
|
[DataRow(0x01, 0x01)]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForTransactionIdOnValidateResponse(int hi, int lo)
|
public void ShouldThrowForTransactionIdOnValidateResponse(int hi, int lo)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1074,14 +1002,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
SetCrc(response);
|
SetCrc(response);
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0x00, 0x01)]
|
[DataRow(0x00, 0x01)]
|
||||||
[DataRow(0x01, 0x00)]
|
[DataRow(0x01, 0x00)]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForProtocolIdOnValidateResponse(int hi, int lo)
|
public void ShouldThrowForProtocolIdOnValidateResponse(int hi, int lo)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1090,12 +1017,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
SetCrc(response);
|
SetCrc(response);
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForFollowingBytesOnValidateResponse()
|
public void ShouldThrowForFollowingBytesOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1104,12 +1030,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
SetCrc(response);
|
SetCrc(response);
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForUnitIdOnValidateResponse()
|
public void ShouldThrowForUnitIdOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1118,12 +1043,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
SetCrc(response);
|
SetCrc(response);
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1132,12 +1056,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
SetCrc(response);
|
SetCrc(response);
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForModbusErrorOnValidateResponse()
|
public void ShouldThrowForModbusErrorOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1146,14 +1069,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
SetCrc(response);
|
SetCrc(response);
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0x59, 0x6C)]
|
[DataRow(0x59, 0x6C)]
|
||||||
[DataRow(0x58, 0x6B)]
|
[DataRow(0x58, 0x6B)]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForCrcOnValidateResponse(int hi, int lo)
|
public void ShouldThrowForCrcOnValidateResponse(int hi, int lo)
|
||||||
{
|
{
|
||||||
// Arrange
|
// 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];
|
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06, UNIT_ID, 0x01, 0x01, 0x00, (byte)hi, (byte)lo];
|
||||||
var protocol = new RtuOverTcpProtocol();
|
var protocol = new RtuOverTcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Validation
|
#endregion Validation
|
||||||
|
|||||||
@@ -43,29 +43,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(2001)]
|
[DataRow(2001)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -94,16 +88,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
_ = protocol.DeserializeReadCoils([UNIT_ID, 0x01, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadCoils([UNIT_ID, 0x01, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Coils
|
#endregion Read Coils
|
||||||
@@ -142,29 +133,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(2001)]
|
[DataRow(2001)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -193,16 +178,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
_ = protocol.DeserializeReadDiscreteInputs([UNIT_ID, 0x02, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDiscreteInputs([UNIT_ID, 0x02, 0x02, 0xCD, 0x6B, 0x05, 0x00, 0x00]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Discrete Inputs
|
#endregion Read Discrete Inputs
|
||||||
@@ -241,29 +223,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(126)]
|
[DataRow(126)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -287,16 +263,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadHoldingRegisters([UNIT_ID, 0x03, 0x04, 0x02, 0x2B, 0x00, 0x00]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadHoldingRegisters([UNIT_ID, 0x03, 0x04, 0x02, 0x2B, 0x00, 0x00]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Holding Registers
|
#endregion Read Holding Registers
|
||||||
@@ -335,29 +308,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(126)]
|
[DataRow(126)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -381,16 +348,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadInputRegisters([UNIT_ID, 0x04, 0x04, 0x02, 0x2B, 0x00, 0x00]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadInputRegisters([UNIT_ID, 0x04, 0x04, 0x02, 0x2B, 0x00, 0x00]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Input Registers
|
#endregion Read Input Registers
|
||||||
@@ -433,16 +397,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification()
|
public void ShouldThrowOutOfRangeExceptionForCategoryOnSerializeReadDeviceIdentification()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
@@ -469,27 +430,25 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] response = [UNIT_ID, 0x2B, 0x0D, 0x00, 0x00];
|
byte[] response = [UNIT_ID, 0x2B, 0x0D, 0x00, 0x00];
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadDeviceIdentification(response);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] response = [UNIT_ID, 0x2B, 0x0E, 0x08, 0x00, 0x00];
|
byte[] response = [UNIT_ID, 0x2B, 0x0E, 0x08, 0x00, 0x00];
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadDeviceIdentification(response);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Device Identification
|
#endregion Read Device Identification
|
||||||
@@ -528,16 +487,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteSingleCoil(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleCoil(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -592,16 +548,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -669,22 +622,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(1969)]
|
[DataRow(1969)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -694,14 +643,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -712,14 +658,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -730,10 +673,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -800,22 +741,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(124)]
|
[DataRow(124)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -825,14 +762,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -843,14 +777,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -861,10 +792,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -1105,7 +1034,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForUnitIdOnValidateResponse()
|
public void ShouldThrowForUnitIdOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1114,14 +1042,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
SetCrc(response);
|
SetCrc(response);
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0x57, 0x6C)]
|
[DataRow(0x57, 0x6C)]
|
||||||
[DataRow(0x58, 0x6B)]
|
[DataRow(0x58, 0x6B)]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForCrcOnValidateResponse(int hi, int lo)
|
public void ShouldThrowForCrcOnValidateResponse(int hi, int lo)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1129,12 +1056,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
byte[] response = [UNIT_ID, 0x01, 0x01, 0x00, (byte)hi, (byte)lo];
|
byte[] response = [UNIT_ID, 0x01, 0x01, 0x00, (byte)hi, (byte)lo];
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1143,12 +1069,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
SetCrc(response);
|
SetCrc(response);
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForErrorOnValidateResponse()
|
public void ShouldThrowForErrorOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1157,8 +1082,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
SetCrc(response);
|
SetCrc(response);
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
@@ -1166,7 +1091,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataRow(0x02)]
|
[DataRow(0x02)]
|
||||||
[DataRow(0x03)]
|
[DataRow(0x03)]
|
||||||
[DataRow(0x04)]
|
[DataRow(0x04)]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForReadLengthOnValidateResponse(int fn)
|
public void ShouldThrowForReadLengthOnValidateResponse(int fn)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1175,8 +1099,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
SetCrc(response);
|
SetCrc(response);
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
@@ -1184,7 +1108,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataRow(0x06)]
|
[DataRow(0x06)]
|
||||||
[DataRow(0x0F)]
|
[DataRow(0x0F)]
|
||||||
[DataRow(0x10)]
|
[DataRow(0x10)]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForWriteLengthOnValidateResponse(int fn)
|
public void ShouldThrowForWriteLengthOnValidateResponse(int fn)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1193,8 +1116,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
SetCrc(response);
|
SetCrc(response);
|
||||||
var protocol = new RtuProtocol();
|
var protocol = new RtuProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -1217,43 +1140,36 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(null)]
|
[DataRow(null)]
|
||||||
[DataRow(new byte[0])]
|
[DataRow(new byte[0])]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShuldThrowArgumentNullExceptionForBytesOnCrc16(byte[] bytes)
|
public void ShuldThrowArgumentNullExceptionForBytesOnCrc16(byte[] bytes)
|
||||||
{
|
{
|
||||||
// Act
|
// Arrange
|
||||||
_ = RtuProtocol.CRC16(bytes);
|
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
// Act + Assert
|
||||||
|
Assert.ThrowsException<ArgumentNullException>(() => RtuProtocol.CRC16(bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(-1)]
|
[DataRow(-1)]
|
||||||
[DataRow(10)]
|
[DataRow(10)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeForStartOnCrc16(int start)
|
public void ShouldThrowArgumentOutOfRangeForStartOnCrc16(int start)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes("0123456789");
|
byte[] bytes = Encoding.UTF8.GetBytes("0123456789");
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
_ = RtuProtocol.CRC16(bytes, start);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => RtuProtocol.CRC16(bytes, start));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(11)]
|
[DataRow(11)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeForLengthOnCrc16(int length)
|
public void ShouldThrowArgumentOutOfRangeForLengthOnCrc16(int length)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes("0123456789");
|
byte[] bytes = Encoding.UTF8.GetBytes("0123456789");
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
_ = RtuProtocol.CRC16(bytes, 0, length);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => RtuProtocol.CRC16(bytes, 0, length));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Validation
|
#endregion Validation
|
||||||
|
|||||||
@@ -53,29 +53,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(2001)]
|
[DataRow(2001)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadCoils(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadCoils(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -104,16 +98,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
public void ShouldThrowExceptionOnDeserializeReadCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
var coils = protocol.DeserializeReadCoils([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x02, 0xCD, 0x6B, 0x05]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadCoils([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x01, 0x02, 0xCD, 0x6B, 0x05]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Coils
|
#endregion Read Coils
|
||||||
@@ -162,29 +153,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(2001)]
|
[DataRow(2001)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadDiscreteInputs(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadDiscreteInputs()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDiscreteInputs(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -213,16 +198,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
public void ShouldThrowExceptionOnDeserializeReadDiscreteInputs()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadDiscreteInputs([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x02, 0x03, 0xCD, 0x6B]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDiscreteInputs([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x02, 0x03, 0xCD, 0x6B]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Discrete Inputs
|
#endregion Read Discrete Inputs
|
||||||
@@ -271,29 +253,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(126)]
|
[DataRow(126)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadHoldingRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadHoldingRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -317,16 +293,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
public void ShouldThrowExceptionOnDeserializeReadHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadHoldingRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x03, 0x04, 0x02, 0x2B]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadHoldingRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x03, 0x04, 0x02, 0x2B]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Holding Registers
|
#endregion Read Holding Registers
|
||||||
@@ -375,29 +348,23 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(126)]
|
[DataRow(126)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeReadInputRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, 19, (ushort)count));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
public void ShouldThrowOutOfRangeForStartingAddressOnSerializeReadInputRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadInputRegisters(UNIT_ID, ushort.MaxValue, 2));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -421,16 +388,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
public void ShouldThrowExceptionOnDeserializeReadInputRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadInputRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x04, 0x04, 0x02, 0x2B]);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadInputRegisters([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x2A, 0x04, 0x04, 0x02, 0x2B]));
|
||||||
|
|
||||||
// Assert - ModbusException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Input Registers
|
#endregion Read Input Registers
|
||||||
@@ -483,16 +447,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeExceptionOnSerializeReadDeviceIdentification()
|
public void ShouldThrowOutOfRangeExceptionOnSerializeReadDeviceIdentification()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeReadDeviceIdentification(UNIT_ID, (ModbusDeviceIdentificationCategory)10, ModbusDeviceIdentificationObject.ProductCode));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
@@ -519,27 +480,25 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForMeiType()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x2A, 0x2B, 0x0D];
|
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x2A, 0x2B, 0x0D];
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadDeviceIdentification(response);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
public void ShouldThrowExceptionOnDeserializeReadDeviceIdentificationForCategory()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x2A, 0x2B, 0x0E, 0x08];
|
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x2A, 0x2B, 0x0E, 0x08];
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.DeserializeReadDeviceIdentification(response);
|
Assert.ThrowsException<ModbusException>(() => protocol.DeserializeReadDeviceIdentification(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Read Device Identification
|
#endregion Read Device Identification
|
||||||
@@ -588,16 +547,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
public void ShouldThrowArgumentNullOnSerializeWriteSingleCoil()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteSingleCoil(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleCoil(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -662,16 +618,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
public void ShouldThrowArgumentNullOnSerializeWriteSingleHoldingRegister()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteSingleHoldingRegister(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -749,22 +702,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
public void ShouldThrowArgumentNullOnSerializeWriteMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(1969)]
|
[DataRow(1969)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleCoils(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -774,14 +723,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -792,14 +738,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleCoils()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -810,10 +753,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleCoils(UNIT_ID, coils);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleCoils(UNIT_ID, coils));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -890,22 +831,18 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
public void ShouldThrowArgumentNullOnSerializeWriteMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null);
|
Assert.ThrowsException<ArgumentNullException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(124)]
|
[DataRow(124)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
public void ShouldThrowOutOfRangeForCountOnSerializeWriteMultipleHoldingRegisters(int count)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -915,14 +852,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
|
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
public void ShouldThrowArgumentExceptionForDuplicateEntryOnSerializeMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -933,14 +867,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
public void ShouldThrowArgumentExceptionForGapInAddressOnSerializeMultipleHoldingRegisters()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -951,10 +882,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
};
|
};
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers);
|
Assert.ThrowsException<ArgumentException>(() => protocol.SerializeWriteMultipleHoldingRegisters(UNIT_ID, registers));
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -1045,7 +974,6 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0x00, 0x00)]
|
[DataRow(0x00, 0x00)]
|
||||||
[DataRow(0x01, 0x01)]
|
[DataRow(0x01, 0x01)]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForTransactionIdOnValidateResponse(int hi, int lo)
|
public void ShouldThrowForTransactionIdOnValidateResponse(int hi, int lo)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1053,14 +981,13 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x01, 0x01, 0x00];
|
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x01, 0x01, 0x00];
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0x00, 0x01)]
|
[DataRow(0x00, 0x01)]
|
||||||
[DataRow(0x01, 0x00)]
|
[DataRow(0x01, 0x00)]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForProtocolIdOnValidateResponse(int hi, int lo)
|
public void ShouldThrowForProtocolIdOnValidateResponse(int hi, int lo)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1068,12 +995,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x01, 0x01, 0x00];
|
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x01, 0x01, 0x00];
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForFollowingBytesOnValidateResponse()
|
public void ShouldThrowForFollowingBytesOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1081,12 +1007,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x2A, 0x01, 0x01, 0x00];
|
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x2A, 0x01, 0x01, 0x00];
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForUnitIdOnValidateResponse()
|
public void ShouldThrowForUnitIdOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1094,12 +1019,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2B, 0x01, 0x01, 0x00];
|
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2B, 0x01, 0x01, 0x00];
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
public void ShouldThrowForFunctionCodeOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1107,12 +1031,11 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x02, 0x01, 0x00];
|
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x2A, 0x02, 0x01, 0x00];
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ModbusException))]
|
|
||||||
public void ShouldThrowForModbusErrorOnValidateResponse()
|
public void ShouldThrowForModbusErrorOnValidateResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -1120,8 +1043,8 @@ namespace AMWD.Protocols.Modbus.Tests.Common.Protocols
|
|||||||
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x2A, 0x81, 0x01];
|
byte[] response = [0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x2A, 0x81, 0x01];
|
||||||
var protocol = new TcpProtocol();
|
var protocol = new TcpProtocol();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
protocol.ValidateResponse(request, response);
|
Assert.ThrowsException<ModbusException>(() => protocol.ValidateResponse(request, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Validation
|
#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]
|
[TestInitialize]
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
|
string portName = "COM-42";
|
||||||
|
|
||||||
_genericConnectionMock = new Mock<IModbusConnection>();
|
_genericConnectionMock = new Mock<IModbusConnection>();
|
||||||
_genericConnectionMock.Setup(c => c.IdleTimeout).Returns(TimeSpan.FromSeconds(40));
|
_genericConnectionMock.Setup(c => c.IdleTimeout).Returns(TimeSpan.FromSeconds(40));
|
||||||
_genericConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(30));
|
_genericConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(30));
|
||||||
_genericConnectionMock.Setup(c => c.ReadTimeout).Returns(TimeSpan.FromSeconds(20));
|
_genericConnectionMock.Setup(c => c.ReadTimeout).Returns(TimeSpan.FromSeconds(20));
|
||||||
_genericConnectionMock.Setup(c => c.WriteTimeout).Returns(TimeSpan.FromSeconds(10));
|
_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.IdleTimeout).Returns(TimeSpan.FromSeconds(10));
|
||||||
_serialConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(20));
|
_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.DriverEnabledRS485).Returns(true);
|
||||||
_serialConnectionMock.Setup(c => c.InterRequestDelay).Returns(TimeSpan.FromSeconds(50));
|
_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.BaudRate).Returns(BaudRate.Baud2400);
|
||||||
_serialConnectionMock.Setup(c => c.DataBits).Returns(7);
|
_serialConnectionMock.Setup(c => c.DataBits).Returns(7);
|
||||||
_serialConnectionMock.Setup(c => c.Handshake).Returns(Handshake.XOnXOff);
|
_serialConnectionMock.Setup(c => c.Handshake).Returns(Handshake.XOnXOff);
|
||||||
@@ -231,5 +233,18 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
|
|
||||||
_serialConnectionMock.VerifyNoOtherCalls();
|
_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();
|
connection.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DataTestMethod]
|
||||||
|
[DataRow(null)]
|
||||||
|
[DataRow("")]
|
||||||
|
[DataRow(" ")]
|
||||||
|
public void ShouldThrowArgumentNullExceptionOnCreate(string portName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
// Act + Assert
|
||||||
|
Assert.ThrowsException<ArgumentNullException>(() => new ModbusSerialClient(portName));
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ObjectDisposedException))]
|
|
||||||
public async Task ShouldThrowDisposedExceptionOnInvokeAsync()
|
public async Task ShouldThrowDisposedExceptionOnInvokeAsync()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var connection = GetConnection();
|
var connection = GetConnection();
|
||||||
connection.Dispose();
|
connection.Dispose();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
await connection.InvokeAsync(null, null);
|
await Assert.ThrowsExceptionAsync<ObjectDisposedException>(() => connection.InvokeAsync(null, null));
|
||||||
|
|
||||||
// Assert - OjbectDisposedException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(null)]
|
[DataRow(null)]
|
||||||
[DataRow(new byte[0])]
|
[DataRow(new byte[0])]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public async Task ShouldThrowArgumentNullExceptionForMissingRequestOnInvokeAsync(byte[] request)
|
public async Task ShouldThrowArgumentNullExceptionForMissingRequestOnInvokeAsync(byte[] request)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var connection = GetConnection();
|
var connection = GetConnection();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
await connection.InvokeAsync(request, null);
|
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => connection.InvokeAsync(request, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public async Task ShouldThrowArgumentNullExceptionForMissingValidationOnInvokeAsync()
|
public async Task ShouldThrowArgumentNullExceptionForMissingValidationOnInvokeAsync()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] request = new byte[1];
|
byte[] request = new byte[1];
|
||||||
var connection = GetConnection();
|
var connection = GetConnection();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
await connection.InvokeAsync(request, null);
|
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => connection.InvokeAsync(request, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -161,10 +164,8 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
_serialPortMock.VerifyNoOtherCalls();
|
_serialPortMock.VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[TestMethod]
|
||||||
[DataRow(false)]
|
public async Task ShouldOpenAndCloseOnInvokeAsyncOnLinuxNotModifyingDriver()
|
||||||
[DataRow(true)]
|
|
||||||
public async Task ShouldOpenAndCloseOnInvokeAsync(bool modifyDriver)
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_alwaysOpen = false;
|
_alwaysOpen = false;
|
||||||
@@ -178,8 +179,9 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
_serialLineResponseQueue.Enqueue(expectedResponse);
|
_serialLineResponseQueue.Enqueue(expectedResponse);
|
||||||
|
|
||||||
var connection = GetSerialConnection();
|
var connection = GetSerialConnection();
|
||||||
|
connection.GetType().GetField("_isLinux", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(connection, true);
|
||||||
connection.IdleTimeout = TimeSpan.FromMilliseconds(200);
|
connection.IdleTimeout = TimeSpan.FromMilliseconds(200);
|
||||||
connection.DriverEnabledRS485 = modifyDriver;
|
connection.DriverEnabledRS485 = false;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await connection.InvokeAsync(request, validation);
|
var response = await connection.InvokeAsync(request, validation);
|
||||||
@@ -198,11 +200,134 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
_serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2));
|
_serialPortMock.Verify(c => c.ResetRS485DriverStateFlags(), Times.Exactly(2));
|
||||||
_serialPortMock.Verify(c => c.Open(), Times.Once);
|
_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.Verify(c => c.GetRS485DriverStateFlags(), Times.Once);
|
|
||||||
_serialPortMock.Verify(c => c.ChangeRS485DriverStateFlags(It.IsAny<RS485Flags>()), 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);
|
||||||
|
|
||||||
|
_serialPortMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
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.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.Verify(ns => ns.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
@@ -211,7 +336,6 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(EndOfStreamException))]
|
|
||||||
public async Task ShouldThrowEndOfStreamExceptionOnInvokeAsync()
|
public async Task ShouldThrowEndOfStreamExceptionOnInvokeAsync()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -220,10 +344,8 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
|
|
||||||
var connection = GetConnection();
|
var connection = GetConnection();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
var response = await connection.InvokeAsync(request, validation);
|
await Assert.ThrowsExceptionAsync<EndOfStreamException>(() => connection.InvokeAsync(request, validation));
|
||||||
|
|
||||||
// Assert - EndOfStreamException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -305,7 +427,6 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(TaskCanceledException))]
|
|
||||||
public async Task ShouldThrowTaskCancelledExceptionForDisposeOnInvokeAsync()
|
public async Task ShouldThrowTaskCancelledExceptionForDisposeOnInvokeAsync()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -317,16 +438,16 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||||
.Returns(Task.Delay(100));
|
.Returns(Task.Delay(100));
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
var task = connection.InvokeAsync(request, validation);
|
await Assert.ThrowsExceptionAsync<TaskCanceledException>(async () =>
|
||||||
connection.Dispose();
|
{
|
||||||
await task;
|
var task = connection.InvokeAsync(request, validation);
|
||||||
|
connection.Dispose();
|
||||||
// Assert - TaskCancelledException
|
await task;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(TaskCanceledException))]
|
|
||||||
public async Task ShouldThrowTaskCancelledExceptionForCancelOnInvokeAsync()
|
public async Task ShouldThrowTaskCancelledExceptionForCancelOnInvokeAsync()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -339,12 +460,13 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||||
.Returns(Task.Delay(100));
|
.Returns(Task.Delay(100));
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
var task = connection.InvokeAsync(request, validation, cts.Token);
|
await Assert.ThrowsExceptionAsync<TaskCanceledException>(async () =>
|
||||||
cts.Cancel();
|
{
|
||||||
await task;
|
var task = connection.InvokeAsync(request, validation, cts.Token);
|
||||||
|
cts.Cancel();
|
||||||
// Assert - TaskCancelledException
|
await task;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -360,7 +482,7 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
var connection = GetConnection();
|
var connection = GetConnection();
|
||||||
_serialPortMock
|
_serialPortMock
|
||||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
.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));
|
.Returns(Task.Delay(100));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -403,7 +525,7 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
var connection = GetConnection();
|
var connection = GetConnection();
|
||||||
_serialPortMock
|
_serialPortMock
|
||||||
.Setup(ns => ns.WriteAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
.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));
|
.Returns(Task.Delay(100));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -467,16 +589,13 @@ namespace AMWD.Protocols.Modbus.Tests.Serial
|
|||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
var connection = new ModbusSerialConnection();
|
var connection = new ModbusSerialConnection("some-port");
|
||||||
|
|
||||||
// Replace real connection with mock
|
// Replace real connection with mock
|
||||||
var connectionField = connection.GetType().GetField("_serialPort", BindingFlags.NonPublic | BindingFlags.Instance);
|
var connectionField = connection.GetType().GetField("_serialPort", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
(connectionField.GetValue(connection) as SerialPortWrapper)?.Dispose();
|
(connectionField.GetValue(connection) as SerialPortWrapper)?.Dispose();
|
||||||
connectionField.SetValue(connection, _serialPortMock.Object);
|
connectionField.SetValue(connection, _serialPortMock.Object);
|
||||||
|
|
||||||
// Set unit test mode
|
|
||||||
connection.GetType().GetField("_isUnitTest", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(connection, true);
|
|
||||||
|
|
||||||
return connection;
|
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();
|
_tcpConnectionMock.VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldPrintCleanString()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using var client = new ModbusTcpClient(_tcpConnectionMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
string str = client.ToString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
SnapshotAssert.AreEqual(str);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -16,6 +17,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
private readonly string _hostname = "127.0.0.1";
|
private readonly string _hostname = "127.0.0.1";
|
||||||
|
|
||||||
private Mock<TcpClientWrapper> _tcpClientMock;
|
private Mock<TcpClientWrapper> _tcpClientMock;
|
||||||
|
private Mock<TcpClientWrapperFactory> _tcpClientFactoryMock;
|
||||||
private Mock<NetworkStreamWrapper> _networkStreamMock;
|
private Mock<NetworkStreamWrapper> _networkStreamMock;
|
||||||
|
|
||||||
private bool _alwaysConnected;
|
private bool _alwaysConnected;
|
||||||
@@ -40,10 +42,19 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ShouldGetAndSetPropertiesOfBaseClient()
|
public async Task ShouldSetPropertiesOfBaseClient()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
byte[] request = [1, 2, 3];
|
||||||
|
byte[] expectedResponse = [9, 8, 7];
|
||||||
|
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||||
|
_networkResponseQueue.Enqueue(expectedResponse);
|
||||||
|
|
||||||
var connection = GetTcpConnection();
|
var connection = GetTcpConnection();
|
||||||
|
await connection.InvokeAsync(request, validation);
|
||||||
|
|
||||||
|
_tcpClientMock.Invocations.Clear();
|
||||||
|
_networkStreamMock.Invocations.Clear();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
connection.ReadTimeout = TimeSpan.FromSeconds(123);
|
connection.ReadTimeout = TimeSpan.FromSeconds(123);
|
||||||
@@ -51,8 +62,8 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
|
|
||||||
// Assert - part 1
|
// Assert - part 1
|
||||||
Assert.AreEqual("TCP", connection.Name);
|
Assert.AreEqual("TCP", connection.Name);
|
||||||
Assert.AreEqual(1, connection.ReadTimeout.TotalSeconds);
|
Assert.AreEqual(123, connection.ReadTimeout.TotalSeconds);
|
||||||
Assert.AreEqual(1, connection.WriteTimeout.TotalSeconds);
|
Assert.AreEqual(456, connection.WriteTimeout.TotalSeconds);
|
||||||
|
|
||||||
Assert.AreEqual(_hostname, connection.Hostname);
|
Assert.AreEqual(_hostname, connection.Hostname);
|
||||||
Assert.AreEqual(502, connection.Port);
|
Assert.AreEqual(502, connection.Port);
|
||||||
@@ -61,9 +72,6 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
_tcpClientMock.VerifySet(c => c.ReceiveTimeout = 123000, Times.Once);
|
_tcpClientMock.VerifySet(c => c.ReceiveTimeout = 123000, Times.Once);
|
||||||
_tcpClientMock.VerifySet(c => c.SendTimeout = 456000, Times.Once);
|
_tcpClientMock.VerifySet(c => c.SendTimeout = 456000, Times.Once);
|
||||||
|
|
||||||
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
|
|
||||||
_tcpClientMock.VerifyGet(c => c.SendTimeout, Times.Once);
|
|
||||||
|
|
||||||
_tcpClientMock.VerifyNoOtherCalls();
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
_networkStreamMock.VerifyNoOtherCalls();
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
@@ -72,31 +80,25 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
[DataRow(null)]
|
[DataRow(null)]
|
||||||
[DataRow("")]
|
[DataRow("")]
|
||||||
[DataRow(" ")]
|
[DataRow(" ")]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public void ShouldThrowArgumentNullExceptionForInvalidHostname(string hostname)
|
public void ShouldThrowArgumentNullExceptionForInvalidHostname(string hostname)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var connection = GetTcpConnection();
|
var connection = GetTcpConnection();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
connection.Hostname = hostname;
|
Assert.ThrowsException<ArgumentNullException>(() => connection.Hostname = hostname);
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(0)]
|
[DataRow(0)]
|
||||||
[DataRow(65536)]
|
[DataRow(65536)]
|
||||||
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
|
||||||
public void ShouldThrowArgumentOutOfRangeExceptionForInvalidPort(int port)
|
public void ShouldThrowArgumentOutOfRangeExceptionForInvalidPort(int port)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var connection = GetTcpConnection();
|
var connection = GetTcpConnection();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
connection.Port = port;
|
Assert.ThrowsException<ArgumentOutOfRangeException>(() => connection.Port = port);
|
||||||
|
|
||||||
// Assert - ArgumentOutOfRangeException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -111,46 +113,37 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ObjectDisposedException))]
|
|
||||||
public async Task ShouldThrowDisposedExceptionOnInvokeAsync()
|
public async Task ShouldThrowDisposedExceptionOnInvokeAsync()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var connection = GetConnection();
|
var connection = GetConnection();
|
||||||
connection.Dispose();
|
connection.Dispose();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
await connection.InvokeAsync(null, null);
|
await Assert.ThrowsExceptionAsync<ObjectDisposedException>(() => connection.InvokeAsync(null, null));
|
||||||
|
|
||||||
// Assert - OjbectDisposedException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow(null)]
|
[DataRow(null)]
|
||||||
[DataRow(new byte[0])]
|
[DataRow(new byte[0])]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public async Task ShouldThrowArgumentNullExceptionForMissingRequestOnInvokeAsync(byte[] request)
|
public async Task ShouldThrowArgumentNullExceptionForMissingRequestOnInvokeAsync(byte[] request)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var connection = GetConnection();
|
var connection = GetConnection();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
await connection.InvokeAsync(request, null);
|
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => connection.InvokeAsync(request, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
|
||||||
public async Task ShouldThrowArgumentNullExceptionForMissingValidationOnInvokeAsync()
|
public async Task ShouldThrowArgumentNullExceptionForMissingValidationOnInvokeAsync()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] request = new byte[1];
|
byte[] request = new byte[1];
|
||||||
var connection = GetConnection();
|
var connection = GetConnection();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
await connection.InvokeAsync(request, null);
|
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => connection.InvokeAsync(request, null));
|
||||||
|
|
||||||
// Assert - ArgumentNullException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -173,6 +166,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||||
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
||||||
|
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
@@ -211,11 +205,10 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||||
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
||||||
|
|
||||||
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
|
|
||||||
|
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
||||||
_tcpClientMock.Verify(c => c.Close(), Times.Exactly(2));
|
_tcpClientMock.Verify(c => c.Close(), Times.Exactly(2));
|
||||||
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
@@ -227,7 +220,6 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(EndOfStreamException))]
|
|
||||||
public async Task ShouldThrowEndOfStreamExceptionOnInvokeAsync()
|
public async Task ShouldThrowEndOfStreamExceptionOnInvokeAsync()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -236,14 +228,11 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
|
|
||||||
var connection = GetConnection();
|
var connection = GetConnection();
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
var response = await connection.InvokeAsync(request, validation);
|
await Assert.ThrowsExceptionAsync<EndOfStreamException>(() => connection.InvokeAsync(request, validation));
|
||||||
|
|
||||||
// Assert - EndOfStreamException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ApplicationException))]
|
|
||||||
public async Task ShouldThrowApplicationExceptionWhenHostNotResolvableOnInvokeAsync()
|
public async Task ShouldThrowApplicationExceptionWhenHostNotResolvableOnInvokeAsync()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -256,10 +245,8 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
var connection = GetConnection();
|
var connection = GetConnection();
|
||||||
connection.GetType().GetField("_hostname", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(connection, "");
|
connection.GetType().GetField("_hostname", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(connection, "");
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
var response = await connection.InvokeAsync(request, validation);
|
await Assert.ThrowsExceptionAsync<ApplicationException>(() => connection.InvokeAsync(request, validation));
|
||||||
|
|
||||||
// Assert - ApplicationException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -289,11 +276,10 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||||
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
||||||
|
|
||||||
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
|
|
||||||
|
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
||||||
_tcpClientMock.Verify(c => c.Close(), Times.Once);
|
_tcpClientMock.Verify(c => c.Close(), Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
@@ -329,11 +315,10 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||||
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
||||||
|
|
||||||
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Exactly(2));
|
|
||||||
|
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(3));
|
||||||
_tcpClientMock.Verify(c => c.Close(), Times.Exactly(2));
|
_tcpClientMock.Verify(c => c.Close(), Times.Exactly(2));
|
||||||
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Exactly(2));
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(3));
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
@@ -345,8 +330,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(TaskCanceledException))]
|
public async Task ShouldThrowTaskCanceledExceptionForDisposeOnInvokeAsync()
|
||||||
public async Task ShouldThrowTaskCancelledExceptionForDisposeOnInvokeAsync()
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] request = [1, 2, 3];
|
byte[] request = [1, 2, 3];
|
||||||
@@ -357,17 +341,17 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
.Returns(new ValueTask(Task.Delay(100)));
|
.Returns(new ValueTask(Task.Delay(100)));
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
var task = connection.InvokeAsync(request, validation);
|
await Assert.ThrowsExceptionAsync<TaskCanceledException>(async () =>
|
||||||
connection.Dispose();
|
{
|
||||||
await task;
|
var task = connection.InvokeAsync(request, validation);
|
||||||
|
connection.Dispose();
|
||||||
// Assert - TaskCancelledException
|
await task;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(TaskCanceledException))]
|
public async Task ShouldThrowTaskCanceledExceptionForCancelOnInvokeAsync()
|
||||||
public async Task ShouldThrowTaskCancelledExceptionForCancelOnInvokeAsync()
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] request = [1, 2, 3];
|
byte[] request = [1, 2, 3];
|
||||||
@@ -379,12 +363,13 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
.Returns(new ValueTask(Task.Delay(100)));
|
.Returns(new ValueTask(Task.Delay(100)));
|
||||||
|
|
||||||
// Act
|
// Act + Assert
|
||||||
var task = connection.InvokeAsync(request, validation, cts.Token);
|
await Assert.ThrowsExceptionAsync<TaskCanceledException>(async () =>
|
||||||
cts.Cancel();
|
{
|
||||||
await task;
|
var task = connection.InvokeAsync(request, validation, cts.Token);
|
||||||
|
cts.Cancel();
|
||||||
// Assert - TaskCancelledException
|
await task;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -426,6 +411,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
CollectionAssert.AreEqual(expectedResponse, response.ToArray());
|
||||||
|
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
@@ -475,6 +461,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks.First());
|
||||||
|
|
||||||
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
|
||||||
@@ -490,7 +477,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
|
|
||||||
private ModbusTcpConnection GetTcpConnection()
|
private ModbusTcpConnection GetTcpConnection()
|
||||||
{
|
{
|
||||||
_networkStreamMock = new Mock<NetworkStreamWrapper>();
|
_networkStreamMock = new Mock<NetworkStreamWrapper>(null);
|
||||||
_networkStreamMock
|
_networkStreamMock
|
||||||
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
.Callback<ReadOnlyMemory<byte>, CancellationToken>((req, _) => _networkRequestCallbacks.Add(req.ToArray()))
|
.Callback<ReadOnlyMemory<byte>, CancellationToken>((req, _) => _networkRequestCallbacks.Add(req.ToArray()))
|
||||||
@@ -508,7 +495,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
return ValueTask.FromResult(0);
|
return ValueTask.FromResult(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
_tcpClientMock = new Mock<TcpClientWrapper>();
|
_tcpClientMock = new Mock<TcpClientWrapper>(AddressFamily.Unknown);
|
||||||
_tcpClientMock.Setup(c => c.Connected).Returns(() => _alwaysConnected || _connectedQueue.Dequeue());
|
_tcpClientMock.Setup(c => c.Connected).Returns(() => _alwaysConnected || _connectedQueue.Dequeue());
|
||||||
_tcpClientMock.Setup(c => c.ReceiveTimeout).Returns(() => _clientReceiveTimeout);
|
_tcpClientMock.Setup(c => c.ReceiveTimeout).Returns(() => _clientReceiveTimeout);
|
||||||
_tcpClientMock.Setup(c => c.SendTimeout).Returns(() => _clientSendTimeout);
|
_tcpClientMock.Setup(c => c.SendTimeout).Returns(() => _clientSendTimeout);
|
||||||
@@ -521,6 +508,11 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
.Setup(c => c.GetStream())
|
.Setup(c => c.GetStream())
|
||||||
.Returns(() => _networkStreamMock.Object);
|
.Returns(() => _networkStreamMock.Object);
|
||||||
|
|
||||||
|
_tcpClientFactoryMock = new Mock<TcpClientWrapperFactory>();
|
||||||
|
_tcpClientFactoryMock
|
||||||
|
.Setup(c => c.Create(It.IsAny<AddressFamily>(), It.IsAny<TimeSpan>(), It.IsAny<TimeSpan>()))
|
||||||
|
.Returns(_tcpClientMock.Object);
|
||||||
|
|
||||||
var connection = new ModbusTcpConnection
|
var connection = new ModbusTcpConnection
|
||||||
{
|
{
|
||||||
Hostname = _hostname,
|
Hostname = _hostname,
|
||||||
@@ -528,9 +520,9 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Replace real connection with mock
|
// Replace real connection with mock
|
||||||
var connectionField = connection.GetType().GetField("_tcpClient", BindingFlags.NonPublic | BindingFlags.Instance);
|
var factoryField = connection.GetType().GetField("_tcpClientFactory", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
(connectionField.GetValue(connection) as TcpClientWrapper)?.Dispose();
|
(factoryField.GetValue(connection) as TcpClientWrapper)?.Dispose();
|
||||||
connectionField.SetValue(connection, _tcpClientMock.Object);
|
factoryField.SetValue(connection, _tcpClientFactoryMock.Object);
|
||||||
|
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
@@ -29,11 +29,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{C8065AE3
|
|||||||
Directory.Build.props = Directory.Build.props
|
Directory.Build.props = Directory.Build.props
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Tests", "AMWD.Protocols.Modbus.Tests\AMWD.Protocols.Modbus.Tests.csproj", "{146070C4-E922-4F5A-AD6F-9A899186E26E}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tests", "AMWD.Protocols.Modbus.Tests\AMWD.Protocols.Modbus.Tests.csproj", "{146070C4-E922-4F5A-AD6F-9A899186E26E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Tcp", "AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj", "{8C888A84-CD09-4087-B5DA-67708ABBABA2}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tcp", "AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj", "{8C888A84-CD09-4087-B5DA-67708ABBABA2}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Protocols.Modbus.Serial", "AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj", "{D966826F-EE6C-4BC0-9185-C2A9A50FD586}"
|
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("{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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -57,6 +61,14 @@ Global
|
|||||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{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.ActiveCfg = Release|Any CPU
|
||||||
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
BIN
AMWD.Protocols.Modbus.snk
Normal file
BIN
AMWD.Protocols.Modbus.snk
Normal file
Binary file not shown.
83
CHANGELOG.md
83
CHANGELOG.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
@@ -10,6 +10,79 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
_nothing changed yet_
|
_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.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 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).
|
||||||
|
|
||||||
|
|
||||||
## [v0.2.0] (2024-04-02)
|
## [v0.2.0] (2024-04-02)
|
||||||
|
|
||||||
First "final" re-implementation.
|
First "final" re-implementation.
|
||||||
@@ -22,5 +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.2.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
|
[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>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
<LangVersion>12.0</LangVersion>
|
||||||
<NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat>
|
<NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat>
|
||||||
|
|
||||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||||
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
|
||||||
|
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<RepositoryUrl>https://github.com/AM-WD/AMWD.Protocols.Modbus.git</RepositoryUrl>
|
<RepositoryUrl>https://github.com/AM-WD/AMWD.Protocols.Modbus.git</RepositoryUrl>
|
||||||
@@ -18,19 +18,30 @@
|
|||||||
|
|
||||||
<PackageIcon>package-icon.png</PackageIcon>
|
<PackageIcon>package-icon.png</PackageIcon>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<PackageProjectUrl>https://wiki.am-wd.de/libs/modbus</PackageProjectUrl>
|
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
|
||||||
|
|
||||||
<Title>Modbus Protocol for .NET</Title>
|
<Title>Modbus Protocol for .NET</Title>
|
||||||
<Company>AM.WD</Company>
|
<Company>AM.WD</Company>
|
||||||
<Authors>Andreas Müller</Authors>
|
<Authors>Andreas Müller</Authors>
|
||||||
<Copyright>© {copyright:2018-} AM.WD</Copyright>
|
<Copyright>© {copyright:2018-} AM.WD</Copyright>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
|
||||||
|
<SignAssembly>true</SignAssembly>
|
||||||
|
<AssemblyOriginatorKeyFile>$(SolutionDir)/AMWD.Protocols.Modbus.snk</AssemblyOriginatorKeyFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
|
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
|
||||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||||
</PropertyGroup>
|
</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'">
|
<ItemGroup Condition="'$(GITLAB_CI)' == 'true'">
|
||||||
<SourceLinkGitLabHost Include="$(CI_SERVER_HOST)" Version="$(CI_SERVER_VERSION)" />
|
<SourceLinkGitLabHost Include="$(CI_SERVER_HOST)" Version="$(CI_SERVER_VERSION)" />
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0">
|
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0">
|
||||||
@@ -40,11 +51,12 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.3">
|
<PackageReference Include="AMWD.NetRevisionTask" Version="1.2.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
The MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) Andreas Müller
|
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
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
furnished to do so, subject to the following conditions:
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
The above copyright notice and this permission notice (including the next
|
||||||
all copies or substantial portions of the Software.
|
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
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
Here you can find a basic implementation of the Modbus protocol.
|
Here you can find a basic implementation of the Modbus protocol.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The project is divided into three parts.
|
The project is divided into multiple parts.
|
||||||
|
|
||||||
To be mentioned at the beginning:
|
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]).
|
Only the clients are build very modular to fit any requirement reached on the first implementation back in 2018 ([see here]).
|
||||||
@@ -34,15 +37,15 @@ It uses a specific TCP connection implementation and plugs all things from the C
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Published under [MIT License] (see [**tl;dr**Legal])
|
Published under [MIT License] (see [choose a license])
|
||||||
[](https://link.am-wd.de/donate)
|
[](https://link.am-wd.de/donate)
|
||||||
[](https://link.am-wd.de/codeium)
|
[](https://link.am-wd.de/codeium)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[see here]: https://github.com/andreasAMmueller/Modbus
|
[see here]: https://github.com/andreasAMmueller/Modbus
|
||||||
[Common]: AMWD.Protocols.Modbus.Common/README.md
|
[Common]: AMWD.Protocols.Modbus.Common/README.md
|
||||||
[Serial]: AMWD.Protocols.Modbus.Serial/README.md
|
[Serial]: AMWD.Protocols.Modbus.Serial/README.md
|
||||||
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md
|
[TCP]: AMWD.Protocols.Modbus.Tcp/README.md
|
||||||
[MIT License]: LICENSE.txt
|
[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