Joseph Doherty f2c1cc84e9 Phase 7 plan doc — scripting runtime + virtual tags + scripted alarms + historian alarm sink. Draft output from the 2026-04-20 interactive planning session. Phase 7 is the last phase before v2 release readiness; adds two additive runtime capabilities on top of the existing driver + Equipment address-space foundation: (1) virtual (calculated) tags — OPC UA variables whose values are computed by user-authored C# scripts against other tags, evaluated on change and/or timer, living in the existing Equipment tree alongside driver tags, behaving identically to clients; (2) Part 9 scripted alarms — full state machine (EnabledState/ActiveState/AckedState/ConfirmedState/ShelvingState) with persistent operator-supplied state across restarts, complementing (not replacing) the existing Galaxy-native and AB CIP ALMD alarm sources. A third tie-in capability — Aveva Historian as alarm system of record — routes every qualifying alarm transition from any IAlarmSource (scripted + Galaxy + ALMD) through a local SQLite store-and-forward queue to Galaxy.Host, which uses its already-loaded aahClientManaged DLLs to write to the Historian alarm schema; per-alarm HistorizeToAveva toggle gates which sources flow (default off for Galaxy-native to avoid duplicating the direct Galaxy historian path, default on for scripted).
Locks in 22 design decisions from the planning conversation: C# via Roslyn scripting; virtual tags in the Equipment tree (not a separate /Virtual/ namespace); change-driven + timer-driven triggers operator-configurable per tag; Shape A one-script-per-tag-or-alarm (no predicate/action split); full OPC UA Part 9 alarm fidelity; read-only sandbox (scripts read any tag, write only to virtual tags, no File/HttpClient/Process/reflection); AST-inferred dependencies via CSharpSyntaxWalker (non-literal tag paths rejected at publish); config DB storage with generation-sealed cache; ctx.GetTag returns a full DataValue {Value, StatusCode, Timestamp}; per-tag Historize checkbox; per-tag error isolation (throwing script sets tag quality BadInternalError, engine unaffected); dedicated scripts-*.log Serilog sink bound to ctx.Logger; alarm message as template with {TagPath} substitution resolved at event emission; ActiveState recomputed from tags on startup while EnabledState/AckedState/ConfirmedState/ShelvingState + audit persist to config DB; historian sink scope = all IAlarmSource impls with per-alarm toggle; SQLite store-and-forward on the node so operators are never blocked by Historian downtime; IPC to Galaxy.Host for ingestion reusing the already-loaded aahClientManaged DLLs; Monaco editor for Admin code editing; serial cascade evaluation for v1 (parallel as follow-up); shelving UX via OPC UA method calls only with no custom Admin controls (operator drives state transitions from plant HMIs or Client.CLI); 30-day dead-letter retention with manual retry button; test harness accepts only declared-input paths so the harness enforces dependency declaration.

Eight streams totaling ~10-12 weeks, scope-comparable to Phase 6: A - Core.Scripting (Roslyn engine + sandbox + AST inference + logger); B - virtual tag engine (dependency graph + change/timer schedulers + historize); C - scripted alarm engine (Part 9 state machine + template messages + startup recovery + OPC UA method binding); D - historian alarm sink (SQLite store-and-forward + Galaxy.Host IPC contract extension); E - config DB schema (four new tables under sp_PublishGeneration); F - Admin UI scripting tab (Monaco + test harness + dependency preview + script-log viewer + historian diagnostics); G - address-space integration (extend EquipmentNodeWalker for virtual source kind + extend DriverNodeManager dispatch); H - exit gate.

Compliance-check surface covers sandbox escape (typeof/Assembly.Load/File/HttpClient attempts must fail at compile), dependency inference (literal-only paths), change cascade (topological ordering), cycle rejection at publish, startup recovery (ack/confirm/shelve survive restart but ActiveState recomputed), ack audit trail persistence, historian queue durability (Galaxy.Host offline → online drains in-order), per-alarm historian toggle gating, script timeout isolation, log sink isolation, ACL binding (virtual tags inherit Equipment scope grants).

