29 Commits

Author SHA1 Message Date
17fc216658 Bump to v0.4.1 2025-02-06 17:04:38 +01:00
885231466b Fix of #7 tried to set DualMode on IPv4 network 2025-02-06 17:02:52 +01:00
5b8a2a8af1 Changed UnitTests for further improvements on testing 2025-02-05 20:46:13 +01:00
980dab22f3 Small .NET optimizations 2025-02-03 22:29:42 +01:00
9270f49519 CLI writing to error 2025-02-03 22:28:59 +01:00
241a9d114c Async optimization 2025-02-03 22:28:31 +01:00
9283b04971 Provide license as file on NuGet.
This will provide the file also in the NuGet package and can be seen when looking into projects dependencies.
2025-02-03 22:26:06 +01:00
8b3441f6dd Bump to v0.4.0 2025-01-29 00:17:48 +01:00
63c88f5da7 Added modbus-proxy CLI tool 2025-01-28 23:55:44 +01:00
e7300bfbde Implemented UnitTests for TCP proxy 2025-01-28 21:22:11 +01:00
38dd94471d Fixed wrong ADU calculation on RTU proxy 2025-01-28 14:00:58 +01:00
56664cdac5 Added TCP wrappers to TCP proxy 2025-01-28 14:00:14 +01:00
4ef7500c3b Added some TCP wrapper classes for testability 2025-01-28 13:58:01 +01:00
05759f8e12 Fixes for SerialRtuProxy
- Adding UnitTests
- Fixing some bugs
- Updating UnitTest dependencies
2025-01-27 17:27:23 +01:00
6fc7cfda9a Updated serial implementations 2025-01-23 22:05:44 +01:00
fb67e0b77e Added VirtualModbusClient to Common 2025-01-23 22:01:45 +01:00
ce3d873cd0 Moving proxies to matching projects 2025-01-23 21:59:46 +01:00
1cf49f74ea Added README for cli tool 2025-01-23 12:05:47 +01:00
39863880d5 Added new cli tool for client connections 2025-01-21 19:27:29 +01:00
ec0ba31b86 Fixed wrong 'following bytes' information in TCP proxy 2025-01-21 19:26:13 +01:00
96b5ee21c8 Updated licensing information (still MIT) 2025-01-21 19:22:30 +01:00
6a231e02cb ReadWriteTimeout for ModbusTcpProxy changed
As suggestion of #5 on GitHub, the default value of the timeout changed.
But to the same timeout as on HttpClient and not infinite.
2024-12-13 17:58:34 +01:00
c1a70de6bb Moved InternalsVisibleTo to directory.build.props 2024-11-12 08:26:25 +01:00
6bf011d53f Updating dependencies and CI 2024-09-09 08:15:31 +02:00
0c81ab6b44 Updating to v0.3.2 2024-09-04 06:55:06 +02:00
3e8f2cd73b Addding configuration for strong named assembly build 2024-09-04 06:54:55 +02:00
e830e43c36 Clearified/fixed description of different protocol implementations 2024-07-17 18:00:16 +02:00
6a63dbb739 Bump to v0.3.1 2024-06-28 21:37:26 +02:00
1536c60336 Fixing wrong range validations 2024-06-28 21:37:00 +02:00
87 changed files with 9059 additions and 3821 deletions

View File

