E2E — Modbus server-side factory + seed SQL #210

Closed
opened 2026-04-21 10:32:06 -04:00 by dohertj2 · 1 comment
Owner

Parent: #209

  1. src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriverFactoryExtensions.cs — mirror GalaxyProxyDriverFactoryExtensions but return a ModbusDriver.
  2. Register in Program.cs after the Galaxy + FOCAS lines.
  3. scripts/smoke/seed-modbus-smoke.sql — seed a Modbus DriverInstance bound to 127.0.0.1:5020 + a Tag at HR[200]:UInt16 (HR[100] is auto-incrementing in pymodbus standard.json — unusable as a write target). NodeId in sample config: ns=2;s=Modbus/HR200.
  4. Verify: docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/docker-compose.yml --profile standard up -d + boot server + ./scripts/e2e/test-modbus.ps1 -BridgeNodeId 'ns=2;s=Modbus/HR200' → PASS on all 5 stages.
Parent: #209 1. `src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriverFactoryExtensions.cs` — mirror `GalaxyProxyDriverFactoryExtensions` but return a `ModbusDriver`. 2. Register in `Program.cs` after the Galaxy + FOCAS lines. 3. `scripts/smoke/seed-modbus-smoke.sql` — seed a Modbus DriverInstance bound to `127.0.0.1:5020` + a Tag at `HR[200]:UInt16` (HR[100] is auto-incrementing in pymodbus `standard.json` — unusable as a write target). NodeId in sample config: `ns=2;s=Modbus/HR200`. 4. Verify: `docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/docker-compose.yml --profile standard up -d` + boot server + `./scripts/e2e/test-modbus.ps1 -BridgeNodeId 'ns=2;s=Modbus/HR200'` → PASS on all 5 stages.
Author
Owner

Factory + seed shipped in PR #216. Closing; live boot verification tracked under parent #209.

Factory + seed shipped in PR #216. Closing; live boot verification tracked under parent #209.
dohertj2 referenced this issue from a commit 2026-04-30 08:21:26 -04:00
EquipmentNodeWalker — pure-function UNS tree materialization (ADR-001 Task A, task #210). The walker traverses the Config-DB snapshot for a single Equipment-kind namespace (Areas / Lines / Equipment / Tags) and streams IAddressSpaceBuilder.Folder + Variable + AddProperty calls to materialize the canonical 5-level Unified Namespace browse tree that decisions #116-#121 promise external consumers. Pure function: no OPC UA SDK dependency, no DB access, no state — consumes pre-loaded EF Core row collections + streams into the supplied builder. Server-side wiring (load snapshot → call walker → per-tag capability probe) is Task B's scope, alongside NodeScopeResolver's Config-DB join + the ACL integration test that closes task #195. This PR is the Core.OpcUa primitive the server will consume. Walk algorithm — content is grouped up-front (lines by area, equipment by line, tags by equipment) into OrdinalIgnoreCase dictionaries so the per-level nested foreach stays O(N+M) rather than O(N·M) at each UNS level; orderings are deterministic on Name with StringComparer.Ordinal so diffs across runs (e.g. integration-test assertions) are stable. Areas → Lines → Equipment emitted as Folder nodes with browse-name = Name per decision #120. Under each Equipment folder: five identifier properties per decision #121 (EquipmentId + EquipmentUuid always; MachineCode always — it's a required column on the entity; ZTag + SAPID skipped when null to avoid empty-string property noise); IdentificationFolderBuilder.Build materializes the OPC 40010 sub-folder when HasAnyFields(equipment) returns true, skipped otherwise to avoid a pointless empty folder; then one Variable node per Tag row bound to this Equipment (Tag.EquipmentId non-null matches Equipment.EquipmentId) emitted in Name order. Tags with null EquipmentId are walker-skipped — those are SystemPlatform-kind (Galaxy) tags that take the driver-native DiscoverAsync path per decision #120. DriverAttributeInfo construction: FullName = Tag.TagConfig (driver-specific wire-level address); DriverDataType parsed from Tag.DataType which stores the enum name string per decision #138; unparseable values fall back to DriverDataType.String so a one-off driver-specific type doesn't abort the whole walk (driver still sees the original address at runtime + can surface its own typed value via the variant). Address validation is deliberately NOT done at build time per ADR-001 Option A: unreachable addresses surface as OPC UA Bad status via the natural driver-read failure path at runtime, legible to operators through their Admin UI + OPC UA client inspection. Eight new EquipmentNodeWalkerTests: empty content emits nothing; Area/Line/Equipment folder emission order matches Name-sorted deterministic traversal; five identifier properties appear on Equipment nodes with correct values, ZTag + SAPID skipped when null + emitted when non-null; Identification sub-folder materialized when at least one OPC 40010 field is non-null + omitted when all are null; tags with matching EquipmentId emit as Variable nodes under the Equipment folder in Name order, tags with null EquipmentId walker-skipped; unparseable DataType falls back to String. RecordingBuilder test double captures Folder/Variable/Property calls into a tree structure tests can navigate. Core project builds 0 errors; Core.Tests 190/190 (was 182, +8 new walker tests). No Server/Admin changes — Task B lands the server-side wiring + consumes this walker from DriverNodeManager.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#210