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

View File

@@ -0,0 +1,118 @@
using Microsoft.Extensions.Logging;
using MxGateway.Contracts.Proto;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
/// <summary>
/// Maps the gateway's <see cref="MxStatusProxy"/> (raw MXAccess HRESULT + category bits)
/// to OPC UA <c>StatusCode</c> uints. Replaces the legacy
/// <c>MxAccessGalaxyBackend.ToWire</c> heuristic (Quality &gt;= 192 → Good, else Uncertain)
/// with an explicit table that preserves specific codes (BadNotConnected, OutOfService,
/// UncertainSubNormal, etc.) instead of collapsing to category buckets.
/// </summary>
/// <remarks>
/// OPC DA quality bytes are 16-bit values arranged as <c>[QQSSSSSSLLNNNN]</c>:
/// Q = quality category (Bad/Uncertain/Good = 0/1/3), S = substatus, L = limit, N = vendor.
/// This mapper consumes the LOW byte (where the Q+S bits live) — the same byte the legacy
/// Wonderware Historian SDK exposed as the raw quality byte. Category-only fallback paths
/// handle deployment versions of MXAccess that surface unfamiliar substatuses.
///
/// Unknown substatus values fall back to the matching category bucket (<c>Good</c>,
/// <c>Uncertain</c>, <c>Bad</c>) and emit a single diagnostic log line per session via
/// the supplied logger so field captures can extend the table.
/// </remarks>
internal static class StatusCodeMap
{
// OPC UA Part 4 standard StatusCodes — top-byte categories are 0x00 (Good),
// 0x40 (Uncertain), 0x80 (Bad). Specific codes layer onto the category byte.
public const uint Good = 0x00000000u;
public const uint GoodLocalOverride = 0x00D80000u;
public const uint Uncertain = 0x40000000u;
public const uint UncertainLastUsableValue = 0x40A40000u;
public const uint UncertainSensorNotAccurate = 0x408D0000u;
public const uint UncertainEngineeringUnitsExceeded = 0x408E0000u;
public const uint UncertainSubNormal = 0x408F0000u;
public const uint Bad = 0x80000000u;
public const uint BadConfigurationError = 0x80890000u;
public const uint BadNotConnected = 0x808A0000u;
public const uint BadDeviceFailure = 0x808B0000u;
public const uint BadSensorFailure = 0x808C0000u;
public const uint BadCommunicationError = 0x80050000u;
public const uint BadOutOfService = 0x808D0000u;
public const uint BadWaitingForInitialData = 0x80320000u;
public const uint BadInternalError = 0x80020000u;
/// <summary>
/// Map a raw OPC DA quality byte (the low byte of an OPC DA <c>OpcQuality</c> ushort,
/// which is what Wonderware Historian + MXAccess surface as <c>OPCITEMSTATE.qLong</c>'s
/// low byte) to the OPC UA StatusCode uint.
/// </summary>
public static uint FromQualityByte(byte q, ILogger? logger = null) => q switch
{
// Good family — top two bits 11b (192-255).
192 => Good,
216 => GoodLocalOverride,
// Uncertain family — top two bits 01b (64-127).
64 => Uncertain,
68 => UncertainLastUsableValue,
80 => UncertainSensorNotAccurate,
84 => UncertainEngineeringUnitsExceeded,
88 => UncertainSubNormal,
// Bad family — top two bits 00b (0-63).
0 => Bad,
4 => BadConfigurationError,
8 => BadNotConnected,
12 => BadDeviceFailure,
16 => BadSensorFailure,
20 => BadCommunicationError,
24 => BadOutOfService,
32 => BadWaitingForInitialData,
_ => Categorize(q, logger),
};
/// <summary>
/// Map a gateway-reported <see cref="MxStatusProxy"/> to OPC UA StatusCode. Honors
/// the success flag, then the detail byte (treated as a quality substatus), with a
/// transport-error fallback for status rows whose detected_by indicates the failure
/// happened before the MXAccess call ran.
/// </summary>
public static uint FromMxStatus(MxStatusProxy? status, ILogger? logger = null)
{
if (status is null) return Good;
if (status.Success != 0) return Good;
// Detail field carries the substatus when the worker translated MX-style codes;
// when zero, infer from category + detected_by.
var detail = (byte)(status.Detail & 0xFF);
if (detail != 0) return FromQualityByte(detail, logger);
// detected_by != Mxaccess (raw_detected_by != the MXAccess source enum) implies
// the failure happened pre-call (gateway, worker, transport) — surface as a
// communication error rather than a generic Bad.
if (status.RawDetectedBy != 0) return BadCommunicationError;
return Bad;
}
private static uint Categorize(byte q, ILogger? logger)
{
if (q >= 192) { Log(logger, q, "Good"); return Good; }
if (q >= 64) { Log(logger, q, "Uncertain"); return Uncertain; }
Log(logger, q, "Bad");
return Bad;
}
private static void Log(ILogger? logger, byte q, string bucket)
{
// Best-effort diagnostic so field captures can extend the table — once per bucket
// per session is plenty (the LogWarning level is rate-limited by Serilog filters
// in production).
logger?.LogWarning(
"Unrecognised MXAccess quality byte 0x{Q:X2} — falling back to {Bucket} category. " +
"Field capture welcome — extend StatusCodeMap.FromQualityByte.", q, bucket);
}
}