using System.Globalization;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
///
/// Parses classic Modicon address strings — both 5-digit (40001) and 6-digit
/// (400001) forms — into the protocol-level +
/// zero-based PDU offset the driver speaks on the wire.
///
///
///
/// Modicon notation uses a leading region digit (0 coil, 1 discrete input,
/// 3 input register, 4 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.
///
///
/// Foundational helper for the addressing grammar work tracked in
/// docs/v2/modbus-addressing.md. 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.
///
///
public static class ModbusModiconAddress
{
/// Parse a Modicon address string.
/// Either 5-digit (40001) or 6-digit (400001) form.
/// Region + zero-based PDU offset the driver uses on the wire.
/// When the input is not a valid Modicon address.
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);
}
///
/// Try-parse variant for hot-path / config-bind scenarios where a parse failure should
/// surface a structured diagnostic rather than throw. is
/// null on success.
///
/// The address string to parse (5-digit or 6-digit form).
/// The parsed Modbus region, or default if parsing fails.
/// The zero-based PDU offset, or 0 if parsing fails.
/// The error message if parsing fails, or null on success.
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;
}
}