@@ -20,17 +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.Proxy/bin/Debug/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Debug/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Debug/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Debug/*.snupkg ./artifacts/
artifacts: artifacts:
paths: paths:
- artifacts/*.nupkg - artifacts/*.nupkg
@@ -47,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
@@ -77,22 +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.Proxy/bin/Release/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Proxy/bin/Release/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Serial/bin/Release/*.snupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.nupkg ./artifacts/
- mv ./AMWD.Protocols.Modbus.Tcp/bin/Release/*.snupkg ./artifacts/
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
@@ -104,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

View File

@@ -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>

View 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);
}
}

View File

@@ -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,20 +40,6 @@ 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>
@@ -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);

View File

@@ -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; }
} }
} }

View File

@@ -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; }
} }
} }

View File

@@ -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
{ {

View File

@@ -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();

View File

@@ -1,4 +0,0 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

View File

@@ -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();
}
} }
} }

View File

@@ -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];
} }

View File

@@ -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);
} }
} }

View File

@@ -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();

View File

@@ -1,4 +1,6 @@
using System; #if NET6_0_OR_GREATER
using System;
#endif
namespace AMWD.Protocols.Modbus.Common namespace AMWD.Protocols.Modbus.Common
{ {

View File

@@ -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>

View File

@@ -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];

View File

@@ -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>

View File

@@ -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];

View 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
}
}
}

View File

@@ -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/

View 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
}
}

View File

@@ -1,42 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<LangVersion>12.0</LangVersion>
<PackageId>AMWD.Protocols.Modbus.Proxy</PackageId>
<AssemblyName>amwd-modbus-proxy</AssemblyName>
<RootNamespace>AMWD.Protocols.Modbus.Proxy</RootNamespace>
<Product>Modbus Proxy Clients</Product>
<Description>Plugging Modbus Servers and Clients together to create Modbus Proxies.</Description>
<PackageTags>Modbus Protocol Proxy</PackageTags>
</PropertyGroup>
<ItemGroup>
<Compile Include="../AMWD.Protocols.Modbus.Common/Extensions/ArrayExtensions.cs" Link="Extensions/ArrayExtensions.cs" />
<Compile Include="../AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs" Link="Extensions/StreamExtensions.cs" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.IO.Ports" Version="4.7.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="System.IO.Ports" Version="6.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
<ProjectReference Include="..\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,10 +0,0 @@
# Modbus Protocol for .NET | Proxy
With this package the server and client implementations will be combined as proxy.
You can use any `ModbusBasClient` implementation as target client and plug it into the implemented `ModbusTcpProxy` or `ModbusRtuProxy`, which implement the server side.
---
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))

View File

@@ -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>

View File

@@ -8,20 +8,20 @@ using System.Threading.Tasks;
using AMWD.Protocols.Modbus.Common; using AMWD.Protocols.Modbus.Common;
using AMWD.Protocols.Modbus.Common.Contracts; using AMWD.Protocols.Modbus.Common.Contracts;
using AMWD.Protocols.Modbus.Common.Protocols; using AMWD.Protocols.Modbus.Common.Protocols;
using AMWD.Protocols.Modbus.Serial; using AMWD.Protocols.Modbus.Serial.Utils;
namespace AMWD.Protocols.Modbus.Proxy namespace AMWD.Protocols.Modbus.Serial
{ {
/// <summary> /// <summary>
/// Implements a Modbus serial line RTU server proxying all requests to a Modbus client of choice. /// Implements a Modbus serial line RTU server proxying all requests to a Modbus client of choice.
/// </summary> /// </summary>
public class ModbusRtuProxy : IDisposable public class ModbusRtuProxy : IModbusProxy
{ {
#region Fields #region Fields
private bool _isDisposed; private bool _isDisposed;
private readonly SerialPort _serialPort; private readonly SerialPortWrapper _serialPort;
private CancellationTokenSource _stopCts; private CancellationTokenSource _stopCts;
#endregion Fields #endregion Fields
@@ -33,31 +33,25 @@ namespace AMWD.Protocols.Modbus.Proxy
/// </summary> /// </summary>
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param> /// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
/// <param name="portName">The name of the serial port to use.</param> /// <param name="portName">The name of the serial port to use.</param>
/// <param name="baudRate">The baud rate of the serial port (Default: 19.200).</param> public ModbusRtuProxy(ModbusClientBase client, string portName)
public ModbusRtuProxy(ModbusClientBase client, string portName, BaudRate baudRate = BaudRate.Baud19200)
{ {
Client = client ?? throw new ArgumentNullException(nameof(client)); Client = client ?? throw new ArgumentNullException(nameof(client));
if (string.IsNullOrWhiteSpace(portName)) if (string.IsNullOrWhiteSpace(portName))
throw new ArgumentNullException(nameof(portName)); throw new ArgumentNullException(nameof(portName));
if (!Enum.IsDefined(typeof(BaudRate), baudRate)) _serialPort = new SerialPortWrapper
throw new ArgumentOutOfRangeException(nameof(baudRate));
if (!ModbusSerialClient.AvailablePortNames.Contains(portName))
throw new ArgumentException($"The serial port ({portName}) is not available.", nameof(portName));
_serialPort = new SerialPort
{ {
PortName = portName, PortName = portName,
BaudRate = (int)baudRate,
Handshake = Handshake.None, BaudRate = (int)BaudRate.Baud19200,
DataBits = 8, DataBits = 8,
ReadTimeout = 1000,
RtsEnable = false,
StopBits = StopBits.One, StopBits = StopBits.One,
Parity = Parity.Even,
Handshake = Handshake.None,
ReadTimeout = 1000,
WriteTimeout = 1000, WriteTimeout = 1000,
Parity = Parity.Even RtsEnable = false,
}; };
} }
@@ -70,80 +64,108 @@ namespace AMWD.Protocols.Modbus.Proxy
/// </summary> /// </summary>
public ModbusClientBase Client { get; } public ModbusClientBase Client { get; }
/// <inheritdoc cref="SerialPort.PortName"/> #region SerialPort Properties
public string PortName => _serialPort.PortName;
/// <summary> /// <inheritdoc cref="SerialPort.PortName" />
/// Gets or sets the baud rate of the serial port. public virtual string PortName
/// </summary> {
public BaudRate BaudRate get => _serialPort.PortName;
set => _serialPort.PortName = value;
}
/// <inheritdoc cref="SerialPort.BaudRate" />
public virtual BaudRate BaudRate
{ {
get => (BaudRate)_serialPort.BaudRate; get => (BaudRate)_serialPort.BaudRate;
set => _serialPort.BaudRate = (int)value; set => _serialPort.BaudRate = (int)value;
} }
/// <inheritdoc cref="SerialPort.Handshake"/> /// <inheritdoc cref="SerialPort.DataBits" />
public Handshake Handshake /// <remarks>
{ /// From the Specs:
get => _serialPort.Handshake; /// <br/>
set => _serialPort.Handshake = value; /// On <see cref="AsciiProtocol"/> it can be 7 or 8.
} /// <br/>
/// On <see cref="RtuProtocol"/> it has to be 8.
/// <inheritdoc cref="SerialPort.DataBits"/> /// </remarks>
public int DataBits public virtual int DataBits
{ {
get => _serialPort.DataBits; get => _serialPort.DataBits;
set => _serialPort.DataBits = value; set => _serialPort.DataBits = value;
} }
/// <inheritdoc cref="SerialPort.IsOpen"/> /// <inheritdoc cref="SerialPort.Handshake" />
public bool IsOpen => _serialPort.IsOpen; public virtual Handshake Handshake
/// <summary>
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a read operation does not finish.
/// </summary>
public TimeSpan ReadTimeout
{ {
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout); get => _serialPort.Handshake;
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds; set => _serialPort.Handshake = value;
} }
/// <inheritdoc cref="SerialPort.RtsEnable"/> /// <inheritdoc cref="SerialPort.Parity" />
public bool RtsEnable /// <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; get => _serialPort.RtsEnable;
set => _serialPort.RtsEnable = value; set => _serialPort.RtsEnable = value;
} }
/// <inheritdoc cref="SerialPort.StopBits"/> /// <inheritdoc cref="SerialPort.StopBits" />
public StopBits StopBits /// <remarks>
/// From the Specs:
/// <br/>
/// Should be <see cref="StopBits.One"/> for <see cref="Parity.Even"/> or <see cref="Parity.Odd"/>.
/// <br/>
/// Should be <see cref="StopBits.Two"/> for <see cref="Parity.None"/>.
/// </remarks>
public virtual StopBits StopBits
{ {
get => _serialPort.StopBits; get => _serialPort.StopBits;
set => _serialPort.StopBits = value; set => _serialPort.StopBits = value;
} }
/// <inheritdoc cref="SerialPortWrapper.IsOpen"/>
public bool IsOpen => _serialPort.IsOpen;
/// <summary> /// <summary>
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a write operation does not finish. /// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a read/receive operation does not finish.
/// </summary> /// </summary>
public TimeSpan WriteTimeout public virtual TimeSpan ReadTimeout
{
get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout);
set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds;
}
/// <summary>
/// Gets or sets the <see cref="TimeSpan"/> before a time-out occurs when a write/send operation does not finish.
/// </summary>
public virtual TimeSpan WriteTimeout
{ {
get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout); get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout);
set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds; set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds;
} }
/// <inheritdoc cref="SerialPort.Parity"/> #endregion SerialPort Properties
public Parity Parity
{
get => _serialPort.Parity;
set => _serialPort.Parity = value;
}
#endregion Properties #endregion Properties
#region Control Methods #region Control Methods
/// <summary> /// <summary>
/// Starts the server. /// Starts the proxy.
/// </summary> /// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param> /// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public Task StartAsync(CancellationToken cancellationToken = default) public Task StartAsync(CancellationToken cancellationToken = default)
@@ -164,23 +186,22 @@ namespace AMWD.Protocols.Modbus.Proxy
} }
/// <summary> /// <summary>
/// Stops the server. /// Stops the proxy.
/// </summary> /// </summary>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param> /// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public Task StopAsync(CancellationToken cancellationToken = default) public Task StopAsync(CancellationToken cancellationToken = default)
{ {
Assertions(); Assertions();
return StopAsyncInternal(cancellationToken); StopAsyncInternal();
return Task.CompletedTask;
} }
private Task StopAsyncInternal(CancellationToken cancellationToken) private void StopAsyncInternal()
{ {
_stopCts.Cancel(); _stopCts?.Cancel();
_serialPort.Close(); _serialPort.Close();
_serialPort.DataReceived -= OnDataReceived; _serialPort.DataReceived -= OnDataReceived;
return Task.CompletedTask;
} }
/// <summary> /// <summary>
@@ -193,7 +214,7 @@ namespace AMWD.Protocols.Modbus.Proxy
_isDisposed = true; _isDisposed = true;
StopAsyncInternal(CancellationToken.None).Wait(); StopAsyncInternal();
_serialPort.Dispose(); _serialPort.Dispose();
_stopCts?.Dispose(); _stopCts?.Dispose();
@@ -207,13 +228,16 @@ namespace AMWD.Protocols.Modbus.Proxy
if (_isDisposed) if (_isDisposed)
throw new ObjectDisposedException(GetType().FullName); throw new ObjectDisposedException(GetType().FullName);
#endif #endif
if (string.IsNullOrWhiteSpace(PortName))
throw new ArgumentNullException(nameof(PortName), "The serial port name cannot be empty.");
} }
#endregion Control Methods #endregion Control Methods
#region Client Handling #region Client Handling
private void OnDataReceived(object _, SerialDataReceivedEventArgs evArgs) private void OnDataReceived(object _, SerialDataReceivedEventArgs __)
{ {
try try
{ {
@@ -282,16 +306,14 @@ namespace AMWD.Protocols.Modbus.Proxy
default: // unknown function default: // unknown function
{ {
byte[] responseBytes = new byte[5]; var responseBytes = new List<byte>();
Array.Copy(requestBytes, 0, responseBytes, 0, 2); responseBytes.AddRange(requestBytes.Take(2));
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
// Mark as error // Mark as error
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes[2] = (byte)ModbusErrorCode.IllegalFunction; return ReturnResponse(responseBytes);
SetCrc(responseBytes);
return responseBytes;
} }
} }
} }
@@ -309,7 +331,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(2)); responseBytes.AddRange(requestBytes.Take(2));
try try
{ {
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken); var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)]; byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
for (int i = 0; i < coils.Count; i++) for (int i = 0; i < coils.Count; i++)
@@ -332,8 +354,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -349,7 +370,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(2)); responseBytes.AddRange(requestBytes.Take(2));
try try
{ {
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken); var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)]; byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
for (int i = 0; i < discreteInputs.Count; i++) for (int i = 0; i < discreteInputs.Count; i++)
@@ -372,8 +393,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -389,7 +409,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(2)); responseBytes.AddRange(requestBytes.Take(2));
try try
{ {
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken); var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[holdingRegisters.Count * 2]; byte[] values = new byte[holdingRegisters.Count * 2];
for (int i = 0; i < holdingRegisters.Count; i++) for (int i = 0; i < holdingRegisters.Count; i++)
@@ -407,8 +427,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -424,7 +443,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(2)); responseBytes.AddRange(requestBytes.Take(2));
try try
{ {
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken); var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[count * 2]; byte[] values = new byte[count * 2];
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
@@ -442,8 +461,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -461,8 +479,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
try try
@@ -474,7 +491,7 @@ namespace AMWD.Protocols.Modbus.Proxy
LowByte = requestBytes[5], LowByte = requestBytes[5],
}; };
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken); bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[0], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -492,8 +509,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -514,7 +530,7 @@ namespace AMWD.Protocols.Modbus.Proxy
LowByte = requestBytes[5] LowByte = requestBytes[5]
}; };
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken); bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[0], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -532,8 +548,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -553,8 +568,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
try try
@@ -576,7 +590,7 @@ namespace AMWD.Protocols.Modbus.Proxy
}); });
} }
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken); bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[0], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -594,8 +608,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -604,7 +617,7 @@ namespace AMWD.Protocols.Modbus.Proxy
return null; return null;
var responseBytes = new List<byte>(); var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(2));
ushort firstAddress = requestBytes.GetBigEndianUInt16(2); ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
ushort count = requestBytes.GetBigEndianUInt16(4); ushort count = requestBytes.GetBigEndianUInt16(4);
@@ -615,8 +628,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
try try
@@ -633,18 +645,18 @@ namespace AMWD.Protocols.Modbus.Proxy
HighByte = requestBytes[baseOffset + i * 2], HighByte = requestBytes[baseOffset + i * 2],
LowByte = requestBytes[baseOffset + i * 2 + 1] LowByte = requestBytes[baseOffset + i * 2 + 1]
}); });
}
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken); bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[0], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
responseBytes.AddRange(requestBytes.Skip(2).Take(4)); responseBytes.AddRange(requestBytes.Skip(2).Take(4));
} }
else else
{ {
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
} }
} }
catch catch
@@ -653,12 +665,14 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
{ {
if (requestBytes.Length < 7)
return null;
var responseBytes = new List<byte>(); var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(2)); responseBytes.AddRange(requestBytes.Take(2));
@@ -667,8 +681,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction); responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[4]; var firstObject = (ModbusDeviceIdentificationObject)requestBytes[4];
@@ -677,8 +690,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
var category = (ModbusDeviceIdentificationCategory)requestBytes[3]; var category = (ModbusDeviceIdentificationCategory)requestBytes[3];
@@ -687,13 +699,12 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
try try
{ {
var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken); var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[0], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
var bodyBytes = new List<byte>(); var bodyBytes = new List<byte>();
@@ -702,31 +713,20 @@ namespace AMWD.Protocols.Modbus.Proxy
// Conformity // Conformity
bodyBytes.Add((byte)category); bodyBytes.Add((byte)category);
if (res.IsIndividualAccessAllowed) if (deviceInfo.IsIndividualAccessAllowed)
bodyBytes[2] |= 0x80; bodyBytes[2] |= 0x80;
// More, NextId, NumberOfObjects // More, NextId, NumberOfObjects
bodyBytes.AddRange(new byte[3]); bodyBytes.AddRange(new byte[3]);
int maxObjectId; int maxObjectId = category switch
switch (category)
{ {
case ModbusDeviceIdentificationCategory.Basic: ModbusDeviceIdentificationCategory.Basic => 0x02,
maxObjectId = 0x02; ModbusDeviceIdentificationCategory.Regular => 0x06,
break; ModbusDeviceIdentificationCategory.Extended => 0xFF,
// Individual
case ModbusDeviceIdentificationCategory.Regular: _ => requestBytes[4],
maxObjectId = 0x06; };
break;
case ModbusDeviceIdentificationCategory.Extended:
maxObjectId = 0xFF;
break;
default: // Individual
maxObjectId = requestBytes[4];
break;
}
byte numberOfObjects = 0; byte numberOfObjects = 0;
for (int i = requestBytes[4]; i <= maxObjectId; i++) for (int i = requestBytes[4]; i <= maxObjectId; i++)
@@ -735,17 +735,19 @@ namespace AMWD.Protocols.Modbus.Proxy
if (0x07 <= i && i <= 0x7F) if (0x07 <= i && i <= 0x7F)
continue; continue;
byte[] objBytes = GetDeviceObject((byte)i, res); byte[] objBytes = GetDeviceObject((byte)i, deviceInfo);
// We need to split the response if it would exceed the max ADU size // We need to split the response if it would exceed the max ADU size.
if (responseBytes.Count + bodyBytes.Count + objBytes.Length > RtuProtocol.MAX_ADU_LENGTH) // 2 bytes of CRC have to be added.
if (responseBytes.Count + bodyBytes.Count + objBytes.Length + 2 > RtuProtocol.MAX_ADU_LENGTH)
{ {
bodyBytes[3] = 0xFF; bodyBytes[3] = 0xFF;
bodyBytes[4] = (byte)i; bodyBytes[4] = (byte)i;
bodyBytes[5] = numberOfObjects; bodyBytes[5] = numberOfObjects;
responseBytes.AddRange(bodyBytes); responseBytes.AddRange(bodyBytes);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
bodyBytes.AddRange(objBytes); bodyBytes.AddRange(objBytes);
@@ -755,16 +757,14 @@ namespace AMWD.Protocols.Modbus.Proxy
bodyBytes[5] = numberOfObjects; bodyBytes[5] = numberOfObjects;
responseBytes.AddRange(bodyBytes); responseBytes.AddRange(bodyBytes);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
catch catch
{ {
responseBytes[1] |= 0x80; responseBytes[1] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
AddCrc(responseBytes); return ReturnResponse(responseBytes);
return [.. responseBytes];
} }
} }
@@ -775,7 +775,7 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
case ModbusDeviceIdentificationObject.VendorName: case ModbusDeviceIdentificationObject.VendorName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -783,7 +783,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ProductCode: case ModbusDeviceIdentificationObject.ProductCode:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -791,7 +791,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.MajorMinorRevision: case ModbusDeviceIdentificationObject.MajorMinorRevision:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -799,7 +799,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.VendorUrl: case ModbusDeviceIdentificationObject.VendorUrl:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -807,7 +807,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ProductName: case ModbusDeviceIdentificationObject.ProductName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -815,7 +815,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ModelName: case ModbusDeviceIdentificationObject.ModelName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -823,7 +823,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.UserApplicationName: case ModbusDeviceIdentificationObject.UserApplicationName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -831,9 +831,8 @@ namespace AMWD.Protocols.Modbus.Proxy
default: default:
{ {
if (deviceIdentification.ExtendedObjects.ContainsKey(objectId)) if (deviceIdentification.ExtendedObjects.TryGetValue(objectId, out byte[] bytes))
{ {
byte[] bytes = deviceIdentification.ExtendedObjects[objectId];
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -848,20 +847,28 @@ namespace AMWD.Protocols.Modbus.Proxy
return [.. result]; return [.. result];
} }
private static void SetCrc(byte[] bytes) private static byte[] ReturnResponse(List<byte> response)
{ {
byte[] crc = RtuProtocol.CRC16(bytes, 0, bytes.Length - 2); response.AddRange(RtuProtocol.CRC16(response));
bytes[bytes.Length - 2] = crc[0]; return [.. response];
bytes[bytes.Length - 1] = crc[1];
}
private static void AddCrc(List<byte> bytes)
{
byte[] crc = RtuProtocol.CRC16(bytes);
bytes.Add(crc[0]);
bytes.Add(crc[1]);
} }
#endregion Request Handling #endregion Request Handling
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"RTU Proxy");
sb.AppendLine($" {nameof(PortName)}: {PortName}");
sb.AppendLine($" {nameof(BaudRate)}: {(int)BaudRate}");
sb.AppendLine($" {nameof(DataBits)}: {DataBits}");
sb.AppendLine($" {nameof(StopBits)}: {StopBits}");
sb.AppendLine($" {nameof(Parity)}: {Parity}");
sb.AppendLine($" {nameof(Client)}: {Client.GetType().Name}");
return sb.ToString();
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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,8 +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 cref="IModbusConnection.IdleTimeout"/> /// <inheritdoc cref="IModbusConnection.IdleTimeout"/>
public TimeSpan IdleTimeout public TimeSpan IdleTimeout
@@ -223,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();
}
} }
} }

View File

@@ -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); 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); await _portLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
try try
{ {
// Ensure connection is up // Ensure connection is up
await AssertConnection(linkedCts.Token); await AssertConnection(linkedCts.Token);
await _serialPort.WriteAsync(item.Request, linkedCts.Token); 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); 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); 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); await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
} }
catch catch
{ /* keep it quiet */ } { /* keep it quiet */ }

