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:
Joseph Doherty
2026-05-17 01:55:28 -04:00
parent 69f02fed7f
commit a25593a9c6
1044 changed files with 365 additions and 343 deletions
@@ -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;
}
}