64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public members surfaced by commentchecker — resolves 5,847 of 5,869 issues (99.6%) across three /fixdocs passes.
126 lines
5.6 KiB
C#
126 lines
5.6 KiB
C#
using System.Globalization;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
|
|
|
/// <summary>
|
|
/// Parses classic Modicon address strings — both 5-digit (<c>40001</c>) and 6-digit
|
|
/// (<c>400001</c>) forms — into the protocol-level <see cref="ModbusRegion"/> +
|
|
/// zero-based PDU offset the driver speaks on the wire.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Modicon notation uses a leading region digit (<c>0</c> coil, <c>1</c> discrete input,
|
|
/// <c>3</c> input register, <c>4</c> holding register) followed by a 1-based register
|
|
/// number. The two forms differ only in how many trailing digits encode the register
|
|
/// number: 5-digit caps at 9999, 6-digit at 65535. Both decode to the same wire
|
|
/// representation — the PDU offset is always 0..65535 — so the only meaningful
|
|
/// distinction is range coverage.
|
|
/// </para>
|
|
/// <para>
|
|
/// Foundational helper for the addressing grammar work tracked in
|
|
/// <c>docs/v2/modbus-addressing.md</c>. The richer suffix grammar (type / bit /
|
|
/// byte-order / array) layered on top in a follow-up calls into this parser to extract
|
|
/// the region + offset before processing modifiers.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class ModbusModiconAddress
|
|
{
|
|
/// <summary>Parse a Modicon address string.</summary>
|
|
/// <param name="address">Either 5-digit (<c>40001</c>) or 6-digit (<c>400001</c>) form.</param>
|
|
/// <returns>Region + zero-based PDU offset the driver uses on the wire.</returns>
|
|
/// <exception cref="FormatException">When the input is not a valid Modicon address.</exception>
|
|
public static (ModbusRegion Region, ushort Offset) Parse(string address)
|
|
{
|
|
if (TryParse(address, out var region, out var offset, out var error))
|
|
return (region, offset);
|
|
throw new FormatException(error);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try-parse variant for hot-path / config-bind scenarios where a parse failure should
|
|
/// surface a structured diagnostic rather than throw. <paramref name="error"/> is
|
|
/// <c>null</c> on success.
|
|
/// </summary>
|
|
/// <param name="address">The address string to parse (5-digit or 6-digit form).</param>
|
|
/// <param name="region">The parsed Modbus region, or default if parsing fails.</param>
|
|
/// <param name="offset">The zero-based PDU offset, or 0 if parsing fails.</param>
|
|
/// <param name="error">The error message if parsing fails, or null on success.</param>
|
|
public static bool TryParse(string? address, out ModbusRegion region, out ushort offset, out string? error)
|
|
{
|
|
region = default;
|
|
offset = 0;
|
|
|
|
if (string.IsNullOrWhiteSpace(address))
|
|
{
|
|
error = "Modicon address is null or empty";
|
|
return false;
|
|
}
|
|
|
|
// Range check up-front — keeps the rest of the parser straight-line. Modicon addresses
|
|
// are exactly 5 or 6 characters: a leading region digit (0/1/3/4 — coils, discrete
|
|
// inputs, input registers, holding registers respectively) followed by 4 (5-digit form)
|
|
// or 5 (6-digit form) trailing digits encoding the 1-based register number. The
|
|
// 5-digit form covers 1..9999 per region (e.g. coils 00001..09999, holding registers
|
|
// 40001..49999); the 6-digit form covers the full 1..65536 wire range (e.g. coils
|
|
// 000001..065536, holding 400001..465536). Anything else is unambiguously malformed so
|
|
// we reject before doing the per-character work.
|
|
var s = address.Trim();
|
|
if (s.Length is not (5 or 6))
|
|
{
|
|
error = $"Modicon address must be 5 or 6 digits, got {s.Length} ('{address}')";
|
|
return false;
|
|
}
|
|
|
|
if (!s.All(char.IsDigit))
|
|
{
|
|
error = $"Modicon address must contain only digits ('{address}')";
|
|
return false;
|
|
}
|
|
|
|
var leading = s[0];
|
|
region = leading switch
|
|
{
|
|
'0' => ModbusRegion.Coils,
|
|
'1' => ModbusRegion.DiscreteInputs,
|
|
'3' => ModbusRegion.InputRegisters,
|
|
'4' => ModbusRegion.HoldingRegisters,
|
|
_ => (ModbusRegion)(-1),
|
|
};
|
|
if ((int)region == -1)
|
|
{
|
|
error = $"Modicon address leading digit must be 0/1/3/4, got '{leading}'";
|
|
return false;
|
|
}
|
|
|
|
// The remaining 4 (5-digit) or 5 (6-digit) digits are the 1-based register number.
|
|
// 1-based-to-0-based conversion happens here so callers downstream uniformly see PDU
|
|
// offsets — which is what the wire format actually uses.
|
|
var registerNumberText = s[1..];
|
|
if (!int.TryParse(registerNumberText, NumberStyles.None, CultureInfo.InvariantCulture, out var registerNumber))
|
|
{
|
|
error = $"Modicon register number is not a valid integer ('{registerNumberText}')";
|
|
return false;
|
|
}
|
|
|
|
if (registerNumber < 1)
|
|
{
|
|
error = $"Modicon register number must be >= 1 (got {registerNumber})";
|
|
return false;
|
|
}
|
|
|
|
// Wire-protocol maximum is register number 65536 (PDU offset 65535). The 5-digit form's
|
|
// 4 trailing digits can only encode up to 9999, so this check is reached only by the
|
|
// 6-digit form in practice — but it is applied to both for safety / simplicity rather
|
|
// than relying on the digit-count invariant.
|
|
if (registerNumber > 65536)
|
|
{
|
|
error = $"Modicon register number {registerNumber} exceeds the wire maximum (65536 / PDU offset 65535)";
|
|
return false;
|
|
}
|
|
|
|
offset = (ushort)(registerNumber - 1);
|
|
error = null;
|
|
return true;
|
|
}
|
|
}
|