View File

@@ -44,10 +44,10 @@ using var client = new ModbusSerialClient(serialPort)
--- ---
Published under MIT License (see [**tl;dr**Legal]) Published under MIT License (see [choose a license])
[v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf [v1.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/

View File

@@ -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>

View File

@@ -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>

View File

@@ -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); 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();

View 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 */ }
}
}
}

View File

@@ -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;
@@ -101,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();
}
} }
} }

View File

@@ -34,8 +34,8 @@ namespace AMWD.Protocols.Modbus.Tcp
private readonly Task _processingTask; private readonly Task _processingTask;
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new(); private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
private TimeSpan _readTimeout = TimeSpan.FromMilliseconds(1); private TimeSpan _readTimeout = TimeSpan.FromSeconds(1);
private TimeSpan _writeTimeout = TimeSpan.FromMilliseconds(1); private TimeSpan _writeTimeout = TimeSpan.FromSeconds(1);
#endregion Fields #endregion Fields
@@ -65,8 +65,12 @@ namespace AMWD.Protocols.Modbus.Tcp
get => _readTimeout; get => _readTimeout;
set set
{ {
#if NET8_0_OR_GREATER
ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
#else
if (value < TimeSpan.Zero) if (value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value)); throw new ArgumentOutOfRangeException(nameof(value));
#endif
_readTimeout = value; _readTimeout = value;
@@ -81,8 +85,12 @@ namespace AMWD.Protocols.Modbus.Tcp
get => _writeTimeout; get => _writeTimeout;
set set
{ {
#if NET8_0_OR_GREATER
ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
#else
if (value < TimeSpan.Zero) if (value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value)); throw new ArgumentOutOfRangeException(nameof(value));
#endif
_writeTimeout = value; _writeTimeout = value;
@@ -208,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); 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();
@@ -216,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); await _clientLock.WaitAsync(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
try try
{ {
// Ensure connection is up // Ensure connection is up
await AssertConnection(linkedCts.Token); await AssertConnection(linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
var stream = _tcpClient.GetStream(); var stream = _tcpClient.GetStream();
await stream.FlushAsync(linkedCts.Token); await stream.FlushAsync(linkedCts.Token);
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
await stream.WriteAsync(item.Request, linkedCts.Token); await stream.WriteAsync(item.Request, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
#else #else
await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token); await stream.WriteAsync(item.Request, 0, item.Request.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
#endif #endif
linkedCts.Token.ThrowIfCancellationRequested(); linkedCts.Token.ThrowIfCancellationRequested();
@@ -239,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); int readCount = await stream.ReadAsync(buffer, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
#else #else
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token); int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, linkedCts.Token).ConfigureAwait(continueOnCapturedContext: false);
#endif #endif
if (readCount < 1) if (readCount < 1)
throw new EndOfStreamException(); throw new EndOfStreamException();
@@ -332,7 +340,7 @@ namespace AMWD.Protocols.Modbus.Tcp
try try
{ {
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken); await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
} }
catch catch
{ /* keep it quiet */ } { /* keep it quiet */ }
@@ -376,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
{ {

View File

@@ -10,62 +10,38 @@ using System.Threading.Tasks;
using AMWD.Protocols.Modbus.Common; using AMWD.Protocols.Modbus.Common;
using AMWD.Protocols.Modbus.Common.Contracts; using AMWD.Protocols.Modbus.Common.Contracts;
using AMWD.Protocols.Modbus.Common.Protocols; using AMWD.Protocols.Modbus.Common.Protocols;
using AMWD.Protocols.Modbus.Tcp.Extensions;
using AMWD.Protocols.Modbus.Tcp.Utils;
namespace AMWD.Protocols.Modbus.Proxy namespace AMWD.Protocols.Modbus.Tcp
{ {
/// <summary> /// <summary>
/// Implements a Modbus TCP server proxying all requests to a Modbus client of choice. /// Implements a Modbus TCP server proxying all requests to a Modbus client of choice.
/// </summary> /// </summary>
public class ModbusTcpProxy : IDisposable /// <remarks>
/// Initializes a new instance of the <see cref="ModbusTcpProxy"/> class.
/// </remarks>
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
/// <param name="listenAddress">An <see cref="IPAddress"/> to listen on.</param>
public class ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress) : IModbusProxy
{ {
#region Fields #region Fields
private bool _isDisposed; private bool _isDisposed;
private TcpListener _listener; private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(100);
private readonly TcpListenerWrapper _tcpListener = new(listenAddress, 502);
private CancellationTokenSource _stopCts; private CancellationTokenSource _stopCts;
private Task _clientConnectTask = Task.CompletedTask; private Task _clientConnectTask = Task.CompletedTask;
private readonly SemaphoreSlim _clientListLock = new(1, 1); private readonly SemaphoreSlim _clientListLock = new(1, 1);
private readonly List<TcpClient> _clients = []; private readonly List<TcpClientWrapper> _clients = [];
private readonly List<Task> _clientTasks = [];
#endregion Fields #endregion Fields
#region Constructors #region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ModbusTcpProxy"/> class.
/// </summary>
/// <param name="client">The <see cref="ModbusClientBase"/> used to request the remote device, that should be proxied.</param>
/// <param name="listenAddress">An <see cref="IPAddress"/> to listen on (Default: <see cref="IPAddress.Loopback"/>).</param>
/// <param name="listenPort">A port to listen on (Default: 502).</param>
public ModbusTcpProxy(ModbusClientBase client, IPAddress listenAddress = null, int listenPort = 502)
{
Client = client ?? throw new ArgumentNullException(nameof(client));
ListenAddress = listenAddress ?? IPAddress.Loopback;
if (ushort.MinValue < listenPort || listenPort < ushort.MaxValue)
throw new ArgumentOutOfRangeException(nameof(listenPort));
try
{
#if NET8_0_OR_GREATER
using var testListener = new TcpListener(ListenAddress, listenPort);
#else
var testListener = new TcpListener(ListenAddress, listenPort);
#endif
testListener.Start(1);
ListenPort = (testListener.LocalEndpoint as IPEndPoint).Port;
testListener.Stop();
}
catch (Exception ex)
{
throw new ArgumentException($"{nameof(ListenPort)} ({listenPort}) is already in use.", ex);
}
}
#endregion Constructors #endregion Constructors
#region Properties #region Properties
@@ -73,27 +49,46 @@ namespace AMWD.Protocols.Modbus.Proxy
/// <summary> /// <summary>
/// Gets the Modbus client used to request the remote device, that should be proxied. /// Gets the Modbus client used to request the remote device, that should be proxied.
/// </summary> /// </summary>
public ModbusClientBase Client { get; } public ModbusClientBase Client { get; } = client ?? throw new ArgumentNullException(nameof(client));
/// <summary> /// <summary>
/// Gets the <see cref="IPAddress"/> to listen on. /// Gets the <see cref="IPAddress"/> to listen on.
/// </summary> /// </summary>
public IPAddress ListenAddress { get; } public IPAddress ListenAddress
{
get => _tcpListener.LocalIPEndPoint.Address;
set => _tcpListener.LocalIPEndPoint.Address = value;
}
/// <summary> /// <summary>
/// Get the port to listen on. /// Get the port to listen on.
/// </summary> /// </summary>
public int ListenPort { get; } public int ListenPort
{
get => _tcpListener.LocalIPEndPoint.Port;
set => _tcpListener.LocalIPEndPoint.Port = value;
}
/// <summary> /// <summary>
/// Gets a value indicating whether the server is running. /// Gets a value indicating whether the server is running.
/// </summary> /// </summary>
public bool IsRunning => _listener?.Server.IsBound ?? false; public bool IsRunning => _tcpListener.Socket.IsBound;
/// <summary> /// <summary>
/// Gets or sets the read/write timeout for the incoming connections (not the <see cref="Client"/>!). /// Gets or sets the read/write timeout for the incoming connections (not the <see cref="Client"/>!).
/// Default: 100 seconds.
/// </summary> /// </summary>
public TimeSpan ReadWriteTimeout { get; set; } public TimeSpan ReadWriteTimeout
{
get => _readWriteTimeout;
set
{
if (value != Timeout.InfiniteTimeSpan && value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value));
_readWriteTimeout = value;
}
}
#endregion Properties #endregion Properties
@@ -108,20 +103,17 @@ namespace AMWD.Protocols.Modbus.Proxy
Assertions(); Assertions();
_stopCts?.Cancel(); _stopCts?.Cancel();
_tcpListener.Stop();
_listener?.Stop();
#if NET8_0_OR_GREATER
_listener?.Dispose();
#endif
_stopCts?.Dispose(); _stopCts?.Dispose();
_stopCts = new CancellationTokenSource(); _stopCts = new CancellationTokenSource();
_listener = new TcpListener(ListenAddress, ListenPort); // Only allowed to set, if the socket is in the InterNetworkV6 address family.
// See: https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.dualmode?view=netstandard-2.0#exceptions
if (ListenAddress.AddressFamily == AddressFamily.InterNetworkV6) if (ListenAddress.AddressFamily == AddressFamily.InterNetworkV6)
_listener.Server.DualMode = true; _tcpListener.Socket.DualMode = true;
_listener.Start(); _tcpListener.Start();
_clientConnectTask = WaitForClientAsync(_stopCts.Token); _clientConnectTask = WaitForClientAsync(_stopCts.Token);
return Task.CompletedTask; return Task.CompletedTask;
@@ -139,24 +131,12 @@ namespace AMWD.Protocols.Modbus.Proxy
private async Task StopAsyncInternal(CancellationToken cancellationToken = default) private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
{ {
_stopCts.Cancel(); _stopCts?.Cancel();
_tcpListener.Stop();
_listener.Stop();
#if NET8_0_OR_GREATER
_listener.Dispose();
#endif
try
{
await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
catch (OperationCanceledException)
{
// Terminated
}
try try
{ {
await Task.WhenAny(Task.WhenAll(_clientTasks), Task.Delay(Timeout.Infinite, cancellationToken)); await Task.WhenAny(_clientConnectTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(continueOnCapturedContext: false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -178,8 +158,10 @@ namespace AMWD.Protocols.Modbus.Proxy
_clientListLock.Dispose(); _clientListLock.Dispose();
_clients.Clear(); _clients.Clear();
_tcpListener.Dispose();
_stopCts?.Dispose(); _stopCts?.Dispose();
GC.SuppressFinalize(this);
} }
private void Assertions() private void Assertions()
@@ -202,16 +184,13 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
try try
{ {
#if NET8_0_OR_GREATER var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
var client = await _listener.AcceptTcpClientAsync(cancellationToken); await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
#else
var client = await _listener.AcceptTcpClientAsync();
#endif
await _clientListLock.WaitAsync(cancellationToken);
try try
{ {
_clients.Add(client); _clients.Add(client);
_clientTasks.Add(HandleClientAsync(client, cancellationToken)); // Can be ignored as it will terminate by itself on cancellation
HandleClientAsync(client, cancellationToken).Forget();
} }
finally finally
{ {
@@ -225,7 +204,7 @@ namespace AMWD.Protocols.Modbus.Proxy
} }
} }
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken) private async Task HandleClientAsync(TcpClientWrapper client, CancellationToken cancellationToken)
{ {
try try
{ {
@@ -237,20 +216,20 @@ namespace AMWD.Protocols.Modbus.Proxy
using (var cts = new CancellationTokenSource(ReadWriteTimeout)) using (var cts = new CancellationTokenSource(ReadWriteTimeout))
using (cancellationToken.Register(cts.Cancel)) using (cancellationToken.Register(cts.Cancel))
{ {
byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token); byte[] headerBytes = await stream.ReadExpectedBytesAsync(6, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
requestBytes.AddRange(headerBytes); requestBytes.AddRange(headerBytes);
byte[] followingCountBytes = headerBytes.Skip(4).Take(2).ToArray(); ushort length = headerBytes
followingCountBytes.SwapBigEndian(); .Skip(4).Take(2).ToArray()
int followingCount = BitConverter.ToUInt16(followingCountBytes, 0); .GetBigEndianUInt16();
byte[] bodyBytes = await stream.ReadExpectedBytesAsync(followingCount, cts.Token); byte[] bodyBytes = await stream.ReadExpectedBytesAsync(length, cts.Token).ConfigureAwait(continueOnCapturedContext: false);
requestBytes.AddRange(bodyBytes); requestBytes.AddRange(bodyBytes);
} }
byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken); byte[] responseBytes = await HandleRequestAsync([.. requestBytes], cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (responseBytes != null) if (responseBytes != null)
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken); await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
} }
} }
catch catch
@@ -259,7 +238,7 @@ namespace AMWD.Protocols.Modbus.Proxy
} }
finally finally
{ {
await _clientListLock.WaitAsync(cancellationToken); await _clientListLock.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
try try
{ {
_clients.Remove(client); _clients.Remove(client);
@@ -309,14 +288,14 @@ namespace AMWD.Protocols.Modbus.Proxy
default: // unknown function default: // unknown function
{ {
byte[] responseBytes = new byte[9]; var responseBytes = new List<byte>();
Array.Copy(requestBytes, 0, responseBytes, 0, 8); responseBytes.AddRange(requestBytes.Take(8));
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
// Mark as error // Mark as error
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes[8] = (byte)ModbusErrorCode.IllegalFunction; return Task.FromResult(ReturnResponse(responseBytes));
return Task.FromResult(responseBytes);
} }
} }
} }
@@ -334,7 +313,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(8));
try try
{ {
var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken); var coils = await Client.ReadCoilsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)]; byte[] values = new byte[(int)Math.Ceiling(coils.Count / 8.0)];
for (int i = 0; i < coils.Count; i++) for (int i = 0; i < coils.Count; i++)
@@ -357,7 +336,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadDiscreteInputsAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -373,7 +352,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(8));
try try
{ {
var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken); var discreteInputs = await Client.ReadDiscreteInputsAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)]; byte[] values = new byte[(int)Math.Ceiling(discreteInputs.Count / 8.0)];
for (int i = 0; i < discreteInputs.Count; i++) for (int i = 0; i < discreteInputs.Count; i++)
@@ -396,7 +375,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadHoldingRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -412,7 +391,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(8));
try try
{ {
var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken); var holdingRegisters = await Client.ReadHoldingRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[holdingRegisters.Count * 2]; byte[] values = new byte[holdingRegisters.Count * 2];
for (int i = 0; i < holdingRegisters.Count; i++) for (int i = 0; i < holdingRegisters.Count; i++)
@@ -430,7 +409,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleReadInputRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -446,7 +425,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(8));
try try
{ {
var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken); var inputRegisters = await Client.ReadInputRegistersAsync(unitId, firstAddress, count, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
byte[] values = new byte[count * 2]; byte[] values = new byte[count * 2];
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
@@ -464,7 +443,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteSingleCoilAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -481,7 +460,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
try try
@@ -493,7 +473,7 @@ namespace AMWD.Protocols.Modbus.Proxy
LowByte = requestBytes[11], LowByte = requestBytes[11],
}; };
bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken); bool isSuccess = await Client.WriteSingleCoilAsync(requestBytes[6], coil, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -511,7 +491,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteSingleRegisterAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -533,7 +513,7 @@ namespace AMWD.Protocols.Modbus.Proxy
LowByte = requestBytes[11] LowByte = requestBytes[11]
}; };
bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken); bool isSuccess = await Client.WriteSingleHoldingRegisterAsync(requestBytes[6], register, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -551,7 +531,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteMultipleCoilsAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -570,7 +550,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
try try
@@ -592,7 +573,7 @@ namespace AMWD.Protocols.Modbus.Proxy
}); });
} }
bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken); bool isSuccess = await Client.WriteMultipleCoilsAsync(requestBytes[6], coils, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
@@ -610,7 +591,7 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleWriteMultipleRegistersAsync(byte[] requestBytes, CancellationToken cancellationToken)
@@ -629,7 +610,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
try try
@@ -646,18 +628,18 @@ namespace AMWD.Protocols.Modbus.Proxy
HighByte = requestBytes[baseOffset + i * 2], HighByte = requestBytes[baseOffset + i * 2],
LowByte = requestBytes[baseOffset + i * 2 + 1] LowByte = requestBytes[baseOffset + i * 2 + 1]
}); });
}
bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken); bool isSuccess = await Client.WriteMultipleHoldingRegistersAsync(requestBytes[6], list, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
if (isSuccess) if (isSuccess)
{ {
// Response is an echo of the request // Response is an echo of the request
responseBytes.AddRange(requestBytes.Skip(8).Take(4)); responseBytes.AddRange(requestBytes.Skip(8).Take(4));
} }
else else
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
}
} }
} }
catch catch
@@ -666,11 +648,14 @@ namespace AMWD.Protocols.Modbus.Proxy
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
} }
return [.. responseBytes]; return ReturnResponse(responseBytes);
} }
private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken) private async Task<byte[]> HandleEncapsulatedInterfaceAsync(byte[] requestBytes, CancellationToken cancellationToken)
{ {
if (requestBytes.Length < 11)
return null;
var responseBytes = new List<byte>(); var responseBytes = new List<byte>();
responseBytes.AddRange(requestBytes.Take(8)); responseBytes.AddRange(requestBytes.Take(8));
@@ -678,7 +663,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalFunction); responseBytes.Add((byte)ModbusErrorCode.IllegalFunction);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
var firstObject = (ModbusDeviceIdentificationObject)requestBytes[10]; var firstObject = (ModbusDeviceIdentificationObject)requestBytes[10];
@@ -686,7 +672,8 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress); responseBytes.Add((byte)ModbusErrorCode.IllegalDataAddress);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
var category = (ModbusDeviceIdentificationCategory)requestBytes[9]; var category = (ModbusDeviceIdentificationCategory)requestBytes[9];
@@ -694,12 +681,13 @@ namespace AMWD.Protocols.Modbus.Proxy
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue); responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
try try
{ {
var res = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken); var deviceInfo = await Client.ReadDeviceIdentificationAsync(requestBytes[6], category, firstObject, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
var bodyBytes = new List<byte>(); var bodyBytes = new List<byte>();
@@ -708,31 +696,20 @@ namespace AMWD.Protocols.Modbus.Proxy
// Conformity // Conformity
bodyBytes.Add((byte)category); bodyBytes.Add((byte)category);
if (res.IsIndividualAccessAllowed) if (deviceInfo.IsIndividualAccessAllowed)
bodyBytes[2] |= 0x80; bodyBytes[2] |= 0x80;
// More, NextId, NumberOfObjects // More, NextId, NumberOfObjects
bodyBytes.AddRange(new byte[3]); bodyBytes.AddRange(new byte[3]);
int maxObjectId; int maxObjectId = category switch
switch (category)
{ {
case ModbusDeviceIdentificationCategory.Basic: ModbusDeviceIdentificationCategory.Basic => 0x02,
maxObjectId = 0x02; ModbusDeviceIdentificationCategory.Regular => 0x06,
break; ModbusDeviceIdentificationCategory.Extended => 0xFF,
// Individual
case ModbusDeviceIdentificationCategory.Regular: _ => requestBytes[10],
maxObjectId = 0x06; };
break;
case ModbusDeviceIdentificationCategory.Extended:
maxObjectId = 0xFF;
break;
default: // Individual
maxObjectId = requestBytes[10];
break;
}
byte numberOfObjects = 0; byte numberOfObjects = 0;
for (int i = requestBytes[10]; i <= maxObjectId; i++) for (int i = requestBytes[10]; i <= maxObjectId; i++)
@@ -741,7 +718,7 @@ namespace AMWD.Protocols.Modbus.Proxy
if (0x07 <= i && i <= 0x7F) if (0x07 <= i && i <= 0x7F)
continue; continue;
byte[] objBytes = GetDeviceObject((byte)i, res); byte[] objBytes = GetDeviceObject((byte)i, deviceInfo);
// We need to split the response if it would exceed the max ADU size // 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) if (responseBytes.Count + bodyBytes.Count + objBytes.Length > TcpProtocol.MAX_ADU_LENGTH)
@@ -751,7 +728,8 @@ namespace AMWD.Protocols.Modbus.Proxy
bodyBytes[5] = numberOfObjects; bodyBytes[5] = numberOfObjects;
responseBytes.AddRange(bodyBytes); responseBytes.AddRange(bodyBytes);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
bodyBytes.AddRange(objBytes); bodyBytes.AddRange(objBytes);
@@ -760,24 +738,26 @@ namespace AMWD.Protocols.Modbus.Proxy
bodyBytes[5] = numberOfObjects; bodyBytes[5] = numberOfObjects;
responseBytes.AddRange(bodyBytes); responseBytes.AddRange(bodyBytes);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
catch catch
{ {
responseBytes[7] |= 0x80; responseBytes[7] |= 0x80;
responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure); responseBytes.Add((byte)ModbusErrorCode.SlaveDeviceFailure);
return [.. responseBytes];
return ReturnResponse(responseBytes);
} }
} }
private byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification) private static byte[] GetDeviceObject(byte objectId, DeviceIdentification deviceIdentification)
{ {
var result = new List<byte> { objectId }; var result = new List<byte> { objectId };
switch ((ModbusDeviceIdentificationObject)objectId) switch ((ModbusDeviceIdentificationObject)objectId)
{ {
case ModbusDeviceIdentificationObject.VendorName: case ModbusDeviceIdentificationObject.VendorName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -785,7 +765,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ProductCode: case ModbusDeviceIdentificationObject.ProductCode:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductCode ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -793,7 +773,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.MajorMinorRevision: case ModbusDeviceIdentificationObject.MajorMinorRevision:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.MajorMinorRevision ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -801,7 +781,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.VendorUrl: case ModbusDeviceIdentificationObject.VendorUrl:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.VendorUrl ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -809,7 +789,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ProductName: case ModbusDeviceIdentificationObject.ProductName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ProductName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -817,7 +797,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.ModelName: case ModbusDeviceIdentificationObject.ModelName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.ModelName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -825,7 +805,7 @@ namespace AMWD.Protocols.Modbus.Proxy
case ModbusDeviceIdentificationObject.UserApplicationName: case ModbusDeviceIdentificationObject.UserApplicationName:
{ {
byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName); byte[] bytes = Encoding.UTF8.GetBytes(deviceIdentification.UserApplicationName ?? "");
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -833,9 +813,8 @@ namespace AMWD.Protocols.Modbus.Proxy
default: default:
{ {
if (deviceIdentification.ExtendedObjects.ContainsKey(objectId)) if (deviceIdentification.ExtendedObjects.TryGetValue(objectId, out byte[] bytes))
{ {
byte[] bytes = deviceIdentification.ExtendedObjects[objectId];
result.Add((byte)bytes.Length); result.Add((byte)bytes.Length);
result.AddRange(bytes); result.AddRange(bytes);
} }
@@ -850,6 +829,29 @@ namespace AMWD.Protocols.Modbus.Proxy
return [.. result]; 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 #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

View File

@@ -23,14 +23,14 @@ 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 `RTU over TCP` protocol instead, you can do this on initialization: If you have a device speaking `RTU` connected over `TCP`, you can use it as followed:
```csharp ```csharp
// [...] // [...]
using var client = new ModbusTcpClient(host, port) using var client = new ModbusTcpClient(host, port)
{ {
Protocol = new RtuOverTcpProtocol(); Protocol = new RtuProtocol()
}; };
// [...] // [...]
@@ -44,10 +44,10 @@ using var client = new ModbusTcpClient(host, port)
--- ---
Published under MIT License (see [**tl;dr**Legal]) Published under MIT License (see [choose a license])
[v1.1b3]: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf [v1.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/

View 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
}
}

View File

@@ -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()

View 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();
}
}

View File

@@ -3,19 +3,30 @@ 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
{ {
/// <inheritdoc cref="TcpClient" /> /// <inheritdoc cref="TcpClient" />
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class TcpClientWrapper(AddressFamily addressFamily) : IDisposable internal class TcpClientWrapper : IDisposable
{ {
#region Fields #region Fields
private readonly TcpClient _client = new(addressFamily); 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" />

View File

@@ -3,6 +3,9 @@ using System.Net.Sockets;
namespace AMWD.Protocols.Modbus.Tcp.Utils namespace AMWD.Protocols.Modbus.Tcp.Utils
{ {
/// <summary>
/// Factory for creating <see cref="TcpClientWrapper"/> instances.
/// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal class TcpClientWrapperFactory internal class TcpClientWrapperFactory
{ {

View 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
}
}

View File

@@ -2,35 +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.collector" Version="6.0.2"> <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="coverlet.msbuild" Version="6.0.2"> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PrivateAssets>all</PrivateAssets> <PackageReference Include="Moq" Version="4.20.72" />
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PackageReference Include="MSTest.TestAdapter" Version="3.7.2" />
</PackageReference> <PackageReference Include="MSTest.TestFramework" Version="3.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
</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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}
} }
} }

View File

@@ -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;
} }
} }

View File

@@ -0,0 +1,9 @@
DeviceIdentification
VendorName: VendorName
ProductCode: ProductCode
MajorMinorRevision: MajorMinorRevision
VendorUrl:
ProductName:
ModelName:
UserApplicationName:
IsIndividualAccessAllowed: False

View File

@@ -0,0 +1,8 @@
Serial Client COM-42
BaudRate: 2400
DataBits: 7
StopBits: 1.5
Parity: space
Handshake: xonxoff
RtsEnable: true
DriverEnabledRS485: true

View 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);
}
}
}
}

