Joseph Doherty a05b84858d Phase 3 PR 43 — Swap ModbusPal to pymodbus for the integration-test simulator. Replaces the .xmpp profiles shipped in PR 42 with pymodbus 3.13.0 ModbusSimulatorServer JSON configs in tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/. Substantive reasons for the swap (rationale block in the test-plan doc): ModbusPal 1.6b is abandoned (last release ~2019), Java GUI-only with no headless mode in the official JAR, and only exposes 2 of the 4 standard Modbus tables (holding_registers + coils — no input_registers, no discrete_inputs). pymodbus is current stable, pure Python CLI (pip install pymodbus[simulator]==3.13.0), exposes all four tables, has built-in declarative actions (increment / random / timestamp / uptime) for dynamic registers, supports custom Python actions for anything more complex, and ships an optional aiohttp-based web UI / REST API for live inspection. Pip-installable on Windows; sidesteps the privileged-port admin requirement by defaulting to TCP 5020.
ModbusSimulatorFixture default port bumped from 502 to 5020 to match the pymodbus convention. Override via MODBUS_SIM_ENDPOINT for a real PLC on its native 502. Skip-message updated to point at the new Pymodbus\serve.ps1 wrapper instead of 'start ModbusPal'. csproj <None Update> rule swapped from ModbusPal/** to Pymodbus/** so the new JSON profiles + serve.ps1 + README copy to test-output as PreserveNewest.
standard.json — generic Modbus TCP server, slave id 1, port 5020, shared blocks=false (independent coils + HR address spaces, more textbook-PLC-like). HR[0..31] seeded with address-as-value via per-register uint16 entries, HR[100] auto-increments via the built-in increment action with parameters minval=0/maxval=65535 (drives subscribe-and-receive integration tests so they have a register that ticks without a write — pymodbus's increment ticks per-access not wall-clock, which is good enough for a 250ms-poll test), HR[200..209] scratch range left at 0 for write tests, coils 0..31 alternating, coils 100..109 scratch. write list covers 0..1023 so any test address is mutable.
dl205.json — AutomationDirect DirectLOGIC DL205/DL260 quirk simulator, slave id 1, port 5020, shared blocks=true (matches DL series memory model where coils/DI/HR overlay the same word address space). Each quirky register seeded with the pre-computed raw uint16 value documented in docs/v2/dl205.md, with an inline _quirk JSON-comment naming the behavior so future-me reading the file knows why HR[1040]=25928 means 'H' lo / 'e' hi (the user's headline string-byte-order finding). Encoded quirks: V0 marker at HR[0]=0xCAFE; V2000 at HR[1024]=0x2000; V40400 at HR[8448]=0x4040; 'Hello' string at HR[1040..1042] first-char-low-byte; Float32 1.5f at HR[1056..1057] in CDAB word order (low word first); BCD register at HR[1072]=0x1234; FC03-128-cap block at HR[1280..1407]; Y0/C0 coil markers at 2048/3072; scratch C-relays at 4000..4007.
serve.ps1 wrapper — pwsh script with a -Profile {standard|dl205} parameter switch. Validates pymodbus.simulator is on PATH (clearer message than the raw CommandNotFoundException), validates the profile JSON exists, builds the right --modbus_server/--modbus_device/--json_file/--http_port arg list, and execs pymodbus.simulator in the foreground. -HttpPort 0 disables the web UI. Foreground exec lets the operator Ctrl+C to stop without an extra control script.
README.md fully rewritten for pymodbus: install command (pip install 'pymodbus[simulator]==3.13.0' — pinned for reproducibility, [simulator] extra pulls aiohttp), per-profile reference tables, the same DL205 quirk → register table from PR 42 but adjusted for pymodbus paths, what's-NEW-vs-ModbusPal section (all four tables, raw uint16 seeding, declarative actions, custom Python action modules, headless, web UI, maintained), trade-offs section (float32-as-two-uint16s for explicit CDAB control, increment ticks per-access not wall-clock, shared-blocks mode for DL205 vs separate for Standard), file-format quick reference for hand-authoring more profiles. References pinned to the pymodbus readthedocs simulator/config + REST API pages.
docs/v2/modbus-test-plan.md harness section rewritten with the swap rationale; PR-history list updated to mark PR 42 SUPERSEDED by PR 43 and call out PR 44+ as the per-quirk implementation track. Test-conventions bullet about 'don't depend on ModbusPal state between tests' generalized to 'don't depend on simulator state' and a note added that pymodbus's REST API can reset state between facts if a test ever needs it.
DL205Profile.cs and DL205SmokeTests.cs xml-doc updated to reference pymodbus / dl205.json instead of ModbusPal / DL205.xmpp.
Functional validation deferred — Python isn't installed on this dev box (winget search returned no matches for Python.Python.3 exact). JSON parses structurally (PowerShell ConvertFrom-Json clean on both files), build clean, .json + serve.ps1 + README all copy to test-output as expected. User installs pymodbus when they want to actually run the simulator end-to-end; if pymodbus rejects the config the README's reference link to pymodbus's simulator/config schema doc is the right next stop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 20:35:26 -04:00
Phase 2 Stream D progress — non-destructive deliverables: appsettings → DriverConfig migration script, two-service Windows installer scripts, process-spawn cross-FX parity test, Stream D removal procedure doc with both Option A (rewrite 494 v1 tests) and Option B (archive + new v2 E2E suite) spelled out step-by-step. Cannot one-shot the actual legacy-Host deletion in any unattended session — explained in the procedure doc; the parity-defect debug cycle is intrinsically interactive (each iteration requires inspecting a v1↔v2 diff and deciding if it's a legitimate v2 improvement or a regression, then either widening the assertion or fixing the v2 code), and git rm -r src/ZB.MOM.WW.OtOpcUa.Host is destructive enough to need explicit operator authorization on a real PR review. scripts/migration/Migrate-AppSettings-To-DriverConfig.ps1 takes a v1 appsettings.json and emits the v2 DriverInstance.DriverConfig JSON blob (MxAccess/Database/Historian sections) ready to upsert into the central Configuration DB; null-leaf stripping; -DryRun mode; smoke-tested against the dev appsettings.json and produces the expected three-section ordered-dictionary output. scripts/install/Install-Services.ps1 registers the two v2 services with sc.exe — OtOpcUaGalaxyHost first (net48 x86 EXE with OTOPCUA_GALAXY_PIPE/OTOPCUA_ALLOWED_SID/OTOPCUA_GALAXY_SECRET/OTOPCUA_GALAXY_BACKEND/OTOPCUA_GALAXY_ZB_CONN/OTOPCUA_GALAXY_CLIENT_NAME env vars set via HKLM:\SYSTEM\CurrentControlSet\Services\OtOpcUaGalaxyHost\Environment registry), then OtOpcUa with depend=OtOpcUaGalaxyHost; resolves down-level account names to SID for the IPC ACL; generates a fresh 32-byte base64 shared secret per install if not supplied (kept out of registry — operators record offline for service rebinding scenarios); echoes start commands. scripts/install/Uninstall-Services.ps1 stops + removes both services. tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/HostSubprocessParityTests.cs is the production-shape parity test — Proxy (.NET 10) spawns the actual OtOpcUa.Driver.Galaxy.Host.exe (net48 x86) as a subprocess via Process.Start with backend=db env vars, connects via real named pipe, calls Discover, asserts at least one Galaxy gobject comes back. Skipped when running as Administrator (PipeAcl denies admins, same guard as other IPC integration tests), when the Host EXE hasn't been built, or when the ZB SQL endpoint is unreachable. This is the cross-FX integration that the parity suite genuinely needs — the previous IPC tests all ran in-process; this one validates the production deployment topology where Proxy and Host are separate processes communicating only over the named pipe. docs/v2/implementation/stream-d-removal-procedure.md is the next-session playbook: Option A (rewrite 494 v1 tests via a ProxyMxAccessClientAdapter that implements v1's IMxAccessClient by forwarding to GalaxyProxyDriver — Vtq↔DataValueSnapshot, Quality↔StatusCode, OnTagValueChanged↔OnDataChange mapping; 3-5 days, full coverage), Option B (rename OtOpcUa.Tests → OtOpcUa.Tests.v1Archive with [Trait("Category", "v1Archive")] for opt-in CI runs; new OtOpcUa.Driver.Galaxy.E2E test project with 10-20 representative tests via the HostSubprocessParityTests pattern; 1-2 days, accreted coverage); deletion checklist with eight pre-conditions, ten ordered steps, and a rollback path (git revert restores the legacy Host alongside the v2 stack — both topologies remain installable until the downstream consumer cutover). Full solution 964 pass / 1 pre-existing Phase 0 baseline; the 494 v1 IntegrationTests + 6 v1 IntegrationTests-net48 still pass because legacy OtOpcUa.Host stays untouched until an interactive session executes the procedure doc.
2026-04-18 00:38:44 -04:00

LmxOpcUa

OPC UA server and cross-platform client tools for AVEVA System Platform (Wonderware) Galaxy. The server exposes Galaxy tags via MXAccess as an OPC UA address space. The client stack provides a shared library, CLI tool, and Avalonia desktop application for browsing, reading/writing, subscriptions, alarms, and historical data.

Architecture

                                    OPC UA Clients
                              (CLI, Desktop UI, 3rd-party)
                                         |
                                         v
+-----------------+     +------------------+     +-----------------+
| Galaxy Repo DB  |---->|   OPC UA Server  |<--->| MXAccess Client |
|   (SQL Server)  |     | (address space)  |     | (STA + COM)     |
+-----------------+     +------------------+     +-----------------+
                                |                        |
                        +-------+--------+     +---------+---------+
                        | Status Dashboard|     | Historian Runtime |
                        |  (HTTP/JSON)   |     |   (SQL Server)    |
                        +----------------+     +-------------------+

Contained Name vs Tag Name

Browse Path (contained names) Runtime Reference (tag name)
TestMachine_001/DelmiaReceiver/DownloadPath DelmiaReceiver_001.DownloadPath
TestMachine_001/MESReceiver/MoveInBatchID MESReceiver_001.MoveInBatchID

Server

The OPC UA server runs on .NET Framework 4.8 (x86) and bridges the Galaxy runtime to OPC UA clients.

Server Prerequisites

  • .NET Framework 4.8 SDK
  • AVEVA System Platform with ArchestrA Framework installed
  • Galaxy repository database (SQL Server, Windows Auth)
  • MXAccess COM registered (LMXProxy.LMXProxyServer)
  • Wonderware Historian (optional, for historical data access)
  • Windows (required for COM interop and MXAccess)

Build and Run Server

dotnet restore ZB.MOM.WW.LmxOpcUa.slnx
dotnet build src/ZB.MOM.WW.LmxOpcUa.Host
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Host

The server starts on opc.tcp://localhost:4840/LmxOpcUa with the None security profile by default. Configure Security.Profiles in appsettings.json to enable Basic256Sha256-Sign or Basic256Sha256-SignAndEncrypt for transport security. See Security Guide.

Install as Windows Service

cd src/ZB.MOM.WW.LmxOpcUa.Host/bin/Debug/net48
ZB.MOM.WW.LmxOpcUa.Host.exe install
ZB.MOM.WW.LmxOpcUa.Host.exe start

Service logon requirement: The service must run under a Windows account that has access to the AVEVA Galaxy and Historian. The default LocalSystem account can connect to MXAccess and SQL Server but cannot authenticate with the Historian SDK (HCAP). Configure the service to "Log on as" a domain or local user that is a recognized ArchestrA platform user. This can be set in services.msc or during install with ZB.MOM.WW.LmxOpcUa.Host.exe install -username DOMAIN\user -password ***.

Run Server Tests

dotnet test tests/ZB.MOM.WW.LmxOpcUa.Tests
dotnet test tests/ZB.MOM.WW.LmxOpcUa.IntegrationTests

Client Stack

The client stack is cross-platform (.NET 10) and consists of three projects sharing a common IOpcUaClientService abstraction. No AVEVA software or COM is required — the clients connect to any OPC UA server.

Client Prerequisites

  • .NET 10 SDK
  • No platform-specific dependencies (runs on Windows, macOS, Linux)

Build All Clients

dotnet build src/ZB.MOM.WW.LmxOpcUa.Client.Shared
dotnet build src/ZB.MOM.WW.LmxOpcUa.Client.CLI
dotnet build src/ZB.MOM.WW.LmxOpcUa.Client.UI

Run Client Tests

dotnet test tests/ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests
dotnet test tests/ZB.MOM.WW.LmxOpcUa.Client.UI.Tests

Client CLI

# Connect
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840/LmxOpcUa

# Browse Galaxy hierarchy
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=ZB" -r -d 5

# Read a tag
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=TestMachine_001.MachineID"

# Write a tag
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- write -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=TestChildObject.TestString" -v "Hello"

# Subscribe to changes
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=TestChildObject.TestInt" -i 500

# Read historical data
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- historyread -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=TestMachine_001.TestHistoryValue" --start "2026-03-25" --end "2026-03-30"

# Subscribe to alarm events
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- alarms -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=3;s=TestMachine_001" --refresh

# Query redundancy state
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.CLI -- redundancy -u opc.tcp://localhost:4840/LmxOpcUa

Client UI

dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Client.UI

The desktop application provides browse tree, subscriptions, alarm monitoring, history reads, and write dialogs. See Client UI Documentation for details.


Project Structure

src/
    ZB.MOM.WW.LmxOpcUa.Host/           OPC UA server (.NET Framework 4.8, x86)
        Configuration/                   Config binding and validation
        Domain/                          Interfaces, DTOs, enums, mappers
        Historian/                       Wonderware Historian data source
        Metrics/                         Performance tracking (rolling P95)
        MxAccess/                        STA thread, COM interop, subscriptions
        GalaxyRepository/                SQL queries, change detection
        OpcUa/                           Server, node manager, address space, alarms, diff
        Status/                          HTTP dashboard, health checks

    ZB.MOM.WW.LmxOpcUa.Client.Shared/   Shared OPC UA client library (.NET 10)
    ZB.MOM.WW.LmxOpcUa.Client.CLI/      Command-line client (.NET 10)
    ZB.MOM.WW.LmxOpcUa.Client.UI/       Avalonia desktop client (.NET 10)

tests/
    ZB.MOM.WW.LmxOpcUa.Tests/           Server unit + integration tests
    ZB.MOM.WW.LmxOpcUa.IntegrationTests/ Server integration tests (live DB)
    ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests/  Shared library tests
    ZB.MOM.WW.LmxOpcUa.Client.CLI.Tests/     CLI command tests
    ZB.MOM.WW.LmxOpcUa.Client.UI.Tests/      UI ViewModel + headless tests

gr/                                      Galaxy repository docs, SQL queries, schema

Documentation

Server

Component Description
OPC UA Server Endpoint, sessions, security policy, server lifecycle
Address Space Hierarchy nodes, variable nodes, primitive grouping, NodeId scheme
Galaxy Repository SQL queries, deployed package chain, change detection
MXAccess Bridge STA thread, COM interop, subscriptions, reconnection
Data Type Mapping Galaxy to OPC UA types, arrays, security classification
Read/Write Operations Value reads, writes, access level enforcement, array element writes
Subscriptions Ref-counted MXAccess subscriptions, data change dispatch
Alarm Tracking AlarmConditionState nodes, InAlarm monitoring, event reporting
Historical Data Access Historian data source, HistoryReadRaw, HistoryReadProcessed
Incremental Sync Diff computation, subtree teardown/rebuild, subscription preservation
Configuration appsettings.json binding, feature flags, validation
Status Dashboard HTTP server, health checks, metrics reporting
Service Hosting TopShelf, startup/shutdown sequence, error handling
Security Transport security profiles, certificate trust, production hardening
Redundancy Non-transparent warm/hot redundancy, ServiceLevel, paired deployment

Client

Component Description
Client CLI Connect, browse, read, write, subscribe, historyread, alarms, redundancy commands
Client UI Avalonia desktop client: browse, subscribe, alarms, history, write values

Reference

License

Internal use only.

Description
No description provided
Readme 14 MiB
Languages
C# 94.6%
TSQL 4.9%
Python 0.3%
Batchfile 0.2%