chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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>
|
||||
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. 5-digit Modicon
|
||||
// is always exactly 5 chars (40001..49999, with the lead digit selecting region), and
|
||||
// 6-digit is exactly 6 (400001..465536-shaped). 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;
|
||||
}
|
||||
|
||||
// 5-digit form caps at 9999 by construction (4 trailing digits); reject if the parsed
|
||||
// value exceeds the wire-protocol maximum of 65536 (i.e. PDU offset 65535). 6-digit
|
||||
// form can address the full 65535-offset range.
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user