View File

@@ -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);
}
} }
} }

View File

@@ -80,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]
@@ -119,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]
@@ -235,7 +220,6 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
} }
[TestMethod] [TestMethod]
[ExpectedException(typeof(EndOfStreamException))]
public async Task ShouldThrowEndOfStreamExceptionOnInvokeAsync() public async Task ShouldThrowEndOfStreamExceptionOnInvokeAsync()
{ {
// Arrange // Arrange
@@ -244,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
@@ -264,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]
@@ -351,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];
@@ -363,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];
@@ -385,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]
@@ -498,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()))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
TCP Client 127.0.0.1
Port: 502

View File

@@ -0,0 +1,9 @@
DeviceIdentification
VendorName: VendorName
ProductCode: ProductCode
MajorMinorRevision: MajorMinorRevision
VendorUrl:
ProductName:
ModelName:
UserApplicationName:
IsIndividualAccessAllowed: False

View File

@@ -35,7 +35,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Tcp",
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Serial", "AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj", "{D966826F-EE6C-4BC0-9185-C2A9A50FD586}" 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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Proxy", "AMWD.Protocols.Modbus.Proxy\AMWD.Protocols.Modbus.Proxy.csproj", "{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliClient", "CliClient\CliClient.csproj", "{B0E53462-B0ED-4685-8AA5-948DC160EE27}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliProxy", "CliProxy\CliProxy.csproj", "{AC922E80-E9B6-493D-B1D1-752527E883ED}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -59,10 +61,14 @@ Global
{D966826F-EE6C-4BC0-9185-C2A9A50FD586}.Debug|Any CPU.Build.0 = Debug|Any CPU {D966826F-EE6C-4BC0-9185-C2A9A50FD586}.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
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B0E53462-B0ED-4685-8AA5-948DC160EE27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU {B0E53462-B0ED-4685-8AA5-948DC160EE27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU {B0E53462-B0ED-4685-8AA5-948DC160EE27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.Build.0 = Release|Any CPU {B0E53462-B0ED-4685-8AA5-948DC160EE27}.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

Binary file not shown.

View File

@@ -2,33 +2,82 @@
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]
_no changes_ _nothing changed yet_
## [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) ## [v0.3.0] (2024-05-31)
### Added ### Added
- New `AMWD.Protocols.Modbus.Proxy` package, that contains the server implementations as proxies - New `AMWD.Protocols.Modbus.Proxy` package, that contains the server implementations as proxies.
### Changed ### Changed
- Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used - Renamed `ModbusSerialServer` to `ModbusRtuServer` to clearify the protocol that is used.
- Made `Protocol` property of `ModbusClientBase` non-abstract - Made `Protocol` property of `ModbusClientBase` non-abstract.
### Fixed ### Fixed
- Issue with missing client on TCP connection when using default constructor (seems that `AddressFamily.Unknown` caused the problem) - Issue with missing client on TCP connection when using default constructor (seems that `AddressFamily.Unknown` caused the problem).
## [v0.2.0] (2024-04-02) ## [v0.2.0] (2024-04-02)
First "final" re-implementation First "final" re-implementation.
## v0.1.0 (2022-08-28) ## v0.1.0 (2022-08-28)
@@ -38,6 +87,10 @@ So this tag is only here for documentation purposes of the NuGet Gallery.
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...HEAD [Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.4.1...HEAD
[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.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
View 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;
}
}

View 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
}
}

