Full OPC UA server on .NET Framework 4.8 (x86) exposing AVEVA System Platform Galaxy tags via MXAccess. Mirrors Galaxy object hierarchy as OPC UA address space, translating contained-name browse paths to tag-name runtime references. Components implemented: - Configuration: AppConfiguration with 4 sections, validator - Domain: ConnectionState, Quality, Vtq, MxDataTypeMapper, error codes - MxAccess: StaComThread, MxAccessClient (partial classes), MxProxyAdapter using strongly-typed ArchestrA.MxAccess COM interop - Galaxy Repository: SQL queries (hierarchy, attributes, change detection), ChangeDetectionService with auto-rebuild on deploy - OPC UA Server: LmxNodeManager (CustomNodeManager2), LmxOpcUaServer, OpcUaServerHost with programmatic config, SecurityPolicy None - Status Dashboard: HTTP server with HTML/JSON/health endpoints - Integration: Full 14-step startup, graceful shutdown, component wiring 175 tests (174 unit + 1 integration), all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
20 KiB
Implementation Plan: LmxOpcUa Server — All 44 Requirements
Context
The LmxOpcUa project is scaffolded (solution, projects, configs, requirements docs) but has no implementation beyond Program.cs and a stub OpcUaService.cs. This plan implements all 44 requirements across 6 phases, each with verification gates and wiring checks to ensure nothing is left unconnected.
Architecture
Five major components wired together in OpcUaService.cs:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Galaxy Repository│────>│ OPC UA Server │<───>│ OPC UA Clients │
│ (SQL queries) │ │ (address space) │ │ │
└─────────────────┘ └────────┬──────────┘ └─────────────────┘
│
┌────────┴──────────┐
│ MxAccessClient │
│ (STA + COM) │
└───────────────────┘
│
┌────────┴──────────┐
│ Status Dashboard │
│ (HTTP + metrics) │
└───────────────────┘
Reference implementation: C:\Users\dohertj2\Desktop\scadalink-design\lmxproxy\src\ZB.MOM.WW.LmxProxy.Host\
PHASE 1: Foundation — Domain Models, Configuration, Interfaces
Reqs: SVC-003, SVC-006 (partial), MXA-008 (interfaces), MXA-009, OPC-005, OPC-012 (partial), GR-005 (config)
Files to Create
Configuration/
AppConfiguration.cs— top-level holder for all config sectionsOpcUaConfiguration.cs— Port, EndpointPath, ServerName, GalaxyName, MaxSessions, SessionTimeoutMinutesMxAccessConfiguration.cs— ClientName, timeouts, concurrency, probe settingsGalaxyRepositoryConfiguration.cs— ConnectionString, intervals, command timeoutDashboardConfiguration.cs— Enabled, Port, RefreshIntervalSecondsConfigurationValidator.cs— validate and log effective config at startup
Domain/
ConnectionState.cs— enum: Disconnected, Connecting, Connected, Disconnecting, Error, ReconnectingConnectionStateChangedEventArgs.cs— PreviousState, CurrentState, MessageVtq.cs— Value/Timestamp/Quality struct with factory methodsQuality.cs— enum with Bad/Uncertain/Good families matching OPC DA codesQualityMapper.cs— MapFromMxAccessQuality(int) and MapToOpcUaStatusCode(Quality)MxDataTypeMapper.cs— MapToOpcUaDataType(int mxDataType), MapToClrType(int). Unknown defaults to StringMxErrorCodes.cs— translate 1008/1012/1013 to human messagesGalaxyObjectInfo.cs— DTO matching hierarchy.sql columnsGalaxyAttributeInfo.cs— DTO matching attributes.sql columnsIMxAccessClient.cs— interface: Connect, Disconnect, Subscribe, Read, Write, OnTagValueChanged delegateIGalaxyRepository.cs— interface: GetHierarchy, GetAttributes, GetLastDeployTime, TestConnection, OnGalaxyChanged eventIMxProxy.cs— abstraction over LMXProxyServer COM object (enables testing without DLL)
Metrics/
PerformanceMetrics.cs— ITimingScope, OperationMetrics (1000-entry rolling buffer), BeginOperation/GetStatistics. Adapt from reference.
Tests
ConfigurationLoadingTests.cs— bind appsettings.json, verify defaultsMxDataTypeMapperTests.cs— all 12 type mappings + unknown defaultQualityMapperTests.cs— boundary values (0, 63, 64, 191, 192)MxErrorCodesTests.cs— known codes + unknownPerformanceMetricsTests.cs— recording, P95, buffer eviction, empty state
Verification Gate 1
dotnet build— zero errors- All Phase 1 tests pass
- Config binding loads all 4 sections from appsettings.json
- MxDataTypeMapper covers every row in
gr/data_type_mapping.md - Quality enum covers all reference impl values
- Builds WITHOUT ArchestrA.MxAccess.dll (interface-based, no COM refs in Phase 1)
- Every new file has doc-comment referencing requirement ID(s)
- IMxAccessClient has every method needed by OPC-007, OPC-008, OPC-009
- IGalaxyRepository has every method needed by GR-001 through GR-004
PHASE 2: MxAccessClient — STA Thread and COM Interop
Reqs: MXA-001, MXA-002, MXA-003, MXA-004, MXA-005, MXA-006, MXA-007, MXA-008 (wiring)
Files to Create
MxAccess/
StaComThread.cs— adapt from reference. STA thread, Win32 message pump, RunAsync(Action)/RunAsync(Func), WM_APP dispatchMxAccessClient.cs— core partial class implementing IMxAccessClient. Fields: StaComThread, IMxProxy, handle, state, semaphores, mapsMxAccessClient.Connection.cs— ConnectAsync (Register on STA), DisconnectAsync (cleanup per MXA-007), COM cleanupMxAccessClient.Subscription.cs— SubscribeAsync (AddItem+AdviseSupervisory), UnsubscribeAsync, ReplayStoredSubscriptionsMxAccessClient.ReadWrite.cs— ReadAsync (subscribe-get-first-unsubscribe), WriteAsync (Write+OnWriteComplete), semaphore-limited, timeout, ITimingScope metricsMxAccessClient.EventHandlers.cs— OnDataChange (resolve handle→address, create Vtq, invoke callback, update probe), OnWriteComplete (complete TCS, translate errors)MxAccessClient.Monitor.cs— monitor loop (reconnect on disconnect, probe staleness→force reconnect), cancellableMxProxyAdapter.cs— wraps real LMXProxyServer COM object, forwards calls to IMxProxy interface
Test Helpers (in Tests project):
FakeMxProxy.cs— implements IMxProxy, simulates connections/data changes for testing
Design Decision: IMxProxy Abstraction
Code against IMxProxy interface (not LMXProxyServer directly). This allows testing without ArchestrA.MxAccess.dll. MxProxyAdapter wraps the real COM object at runtime.
Tests
StaComThreadTests.cs— STA apartment verified, work item execution, disposeMxAccessClientConnectionTests.cs— state transitions, cleanup orderMxAccessClientSubscriptionTests.cs— subscribe/unsubscribe, stored subscriptions, reconnect replay, OnDataChange→callbackMxAccessClientReadWriteTests.cs— read returns value, read timeout, write completes on callback, write timeout, semaphore limitingMxAccessClientMonitorTests.cs— reconnect on disconnect, probe staleness
Verification Gate 2
- Solution builds without ArchestrA.MxAccess.dll
- STA thread test proves work items execute on STA apartment
- Connection lifecycle: Disconnected→Connecting→Connected→Disconnecting→Disconnected
- Subscription replay: stored subscriptions replayed after simulated reconnect
- Read/Write: timeout behavior returns error within expected window
- Metrics: Read/Write record timing in PerformanceMetrics
- WIRING CHECK: OnDataChange callback reaches OnTagValueChanged delegate
- COM cleanup order: UnAdvise→RemoveItem→unwire events→Unregister→ReleaseComObject
- Error codes 1008/1012/1013 translate correctly in OnWriteComplete path
PHASE 3: Galaxy Repository — SQL Queries and Change Detection
Reqs: GR-001, GR-002, GR-003, GR-004, GR-006, GR-007
Files to Create
GalaxyRepository/
GalaxyRepositoryService.cs— implements IGalaxyRepository. SQL embedded asconst string(from gr/queries/). ADO.NET SqlConnection per-query. GetHierarchyAsync, GetAttributesAsync, GetLastDeployTimeAsync, TestConnectionAsyncChangeDetectionService.cs— background Timer at configured interval. Polls GetLastDeployTimeAsync, compares to last known, fires OnGalaxyChanged on change. First poll always triggers. Failed poll logs Warning, retries next intervalGalaxyRepositoryStats.cs— POCO for dashboard: GalaxyName, DbConnected, LastDeployTime, ObjectCount, AttributeCount, LastRebuildTime
Tests
ChangeDetectionServiceTests.cs— first poll triggers, same timestamp skips, changed triggers, failed poll retriesGalaxyRepositoryServiceTests.cs(integration, in IntegrationTests) — TestConnection, GetHierarchy returns rows, GetAttributes returns rows
Verification Gate 3
- All SQL is
const string— no concatenation, no parameters, no INSERT/UPDATE/DELETE (GR-006 code review) - GetHierarchyAsync maps all columns: gobject_id, tag_name, contained_name, browse_name, parent_gobject_id, is_area
- GetAttributesAsync maps all columns including array_dimension
- Change detection: first poll fires, same timestamp skips, changed fires
- Failed query does NOT crash or trigger false rebuild
- GalaxyRepositoryStats populated for dashboard
- Zero rows from hierarchy logs Warning
PHASE 4: OPC UA Server — Address Space and Node Manager
Reqs: OPC-001, OPC-002, OPC-003, OPC-004, OPC-005, OPC-006, OPC-007, OPC-008, OPC-009, OPC-010, OPC-011, OPC-012, OPC-013
Files to Create
OpcUa/
LmxOpcUaServer.cs— inherits StandardServer. Creates custom node manager. SecurityPolicy None. Registers namespaceurn:{GalaxyName}:LmxOpcUaLmxNodeManager.cs— inherits CustomNodeManager2. Core class:BuildAddressSpace(hierarchy, attributes)— creates folder/object/variable nodes from Galaxy data. NodeId:ns=1;s={tag_name}/ns=1;s={tag_name}.{attr}. Stores full_tag_reference lookupRebuildAddressSpace(hierarchy, attributes)— removes old nodes, rebuilds. Preserves sessions- Read/Write overrides delegate to IMxAccessClient via stored full_tag_reference
- Subscription management: ref-counted shared MXAccess subscriptions
OpcUaServerHost.cs— manages ApplicationInstance lifecycle. Programmatic config (no XML). Start/Stop. Exposes ActiveSessionCountOpcUaQualityMapper.cs— domain Quality → OPC UA StatusCodesDataValueConverter.cs— COM variant ↔ OPC UA DataValue. Handles all types from data_type_mapping.md. DateTime UTC. Arrays
Tests
DataValueConverterTests.cs— all type conversions, arrays, DateTime UTCLmxNodeManagerBuildTests.cs— synthetic hierarchy matching gr/layout.md, verify node types, NodeIds, data types, ValueRank, ArrayDimensionsLmxNodeManagerRebuildTests.cs— rebuild replaces nodes, old nodes gone, new nodes presentOpcUaQualityMapperTests.cs— all quality families
Verification Gate 4
- Endpoint URL:
opc.tcp://{hostname}:{port}/LmxOpcUa - Namespace:
urn:{GalaxyName}:LmxOpcUaat index 1 - Root ZB folder under Objects
- Areas → FolderType + Organizes reference
- Non-areas → BaseObjectType + HasComponent reference
- Variable nodes: correct DataType, ValueRank, ArrayDimensions per data_type_mapping.md
- WIRING CHECK: Read handler resolves NodeId → full_tag_reference → calls IMxAccessClient.ReadAsync
- WIRING CHECK: Write handler resolves NodeId → full_tag_reference → calls IMxAccessClient.WriteAsync
- Rebuild removes old nodes, creates new ones without crash
- SecurityPolicy is None
- MaxSessions/SessionTimeout configured from appsettings
PHASE 5: Status Dashboard — HTTP, HTML, JSON, Health
Reqs: DASH-001 through DASH-009
Files to Create
Status/
StatusData.cs— DTO: ConnectionInfo, HealthInfo, SubscriptionInfo, GalaxyInfo, OperationMetrics, FooterHealthCheckService.cs— rules: not connected→Unhealthy, success rate<50% w/>100 ops→Degraded, else HealthyStatusReportService.cs— aggregates from all components. GenerateHtml (self-contained, inline CSS, color-coded panels, meta-refresh). GenerateJson. IsHealthyStatusWebServer.cs— HttpListener. Routes: / → HTML, /api/status → JSON, /api/health → 200/503. GET only. no-cache headers. Disableable
Tests
HealthCheckServiceTests.cs— three health rules, messagesStatusReportServiceTests.cs— HTML contains all panels, JSON deserializes, meta-refresh tagStatusWebServerTests.cs— routing (200/405/404), cache headers, start/stop
Verification Gate 5
- HTML contains all panels: Connection, Health, Subscriptions, Galaxy Info, Operations table, Footer
- Connection panel: green/red/yellow border per state
- Health panel: three states with correct colors
- Operations table: Read/Write/Subscribe/Browse with Count/SuccessRate/Avg/Min/Max/P95
- Galaxy Info panel: galaxy name, DB status, last deploy, object/attribute counts, last rebuild
- Footer: timestamp + assembly version
- JSON API: all same data as HTML
- /api/health: 200 when healthy, 503 when unhealthy
- Meta-refresh tag with configured interval
- Port conflict does not prevent service startup
- Dashboard disabled via config skips HttpListener
PHASE 6: Integration Wiring and End-to-End Verification
Reqs: SVC-004, SVC-005, SVC-006, ALL wiring verification
OpcUaService.cs — Full Implementation
Start() sequence (SVC-005):
- Load AppConfiguration via IConfiguration
- ConfigurationValidator.ValidateAndLog()
- Register AppDomain.UnhandledException handler (SVC-006)
- Create PerformanceMetrics
- Create MxAccessClient → ConnectAsync (failure = fatal, don't start)
- Start MxAccessClient monitor loop
- Create GalaxyRepositoryService → TestConnectionAsync (failure = warning, continue)
- Create OpcUaServerHost + LmxNodeManager, inject IMxAccessClient
- Query initial hierarchy + attributes → BuildAddressSpace
- Start OPC UA server listener (failure = fatal)
- Create ChangeDetectionService → wire OnGalaxyChanged → nodeManager.RebuildAddressSpace
- Start change detection polling
- Create HealthCheckService, StatusReportService, StatusWebServer → Start (failure = warning)
- Log "LmxOpcUa service started successfully"
Critical wiring (GUARDRAILS):
_mxAccessClient.OnTagValueChanged→ node manager subscription delivery_changeDetectionService.OnGalaxyChanged→_nodeManager.RebuildAddressSpace_mxAccessClient.ConnectionStateChanged→ health check updates- Node manager Read/Write →
_mxAccessClient.ReadAsync/WriteAsync - StatusReportService reads from: MxAccessClient, PerformanceMetrics, GalaxyRepositoryStats, OpcUaServerHost
Stop() sequence (SVC-004, reverse order, 30s max):
- Cancel CancellationTokenSource (stops all background loops)
- Stop change detection
- Stop OPC UA server
- Disconnect MXAccess (full COM cleanup)
- Stop StatusWebServer
- Dispose PerformanceMetrics
- Log "Service shutdown complete"
Wiring Verification Tests (GUARDRAILS)
These tests prove components are connected end-to-end, not just implemented in isolation:
Wiring/MxAccessToNodeManagerWiringTest.cs— simulate OnDataChange on FakeMxProxy → verify data reaches node manager subscription deliveryWiring/ChangeDetectionToRebuildWiringTest.cs— mock GalaxyRepository returns changed timestamp → verify RebuildAddressSpace calledWiring/OpcUaReadToMxAccessWiringTest.cs— issue Read via NodeManager → verify FakeMxProxy receives correct full_tag_referenceWiring/OpcUaWriteToMxAccessWiringTest.cs— issue Write via NodeManager → verify FakeMxProxy receives correct tag + valueWiring/ServiceStartupSequenceTest.cs— create OpcUaService with fakes, call Start(), verify all components created and wiredWiring/ShutdownCompletesTest.cs— Start then Stop, verify completes within 30sEndToEnd/FullDataFlowTest.cs— THE ULTIMATE SMOKE TEST: full service with fakes, verify: (1) address space built, (2) MXAccess data change → OPC UA variable, (3) read → correct tag ref, (4) write → correct tag+value, (5) dashboard HTML has real data
Verification Gate 6 (FINAL)
- Startup: all 14 steps execute in order
- Shutdown: completes within 30s, all components disposed in reverse order
- WIRING: MXAccess OnDataChange → node manager subscription delivery
- WIRING: Galaxy change → address space rebuild
- WIRING: OPC UA Read → MXAccess ReadAsync with correct tag reference
- WIRING: OPC UA Write → MXAccess WriteAsync with correct tag+value
- WIRING: Dashboard aggregates data from all components
- WIRING: Health endpoint reflects actual connection state
- AppDomain.UnhandledException registered
- TopShelf recovery configured (restart, 60s delay)
- FullDataFlowTest passes end-to-end
Master Requirement Traceability (all 44)
| Req | Phase | Verified By |
|---|---|---|
| SVC-001 | Done | Program.cs already configured |
| SVC-002 | Done | Program.cs already configured |
| SVC-003 | 1 | ConfigurationLoadingTests |
| SVC-004 | 6 | ShutdownCompletesTest |
| SVC-005 | 6 | ServiceStartupSequenceTest |
| SVC-006 | 6 | AppDomain handler registration test |
| MXA-001 | 2 | StaComThreadTests |
| MXA-002 | 2 | MxAccessClientConnectionTests |
| MXA-003 | 2 | MxAccessClientSubscriptionTests |
| MXA-004 | 2 | MxAccessClientReadWriteTests |
| MXA-005 | 2 | MxAccessClientMonitorTests |
| MXA-006 | 2 | MxAccessClientMonitorTests (probe) |
| MXA-007 | 2 | Cleanup order test |
| MXA-008 | 2 | Metrics integration in ReadWrite |
| MXA-009 | 1+2 | MxErrorCodesTests + write error path |
| GR-001 | 3 | GetHierarchyAsync maps all columns |
| GR-002 | 3 | GetAttributesAsync maps all columns |
| GR-003 | 3 | ChangeDetectionServiceTests |
| GR-004 | 3+6 | ChangeDetectionToRebuildWiringTest |
| GR-005 | 1+3 | Config tests + ADO.NET usage |
| GR-006 | 3 | Code review: const string SQL only |
| GR-007 | 3 | TestConnectionAsync test |
| OPC-001 | 4 | Endpoint URL test |
| OPC-002 | 4 | BuildTests: node types + references |
| OPC-003 | 4 | BuildTests: variable nodes |
| OPC-004 | 4+6 | ReadWiringTest: browse→tag_name |
| OPC-005 | 1+4 | MxDataTypeMapperTests + variable node DataType |
| OPC-006 | 4 | BuildTests: ValueRank + ArrayDimensions |
| OPC-007 | 4+6 | OpcUaReadToMxAccessWiringTest |
| OPC-008 | 4+6 | OpcUaWriteToMxAccessWiringTest |
| OPC-009 | 4+6 | MxAccessToNodeManagerWiringTest |
| OPC-010 | 4+6 | RebuildTests + ChangeDetectionToRebuildWiringTest |
| OPC-011 | 4 | ServerStatus node test |
| OPC-012 | 4 | Namespace URI test |
| OPC-013 | 4 | Session config test |
| DASH-001 | 5 | StatusWebServerTests routing |
| DASH-002 | 5 | HTML contains Connection panel |
| DASH-003 | 5 | HealthCheckServiceTests |
| DASH-004 | 5 | HTML contains Subscriptions panel |
| DASH-005 | 5 | HTML contains Operations table |
| DASH-006 | 5 | HTML contains Footer |
| DASH-007 | 5 | Meta-refresh tag test |
| DASH-008 | 5 | JSON API deserialization test |
| DASH-009 | 5 | HTML contains Galaxy Info panel |
Final Folder Structure
src/ZB.MOM.WW.LmxOpcUa.Host/
Configuration/ (Phase 1)
Domain/ (Phase 1)
Metrics/ (Phase 1)
MxAccess/ (Phase 2)
GalaxyRepository/ (Phase 3)
OpcUa/ (Phase 4)
Status/ (Phase 5)
OpcUaService.cs (Phase 6 — full wiring)
Program.cs (existing)
appsettings.json (existing)
tests/ZB.MOM.WW.LmxOpcUa.Tests/
Configuration/ (Phase 1)
Domain/ (Phase 1)
Metrics/ (Phase 1)
MxAccess/ (Phase 2)
GalaxyRepository/ (Phase 3)
OpcUa/ (Phase 4)
Status/ (Phase 5)
Wiring/ (Phase 6 — GUARDRAILS)
EndToEnd/ (Phase 6 — GUARDRAILS)
Helpers/FakeMxProxy.cs (Phase 2)
Verification: How to Run
# Build
dotnet build ZB.MOM.WW.LmxOpcUa.slnx
# All tests
dotnet test ZB.MOM.WW.LmxOpcUa.slnx
# Phase-specific (by namespace convention)
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~Configuration"
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~MxAccess"
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~GalaxyRepository"
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~OpcUa"
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~Status"
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~Wiring"
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests --filter "FullyQualifiedName~EndToEnd"
# Integration tests (requires ZB database)
dotnet test tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests