Added new cli tool for client connections
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Seria
|
|||||||
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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Protocols.Modbus.Proxy", "AMWD.Protocols.Modbus.Proxy\AMWD.Protocols.Modbus.Proxy.csproj", "{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI Client", "CliClient\CliClient.csproj", "{B0E53462-B0ED-4685-8AA5-948DC160EE27}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -63,6 +65,10 @@ Global
|
|||||||
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C30EBE45-E3B8-4997-95DF-8F94B31C8E1A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B0E53462-B0ED-4685-8AA5-948DC160EE27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Small CLI tool to test Modbus client communication.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- The `ModbusTcpProxy.ReadWriteTimeout` has a default value of 100 seconds (same default as a `HttpClient` has).
|
- The `ModbusTcpProxy.ReadWriteTimeout` has a default value of 100 seconds (same default as a `HttpClient` has).
|
||||||
|
|||||||
35
CliClient/Cli/Argument.cs
Normal file
35
CliClient/Cli/Argument.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
namespace AMWD.Common.Cli
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a logical argument in the command line. Options with their additional
|
||||||
|
/// parameters are combined in one argument.
|
||||||
|
/// </summary>
|
||||||
|
internal class Argument
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initialises a new instance of the <see cref="Argument"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="option">The <see cref="Option"/> that is set in this argument; or null.</param>
|
||||||
|
/// <param name="values">The additional parameter values for the option; or the argument value.</param>
|
||||||
|
internal Argument(Option option, string[] values)
|
||||||
|
{
|
||||||
|
Option = option;
|
||||||
|
Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="Option"/> that is set in this argument; or null.
|
||||||
|
/// </summary>
|
||||||
|
public Option Option { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the additional parameter values for the option; or the argument value.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Values { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the first item of <see cref="Values"/>; or null.
|
||||||
|
/// </summary>
|
||||||
|
public string Value => Values.Length > 0 ? Values[0] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
366
CliClient/Cli/CommandLineParser.cs
Normal file
366
CliClient/Cli/CommandLineParser.cs
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AMWD.Common.Cli
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides options and arguments parsing from command line arguments or a single string.
|
||||||
|
/// </summary>
|
||||||
|
internal class CommandLineParser
|
||||||
|
{
|
||||||
|
#region Private data
|
||||||
|
|
||||||
|
private string[] _args;
|
||||||
|
private List<Argument> _parsedArguments;
|
||||||
|
private readonly List<Option> _options = [];
|
||||||
|
|
||||||
|
#endregion Private data
|
||||||
|
|
||||||
|
#region Configuration properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the option names are case-sensitive.
|
||||||
|
/// (Default: false)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCaseSensitive { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether incomplete options can be automatically
|
||||||
|
/// completed if there is only a single matching option.
|
||||||
|
/// (Default: true)
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoCompleteOptions { get; set; } = true;
|
||||||
|
|
||||||
|
#endregion Configuration properties
|
||||||
|
|
||||||
|
#region Custom arguments line parsing
|
||||||
|
|
||||||
|
// Source: http://stackoverflow.com/a/23961658/143684
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a single string into an arguments array.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="argsString">The string that contains the entire command line.</param>
|
||||||
|
public static string[] ParseArgsString(string argsString)
|
||||||
|
{
|
||||||
|
// Collects the split argument strings
|
||||||
|
var args = new List<string>();
|
||||||
|
|
||||||
|
// Builds the current argument
|
||||||
|
var currentArg = new StringBuilder();
|
||||||
|
|
||||||
|
// Indicates whether the last character was a backslash escape character
|
||||||
|
bool escape = false;
|
||||||
|
|
||||||
|
// Indicates whether we're in a quoted range
|
||||||
|
bool inQuote = false;
|
||||||
|
|
||||||
|
// Indicates whether there were quotes in the current arguments
|
||||||
|
bool hadQuote = false;
|
||||||
|
|
||||||
|
// Remembers the previous character
|
||||||
|
char prevCh = '\0';
|
||||||
|
|
||||||
|
// Iterate all characters from the input string
|
||||||
|
for (int i = 0; i < argsString.Length; i++)
|
||||||
|
{
|
||||||
|
char ch = argsString[i];
|
||||||
|
if (ch == '\\' && !escape)
|
||||||
|
{
|
||||||
|
// Beginning of a backslash-escape sequence
|
||||||
|
escape = true;
|
||||||
|
}
|
||||||
|
else if (ch == '\\' && escape)
|
||||||
|
{
|
||||||
|
// Double backslash, keep one
|
||||||
|
currentArg.Append(ch);
|
||||||
|
escape = false;
|
||||||
|
}
|
||||||
|
else if (ch == '"' && !escape)
|
||||||
|
{
|
||||||
|
// Toggle quoted range
|
||||||
|
inQuote = !inQuote;
|
||||||
|
hadQuote = true;
|
||||||
|
if (inQuote && prevCh == '"')
|
||||||
|
{
|
||||||
|
// Doubled quote within a quoted range is like escaping
|
||||||
|
currentArg.Append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ch == '"' && escape)
|
||||||
|
{
|
||||||
|
// Backslash-escaped quote, keep it
|
||||||
|
currentArg.Append(ch);
|
||||||
|
escape = false;
|
||||||
|
}
|
||||||
|
else if (char.IsWhiteSpace(ch) && !inQuote)
|
||||||
|
{
|
||||||
|
if (escape)
|
||||||
|
{
|
||||||
|
// Add pending escape char
|
||||||
|
currentArg.Append('\\');
|
||||||
|
escape = false;
|
||||||
|
}
|
||||||
|
// Accept empty arguments only if they are quoted
|
||||||
|
if (currentArg.Length > 0 || hadQuote)
|
||||||
|
{
|
||||||
|
args.Add(currentArg.ToString());
|
||||||
|
}
|
||||||
|
// Reset for next argument
|
||||||
|
currentArg.Clear();
|
||||||
|
hadQuote = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (escape)
|
||||||
|
{
|
||||||
|
// Add pending escape char
|
||||||
|
currentArg.Append('\\');
|
||||||
|
escape = false;
|
||||||
|
}
|
||||||
|
// Copy character from input, no special meaning
|
||||||
|
currentArg.Append(ch);
|
||||||
|
}
|
||||||
|
prevCh = ch;
|
||||||
|
}
|
||||||
|
// Save last argument
|
||||||
|
if (currentArg.Length > 0 || hadQuote)
|
||||||
|
{
|
||||||
|
args.Add(currentArg.ToString());
|
||||||
|
}
|
||||||
|
return [.. args];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the command line arguments from a single string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="argsString">The string that contains the entire command line.</param>
|
||||||
|
public void ReadArgs(string argsString)
|
||||||
|
{
|
||||||
|
_args = ParseArgsString(argsString);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Custom arguments line parsing
|
||||||
|
|
||||||
|
#region Options management
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a named option without additional parameters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The option name.</param>
|
||||||
|
/// <returns>The option instance.</returns>
|
||||||
|
public Option RegisterOption(string name)
|
||||||
|
{
|
||||||
|
return RegisterOption(name, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a named option.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The option name.</param>
|
||||||
|
/// <param name="parameterCount">The number of additional parameters for this option.</param>
|
||||||
|
/// <returns>The option instance.</returns>
|
||||||
|
public Option RegisterOption(string name, int parameterCount)
|
||||||
|
{
|
||||||
|
var option = new Option(name, parameterCount);
|
||||||
|
_options.Add(option);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Options management
|
||||||
|
|
||||||
|
#region Parsing method
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses all command line arguments.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">The command line arguments.</param>
|
||||||
|
public void Parse(string[] args)
|
||||||
|
{
|
||||||
|
_args = args ?? throw new ArgumentNullException(nameof(args));
|
||||||
|
Parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses all command line arguments.
|
||||||
|
/// </summary>
|
||||||
|
public void Parse()
|
||||||
|
{
|
||||||
|
// Use args of the current process if no other source was given
|
||||||
|
if (_args == null)
|
||||||
|
{
|
||||||
|
_args = Environment.GetCommandLineArgs();
|
||||||
|
if (_args.Length > 0)
|
||||||
|
{
|
||||||
|
// Skip myself (args[0])
|
||||||
|
_args = _args.Skip(1).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear/reset data
|
||||||
|
_parsedArguments = [];
|
||||||
|
foreach (var option in _options)
|
||||||
|
{
|
||||||
|
option.IsSet = false;
|
||||||
|
option.SetCount = 0;
|
||||||
|
option.Argument = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var comparison = IsCaseSensitive
|
||||||
|
? StringComparison.Ordinal
|
||||||
|
: StringComparison.OrdinalIgnoreCase;
|
||||||
|
var argumentWalker = new EnumerableWalker<string>(_args);
|
||||||
|
bool optMode = true;
|
||||||
|
foreach (string arg in argumentWalker.Cast<string>())
|
||||||
|
{
|
||||||
|
if (arg == "--")
|
||||||
|
{
|
||||||
|
optMode = false;
|
||||||
|
}
|
||||||
|
else if (optMode && (arg.StartsWith("/") || arg.StartsWith("-")))
|
||||||
|
{
|
||||||
|
string optName = arg.Substring(arg.StartsWith("--") ? 2 : 1);
|
||||||
|
|
||||||
|
// Split option value if separated with : or = instead of whitespace
|
||||||
|
int separatorIndex = optName.IndexOfAny([':', '=']);
|
||||||
|
string optValue = null;
|
||||||
|
if (separatorIndex != -1)
|
||||||
|
{
|
||||||
|
optValue = optName.Substring(separatorIndex + 1);
|
||||||
|
optName = optName.Substring(0, separatorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the option with complete name match
|
||||||
|
var option = _options.FirstOrDefault(o => o.Names.Any(n => n.Equals(optName, comparison)));
|
||||||
|
if (option == null)
|
||||||
|
{
|
||||||
|
// Try to complete the name to a unique registered option
|
||||||
|
var matchingOptions = _options.Where(o => o.Names.Any(n => n.StartsWith(optName, comparison))).ToList();
|
||||||
|
if (AutoCompleteOptions && matchingOptions.Count > 1)
|
||||||
|
throw new Exception("Invalid option, completion is not unique: " + arg);
|
||||||
|
|
||||||
|
if (!AutoCompleteOptions || matchingOptions.Count == 0)
|
||||||
|
throw new Exception("Unknown option: " + arg);
|
||||||
|
|
||||||
|
// Accept the single auto-completed option
|
||||||
|
option = matchingOptions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for single usage
|
||||||
|
if (option.IsSingle && option.IsSet)
|
||||||
|
throw new Exception("Option cannot be set multiple times: " + arg);
|
||||||
|
|
||||||
|
// Collect option values from next argument strings
|
||||||
|
string[] values = new string[option.ParameterCount];
|
||||||
|
for (int i = 0; i < option.ParameterCount; i++)
|
||||||
|
{
|
||||||
|
if (optValue != null)
|
||||||
|
{
|
||||||
|
// The first value was included in this argument string
|
||||||
|
values[i] = optValue;
|
||||||
|
optValue = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fetch another argument string
|
||||||
|
values[i] = argumentWalker.GetNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values[i] == null)
|
||||||
|
throw new Exception("Missing argument " + (i + 1) + " for option: " + arg);
|
||||||
|
}
|
||||||
|
var argument = new Argument(option, values);
|
||||||
|
|
||||||
|
// Set usage data on the option instance for quick access
|
||||||
|
option.IsSet = true;
|
||||||
|
option.SetCount++;
|
||||||
|
option.Argument = argument;
|
||||||
|
|
||||||
|
if (option.Action != null)
|
||||||
|
{
|
||||||
|
option.Action(argument);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_parsedArguments.Add(argument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_parsedArguments.Add(new Argument(null, [arg]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var missingOption = _options.FirstOrDefault(o => o.IsRequired && !o.IsSet);
|
||||||
|
if (missingOption != null)
|
||||||
|
throw new Exception("Missing required option: /" + missingOption.Names[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Parsing method
|
||||||
|
|
||||||
|
#region Parsed data properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parsed arguments.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||||
|
/// exception handling.
|
||||||
|
/// </remarks>
|
||||||
|
public Argument[] Arguments
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_parsedArguments == null)
|
||||||
|
Parse();
|
||||||
|
|
||||||
|
return [.. _parsedArguments];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the options that are set in the command line, including their value.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||||
|
/// exception handling.
|
||||||
|
/// </remarks>
|
||||||
|
public Option[] SetOptions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_parsedArguments == null)
|
||||||
|
Parse();
|
||||||
|
|
||||||
|
return _parsedArguments
|
||||||
|
.Where(a => a.Option != null)
|
||||||
|
.Select(a => a.Option)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the free arguments that are set in the command line and don't belong to an option.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||||
|
/// exception handling.
|
||||||
|
/// </remarks>
|
||||||
|
public string[] FreeArguments
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_parsedArguments == null)
|
||||||
|
Parse();
|
||||||
|
|
||||||
|
return _parsedArguments
|
||||||
|
.Where(a => a.Option == null)
|
||||||
|
.Select(a => a.Value)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Parsed data properties
|
||||||
|
}
|
||||||
|
}
|
||||||
53
CliClient/Cli/EnumerableWalker.cs
Normal file
53
CliClient/Cli/EnumerableWalker.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AMWD.Common.Cli
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Walks through an <see cref="IEnumerable{T}"/> and allows retrieving additional items.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <remarks>
|
||||||
|
/// Initialises a new instance of the <see cref="EnumerableWalker{T}"/> class.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="array">The array to walk though.</param>
|
||||||
|
internal class EnumerableWalker<T>(IEnumerable<T> array)
|
||||||
|
: IEnumerable<T> where T : class
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<T> _array = array ?? throw new ArgumentNullException(nameof(array));
|
||||||
|
private IEnumerator<T> _enumerator;
|
||||||
|
|
||||||
|
IEnumerator<T> IEnumerable<T>.GetEnumerator()
|
||||||
|
{
|
||||||
|
_enumerator = _array.GetEnumerator();
|
||||||
|
return _enumerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the enumerator.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The enumerator.</returns>
|
||||||
|
public IEnumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
_enumerator = _array.GetEnumerator();
|
||||||
|
return _enumerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the next item.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The next item.</returns>
|
||||||
|
public T GetNext()
|
||||||
|
{
|
||||||
|
if (_enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
return _enumerator.Current;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
CliClient/Cli/Option.cs
Normal file
112
CliClient/Cli/Option.cs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AMWD.Common.Cli
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a named option.
|
||||||
|
/// </summary>
|
||||||
|
internal class Option
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initialises a new instance of the <see cref="Option"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The primary name of the option.</param>
|
||||||
|
/// <param name="parameterCount">The number of additional parameters for this option.</param>
|
||||||
|
internal Option(string name, int parameterCount)
|
||||||
|
{
|
||||||
|
Names = [name];
|
||||||
|
ParameterCount = parameterCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the names of this option.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Names { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of additional parameters for this option.
|
||||||
|
/// </summary>
|
||||||
|
public int ParameterCount { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this option is required.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRequired { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this option can only be specified once.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSingle { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the action to invoke when the option is set.
|
||||||
|
/// </summary>
|
||||||
|
public Action<Argument> Action { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this option is set in the command line.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSet { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of times that this option is set in the command line.
|
||||||
|
/// </summary>
|
||||||
|
public int SetCount { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="Argument"/> instance that contains additional parameters set
|
||||||
|
/// for this option.
|
||||||
|
/// </summary>
|
||||||
|
public Argument Argument { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value of the <see cref="Argument"/> instance for this option.
|
||||||
|
/// </summary>
|
||||||
|
public string Value => Argument?.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets alias names for this option.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="names">The alias names for this option.</param>
|
||||||
|
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||||
|
public Option Alias(params string[] names)
|
||||||
|
{
|
||||||
|
Names.AddRange(names);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks this option as required. If a required option is not set in the command line,
|
||||||
|
/// an exception is thrown on parsing.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||||
|
public Option Required()
|
||||||
|
{
|
||||||
|
IsRequired = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks this option as single. If a single option is set multiple times in the
|
||||||
|
/// command line, an exception is thrown on parsing.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||||
|
public Option Single()
|
||||||
|
{
|
||||||
|
IsSingle = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the action to invoke when the option is set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">The action to invoke when the option is set.</param>
|
||||||
|
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||||
|
public Option Do(Action<Argument> action)
|
||||||
|
{
|
||||||
|
Action = action;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
CliClient/CliClient.csproj
Normal file
31
CliClient/CliClient.csproj
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<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>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="$(SolutionDir)/package-icon.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Serial\AMWD.Protocols.Modbus.Serial.csproj" />
|
||||||
|
<ProjectReference Include="$(SolutionDir)\AMWD.Protocols.Modbus.Tcp\AMWD.Protocols.Modbus.Tcp.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
628
CliClient/Program.cs
Normal file
628
CliClient/Program.cs
Normal file
@@ -0,0 +1,628 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Common.Cli;
|
||||||
|
using AMWD.Protocols.Modbus.Common;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Protocols;
|
||||||
|
using AMWD.Protocols.Modbus.Serial;
|
||||||
|
using AMWD.Protocols.Modbus.Tcp;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.CliClient
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
// General
|
||||||
|
private static string _target;
|
||||||
|
private static Option _helpOption;
|
||||||
|
private static Option _debugOption;
|
||||||
|
|
||||||
|
private static Option _protocolOption;
|
||||||
|
private static Option _addressOption;
|
||||||
|
private static Option _referenceOption;
|
||||||
|
private static Option _countOption;
|
||||||
|
private static Option _typeOption;
|
||||||
|
private static Option _intervalOption;
|
||||||
|
private static Option _timeoutOption;
|
||||||
|
private static Option _onceOption;
|
||||||
|
|
||||||
|
// Serial
|
||||||
|
private static Option _baudOption;
|
||||||
|
private static Option _dataBitsOption;
|
||||||
|
private static Option _stopBitsOption;
|
||||||
|
private static Option _parityOption;
|
||||||
|
private static Option _softSwitchOption;
|
||||||
|
|
||||||
|
// TCP
|
||||||
|
private static Option _portOption;
|
||||||
|
|
||||||
|
private static async Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
if (!ParseArguments(args))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Could not parse arguments.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_helpOption.IsSet)
|
||||||
|
{
|
||||||
|
PrintHelp();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_target))
|
||||||
|
{
|
||||||
|
Console.WriteLine("No serial port or tcp host specified.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_typeOption.IsSet)
|
||||||
|
{
|
||||||
|
Console.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.Write(".");
|
||||||
|
await Task.Delay(1000, cts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Console.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: amwd-modbus [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, 19200, 38400, 115200). Default: 19200");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine(" -d, --databits #");
|
||||||
|
Console.WriteLine(" The number of data bits (7/8 for ASCII, otherwise 8). Default: 8");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine(" -s, --stopbits #");
|
||||||
|
Console.WriteLine(" The number of stop bits (1/2). Default: 1");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine(" -p, --parity <none|odd|even>");
|
||||||
|
Console.WriteLine(" The kind of parity. Default: even");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine(" --enable-rs485");
|
||||||
|
Console.WriteLine(" Enables the RS485 software switch for serial adapters capable of RS232 and RS485.");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("TCP Options:");
|
||||||
|
Console.WriteLine(" -p, --port #");
|
||||||
|
Console.WriteLine(" The TCP port of the remote device. Default: 502");
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSerialTarget()
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsWindows()
|
||||||
|
? _target.StartsWith("COM", StringComparison.OrdinalIgnoreCase)
|
||||||
|
: _target.StartsWith("/dev/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModbusClientBase CreateClient()
|
||||||
|
{
|
||||||
|
int timeout = 1000;
|
||||||
|
if (_timeoutOption.IsSet && int.TryParse(_timeoutOption.Value, out int timeoutValue))
|
||||||
|
timeout = timeoutValue;
|
||||||
|
|
||||||
|
if (IsSerialTarget())
|
||||||
|
{
|
||||||
|
BaudRate baudRate = BaudRate.Baud19200;
|
||||||
|
if (_baudOption.IsSet && int.TryParse(_baudOption.Value, out int baudRateValue))
|
||||||
|
baudRate = (BaudRate)baudRateValue;
|
||||||
|
|
||||||
|
int dataBits = 8;
|
||||||
|
if (_dataBitsOption.IsSet && int.TryParse(_dataBitsOption.Value, out int dataBitsValue))
|
||||||
|
dataBits = dataBitsValue;
|
||||||
|
|
||||||
|
StopBits stopBits = StopBits.One;
|
||||||
|
if (_stopBitsOption.IsSet && float.TryParse(_stopBitsOption.Value, out float stopBitsValue))
|
||||||
|
{
|
||||||
|
switch (stopBitsValue)
|
||||||
|
{
|
||||||
|
case 1.0f: stopBits = StopBits.One; break;
|
||||||
|
case 1.5f: stopBits = StopBits.OnePointFive; break;
|
||||||
|
case 2.0f: stopBits = StopBits.Two; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Parity parity = Parity.Even;
|
||||||
|
if (_parityOption.IsSet)
|
||||||
|
{
|
||||||
|
switch (_parityOption.Value.ToLower())
|
||||||
|
{
|
||||||
|
case "none": parity = Parity.None; break;
|
||||||
|
case "odd": parity = Parity.Odd; break;
|
||||||
|
case "even": parity = Parity.Even; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool enableRs485 = _softSwitchOption.IsSet;
|
||||||
|
|
||||||
|
var client = new ModbusSerialClient(_target)
|
||||||
|
{
|
||||||
|
BaudRate = baudRate,
|
||||||
|
DataBits = dataBits,
|
||||||
|
StopBits = stopBits,
|
||||||
|
Parity = parity,
|
||||||
|
|
||||||
|
ReadTimeout = TimeSpan.FromMilliseconds(timeout),
|
||||||
|
WriteTimeout = TimeSpan.FromMilliseconds(timeout),
|
||||||
|
|
||||||
|
DriverEnabledRS485 = enableRs485
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine(client);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int port = 502;
|
||||||
|
if (_portOption.IsSet && int.TryParse(_portOption.Value, out int portValue))
|
||||||
|
port = portValue;
|
||||||
|
|
||||||
|
var client = new ModbusTcpClient(_target)
|
||||||
|
{
|
||||||
|
Port = port,
|
||||||
|
|
||||||
|
ReadTimeout = TimeSpan.FromMilliseconds(timeout),
|
||||||
|
WriteTimeout = TimeSpan.FromMilliseconds(timeout),
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine(client);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
CliClient/Properties/launchSettings.json
Normal file
9
CliClient/Properties/launchSettings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"ConsoleApp": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"commandLineArgs": "--debug COM1",
|
||||||
|
"remoteDebugEnabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user