View 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
View 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;
}
}
}

View 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
View 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;
}
}
}
}

View File

@@ -0,0 +1,9 @@
{
"profiles": {
"ConsoleApp": {
"commandName": "Project",
"commandLineArgs": "--debug COM1",
"remoteDebugEnabled": false
}
}
}

95
CliClient/README.md Normal file
View 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
View 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;
}
}

View 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
}
}

View 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
View 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
View 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
View 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
View 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/

View File

@@ -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>

View File

@@ -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,

View File

@@ -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.
![NuGet Version](https://shields.io/nuget/v/AMWD.Protocols.Modbus.Common?style=flat&logo=nuget)
![Test Coverage](https://git.am-wd.de/am-wd/amwd.protocols.modbus/badges/main/coverage.svg?style=flat)
## Overview ## Overview
The project is divided into four 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]).
@@ -20,11 +23,6 @@ For example the default protocol versions: `TCP`, `RTU` and `ASCII`.
With this package you'll have anything you need to create your own client implementations. With this package you'll have anything you need to create your own client implementations.
### [Proxy]
The package contains a TCP and a RTU server implementation as proxy which contains a client of your choice to connect to.
### [Serial] ### [Serial]
This package contains some wrappers and implementations for the serial protocol. This package contains some wrappers and implementations for the serial protocol.
@@ -39,16 +37,15 @@ It uses a specific TCP connection implementation and plugs all things from the C
--- ---
Published under [MIT License] (see [**tl;dr**Legal]) Published under [MIT License] (see [choose a license])
[![Buy me a Coffee](https://shields.am-wd.de/badge/PayPal-Buy_me_a_Coffee-yellow?style=flat&logo=paypal)](https://link.am-wd.de/donate) [![Buy me a Coffee](https://shields.io/badge/PayPal-Buy_me_a_Coffee-yellow?style=flat&logo=paypal)](https://link.am-wd.de/donate)
[![built with Codeium](https://codeium.com/badges/main)](https://link.am-wd.de/codeium) [![built with Codeium](https://codeium.com/badges/main)](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
[Proxy]: AMWD.Protocols.Modbus.Proxy/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 [choose a license]: https://choosealicense.com/licenses/mit/
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license