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