Follow-up artifacts tracked as tasks #231-#238 (stream placeholders). Supporting doc updates (plan.md §6 Migration Strategy, config-db-schema.md §§ for the four new tables, driver-specs.md §Alarm semantics clarification, new ADR-002 for driver-vs-virtual dispatch) will land alongside the streams that touch them, not in this doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:05:12 -04:00
Pin libplctag ab_server to v2.6.16 — real release tag + SHA256 hashes for all three Windows arches. Closes the "pick a current version + pin" deferral left by the #180 PR docs stub. Verified the release lands ab_server.exe inside libplctag_2.6.16_windows_<arch>_tools.zip alongside plctag.dll + list_tags_* helpers by downloading each tools zip + unzip -l'ing to confirm ab_server.exe is present at 331264 bytes. New ci/ab-server.lock.json is the single source of truth — one file the CI YAML reads via ConvertFrom-Json instead of duplicating the hash across the workflow + the docs. Structure: repo (libplctag/libplctag) + tag (v2.6.16) + published date (2026-03-29) + assets keyed by platform (windows-x64 / windows-x86 / windows-arm64) each carrying filename + sha256. docs/v2/test-data-sources.md §2.CI updated — replaces the prior placeholder (ver = '<pinned libplctag release tag>', expected = '<pinned sha256>') with the real v2.6.16 + 9b78a3de... hashes pinned table, and replaces the hardcoded URL with a lockfile-driven pwsh step that picks windows-x64 by default but swaps to x86/arm64 by changing one line for non-x64 CI runners. Hash-mismatch path throws with both the expected + actual values so on the first drift the CI log tells the maintainer exactly what to update in the lockfile. Two verification notes from the release fetch: (1) libplctag v2.6.16 tools zips ship ab_server.exe + plctag.dll together — tests don't need a separate libplctag NuGet download for the integration path, the extracted tools dir covers both the simulator + the driver's native dependency; (2) the three Windows arches all carry ab_server.exe, so ARM64 Windows GitHub runners (when they arrive) can run the integration suite without changes beyond swapping the asset key. No code changes in this PR — purely docs + the new lockfile. Admin tests + Core tests unchanged + passing per the prior commit.
2026-04-20 00:04:35 -04:00
Phase 3 PR 56 -- Siemens S7-1500 pymodbus profile + smoke integration test. Adds tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/s7_1500.json modelling the SIMATIC S7-1500 + MB_SERVER default deployment documented in docs/v2/s7.md: DB1.DBW0 = 0xABCD fingerprint marker (operators reserve this so clients can verify they're talking to the right DB), scratch HR range 200..209 for write-roundtrip tests mirroring dl205.json + standard.json, Float32 1.5f at HR[100..101] in ABCD word order (high word first -- OPPOSITE of DL260 CDAB), Int32 0x12345678 at HR[300..301] in ABCD. Also seeds a coil at bit-addr 400 (= cell 25 bit 0) and a discrete input at bit-addr 500 (= cell 31 bit 0) so future S7-specific tests for FC01/FC02 have stable markers. shared blocks=true to match the proven dl205.json pattern (pymodbus's bits/uint16 cells coexist cleanly when addresses don't collide). Write list references cells (0, 25, 100-101, 200-209, 300-301), not bit addresses -- pymodbus's write-range entries are cell-indexed, not bit-indexed. Adds tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/ directory with S7_1500Profile.cs (mirrors DL205Profile pattern: SmokeHoldingRegister=200, SmokeHoldingValue=4321, BuildOptions tags + probe-disabled + 2s timeout) and S7_1500SmokeTests.cs (single fact S7_1500_roundtrip_write_then_read_of_holding_register that writes SmokeHoldingValue then reads it back, asserting both write status 0 and read status 0 + value equality). Gates on MODBUS_SIM_PROFILE=s7_1500 so the test skips cleanly against other profiles. csproj updated to copy S7/** to test output as PreserveNewest (pattern matching DL205/**). Pymodbus/serve.ps1 ValidateSet extended from {standard,dl205} to {standard,dl205,s7_1500,mitsubishi} -- mitsubishi.json lands in PR 58 but the validator slot is claimed now so the serve.ps1 diff is one line in this PR and zero lines in future PRs. Verified end-to-end: smoke test 1/1 passes against the running pymodbus s7_1500 profile (localhost:5020 FC06 write of 4321 at HR[200] + FC03 read back). 143/143 Modbus.Tests pass, no regression in driver code because this PR is purely test-asset. Per-quirk S7 integration tests (ABCD word order default, FC23 IllegalFunction, MB_SERVER STATUS 0x8383 behaviour, port-per-connection semantics) land in PR 57+.
2026-04-18 22:57:03 -04:00
Phase 3 PR 56 -- Siemens S7-1500 pymodbus profile + smoke integration test. Adds tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/s7_1500.json modelling the SIMATIC S7-1500 + MB_SERVER default deployment documented in docs/v2/s7.md: DB1.DBW0 = 0xABCD fingerprint marker (operators reserve this so clients can verify they're talking to the right DB), scratch HR range 200..209 for write-roundtrip tests mirroring dl205.json + standard.json, Float32 1.5f at HR[100..101] in ABCD word order (high word first -- OPPOSITE of DL260 CDAB), Int32 0x12345678 at HR[300..301] in ABCD. Also seeds a coil at bit-addr 400 (= cell 25 bit 0) and a discrete input at bit-addr 500 (= cell 31 bit 0) so future S7-specific tests for FC01/FC02 have stable markers. shared blocks=true to match the proven dl205.json pattern (pymodbus's bits/uint16 cells coexist cleanly when addresses don't collide). Write list references cells (0, 25, 100-101, 200-209, 300-301), not bit addresses -- pymodbus's write-range entries are cell-indexed, not bit-indexed. Adds tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/ directory with S7_1500Profile.cs (mirrors DL205Profile pattern: SmokeHoldingRegister=200, SmokeHoldingValue=4321, BuildOptions tags + probe-disabled + 2s timeout) and S7_1500SmokeTests.cs (single fact S7_1500_roundtrip_write_then_read_of_holding_register that writes SmokeHoldingValue then reads it back, asserting both write status 0 and read status 0 + value equality). Gates on MODBUS_SIM_PROFILE=s7_1500 so the test skips cleanly against other profiles. csproj updated to copy S7/** to test output as PreserveNewest (pattern matching DL205/**). Pymodbus/serve.ps1 ValidateSet extended from {standard,dl205} to {standard,dl205,s7_1500,mitsubishi} -- mitsubishi.json lands in PR 58 but the validator slot is claimed now so the serve.ps1 diff is one line in this PR and zero lines in future PRs. Verified end-to-end: smoke test 1/1 passes against the running pymodbus s7_1500 profile (localhost:5020 FC06 write of 4321 at HR[200] + FC03 read back). 143/143 Modbus.Tests pass, no regression in driver code because this PR is purely test-asset. Per-quirk S7 integration tests (ABCD word order default, FC23 IllegalFunction, MB_SERVER STATUS 0x8383 behaviour, port-per-connection semantics) land in PR 57+.
2026-04-18 22:57:03 -04:00
FOCAS Tier-C PR B — Driver.FOCAS.Host net48 x86 skeleton + pipe server. Second PR of the 5-PR #220 split. Stands up the Windows Service entry point + named-pipe scaffolding so PR C has a place to move the Fwlib32 calls into. New net48 x86 project (Fwlib32.dll is 32-bit-only, same bitness constraint as Galaxy.Host/MXAccess); references Driver.FOCAS.Shared for framing + DTOs so the wire format Proxy<->Host stays a single type system. Ships four files: PipeAcl creates a PipeSecurity where only the configured server principal SID gets ReadWrite+Synchronize + LocalSystem/BuiltinAdministrators are explicitly denied (so a compromised service account on the same host can't escalate via the pipe); IFrameHandler abstracts the dispatch surface with HandleAsync + AttachConnection for server-push event sinks; PipeServer accepts one connection at a time, verifies the peer SID via RunAsClient, reads the first Hello frame + matches the shared-secret and protocol major version, sends HelloAck, then hands off to the handler until EOF or cancel; StubFrameHandler fully handles Heartbeat/HeartbeatAck so a future supervisor's liveness detector stays happy, and returns ErrorResponse{Code=not-implemented,Message="Kind X is stubbed - Fwlib32 lift lands in PR C"} for every data-plane request. Program.cs mirrors the Galaxy.Host startup exactly: reads OTOPCUA_FOCAS_PIPE / OTOPCUA_ALLOWED_SID / OTOPCUA_FOCAS_SECRET from the env (supervisor passes these at spawn time), builds Serilog rolling-file logger into %ProgramData%\OtOpcUa\focas-host-*.log, constructs the pipe server with StubFrameHandler, loops on RunAsync until SIGINT. Log messages mark the backend as "stub" so it's visible in logs that Fwlib32 isn't actually connected yet. Driver.FOCAS.Host.Tests (net48 x86) ships three integration tests mirroring IpcHandshakeIntegrationTests from Galaxy.Host: correct-secret handshake + heartbeat round-trip, wrong-secret rejection with UnauthorizedAccessException, and a new test that sends a ReadRequest and asserts the StubFrameHandler returns ErrorResponse{not-implemented} mentioning PR C in the message so the wiring between frame dispatch + kind → error mapping is locked. Tests follow the same is-Administrator skip guard as Galaxy because PipeAcl denies BuiltinAdministrators. No changes to existing driver code; FOCAS unit tests still at 165/165 + Shared tests at 24/24. PR C wires the real Fwlib32 backend.
2026-04-20 14:00:56 -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 13 MiB
Languages
C# 94.6%
TSQL 4.9%
Python 0.3%
Batchfile 0.2%