Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56bb1ceaf5 | |||
| 8a51842e89 | |||
| fea2b34e9a | |||
| 392b219233 | |||
| 41f133a337 | |||
| bc8ff7a5fe | |||
| ca149ce907 | |||
| 1913bda6b8 | |||
| fa965ede3d | |||
| 7b3b6580b3 | |||
| 41da84293a | |||
| 16a87b08f3 | |||
| da8a3e46f7 | |||
| 09af8d2830 | |||
| 6c78027b5a | |||
| bb1854b2f8 | |||
| 70d7166a39 | |||
| 6968872e5d | |||
| 020c30f9a6 | |||
| a8dabc47f9 | |||
| 43291d7fdd | |||
| 75b91ebb97 | |||
| 412cdec9b1 | |||
| b90718013e | |||
| 4a2e993a95 | |||
| adbbb5e7d0 | |||
| a8ef73dcb5 | |||
| 22fd314694 | |||
| 8adb83afee | |||
| 1e04796953 | |||
| 5f5bfe1ea5 | |||
| 482d5f5637 | |||
| 31b9468102 | |||
| cf024c8150 | |||
| 0aee14686b | |||
| 4e1751e1a4 | |||
| 969b0847a1 | |||
| a25593a9c6 | |||
| 69f02fed7f | |||
| 5ed26d2ec6 | |||
| 439b39463b | |||
| 62d01e76e5 | |||
| 32b872d5c7 | |||
| 89004c052c | |||
| 2baca785ad | |||
| 1d62709060 | |||
| 0b5a4a676e | |||
| edc984987b | |||
| 6126374594 | |||
| 38afc234ff | |||
| 95422995c0 | |||
| 6e282b9946 | |||
| f67b3b1b30 | |||
| ffacbe0370 | |||
| 8a4526a376 | |||
| f99cf5033a | |||
| c59bf59635 | |||
| 7853e94f4b | |||
| 49ae6e7b6f | |||
| 8d0e13e69e | |||
| 7367b3e23f | |||
| 65a5f64931 | |||
| 80104caf09 | |||
| 493a0ba613 | |||
| ea045477ad | |||
| 33054c3275 | |||
| 77229dfaf3 | |||
| 99016c3137 | |||
| 006af51768 |
@@ -37,3 +37,6 @@ src/ZB.MOM.WW.OtOpcUa.Server/config_cache.db
|
|||||||
# E2E sidecar config — NodeIds are specific to each dev's local seed (see scripts/e2e/README.md)
|
# E2E sidecar config — NodeIds are specific to each dev's local seed (see scripts/e2e/README.md)
|
||||||
scripts/e2e/e2e-config.json
|
scripts/e2e/e2e-config.json
|
||||||
config_cache*.db
|
config_cache*.db
|
||||||
|
|
||||||
|
# Client CLI/UI runtime scratch (last-connected endpoint cache)
|
||||||
|
session.dat
|
||||||
|
|||||||
@@ -9,15 +9,14 @@ Build an OPC UA server (.NET 10) that exposes AVEVA System Platform
|
|||||||
hierarchy as an OPC UA address space, translating between
|
hierarchy as an OPC UA address space, translating between
|
||||||
contained-name browse paths and tag-name runtime references. Galaxy
|
contained-name browse paths and tag-name runtime references. Galaxy
|
||||||
access flows through the in-process `GalaxyDriver`
|
access flows through the in-process `GalaxyDriver`
|
||||||
(`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/`) talking gRPC to a separately
|
(`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/`) talking gRPC to a separately
|
||||||
installed **mxaccessgw** gateway process. The gateway owns the
|
installed **mxaccessgw** gateway process. The gateway owns the
|
||||||
MXAccess COM bitness constraint (its worker is x86 net48); everything
|
MXAccess COM bitness constraint (its worker is x86 net48); everything
|
||||||
in this repo is .NET 10. PR 7.2 retired the legacy in-process
|
in this repo is .NET 10. PR 7.2 retired the legacy in-process
|
||||||
`Galaxy.Host` / `Galaxy.Proxy` / `Galaxy.Shared` projects + the
|
`Galaxy.Host` / `Galaxy.Proxy` / `Galaxy.Shared` projects + the
|
||||||
`OtOpcUaGalaxyHost` Windows service.
|
`OtOpcUaGalaxyHost` Windows service.
|
||||||
|
|
||||||
See `lmx_mxgw.md` for the migration design and
|
See `docs/v2/Galaxy.Performance.md` for the runtime perf surface
|
||||||
`docs/v2/Galaxy.Performance.md` for the runtime perf surface
|
|
||||||
(tracing, metrics, soak harness).
|
(tracing, metrics, soak harness).
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
@@ -48,40 +47,33 @@ Example: browsing `TestMachine_001/DelmiaReceiver/DownloadPath` translates to MX
|
|||||||
|
|
||||||
### Data Type Mapping
|
### Data Type Mapping
|
||||||
|
|
||||||
Galaxy `mx_data_type` values map to OPC UA types (Boolean, Int32, Float, Double, String, DateTime, etc.). Array attributes use ValueRank=1 with ArrayDimensions from the Galaxy attribute definition. The driver-side mapping lives in `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`.
|
Galaxy `mx_data_type` values map to OPC UA types (Boolean, Int32, Float, Double, String, DateTime, etc.). Array attributes use ValueRank=1 with ArrayDimensions from the Galaxy attribute definition. The driver-side mapping lives in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`.
|
||||||
|
|
||||||
### Change Detection
|
### Change Detection
|
||||||
|
|
||||||
`DeployWatcher` (`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DeployWatcher.cs`) polls the gateway's deploy-event signal and raises `IRediscoverable.OnRediscoveryNeeded` when the Galaxy redeploys. The server's `DriverHost` consumes the signal and rebuilds the address space.
|
`DeployWatcher` (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DeployWatcher.cs`) polls the gateway's deploy-event signal and raises `IRediscoverable.OnRediscoveryNeeded` when the Galaxy redeploys. The server's `DriverHost` consumes the signal and rebuilds the address space.
|
||||||
|
|
||||||
## mxaccessgw
|
## mxaccessgw
|
||||||
|
|
||||||
The gateway lives in a sibling repo at `c:\Users\dohertj2\Desktop\mxaccessgw\`. See `docs/v2/Galaxy.ParityRig.md` for the gw setup recipe (build, API key provisioning via `apikey create-key`, env-var overrides for HTTP/2 cleartext + worker path). The gw's MXAccess Toolkit reference (its `gateway.md`) is the canonical MxAccess API doc; the standalone `mxaccess_documentation.md` previously kept in this repo retired in PR 7.3.
|
The gateway lives in a sibling repo at `c:\Users\dohertj2\Desktop\mxaccessgw\`. See `docs/v2/Galaxy.ParityRig.md` for the gw setup recipe (build, API key provisioning via `apikey create-key`, env-var overrides for HTTP/2 cleartext + worker path). The gw's MXAccess Toolkit reference (its `gateway.md`) is the canonical MxAccess API doc; the standalone `mxaccess_documentation.md` previously kept in this repo retired in PR 7.3.
|
||||||
|
|
||||||
## Galaxy Repository Database
|
|
||||||
|
|
||||||
Connection: `sqlcmd -S localhost -d ZB -E` (Windows Auth). See `gr/connectioninfo.md`.
|
|
||||||
|
|
||||||
The `gr/` folder contains:
|
|
||||||
- `queries/` — SQL for hierarchy extraction, attribute lookup, and change detection
|
|
||||||
- `ddl/tables/` and `ddl/views/` — Schema definitions
|
|
||||||
- `schema.md` — Full table/view reference
|
|
||||||
- `build_layout_plan.md` — Step-by-step plan for building the OPC UA address space from DB queries
|
|
||||||
- `gr/CLAUDE.md` — Detailed guidance for working within the `gr/` subfolder
|
|
||||||
|
|
||||||
Key tables: `gobject` (hierarchy/deployment), `template_definition` (object categories), `dynamic_attribute` (user-defined attributes), `primitive_instance` (primitive-to-attribute links), `galaxy` (change detection).
|
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet restore ZB.MOM.WW.OtOpcUa.slnx
|
dotnet restore ZB.MOM.WW.OtOpcUa.slnx
|
||||||
dotnet build ZB.MOM.WW.OtOpcUa.slnx
|
dotnet build ZB.MOM.WW.OtOpcUa.slnx
|
||||||
dotnet test ZB.MOM.WW.OtOpcUa.slnx # all tests
|
dotnet test ZB.MOM.WW.OtOpcUa.slnx # all tests
|
||||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.Tests # unit tests only
|
dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests # a single test project
|
||||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.IntegrationTests # integration tests only
|
dotnet test --filter "FullyQualifiedName~MyTestClass.MyMethod" # a single test
|
||||||
dotnet test --filter "FullyQualifiedName~MyTestClass.MyMethod" # single test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Test projects live under `tests/<module>/` (Core, Server, Drivers,
|
||||||
|
Drivers/Cli, Client, Tooling) — there is no single unit-test project.
|
||||||
|
Unit suites are named `*.Tests`; integration suites are `*.IntegrationTests`
|
||||||
|
and need their Docker fixture up (see Docker Workflow). DB-backed tests in
|
||||||
|
`*.Configuration.Tests`, `*.Admin.Tests`, and `*.Server.Tests` require the
|
||||||
|
central SQL Server.
|
||||||
|
|
||||||
## Docker Workflow (driver fixtures + central SQL Server)
|
## Docker Workflow (driver fixtures + central SQL Server)
|
||||||
|
|
||||||
> **Migrated 2026-04-28**: Docker config + host moved off this dev VM (DESKTOP-6JL3KKO) onto the shared Linux Docker host (`DOCKER`, 10.100.0.35) so the dev VM could shed WSL2/Hyper-V and have its GPU re-attached via ESXi passthrough. Docker Desktop is no longer installed here. All checked-in `appsettings.json` defaults, fixture-class default endpoints, and `e2e-config.sample.json` were rewritten to target `10.100.0.35`. The driver fixture compose files under `tests/.../Docker/docker-compose.yml` now carry a `project: lmxopcua` label on every service. See `docs/v2/dev-environment.md` for the full rewrite (header dated 2026-04-28).
|
> **Migrated 2026-04-28**: Docker config + host moved off this dev VM (DESKTOP-6JL3KKO) onto the shared Linux Docker host (`DOCKER`, 10.100.0.35) so the dev VM could shed WSL2/Hyper-V and have its GPU re-attached via ESXi passthrough. Docker Desktop is no longer installed here. All checked-in `appsettings.json` defaults, fixture-class default endpoints, and `e2e-config.sample.json` were rewritten to target `10.100.0.35`. The driver fixture compose files under `tests/.../Docker/docker-compose.yml` now carry a `project: lmxopcua` label on every service. See `docs/v2/dev-environment.md` for the full rewrite (header dated 2026-04-28).
|
||||||
@@ -135,13 +127,13 @@ The server supports non-transparent warm/hot redundancy via the `Redundancy` sec
|
|||||||
|
|
||||||
## LDAP Authentication
|
## LDAP Authentication
|
||||||
|
|
||||||
The server uses LDAP-based user authentication via the `Authentication.Ldap` section in `appsettings.json`. When enabled, credentials are validated by LDAP bind against a GLAuth server (installed at `C:\publish\glauth\`), and LDAP group membership maps to OPC UA permissions: `ReadOnly` (browse/read), `WriteOperate` (write FreeAccess/Operate attributes), `WriteTune` (write Tune attributes), `WriteConfigure` (write Configure attributes), `AlarmAck` (alarm acknowledgment). `LdapUserAuthenticator` (`src/ZB.MOM.WW.OtOpcUa.Server/Security/LdapUserAuthenticator.cs`) implements `IUserAuthenticator`. See `docs/Security.md` for the full guide and `C:\publish\glauth\auth.md` for LDAP user/group reference.
|
The server uses LDAP-based user authentication via the `Authentication.Ldap` section in `appsettings.json`. When enabled, credentials are validated by LDAP bind against a GLAuth server (installed at `C:\publish\glauth\`), and LDAP group membership maps to OPC UA permissions: `ReadOnly` (browse/read), `WriteOperate` (write FreeAccess/Operate attributes), `WriteTune` (write Tune attributes), `WriteConfigure` (write Configure attributes), `AlarmAck` (alarm acknowledgment). `LdapUserAuthenticator` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/LdapUserAuthenticator.cs`) implements `IUserAuthenticator`. See `docs/Security.md` for the full guide and `C:\publish\glauth\auth.md` for LDAP user/group reference.
|
||||||
|
|
||||||
## Library Preferences
|
## Library Preferences
|
||||||
|
|
||||||
- **Logging**: Serilog with rolling daily file sink
|
- **Logging**: Serilog with rolling daily file sink
|
||||||
- **Unit tests**: xUnit + Shouldly for assertions
|
- **Unit tests**: xUnit + Shouldly for assertions
|
||||||
- **Service hosting (Server, Admin)**: .NET generic host with `AddWindowsService` (decision #30 — replaced TopShelf in v2; see `src/ZB.MOM.WW.OtOpcUa.Server/OpcUaServerService.cs`)
|
- **Service hosting (Server, Admin)**: .NET generic host with `AddWindowsService` (decision #30 — replaced TopShelf in v2; see `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUaServerService.cs`)
|
||||||
- **OPC UA**: OPC Foundation UA .NET Standard stack (https://github.com/opcfoundation/ua-.netstandard) — NuGet: `OPCFoundation.NetStandard.Opc.Ua.Server`
|
- **OPC UA**: OPC Foundation UA .NET Standard stack (https://github.com/opcfoundation/ua-.netstandard) — NuGet: `OPCFoundation.NetStandard.Opc.Ua.Server`
|
||||||
|
|
||||||
## OPC UA .NET Standard Documentation
|
## OPC UA .NET Standard Documentation
|
||||||
@@ -150,11 +142,11 @@ Use the DeepWiki MCP (`mcp__deepwiki`) to query documentation for the OPC UA .NE
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Use the Client CLI at `src/ZB.MOM.WW.OtOpcUa.Client.CLI/` for manual testing against the running OPC UA server. Supports connect, read, write, browse, subscribe, historyread, alarms, and redundancy commands. See `docs/Client.CLI.md` for full documentation.
|
Use the Client CLI at `src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/` for manual testing against the running OPC UA server. Supports connect, read, write, browse, subscribe, historyread, alarms, and redundancy commands. See `docs/Client.CLI.md` for full documentation.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 3
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 3
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode"
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode"
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,200 +1,115 @@
|
|||||||
# LmxOpcUa
|
# OtOpcUa
|
||||||
|
|
||||||
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.
|
OPC UA server (.NET 10 AnyCPU) that exposes a fleet of industrial drivers as a single OPC UA address space. Drivers ship in-process for AVEVA System Platform Galaxy (via the sibling `mxaccessgw` repo), Modbus TCP, Siemens S7, Allen-Bradley CIP (ControlLogix / CompactLogix), Allen-Bradley Legacy (SLC 500 / MicroLogix), Beckhoff TwinCAT (ADS), FANUC FOCAS, and OPC UA Client (gateway).
|
||||||
|
|
||||||
|
A cross-platform client stack (.NET 10) — shared library, CLI, and Avalonia desktop app — connects to any OPC UA server.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
OPC UA Clients
|
OPC UA Clients (CLI, Desktop UI, 3rd-party)
|
||||||
(CLI, Desktop UI, 3rd-party)
|
|
|
||||||
|
|
v
|
||||||
v
|
+-------------------------------------+
|
||||||
+-----------------+ +------------------+ +-----------------+
|
| OtOpcUa.Server (.NET 10 AnyCPU) |
|
||||||
| Galaxy Repo DB |---->| OPC UA Server |<--->| MXAccess Client |
|
| address space + capability fan-out|
|
||||||
| (SQL Server) | | (address space) | | (STA + COM) |
|
+-------------------------------------+
|
||||||
+-----------------+ +------------------+ +-----------------+
|
| | | | | | | |
|
||||||
| |
|
Galaxy Modbus S7 AbCip AbLeg TwinCAT FOCAS OpcUaClient
|
||||||
+-------+--------+ +---------+---------+
|
|
|
||||||
| Status Dashboard| | Historian Runtime |
|
v
|
||||||
| (HTTP/JSON) | | (SQL Server) |
|
mxaccessgw (sibling repo, gRPC)
|
||||||
+----------------+ +-------------------+
|
|
|
||||||
|
v
|
||||||
|
MXAccess COM (x86 worker, on AVEVA box)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contained Name vs Tag Name
|
Galaxy is the only driver with an external runtime: it speaks gRPC to a separately installed `mxaccessgw` server (sibling repo at `c:\Users\dohertj2\Desktop\mxaccessgw\`) which owns the MXAccess COM apartment and the x86/STA bitness constraint server-side. Everything in this repo is platform-agnostic .NET 10.
|
||||||
|
|
||||||
| Browse Path (contained names) | Runtime Reference (tag name) |
|
## Prerequisites
|
||||||
|-------------------------------|------------------------------|
|
|
||||||
| `TestMachine_001/DelmiaReceiver/DownloadPath` | `DelmiaReceiver_001.DownloadPath` |
|
|
||||||
| `TestMachine_001/MESReceiver/MoveInBatchID` | `MESReceiver_001.MoveInBatchID` |
|
|
||||||
|
|
||||||
---
|
- .NET 10 SDK (server, drivers, clients all target .NET 10)
|
||||||
|
- SQL Server reachable for the central config DB
|
||||||
|
- For Galaxy specifically: a running `mxaccessgw` deployment — see [docs/v2/Galaxy.ParityRig.md](docs/v2/Galaxy.ParityRig.md)
|
||||||
|
- For Wonderware Historian read-back: optional `OtOpcUaWonderwareHistorian` sidecar — see [docs/ServiceHosting.md](docs/ServiceHosting.md)
|
||||||
|
|
||||||
## Server
|
## Quick Start
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet restore ZB.MOM.WW.LmxOpcUa.slnx
|
dotnet restore ZB.MOM.WW.OtOpcUa.slnx
|
||||||
dotnet build src/ZB.MOM.WW.LmxOpcUa.Host
|
dotnet build ZB.MOM.WW.OtOpcUa.slnx
|
||||||
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Host
|
dotnet test ZB.MOM.WW.OtOpcUa.slnx
|
||||||
|
|
||||||
|
# Run the server in dev (foreground)
|
||||||
|
dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server
|
||||||
```
|
```
|
||||||
|
|
||||||
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](docs/security.md).
|
The server starts on `opc.tcp://localhost:4840` with the `None` security profile. Configure `Security.Profiles` in `src/Server/ZB.MOM.WW.OtOpcUa.Server/appsettings.json` to enable `Basic256Sha256-Sign` or `Basic256Sha256-SignAndEncrypt`. See [docs/security.md](docs/security.md).
|
||||||
|
|
||||||
### Install as Windows Service
|
## Install as Windows Services
|
||||||
|
|
||||||
|
Production deployment is driven by `scripts/install/Install-Services.ps1`, which registers the `OtOpcUa` server service (and optionally the `OtOpcUaWonderwareHistorian` sidecar) under a chosen service account. Galaxy support requires a separately installed `mxaccessgw` — neither this repo nor the install script provisions it.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\install\Install-Services.ps1 `
|
||||||
|
-InstallRoot 'C:\Program Files\OtOpcUa' `
|
||||||
|
-ServiceAccount 'DOMAIN\svc-otopcua'
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `-InstallWonderwareHistorian` for the historian sidecar. See the script header and [docs/ServiceHosting.md](docs/ServiceHosting.md) for full options.
|
||||||
|
|
||||||
|
## Client CLI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd src/ZB.MOM.WW.LmxOpcUa.Host/bin/Debug/net48
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
|
||||||
ZB.MOM.WW.LmxOpcUa.Host.exe install
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 3
|
||||||
ZB.MOM.WW.LmxOpcUa.Host.exe start
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode"
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- write -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -v 42
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500
|
||||||
```
|
```
|
||||||
|
|
||||||
**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 ***`.
|
See [docs/Client.CLI.md](docs/Client.CLI.md) and [docs/Client.UI.md](docs/Client.UI.md).
|
||||||
|
|
||||||
### Run Server Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 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
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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](docs/Client.UI.md) 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
|
## Documentation
|
||||||
|
|
||||||
### Server
|
### Architecture deep-dives
|
||||||
|
|
||||||
| Component | Description |
|
| Topic | Doc |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [OPC UA Server](docs/OpcUaServer.md) | Endpoint, sessions, security policy, server lifecycle |
|
| OPC UA server composition, namespace fan-out, Polly invoker | [docs/OpcUaServer.md](docs/OpcUaServer.md) |
|
||||||
| [Address Space](docs/AddressSpace.md) | Hierarchy nodes, variable nodes, primitive grouping, NodeId scheme |
|
| Address space layout | [docs/AddressSpace.md](docs/AddressSpace.md) |
|
||||||
| [Galaxy Repository](docs/GalaxyRepository.md) | SQL queries, deployed package chain, change detection |
|
| Read / Write dispatch (driver vs virtual vs scripted-alarm) | [docs/ReadWriteOperations.md](docs/ReadWriteOperations.md) |
|
||||||
| [MXAccess Bridge](docs/MxAccessBridge.md) | STA thread, COM interop, subscriptions, reconnection |
|
| Incremental sync (driver-backend rediscovery + config publishes) | [docs/IncrementalSync.md](docs/IncrementalSync.md) |
|
||||||
| [Data Type Mapping](docs/DataTypeMapping.md) | Galaxy to OPC UA types, arrays, security classification |
|
| Service hosting (Server + Admin + optional historian sidecar) | [docs/ServiceHosting.md](docs/ServiceHosting.md) |
|
||||||
| [Read/Write Operations](docs/ReadWriteOperations.md) | Value reads, writes, access level enforcement, array element writes |
|
| Security (transport, LDAP, certificates) | [docs/security.md](docs/security.md) |
|
||||||
| [Subscriptions](docs/Subscriptions.md) | Ref-counted MXAccess subscriptions, data change dispatch |
|
| Redundancy | [docs/Redundancy.md](docs/Redundancy.md) |
|
||||||
| [Alarm Tracking](docs/AlarmTracking.md) | AlarmConditionState nodes, InAlarm monitoring, event reporting |
|
| Status dashboard | [docs/StatusDashboard.md](docs/StatusDashboard.md) |
|
||||||
| [Historical Data Access](docs/HistoricalDataAccess.md) | Historian data source, HistoryReadRaw, HistoryReadProcessed |
|
|
||||||
| [Incremental Sync](docs/IncrementalSync.md) | Diff computation, subtree teardown/rebuild, subscription preservation |
|
|
||||||
| [Configuration](docs/Configuration.md) | appsettings.json binding, feature flags, validation |
|
|
||||||
| [Status Dashboard](docs/StatusDashboard.md) | HTTP server, health checks, metrics reporting |
|
|
||||||
| [Service Hosting](docs/ServiceHosting.md) | TopShelf, startup/shutdown sequence, error handling |
|
|
||||||
| [Security](docs/security.md) | Transport security profiles, certificate trust, production hardening |
|
|
||||||
| [Redundancy](docs/Redundancy.md) | Non-transparent warm/hot redundancy, ServiceLevel, paired deployment |
|
|
||||||
|
|
||||||
### Client
|
### Drivers
|
||||||
|
|
||||||
| Component | Description |
|
| Topic | Doc |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [Client CLI](docs/Client.CLI.md) | Connect, browse, read, write, subscribe, historyread, alarms, redundancy commands |
|
| Driver specs (per-driver capability surface, config, addressing) | [docs/v2/driver-specs.md](docs/v2/driver-specs.md) |
|
||||||
| [Client UI](docs/Client.UI.md) | Avalonia desktop client: browse, subscribe, alarms, history, write values |
|
| Galaxy driver | [docs/drivers/Galaxy.md](docs/drivers/Galaxy.md) |
|
||||||
|
| Modbus / S7 / AbCip / AbLegacy / TwinCAT / FOCAS / OpcUaClient | [docs/drivers/](docs/drivers/) |
|
||||||
|
| Galaxy parity rig (mxaccessgw setup) | [docs/v2/Galaxy.ParityRig.md](docs/v2/Galaxy.ParityRig.md) |
|
||||||
|
| Galaxy performance + tracing | [docs/v2/Galaxy.Performance.md](docs/v2/Galaxy.Performance.md) |
|
||||||
|
|
||||||
### Reference
|
### Clients
|
||||||
|
|
||||||
- [Galaxy Repository Queries](gr/CLAUDE.md) — SQL queries for hierarchy, attributes, and change detection
|
| Topic | Doc |
|
||||||
- [Data Type Mapping](gr/data_type_mapping.md) — Galaxy to OPC UA type mapping with security classification
|
|---|---|
|
||||||
|
| Client CLI | [docs/Client.CLI.md](docs/Client.CLI.md) |
|
||||||
|
| Client UI (Avalonia desktop) | [docs/Client.UI.md](docs/Client.UI.md) |
|
||||||
|
|
||||||
|
### v1 archive
|
||||||
|
|
||||||
|
The original v1 in-process MXAccess docs (Galaxy.Host topology,
|
||||||
|
Configuration env vars, AlarmTracking, DataTypeMapping,
|
||||||
|
HistoricalDataAccess, Subscriptions, etc.) are preserved under
|
||||||
|
[docs/v1/](docs/v1/) — historical reference only. PR 7.2 retired the
|
||||||
|
v1 architecture on 2026-04-30; current state is documented in the
|
||||||
|
sections above.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+95
-73
@@ -1,75 +1,97 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Folder Name="/src/">
|
<Folder Name="/src/" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
<Folder Name="/src/Core/">
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj"/>
|
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core/ZB.MOM.WW.OtOpcUa.Core.csproj"/>
|
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ZB.MOM.WW.OtOpcUa.Core.Scripting.csproj"/>
|
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core/ZB.MOM.WW.OtOpcUa.Core.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.csproj"/>
|
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ZB.MOM.WW.OtOpcUa.Core.Scripting.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.csproj"/>
|
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"/>
|
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj"/>
|
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Admin/ZB.MOM.WW.OtOpcUa.Admin.csproj"/>
|
</Folder>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.csproj"/>
|
<Folder Name="/src/Server/">
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.csproj"/>
|
<Project Path="src/Server/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.csproj"/>
|
<Project Path="src/Server/ZB.MOM.WW.OtOpcUa.Admin/ZB.MOM.WW.OtOpcUa.Admin.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ZB.MOM.WW.OtOpcUa.Driver.Modbus.csproj"/>
|
</Folder>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.csproj"/>
|
<Folder Name="/src/Drivers/">
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.S7/ZB.MOM.WW.OtOpcUa.Driver.S7.csproj"/>
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/ZB.MOM.WW.OtOpcUa.Driver.AbCip.csproj"/>
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.csproj"/>
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.csproj"/>
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ZB.MOM.WW.OtOpcUa.Driver.Modbus.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.csproj"/>
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj"/>
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/ZB.MOM.WW.OtOpcUa.Driver.S7.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.Shared/ZB.MOM.WW.OtOpcUa.Client.Shared.csproj"/>
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/ZB.MOM.WW.OtOpcUa.Driver.AbCip.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj"/>
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.UI/ZB.MOM.WW.OtOpcUa.Client.UI.csproj"/>
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.csproj"/>
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.csproj"/>
|
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.csproj"/>
|
</Folder>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.csproj"/>
|
<Folder Name="/src/Drivers/Driver CLIs/">
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.csproj"/>
|
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.csproj"/>
|
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli.csproj"/>
|
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.csproj" />
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Analyzers/ZB.MOM.WW.OtOpcUa.Analyzers.csproj"/>
|
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.csproj" />
|
||||||
</Folder>
|
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.csproj" />
|
||||||
<Folder Name="/tests/">
|
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.csproj"/>
|
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests.csproj"/>
|
</Folder>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.Tests/ZB.MOM.WW.OtOpcUa.Core.Tests.csproj"/>
|
<Folder Name="/src/Client/">
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests.csproj"/>
|
<Project Path="src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/ZB.MOM.WW.OtOpcUa.Client.Shared.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests.csproj"/>
|
<Project Path="src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests.csproj"/>
|
<Project Path="src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ZB.MOM.WW.OtOpcUa.Client.UI.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests.csproj"/>
|
</Folder>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Server.Tests/ZB.MOM.WW.OtOpcUa.Server.Tests.csproj"/>
|
<Folder Name="/src/Tooling/">
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ZB.MOM.WW.OtOpcUa.Admin.Tests.csproj"/>
|
<Project Path="src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/ZB.MOM.WW.OtOpcUa.Analyzers.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests.csproj"/>
|
</Folder>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.csproj"/>
|
<Folder Name="/tests/" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.csproj"/>
|
<Folder Name="/tests/Core/">
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests.csproj"/>
|
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests.csproj"/>
|
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests.csproj"/>
|
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/ZB.MOM.WW.OtOpcUa.Core.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj"/>
|
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests.csproj"/>
|
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests.csproj"/>
|
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests.csproj"/>
|
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests.csproj"/>
|
</Folder>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests.csproj"/>
|
<Folder Name="/tests/Server/">
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests.csproj"/>
|
<Project Path="tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/ZB.MOM.WW.OtOpcUa.Server.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.csproj"/>
|
<Project Path="tests/Server/ZB.MOM.WW.OtOpcUa.Admin.Tests/ZB.MOM.WW.OtOpcUa.Admin.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.csproj"/>
|
<Project Path="tests/Server/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj"/>
|
</Folder>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests.csproj"/>
|
<Folder Name="/tests/Drivers/">
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests.csproj" />
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Analyzers.Tests/ZB.MOM.WW.OtOpcUa.Analyzers.Tests.csproj"/>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests.csproj" />
|
||||||
</Folder>
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests.csproj" />
|
||||||
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests.csproj" />
|
||||||
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests.csproj" />
|
||||||
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests.csproj" />
|
||||||
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests.csproj" />
|
||||||
|
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/tests/Drivers/Driver CLIs/">
|
||||||
|
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests.csproj" />
|
||||||
|
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests.csproj" />
|
||||||
|
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests.csproj" />
|
||||||
|
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests.csproj" />
|
||||||
|
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests.csproj" />
|
||||||
|
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/tests/Client/">
|
||||||
|
<Project Path="tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.csproj" />
|
||||||
|
<Project Path="tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.csproj" />
|
||||||
|
<Project Path="tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/tests/Tooling/">
|
||||||
|
<Project Path="tests/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers.Tests/ZB.MOM.WW.OtOpcUa.Analyzers.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{"title":"Phase 3 PR 54 -- Siemens S7 Modbus TCP quirks research doc","body":"## Summary\n\nAdds `docs/v2/s7.md` (485 lines) covering Siemens SIMATIC S7 family Modbus TCP behavior. Mirrors the `docs/v2/dl205.md` template for future per-quirk implementation PRs.\n\n## Key findings for the implementation track\n\n- **No fixed memory map** — every S7 Modbus server is user-wired via `MB_SERVER`/`MODBUSCP`/`MODBUSPN` library blocks. Driver must accept per-site config, not assume a vendor layout.\n- **MB_SERVER requires non-optimized DBs** (STATUS `0x8383` if optimized). Most common field bug.\n- **Word order default = ABCD** (opposite of DL260). Driver's S7 profile default must be `ByteOrder.BigEndian`, not `WordSwap`.\n- **One port per MB_SERVER instance** — multi-client requires parallel FBs on 503/504/… Most clients assume port 502 multiplexes (wrong on S7).\n- **CP 343-1 Lean is server-only**, requires the `2XV9450-1MB00` license.\n- **FC20/21/22/23/43 all return Illegal Function** on every S7 variant — driver must not attempt FC23 bulk-read optimization for S7.\n- **STOP-mode behavior non-deterministic** across firmware bands — treat both read/write STOP-mode responses as unavailable.\n\nTwo items flagged as unconfirmed rumour (V2.0+ float byte-order claim, STOP-mode caching location).\n\nNo code, no tests — implementation lands in PRs 56+.\n\n## Test plan\n- [x] Doc renders as markdown\n- [x] 31 citations present\n- [x] Section structure matches dl205.md template","head":"phase-3-pr54-s7-research-doc","base":"v2"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"title":"Phase 3 PR 55 -- Mitsubishi MELSEC Modbus TCP quirks research doc","body":"## Summary\n\nAdds `docs/v2/mitsubishi.md` (451 lines) covering MELSEC Q/L/iQ-R/iQ-F/FX3U Modbus TCP behavior. Mirrors `docs/v2/dl205.md` template for per-quirk implementation PRs.\n\n## Key findings for the implementation track\n\n- **Module naming trap** — `QJ71MB91` is SERIAL RTU, not TCP. TCP module is `QJ71MT91`. Surface clearly in driver docs.\n- **No canonical mapping** — per-site 'Modbus Device Assignment Parameter' block (up to 16 entries). Treat mapping as runtime config.\n- **X/Y hex vs octal depends on family** — Q/L/iQ-R use HEX (X20 = decimal 32); FX/iQ-F use OCTAL (X20 = decimal 16). Helper must take a family selector.\n- **Word order CDAB default** across all MELSEC families (opposite of Siemens S7). Driver Mitsubishi profile default: `ByteOrder.WordSwap`.\n- **D-registers binary by default** (opposite of DL205's BCD default). Caller opts in to `Bcd16`/`Bcd32` when ladder uses BCD.\n- **FX5U needs firmware ≥ 1.060** for Modbus TCP server — older is client-only.\n- **FX3U-ENET vs FX3U-ENET-P502 vs FX3U-ENET-ADP** — only the middle one binds port 502; the last has no Modbus at all. Common operator mis-purchase.\n- **QJ71MT91 does NOT support FC22 / FC23** — iQ-R / iQ-F do. Bulk-read optimization must gate on capability.\n- **STOP-mode writes configurable** on Q/L/iQ-R/iQ-F (default accept), always rejected on FX3U-ENET.\n\nThree unconfirmed rumours flagged separately.\n\nNo code, no tests — implementation lands in PRs 58+.\n\n## Test plan\n- [x] Doc renders as markdown\n- [x] 17 citations present\n- [x] Per-model test naming matrix included (`Mitsubishi_QJ71MT91_*`, `Mitsubishi_FX5U_*`, `Mitsubishi_FX3U_ENET_*`, shared `Mitsubishi_Common_*`)","head":"phase-3-pr55-mitsubishi-research-doc","base":"v2"}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Address Space
|
# Address Space
|
||||||
|
|
||||||
Each driver's browsable subtree is built by streaming nodes from the driver's `ITagDiscovery.DiscoverAsync` implementation into an `IAddressSpaceBuilder`. `GenericDriverNodeManager` (`src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs`) owns the shared orchestration; `DriverNodeManager` (`src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) implements `IAddressSpaceBuilder` against the OPC Foundation stack's `CustomNodeManager2`. The same code path serves Galaxy object hierarchies, Modbus PLC registers, AB CIP tags, TwinCAT symbols, FOCAS CNC parameters, and OPC UA Client aggregations — Galaxy is one driver of seven, not the driver.
|
Each driver's browsable subtree is built by streaming nodes from the driver's `ITagDiscovery.DiscoverAsync` implementation into an `IAddressSpaceBuilder`. `GenericDriverNodeManager` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs`) owns the shared orchestration; `DriverNodeManager` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) implements `IAddressSpaceBuilder` against the OPC Foundation stack's `CustomNodeManager2`. The same code path serves Galaxy object hierarchies, Modbus PLC registers, AB CIP tags, TwinCAT symbols, FOCAS CNC parameters, and OPC UA Client aggregations — Galaxy is one driver of seven, not the driver.
|
||||||
|
|
||||||
## Driver root folder
|
## Driver root folder
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ Every driver's subtree starts with a root `FolderState` under the standard OPC U
|
|||||||
|
|
||||||
## IAddressSpaceBuilder surface
|
## IAddressSpaceBuilder surface
|
||||||
|
|
||||||
`IAddressSpaceBuilder` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs`) offers three calls:
|
`IAddressSpaceBuilder` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs`) offers three calls:
|
||||||
|
|
||||||
- `Folder(browseName, displayName)` — creates a child `FolderState` and returns a child builder scoped to it.
|
- `Folder(browseName, displayName)` — creates a child `FolderState` and returns a child builder scoped to it.
|
||||||
- `Variable(browseName, displayName, DriverAttributeInfo attributeInfo)` — creates a `BaseDataVariableState` and returns an `IVariableHandle` the driver keeps for alarm wiring.
|
- `Variable(browseName, displayName, DriverAttributeInfo attributeInfo)` — creates a `BaseDataVariableState` and returns an `IVariableHandle` the driver keeps for alarm wiring.
|
||||||
@@ -18,7 +18,7 @@ Drivers drive ordering. Typical pattern: root → folder per equipment → varia
|
|||||||
|
|
||||||
## DriverAttributeInfo → OPC UA variable
|
## DriverAttributeInfo → OPC UA variable
|
||||||
|
|
||||||
Each variable carries a `DriverAttributeInfo` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs`):
|
Each variable carries a `DriverAttributeInfo` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs`):
|
||||||
|
|
||||||
| Field | OPC UA target |
|
| Field | OPC UA target |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -65,8 +65,8 @@ Drivers that implement `IRediscoverable` fire `OnRediscoveryNeeded` when their b
|
|||||||
|
|
||||||
## Key source files
|
## Key source files
|
||||||
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — orchestration + `CapturingBuilder`
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — orchestration + `CapturingBuilder`
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — OPC UA materialization (`IAddressSpaceBuilder` impl + `NestedBuilder`)
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — OPC UA materialization (`IAddressSpaceBuilder` impl + `NestedBuilder`)
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs` — builder contract
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs` — builder contract
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs` — driver discovery capability
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs` — driver discovery capability
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs` — per-attribute descriptor
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs` — per-attribute descriptor
|
||||||
|
|||||||
+107
-106
@@ -1,128 +1,129 @@
|
|||||||
# Alarm Tracking
|
# Alarm tracking — v2 final architecture
|
||||||
|
|
||||||
Alarm surfacing is an optional driver capability exposed via `IAlarmSource` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs`). Drivers whose backends have an alarm concept implement it — today: Galaxy (MXAccess alarms), FOCAS (CNC alarms), OPC UA Client (A&C events from the upstream server). Modbus / S7 / AB CIP / AB Legacy / TwinCAT do not implement the interface and the feature is simply absent from their subtrees.
|
This document describes how OtOpcUa surfaces alarms to OPC UA Part 9
|
||||||
|
clients after the **alarms-over-gateway** epic
|
||||||
|
([docs/plans/alarms-over-gateway.md](plans/alarms-over-gateway.md))
|
||||||
|
landed. The v1 architecture (Galaxy.Host's COM-side `GalaxyAlarmTracker`)
|
||||||
|
is preserved at [docs/v1/AlarmTracking.md](v1/AlarmTracking.md) for
|
||||||
|
historical reference.
|
||||||
|
|
||||||
## IAlarmSource surface
|
## Three alarm sources, one OPC UA Part 9 surface
|
||||||
|
|
||||||
```csharp
|
| Source | Driver capability | Path |
|
||||||
Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
|----------------------------------|--------------------------|------|
|
||||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken);
|
| **Galaxy MxAccess (driver-native)** | `GalaxyDriver : IAlarmSource` | gateway → worker → MxAccess alarm sink → `MX_EVENT_FAMILY_ON_ALARM_TRANSITION` → `EventPump` → driver `OnAlarmEvent` → `AlarmConditionService` |
|
||||||
Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken);
|
| **Galaxy sub-attribute fallback** | `IWritable` writes to `$Alarm*` sub-attributes | gateway data subscription → driver `OnDataChange` → `DriverNodeManager` ConditionSink → `AlarmConditionService` |
|
||||||
Task AcknowledgeAsync(IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
|
| **Scripted alarms** | `Phase7EngineComposer` | server-side script evaluator → `Phase7EngineComposer.RouteToHistorianAsync` + `AlarmConditionService` |
|
||||||
CancellationToken cancellationToken);
|
|
||||||
event EventHandler<AlarmEventArgs>? OnAlarmEvent;
|
|
||||||
```
|
|
||||||
|
|
||||||
The driver fires `OnAlarmEvent` for every transition (`Active`, `Acknowledged`, `Inactive`) with an `AlarmEventArgs` carrying the source node id, condition id, alarm type, message, severity (`AlarmSeverity` enum), and source timestamp.
|
All three converge on `AlarmConditionService` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Alarms/AlarmConditionService.cs`),
|
||||||
|
which owns the OPC UA Part 9 state machine and dispatches transitions
|
||||||
|
to the OPC UA condition node managers. Driver-native transitions take
|
||||||
|
precedence over sub-attribute synthesis when both arrive for the same
|
||||||
|
condition — the dedup logic prefers the richer driver-native record
|
||||||
|
because it carries the full operator + raise-time + category metadata
|
||||||
|
that the value-driven path collapses.
|
||||||
|
|
||||||
## AlarmSurfaceInvoker
|
## Galaxy driver path (driver-native)
|
||||||
|
|
||||||
`AlarmSurfaceInvoker` (`src/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs`) wraps the three mutating surfaces through `CapabilityInvoker`:
|
Restored in PR B.2 of the epic. `GalaxyDriver` implements
|
||||||
|
`IAlarmSource` with these surfaces:
|
||||||
|
|
||||||
- `SubscribeAlarmsAsync` / `UnsubscribeAlarmsAsync` run through the `DriverCapability.AlarmSubscribe` pipeline — retries apply under the tier configuration.
|
- `SubscribeAlarmsAsync(sourceNodeIds)` → returns a sentinel handle.
|
||||||
- `AcknowledgeAsync` runs through `DriverCapability.AlarmAcknowledge` which does NOT retry per decision #143. A timed-out ack may have already registered at the plant floor; replay would silently double-acknowledge.
|
The driver doesn't multiplex per source-node-id today; every
|
||||||
|
active handle observes the gateway's alarm-event stream. The
|
||||||
|
server-side `AlarmConditionService` filters by source-node before
|
||||||
|
raising the OPC UA condition.
|
||||||
|
- `UnsubscribeAlarmsAsync(handle)` → symmetric handle removal.
|
||||||
|
- `AcknowledgeAsync(requests)` → routes one gateway RPC per
|
||||||
|
acknowledgement through `IGalaxyAlarmAcknowledger`. Production
|
||||||
|
uses `GatewayGalaxyAlarmAcknowledger` calling
|
||||||
|
`MxGatewayClient.AcknowledgeAlarmAsync` (PR E.2 SDK method).
|
||||||
|
- `OnAlarmEvent` → bridges `EventPump.OnAlarmTransition` (PR B.1)
|
||||||
|
onto `AlarmEventArgs`. Suppressed when no alarm subscription is
|
||||||
|
active so untracked transitions don't leak through.
|
||||||
|
|
||||||
Multi-host fan-out: when the driver implements `IPerCallHostResolver`, each source node id is resolved individually and batches are grouped by host so a dead PLC inside a multi-device driver doesn't poison sibling breakers. Single-host drivers fall back to `IDriver.DriverInstanceId` as the pipeline-key host.
|
The proto contract carries the rich payload — alarm full reference,
|
||||||
|
source-object reference, alarm-type-name, transition kind (Raise /
|
||||||
|
Acknowledge / Clear / Retrigger), severity (raw MxAccess scale),
|
||||||
|
original raise timestamp, transition timestamp, operator user,
|
||||||
|
operator comment, alarm category, description. `MxAccessSeverityMapper`
|
||||||
|
(PR B.1) translates the raw severity onto the four-bucket
|
||||||
|
`AlarmSeverity` ladder — boundaries match v1's `GalaxyAlarmTracker`
|
||||||
|
so customers see no surprise re-classification.
|
||||||
|
|
||||||
## Condition-node creation via CapturingBuilder
|
The richer fields surface on `Core.Abstractions.AlarmEventArgs` via
|
||||||
|
the optional properties added in PR E.7 (`OperatorComment`,
|
||||||
|
`OriginalRaiseTimestampUtc`, `AlarmCategory`). Consumers that don't
|
||||||
|
need them are unaffected; consumers that do (Client.UI, Client.CLI
|
||||||
|
verbose mode) read the new fields when present.
|
||||||
|
|
||||||
Alarm-condition nodes are materialized at address-space build time. During `GenericDriverNodeManager.BuildAddressSpaceAsync` the builder is wrapped in a `CapturingBuilder` that observes every `Variable()` call. When a driver calls `IVariableHandle.MarkAsAlarmCondition(AlarmConditionInfo)` on a returned handle, the server-side `DriverNodeManager.VariableHandle` creates a sibling `AlarmConditionState` node and returns an `IAlarmConditionSink`. The wrapper stores the sink in `_alarmSinks` keyed by the variable's full reference, then `GenericDriverNodeManager` registers a forwarder on `IAlarmSource.OnAlarmEvent` that routes each push to the matching sink by `SourceNodeId`. Unknown source ids are dropped silently — they may belong to another driver.
|
## Galaxy sub-attribute fallback
|
||||||
|
|
||||||
The `AlarmConditionState` layout matches OPC UA Part 9:
|
For Galaxy templates without `$Alarm*` extensions, the value-driven
|
||||||
|
path stays in place: `DriverNodeManager` registers an
|
||||||
|
`AlarmConditionState` per Galaxy variable that bears alarm-bearing
|
||||||
|
sub-attributes (`InAlarm`, `Acked`, `Priority`, `Description`),
|
||||||
|
subscribes to those sub-attributes, and synthesizes Part 9 transitions
|
||||||
|
when the values change. This path operated as the only Galaxy alarm
|
||||||
|
path between PR 7.2 and the alarms-over-gateway epic; it remains the
|
||||||
|
fallback today.
|
||||||
|
|
||||||
- `SourceNode` → the originating variable
|
When both paths report the same condition,
|
||||||
- `SourceName` / `ConditionName` → from `AlarmConditionInfo.SourceName`
|
`AlarmConditionService.AlarmConditionState` keeps the
|
||||||
- Initial state: enabled, inactive, acknowledged, severity per `InitialSeverity`, retain false
|
driver-native record and discards the duplicate sub-attribute
|
||||||
- `HasCondition` references wire the source variable ↔ the condition node bidirectionally
|
synthesis. Driver-native transitions are richer (carry operator
|
||||||
|
comment + original raise time) and arrive lower-latency (no
|
||||||
|
publishing-interval delay on the sub-attribute reads), so they win
|
||||||
|
the dedup.
|
||||||
|
|
||||||
Drivers flag alarm-bearing variables at discovery time via `DriverAttributeInfo.IsAlarm = true`. The Galaxy driver, for example, sets this on attributes that have an `AlarmExtension` primitive in the Galaxy repository DB; FOCAS sets it on the CNC alarm register.
|
## Acknowledge routing
|
||||||
|
|
||||||
## State transitions
|
`DriverNodeManager` picks the acknowledger when registering each
|
||||||
|
condition (PR B.3 logic):
|
||||||
|
|
||||||
`ConditionSink.OnTransition` runs under the node manager's `Lock` and maps the `AlarmEventArgs.AlarmType` string to Part 9 state:
|
- Driver implements `IAlarmSource` →
|
||||||
|
`DriverAlarmSourceAcknowledger` routes the operator comment
|
||||||
|
through `IAlarmSource.AcknowledgeAsync` via the existing
|
||||||
|
`AlarmSurfaceInvoker` (Phase 6.1 resilience pipeline; no-retry
|
||||||
|
per decision #143). End-to-end operator-comment fidelity is
|
||||||
|
preserved.
|
||||||
|
- Driver doesn't implement `IAlarmSource` →
|
||||||
|
`DriverWritableAcknowledger` writes the comment into the
|
||||||
|
`AckMsgWriteRef` sub-attribute via `IWritable.WriteAsync`. Same
|
||||||
|
resilience pipeline; collapses the comment into a single string
|
||||||
|
write at the wire level.
|
||||||
|
|
||||||
| AlarmType | Action |
|
The OPC UA Part 9 `AlarmConditionState.OnAcknowledge` delegate
|
||||||
|---|---|
|
already validates the session's `AlarmAck` role before dispatching,
|
||||||
| `Active` | `SetActiveState(true)`, `SetAcknowledgedState(false)`, `Retain = true` |
|
so the gateway-side ack RPC only sees authenticated, authorised
|
||||||
| `Acknowledged` | `SetAcknowledgedState(true)` |
|
calls.
|
||||||
| `Inactive` | `SetActiveState(false)`; `Retain = false` once both inactive and acknowledged |
|
|
||||||
|
|
||||||
Severity is remapped: `AlarmSeverity.Low/Medium/High/Critical` → OPC UA numeric 250 / 500 / 700 / 900. `Message.Value` is set from `AlarmEventArgs.Message` on every transition. `ClearChangeMasks(true)` and `ReportEvent(condition)` fire the OPC UA event notification for clients subscribed to any ancestor notifier.
|
## Historian write-back (non-Galaxy alarms)
|
||||||
|
|
||||||
## Acknowledge dispatch
|
Scripted alarms (and any future non-Galaxy `IAlarmSource` like
|
||||||
|
AB CIP ALMD) route to AVEVA Historian via the Wonderware sidecar:
|
||||||
|
|
||||||
Alarm acknowledgement initiated by an OPC UA client flows:
|
- `Phase7Composer.ResolveHistorianSink` resolves an
|
||||||
|
`IAlarmHistorianWriter` from either a driver that natively
|
||||||
|
implements it or the DI-registered `WonderwareHistorianClient`
|
||||||
|
(the sidecar IPC client). Driver-provided wins when both are
|
||||||
|
present.
|
||||||
|
- `SqliteStoreAndForwardSink` queues each transition to a local
|
||||||
|
SQLite database and drains in the background via the resolved
|
||||||
|
writer.
|
||||||
|
- Sidecar (PR C.1 + C.2) forwards the events to `aahClientManaged`'s
|
||||||
|
alarm-event write API; the live SDK call site is pinned during
|
||||||
|
PR D.1's deploy-rig validation.
|
||||||
|
|
||||||
1. The SDK invokes the `AlarmConditionState.OnAcknowledge` method delegate.
|
Galaxy-native alarms with `$Alarm*` extensions reach AVEVA Historian
|
||||||
2. The handler checks the session's roles for `AlarmAck` — drivers never see a request the session wasn't entitled to make.
|
directly via System Platform's `HistorizeToAveva` toggle on the
|
||||||
3. `AlarmSurfaceInvoker.AcknowledgeAsync` is called with the source / condition / comment tuple. The invoker groups by host and runs each batch through the no-retry `AlarmAcknowledge` pipeline.
|
alarm primitive — no involvement from OtOpcUa. This sidecar path is
|
||||||
|
exclusively for non-Galaxy alarm producers.
|
||||||
|
|
||||||
Drivers return normally for success or throw to signal the ack failed at the backend.
|
## Cross-references
|
||||||
|
|
||||||
## EventNotifier propagation
|
- Plan: [docs/plans/alarms-over-gateway.md](plans/alarms-over-gateway.md)
|
||||||
|
- v1 archive: [docs/v1/AlarmTracking.md](v1/AlarmTracking.md)
|
||||||
Drivers that want hierarchical alarm subscriptions propagate `EventNotifier.SubscribeToEvents` up the containment chain during discovery — the Galaxy driver flips the flag on every ancestor of an alarm-bearing object up to the driver root, mirroring v1 behavior. Clients subscribed at the driver root, a mid-level folder, or the `Objects/` root see alarm events from every descendant with an `AlarmConditionState` sibling. The driver-root `FolderState` is created in `DriverNodeManager.CreateAddressSpace` with `EventNotifier = SubscribeToEvents | HistoryRead` so alarm event subscriptions and alarm history both have a single natural target.
|
- Galaxy driver: [docs/drivers/Galaxy.md](drivers/Galaxy.md)
|
||||||
|
- Phase 7 scripting + alarming: [docs/v2/implementation/phase-7-scripting-and-alarming.md](v2/implementation/phase-7-scripting-and-alarming.md)
|
||||||
## ConditionRefresh
|
- Security + ACL: [docs/Security.md](Security.md)
|
||||||
|
|
||||||
The OPC UA `ConditionRefresh` service queues the current state of every retained condition back to the requesting monitored items. `DriverNodeManager` iterates the node manager's `AlarmConditionState` collection and queues each condition whose `Retain.Value == true` — matching the Part 9 requirement.
|
|
||||||
|
|
||||||
## Alarm historian sink
|
|
||||||
|
|
||||||
Distinct from the live `IAlarmSource` stream and the Part 9 `AlarmConditionState` materialization above, qualifying alarm transitions are **also** persisted to a durable event log for downstream AVEVA Historian ingestion. This is a separate subsystem from the `IHistoryProvider` capability used by `HistoryReadEvents` (see [HistoricalDataAccess.md](HistoricalDataAccess.md#alarm-event-history-vs-ihistoryprovider)): the sink is a *producer* path (server → Historian) that runs independently of any client HistoryRead call.
|
|
||||||
|
|
||||||
### `IAlarmHistorianSink`
|
|
||||||
|
|
||||||
`src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs` defines the intake contract:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken);
|
|
||||||
HistorianSinkStatus GetStatus();
|
|
||||||
```
|
|
||||||
|
|
||||||
`EnqueueAsync` is fire-and-forget from the producer's perspective — it must never block the emitting thread. The event payload (`AlarmHistorianEvent` — same file) is source-agnostic: `AlarmId`, `EquipmentPath`, `AlarmName`, `AlarmTypeName` (Part 9 subtype name), `Severity`, `EventKind` (free-form transition string — `Activated` / `Cleared` / `Acknowledged` / `Confirmed` / `Shelved` / …), `Message`, `User`, `Comment`, `TimestampUtc`.
|
|
||||||
|
|
||||||
The sink scope is defined to span every alarm source (plan decision #15: scripted, Galaxy-native, AB CIP ALMD, any future `IAlarmSource`), gated per-alarm by a `HistorizeToAveva` toggle on the producer. Today only `Phase7EngineComposer.RouteToHistorianAsync` (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs`) is wired — it subscribes to `ScriptedAlarmEngine.OnEvent` and marshals each emission into `AlarmHistorianEvent`. Galaxy-native alarms continue to reach AVEVA Historian via the driver's direct `aahClientManaged` path and do not flow through the sink; the AB CIP ALMD path remains unwired pending a producer-side integration.
|
|
||||||
|
|
||||||
### `SqliteStoreAndForwardSink`
|
|
||||||
|
|
||||||
Default production implementation (`src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs`). A local SQLite queue absorbs every `EnqueueAsync` synchronously; a background `Timer` drains batches asynchronously to an `IAlarmHistorianWriter` so operator actions are never blocked on historian reachability.
|
|
||||||
|
|
||||||
Queue schema (single table `Queue`): `RowId PK autoincrement`, `AlarmId`, `EnqueuedUtc`, `PayloadJson` (serialized `AlarmHistorianEvent`), `AttemptCount`, `LastAttemptUtc`, `LastError`, `DeadLettered` (bool), plus `IX_Queue_Drain (DeadLettered, RowId)`. Default capacity `1_000_000` non-dead-lettered rows; oldest rows evict with a WARN log past the cap.
|
|
||||||
|
|
||||||
Drain cadence: `StartDrainLoop(tickInterval)` arms a periodic timer. `DrainOnceAsync` reads up to `batchSize` rows (default 100) in `RowId` order and forwards them through `IAlarmHistorianWriter.WriteBatchAsync`, which returns one `HistorianWriteOutcome` per row:
|
|
||||||
|
|
||||||
| Outcome | Action |
|
|
||||||
|---|---|
|
|
||||||
| `Ack` | Row deleted. |
|
|
||||||
| `PermanentFail` | Row flipped to `DeadLettered = 1` with reason. Peers in the batch retry independently. |
|
|
||||||
| `RetryPlease` | `AttemptCount` bumped; row stays queued. Drain worker enters `BackingOff`. |
|
|
||||||
|
|
||||||
Writer-side exceptions treat the whole batch as `RetryPlease`.
|
|
||||||
|
|
||||||
Backoff ladder on `RetryPlease` (hard-coded): 1s → 2s → 5s → 15s → 60s cap. Reset to 0 on any batch with no retries. `CurrentBackoff` exposes the current step for instrumentation; the drain timer itself fires on `tickInterval`, so the ladder governs write cadence rather than timer period.
|
|
||||||
|
|
||||||
Dead-letter retention defaults to 30 days (plan decision #21). `PurgeAgedDeadLetters` runs each drain pass and deletes rows whose `LastAttemptUtc` is past the cutoff. `RetryDeadLettered()` is an operator action that clears `DeadLettered` + resets `AttemptCount` on every dead-lettered row so they rejoin the main queue.
|
|
||||||
|
|
||||||
### Composition and writer resolution
|
|
||||||
|
|
||||||
`Phase7Composer.ResolveHistorianSink` (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs`) scans the registered drivers for one that implements `IAlarmHistorianWriter`. Today that is `GalaxyProxyDriver` via `GalaxyHistorianWriter` (`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/Ipc/GalaxyHistorianWriter.cs`), which forwards batches over the Galaxy.Host pipe to the `aahClientManaged` alarm schema. When a writer is found, a `SqliteStoreAndForwardSink` is instantiated against `%ProgramData%/OtOpcUa/alarm-historian-queue.db` with a 2 s drain tick and the writer attached. When no driver provides a writer the fallback is the DI-registered `NullAlarmHistorianSink` (`src/ZB.MOM.WW.OtOpcUa.Server/Program.cs`), which silently discards and reports `HistorianDrainState.Disabled`.
|
|
||||||
|
|
||||||
### Status and observability
|
|
||||||
|
|
||||||
`GetStatus()` returns `HistorianSinkStatus(QueueDepth, DeadLetterDepth, LastDrainUtc, LastSuccessUtc, LastError, DrainState)` — two `COUNT(*)` scalars plus last-drain telemetry. `DrainState` is one of `Disabled` / `Idle` / `Draining` / `BackingOff`.
|
|
||||||
|
|
||||||
The Admin UI `/alarms/historian` page surfaces this through `HistorianDiagnosticsService` (`src/ZB.MOM.WW.OtOpcUa.Admin/Services/HistorianDiagnosticsService.cs`), which also exposes `TryRetryDeadLettered` — it calls through to `SqliteStoreAndForwardSink.RetryDeadLettered` when the live sink is the SQLite implementation and returns 0 otherwise.
|
|
||||||
|
|
||||||
## Key source files
|
|
||||||
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs` — capability contract + `AlarmEventArgs`
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs` — per-host fan-out + no-retry ack
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — `CapturingBuilder` + alarm forwarder
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `VariableHandle.MarkAsAlarmCondition` + `ConditionSink`
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/Alarms/GalaxyAlarmTracker.cs` — Galaxy-specific alarm-event production
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs` — historian sink intake contract + `AlarmHistorianEvent` + `HistorianSinkStatus` + `IAlarmHistorianWriter`
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs` — durable queue + drain worker + backoff ladder + dead-letter retention
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — `RouteToHistorianAsync` wires scripted-alarm emissions into the sink
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` — `ResolveHistorianSink` selects `SqliteStoreAndForwardSink` vs `NullAlarmHistorianSink`
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Admin/Services/HistorianDiagnosticsService.cs` — Admin UI `/alarms/historian` status + retry-dead-lettered operator action
|
|
||||||
|
|||||||
+3
-3
@@ -9,12 +9,12 @@ The CLI is the primary tool for operators and developers to test and interact wi
|
|||||||
## Build and Run
|
## Build and Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd src/ZB.MOM.WW.OtOpcUa.Client.CLI
|
cd src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI
|
||||||
dotnet build
|
dotnet build
|
||||||
dotnet run -- <command> [options]
|
dotnet run -- <command> [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
The executable name is `otopcua-cli`. Dev boxes carrying a pre-task-#208 install may still have the legacy `{LocalAppData}/LmxOpcUaClient/` folder on disk; on first launch of any post-#208 CLI or UI build, `ClientStoragePaths` (`src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs`) migrates it to `{LocalAppData}/OtOpcUaClient/` automatically so trusted certificates + saved settings survive the rename.
|
The executable name is `otopcua-cli`. Dev boxes carrying a pre-task-#208 install may still have the legacy `{LocalAppData}/LmxOpcUaClient/` folder on disk; on first launch of any post-#208 CLI or UI build, `ClientStoragePaths` (`src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs`) migrates it to `{LocalAppData}/OtOpcUaClient/` automatically so trusted certificates + saved settings survive the rename.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -240,5 +240,5 @@ Application URI: urn:localhost:OtOpcUa:instance1
|
|||||||
The Client CLI has 52 unit tests covering option parsing, service invocation, output formatting, and cleanup behavior:
|
The Client CLI has 52 unit tests covering option parsing, service invocation, output formatting, and cleanup behavior:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests
|
dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests
|
||||||
```
|
```
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ The UI provides a single-window interface for browsing the address space, readin
|
|||||||
## Build and Run
|
## Build and Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd src/ZB.MOM.WW.OtOpcUa.Client.UI
|
cd src/Client/ZB.MOM.WW.OtOpcUa.Client.UI
|
||||||
dotnet build
|
dotnet build
|
||||||
dotnet run
|
dotnet run
|
||||||
```
|
```
|
||||||
@@ -254,7 +254,7 @@ All service event handlers (data changes, alarm events, connection state changes
|
|||||||
The UI has 102 unit tests covering ViewModel logic and headless rendering:
|
The UI has 102 unit tests covering ViewModel logic and headless rendering:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests
|
dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests
|
||||||
```
|
```
|
||||||
|
|
||||||
Tests use:
|
Tests use:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ TwinCAT). Shares `Driver.Cli.Common` with the others.
|
|||||||
## Build + run
|
## Build + run
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli -- --help
|
dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common flags
|
## Common flags
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ others.
|
|||||||
## Build + run
|
## Build + run
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli -- --help
|
dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common flags
|
## Common flags
|
||||||
@@ -99,7 +99,7 @@ otopcua-ablegacy-cli subscribe -g ab://192.168.1.20/1,0 -a N7:10 -t Int -i 500
|
|||||||
|
|
||||||
The integration-fixture `ab_server` Docker container accepts TCP but its PCCC
|
The integration-fixture `ab_server` Docker container accepts TCP but its PCCC
|
||||||
dispatcher doesn't actually respond — see
|
dispatcher doesn't actually respond — see
|
||||||
[`tests/...AbLegacy.IntegrationTests/Docker/README.md`](../tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md).
|
[`tests/...AbLegacy.IntegrationTests/Docker/README.md`](../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md).
|
||||||
Point `--gateway` at real hardware or an RSEmulate 500 box for end-to-end
|
Point `--gateway` at real hardware or an RSEmulate 500 box for end-to-end
|
||||||
wire-level validation. The CLI itself is correct regardless of which endpoint
|
wire-level validation. The CLI itself is correct regardless of which endpoint
|
||||||
you target.
|
you target.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ process Host arrangement required. The CLI loads `FocasDriver` with
|
|||||||
components.
|
components.
|
||||||
|
|
||||||
A dev-friendly mock is available — start
|
A dev-friendly mock is available — start
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml`
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml`
|
||||||
and point `--cnc-host` at `localhost` for end-to-end CLI exercises
|
and point `--cnc-host` at `localhost` for end-to-end CLI exercises
|
||||||
without a real CNC. See
|
without a real CNC. See
|
||||||
[drivers/FOCAS-Test-Fixture.md](drivers/FOCAS-Test-Fixture.md).
|
[drivers/FOCAS-Test-Fixture.md](drivers/FOCAS-Test-Fixture.md).
|
||||||
@@ -25,14 +25,14 @@ without a real CNC. See
|
|||||||
## Build + run
|
## Build + run
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet build src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli
|
dotnet build src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli -- --help
|
dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
Or publish a self-contained binary:
|
Or publish a self-contained binary:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet publish src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli -c Release -o publish/focas-cli
|
dotnet publish src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli -c Release -o publish/focas-cli
|
||||||
publish/focas-cli/otopcua-focas-cli.exe --help
|
publish/focas-cli/otopcua-focas-cli.exe --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ without copy-paste.
|
|||||||
## Build + run
|
## Build + run
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet build src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli
|
dotnet build src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -- --help
|
dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
Or publish a self-contained binary:
|
Or publish a self-contained binary:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet publish src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -c Release -o publish/modbus-cli
|
dotnet publish src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -c Release -o publish/modbus-cli
|
||||||
publish/modbus-cli/otopcua-modbus-cli.exe --help
|
publish/modbus-cli/otopcua-modbus-cli.exe --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Fourth of four driver test-client CLIs.
|
|||||||
## Build + run
|
## Build + run
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli -- --help
|
dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common flags
|
## Common flags
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Fifth (final) of the driver test-client CLIs.
|
|||||||
## Build + run
|
## Build + run
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli -- --help
|
dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Prerequisite: AMS router
|
## Prerequisite: AMS router
|
||||||
|
|||||||
+2
-2
@@ -37,7 +37,7 @@ Every driver CLI exposes the same four verbs:
|
|||||||
|
|
||||||
## Shared infrastructure
|
## Shared infrastructure
|
||||||
|
|
||||||
All six CLIs depend on `src/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/`:
|
All six CLIs depend on `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/`:
|
||||||
|
|
||||||
- `DriverCommandBase` — `--verbose` + Serilog configuration + the abstract
|
- `DriverCommandBase` — `--verbose` + Serilog configuration + the abstract
|
||||||
`Timeout` surface every protocol-specific base overrides with its own
|
`Timeout` surface every protocol-specific base overrides with its own
|
||||||
@@ -91,5 +91,5 @@ Tasks #249 / #250 / #251 shipped the original five. The FOCAS CLI followed
|
|||||||
alongside the Tier-C isolation work on task #220 — no CLI-level test
|
alongside the Tier-C isolation work on task #220 — no CLI-level test
|
||||||
project (hardware-gated). 122 unit tests cumulative across the first five
|
project (hardware-gated). 122 unit tests cumulative across the first five
|
||||||
(16 shared-lib + 106 CLI-specific) — run
|
(16 shared-lib + 106 CLI-specific) — run
|
||||||
`dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests` +
|
`dotnet test tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests` +
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.*.Cli.Tests` to re-verify.
|
`tests/ZB.MOM.WW.OtOpcUa.Driver.*.Cli.Tests` to re-verify.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Two distinct change-detection paths feed the running server: driver-backend redi
|
|||||||
|
|
||||||
## Driver-backend rediscovery — IRediscoverable
|
## Driver-backend rediscovery — IRediscoverable
|
||||||
|
|
||||||
Drivers whose backend has a native change signal implement `IRediscoverable` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs`):
|
Drivers whose backend has a native change signal implement `IRediscoverable` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs`):
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public interface IRediscoverable
|
public interface IRediscoverable
|
||||||
@@ -28,7 +28,7 @@ Static drivers (Modbus, S7, AB CIP, AB Legacy, FOCAS) do not implement `IRedisco
|
|||||||
|
|
||||||
Tag-set changes authored in the Admin UI (UNS edits, CSV imports, driver-config edits) accumulate in a draft generation and commit via `sp_PublishGeneration`. The delta between the currently-published generation and the proposed next one is computed by `sp_ComputeGenerationDiff`, which drives:
|
Tag-set changes authored in the Admin UI (UNS edits, CSV imports, driver-config edits) accumulate in a draft generation and commit via `sp_PublishGeneration`. The delta between the currently-published generation and the proposed next one is computed by `sp_ComputeGenerationDiff`, which drives:
|
||||||
|
|
||||||
- The **DiffViewer** in Admin (`src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffViewer.razor`) so operators can preview what will change before clicking Publish.
|
- The **DiffViewer** in Admin (`src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffViewer.razor`) so operators can preview what will change before clicking Publish.
|
||||||
- The 409-on-stale-draft flow (decision #161) — a UNS drag-reorder preview carries a `DraftRevisionToken` so Confirm returns `409 Conflict / refresh-required` if the draft advanced between preview and commit.
|
- The 409-on-stale-draft flow (decision #161) — a UNS drag-reorder preview carries a `DraftRevisionToken` so Confirm returns `409 Conflict / refresh-required` if the draft advanced between preview and commit.
|
||||||
|
|
||||||
After publish, the server's generation applier invokes `IDriver.ReinitializeAsync(driverConfigJson, ct)` on every driver whose `DriverInstance.DriverConfig` row changed in the new generation. Reinitialize is the in-process recovery path for Tier A/B drivers; if it fails the driver is marked `DriverState.Faulted` and its nodes go Bad quality — but the server process stays running. See `docs/v2/driver-stability.md`.
|
After publish, the server's generation applier invokes `IDriver.ReinitializeAsync(driverConfigJson, ct)` on every driver whose `DriverInstance.DriverConfig` row changed in the new generation. Reinitialize is the in-process recovery path for Tier A/B drivers; if it fails the driver is marked `DriverState.Faulted` and its nodes go Bad quality — but the server process stays running. See `docs/v2/driver-stability.md`.
|
||||||
@@ -53,7 +53,7 @@ When `RediscoveryEventArgs.ScopeHint` is non-null (e.g. a folder path), Core res
|
|||||||
|
|
||||||
## Virtual tags in the rebuild
|
## Virtual tags in the rebuild
|
||||||
|
|
||||||
Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), virtual (scripted) tags live in the same address space as driver tags and flow through the same rebuild. `EquipmentNodeWalker` (`src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs`) emits virtual-tag children alongside driver-tag children with `DriverAttributeInfo.Source = NodeSourceKind.Virtual`, and `DriverNodeManager` registers each variable's source in `_sourceByFullRef` so the dispatch branches correctly after rebuild. Virtual-tag script changes published from the Admin UI land through the same generation-publish path — the `VirtualTagEngine` recompiles its script bundle when its config row changes and `DriverNodeManager` re-registers any added/removed virtual variables through the standard diff path. Subscription restoration after rebuild runs through each source's `ISubscribable` — either the driver's or `VirtualTagSource` — without special-casing.
|
Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), virtual (scripted) tags live in the same address space as driver tags and flow through the same rebuild. `EquipmentNodeWalker` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs`) emits virtual-tag children alongside driver-tag children with `DriverAttributeInfo.Source = NodeSourceKind.Virtual`, and `DriverNodeManager` registers each variable's source in `_sourceByFullRef` so the dispatch branches correctly after rebuild. Virtual-tag script changes published from the Admin UI land through the same generation-publish path — the `VirtualTagEngine` recompiles its script bundle when its config row changes and `DriverNodeManager` re-registers any added/removed virtual variables through the standard diff path. Subscription restoration after rebuild runs through each source's `ISubscribable` — either the driver's or `VirtualTagSource` — without special-casing.
|
||||||
|
|
||||||
## Active subscriptions survive rebuild
|
## Active subscriptions survive rebuild
|
||||||
|
|
||||||
@@ -61,9 +61,9 @@ Subscriptions for unchanged references stay live across rebuilds — their ref-c
|
|||||||
|
|
||||||
## Key source files
|
## Key source files
|
||||||
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs` — backend-change capability
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs` — backend-change capability
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — discovery orchestration
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — discovery orchestration
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs` — `ReinitializeAsync` contract
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs` — `ReinitializeAsync` contract
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Admin/Services/GenerationService.cs` — publish-flow driver
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/GenerationService.cs` — publish-flow driver
|
||||||
- `docs/v2/config-db-schema.md` — `sp_PublishGeneration` + `sp_ComputeGenerationDiff`
|
- `docs/v2/config-db-schema.md` — `sp_PublishGeneration` + `sp_ComputeGenerationDiff`
|
||||||
- `docs/v2/admin-ui.md` — DiffViewer + draft-revision-token flow
|
- `docs/v2/admin-ui.md` — DiffViewer + draft-revision-token flow
|
||||||
|
|||||||
+12
-12
@@ -1,14 +1,14 @@
|
|||||||
# OPC UA Server
|
# OPC UA Server
|
||||||
|
|
||||||
The OPC UA server component (`src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs`) hosts the OPC UA stack and exposes one browsable subtree per registered driver. The server itself is driver-agnostic — Galaxy/MXAccess, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client are all plugged in as `IDriver` implementations via the capability interfaces in `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/`.
|
The OPC UA server component (`src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs`) hosts the OPC UA stack and exposes one browsable subtree per registered driver. The server itself is driver-agnostic — Galaxy/MXAccess, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client are all plugged in as `IDriver` implementations via the capability interfaces in `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/`.
|
||||||
|
|
||||||
## Composition
|
## Composition
|
||||||
|
|
||||||
`OtOpcUaServer` subclasses the OPC Foundation `StandardServer` and wires:
|
`OtOpcUaServer` subclasses the OPC Foundation `StandardServer` and wires:
|
||||||
|
|
||||||
- A `DriverHost` (`src/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs`) which registers drivers and holds the per-instance `IDriver` references.
|
- A `DriverHost` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs`) which registers drivers and holds the per-instance `IDriver` references.
|
||||||
- One `DriverNodeManager` per registered driver (`src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`), constructed in `CreateMasterNodeManager`. Each manager owns its own namespace URI (`urn:OtOpcUa:{DriverInstanceId}`) and exposes the driver as a subtree under the standard `Objects` folder.
|
- One `DriverNodeManager` per registered driver (`src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`), constructed in `CreateMasterNodeManager`. Each manager owns its own namespace URI (`urn:OtOpcUa:{DriverInstanceId}`) and exposes the driver as a subtree under the standard `Objects` folder.
|
||||||
- A `CapabilityInvoker` (`src/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs`) per driver instance, keyed on `(DriverInstanceId, HostName, DriverCapability)` against the shared `DriverResiliencePipelineBuilder`. Every Read/Write/Discovery/Subscribe/HistoryRead/AlarmSubscribe call on the driver flows through this invoker so the Polly pipeline (retry / timeout / breaker / bulkhead) applies. The OTOPCUA0001 Roslyn analyzer enforces the wrapping at compile time.
|
- A `CapabilityInvoker` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs`) per driver instance, keyed on `(DriverInstanceId, HostName, DriverCapability)` against the shared `DriverResiliencePipelineBuilder`. Every Read/Write/Discovery/Subscribe/HistoryRead/AlarmSubscribe call on the driver flows through this invoker so the Polly pipeline (retry / timeout / breaker / bulkhead) applies. The OTOPCUA0001 Roslyn analyzer enforces the wrapping at compile time.
|
||||||
- An `IUserAuthenticator` (LDAP in production, injected stub in tests) for `UserName` token validation in the `ImpersonateUser` hook.
|
- An `IUserAuthenticator` (LDAP in production, injected stub in tests) for `UserName` token validation in the `ImpersonateUser` hook.
|
||||||
- Optional `AuthorizationGate` + `NodeScopeResolver` (Phase 6.2) that sit in front of every dispatch call. In lax mode the gate passes through when the identity lacks LDAP groups so existing integration tests keep working; strict mode (`Authorization:StrictMode = true`) denies those cases.
|
- Optional `AuthorizationGate` + `NodeScopeResolver` (Phase 6.2) that sit in front of every dispatch call. In lax mode the gate passes through when the identity lacks LDAP groups so existing integration tests keep working; strict mode (`Authorization:StrictMode = true`) denies those cases.
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ The host name fed to the invoker comes from `IPerCallHostResolver.ResolveHost(fu
|
|||||||
|
|
||||||
## Redundancy
|
## Redundancy
|
||||||
|
|
||||||
`Redundancy.Enabled = true` on the `ServerInstance` activates the `RedundancyCoordinator` + `ServiceLevelCalculator` (`src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`). Standard OPC UA redundancy nodes (`Server/ServerRedundancy/RedundancySupport`, `ServerUriArray`, `Server/ServiceLevel`) are populated on startup; `ServiceLevel` recomputes whenever any driver's `DriverHealth` changes. The apply-lease mechanism prevents two instances from concurrently applying a generation. See `docs/Redundancy.md`.
|
`Redundancy.Enabled = true` on the `ServerInstance` activates the `RedundancyCoordinator` + `ServiceLevelCalculator` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`). Standard OPC UA redundancy nodes (`Server/ServerRedundancy/RedundancySupport`, `ServerUriArray`, `Server/ServiceLevel`) are populated on startup; `ServiceLevel` recomputes whenever any driver's `DriverHealth` changes. The apply-lease mechanism prevents two instances from concurrently applying a generation. See `docs/Redundancy.md`.
|
||||||
|
|
||||||
## Server class hierarchy
|
## Server class hierarchy
|
||||||
|
|
||||||
@@ -79,10 +79,10 @@ Certificate stores default to `%LOCALAPPDATA%\OPC Foundation\pki\` (directory-ba
|
|||||||
|
|
||||||
## Key source files
|
## Key source files
|
||||||
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs` — `StandardServer` subclass + `ImpersonateUser` hook
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs` — `StandardServer` subclass + `ImpersonateUser` hook
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — per-driver `CustomNodeManager2` + dispatch surface
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — per-driver `CustomNodeManager2` + dispatch surface
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs` — programmatic `ApplicationConfiguration` + lifecycle
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs` — programmatic `ApplicationConfiguration` + lifecycle
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs` — driver registration
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs` — driver registration
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — Polly pipeline entry point
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — Polly pipeline entry point
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core/Authorization/` — Phase 6.2 permission trie + evaluator
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/` — Phase 6.2 permission trie + evaluator
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` — stack-to-evaluator bridge
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` — stack-to-evaluator bridge
|
||||||
|
|||||||
+21
-14
@@ -11,9 +11,8 @@ The project was originally called **LmxOpcUa** (a single-driver Galaxy/MXAccess
|
|||||||
|
|
||||||
- **Core** owns the OPC UA stack, address space, session/security/subscription machinery.
|
- **Core** owns the OPC UA stack, address space, session/security/subscription machinery.
|
||||||
- **Drivers** plug in via capability interfaces in `ZB.MOM.WW.OtOpcUa.Core.Abstractions`: `IDriver`, `IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider`, `IPerCallHostResolver`. Each driver opts into whichever it supports.
|
- **Drivers** plug in via capability interfaces in `ZB.MOM.WW.OtOpcUa.Core.Abstractions`: `IDriver`, `IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider`, `IPerCallHostResolver`. Each driver opts into whichever it supports.
|
||||||
- **Server** is the OPC UA endpoint process (net10, x64). Hosts every driver except Galaxy in-process; talks to Galaxy via a named pipe because MXAccess COM is 32-bit-only.
|
- **Server** is the OPC UA endpoint process (net10, AnyCPU). Hosts every driver in-process. The Galaxy driver reaches MXAccess via gRPC to a separately-installed **mxaccessgw** sidecar (sibling repo); it is no longer hosted from this repo.
|
||||||
- **Admin** is the Blazor Server operator UI (net10, x64). Owns the Config DB draft/publish flow, ACL + role-grant authoring, fleet status + `/metrics` scrape endpoint.
|
- **Admin** is the Blazor Server operator UI (net10, x64). Owns the Config DB draft/publish flow, ACL + role-grant authoring, fleet status + `/metrics` scrape endpoint.
|
||||||
- **Galaxy.Host** is a .NET Framework 4.8 x86 Windows service that wraps MXAccess COM on an STA thread for the Galaxy driver.
|
|
||||||
|
|
||||||
## Where to find what
|
## Where to find what
|
||||||
|
|
||||||
@@ -24,11 +23,11 @@ The project was originally called **LmxOpcUa** (a single-driver Galaxy/MXAccess
|
|||||||
| [OpcUaServer.md](OpcUaServer.md) | Top-level server architecture — Core, driver dispatch, Config DB, generations |
|
| [OpcUaServer.md](OpcUaServer.md) | Top-level server architecture — Core, driver dispatch, Config DB, generations |
|
||||||
| [AddressSpace.md](AddressSpace.md) | `GenericDriverNodeManager` + `ITagDiscovery` + `IAddressSpaceBuilder` |
|
| [AddressSpace.md](AddressSpace.md) | `GenericDriverNodeManager` + `ITagDiscovery` + `IAddressSpaceBuilder` |
|
||||||
| [ReadWriteOperations.md](ReadWriteOperations.md) | OPC UA Read/Write → `CapabilityInvoker` → `IReadable`/`IWritable` |
|
| [ReadWriteOperations.md](ReadWriteOperations.md) | OPC UA Read/Write → `CapabilityInvoker` → `IReadable`/`IWritable` |
|
||||||
| [Subscriptions.md](Subscriptions.md) | Monitored items → `ISubscribable` + per-driver subscription refcount |
|
| [Subscriptions.md](v1/Subscriptions.md) | Monitored items → `ISubscribable` + per-driver subscription refcount (v1 archive) |
|
||||||
| [AlarmTracking.md](AlarmTracking.md) | `IAlarmSource` + `AlarmSurfaceInvoker` + OPC UA alarm conditions |
|
| [AlarmTracking.md](v1/AlarmTracking.md) | `IAlarmSource` + `AlarmSurfaceInvoker` + OPC UA alarm conditions (v1 archive) |
|
||||||
| [DataTypeMapping.md](DataTypeMapping.md) | Per-driver `DriverAttributeInfo` → OPC UA variable types |
|
| [DataTypeMapping.md](v1/DataTypeMapping.md) | Per-driver `DriverAttributeInfo` → OPC UA variable types (v1 archive — live mapping is in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`) |
|
||||||
| [IncrementalSync.md](IncrementalSync.md) | Address-space rebuild on redeploy + `sp_ComputeGenerationDiff` |
|
| [IncrementalSync.md](IncrementalSync.md) | Address-space rebuild on redeploy + `sp_ComputeGenerationDiff` |
|
||||||
| [HistoricalDataAccess.md](HistoricalDataAccess.md) | `IHistoryProvider` as a per-driver optional capability |
|
| [HistoricalDataAccess.md](v1/HistoricalDataAccess.md) | `IHistoryProvider` as a per-driver optional capability (v1 archive) |
|
||||||
| [VirtualTags.md](VirtualTags.md) | `Core.Scripting` + `Core.VirtualTags` — Roslyn script sandbox, engine, dispatch alongside driver tags |
|
| [VirtualTags.md](VirtualTags.md) | `Core.Scripting` + `Core.VirtualTags` — Roslyn script sandbox, engine, dispatch alongside driver tags |
|
||||||
| [ScriptedAlarms.md](ScriptedAlarms.md) | `Core.ScriptedAlarms` — script-predicate `IAlarmSource` + Part 9 state machine |
|
| [ScriptedAlarms.md](ScriptedAlarms.md) | `Core.ScriptedAlarms` — script-predicate `IAlarmSource` + Part 9 state machine |
|
||||||
|
|
||||||
@@ -36,7 +35,7 @@ Two Core subsystems are shipped without a dedicated top-level doc; see the secti
|
|||||||
|
|
||||||
| Project | See |
|
| Project | See |
|
||||||
|---------|-----|
|
|---------|-----|
|
||||||
| `Core.AlarmHistorian` | [AlarmTracking.md](AlarmTracking.md) § Alarm historian sink |
|
| `Core.AlarmHistorian` | [AlarmTracking.md](v1/AlarmTracking.md) § Alarm historian sink (v1 archive) |
|
||||||
| `Analyzers` (Roslyn OTOPCUA0001) | [security.md](security.md) § OTOPCUA0001 Analyzer |
|
| `Analyzers` (Roslyn OTOPCUA0001) | [security.md](security.md) § OTOPCUA0001 Analyzer |
|
||||||
|
|
||||||
### Drivers
|
### Drivers
|
||||||
@@ -44,8 +43,8 @@ Two Core subsystems are shipped without a dedicated top-level doc; see the secti
|
|||||||
| Doc | Covers |
|
| Doc | Covers |
|
||||||
|-----|--------|
|
|-----|--------|
|
||||||
| [drivers/README.md](drivers/README.md) | Index of the eight shipped drivers + capability matrix |
|
| [drivers/README.md](drivers/README.md) | Index of the eight shipped drivers + capability matrix |
|
||||||
| [drivers/Galaxy.md](drivers/Galaxy.md) | Galaxy driver — MXAccess bridge, Host/Proxy split, named-pipe IPC |
|
| [drivers/Galaxy.md](drivers/Galaxy.md) | Galaxy driver — in-process gRPC client to the mxaccessgw sidecar |
|
||||||
| [drivers/Galaxy-Repository.md](drivers/Galaxy-Repository.md) | Galaxy-specific discovery via the ZB SQL database |
|
| [v1/drivers/Galaxy-Repository.md](v1/drivers/Galaxy-Repository.md) | Galaxy-specific discovery via the ZB SQL database (v1 archive — the gateway owns this path now) |
|
||||||
|
|
||||||
For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics, see [v2/driver-specs.md](v2/driver-specs.md).
|
For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics, see [v2/driver-specs.md](v2/driver-specs.md).
|
||||||
|
|
||||||
@@ -53,10 +52,11 @@ For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics
|
|||||||
|
|
||||||
| Doc | Covers |
|
| Doc | Covers |
|
||||||
|-----|--------|
|
|-----|--------|
|
||||||
| [Configuration.md](Configuration.md) | appsettings bootstrap + Config DB + Admin UI draft/publish |
|
| [Configuration.md](v1/Configuration.md) | appsettings bootstrap + Config DB + Admin UI draft/publish (v1 archive — `OTOPCUA_GALAXY_*` env vars now live in mxaccessgw config) |
|
||||||
| [security.md](security.md) | Transport security profiles, LDAP auth, ACL trie, role grants, OTOPCUA0001 analyzer |
|
| [security.md](security.md) | Transport security profiles, LDAP auth, ACL trie, role grants, OTOPCUA0001 analyzer |
|
||||||
| [Redundancy.md](Redundancy.md) | `RedundancyCoordinator`, `ServiceLevelCalculator`, apply-lease, Prometheus metrics |
|
| [Redundancy.md](Redundancy.md) | `RedundancyCoordinator`, `ServiceLevelCalculator`, apply-lease, Prometheus metrics |
|
||||||
| [ServiceHosting.md](ServiceHosting.md) | Three-process deploy (Server + Admin + Galaxy.Host) install/uninstall |
|
| [Reservations.md](Reservations.md) | Fleet-wide ZTag / SAPID external-ID reservations — publish-time claim, release flow |
|
||||||
|
| [ServiceHosting.md](ServiceHosting.md) | Two-process deploy (Server + Admin) install/uninstall, plus the optional `OtOpcUaWonderwareHistorian` sidecar |
|
||||||
| [StatusDashboard.md](StatusDashboard.md) | Pointer — superseded by [v2/admin-ui.md](v2/admin-ui.md) |
|
| [StatusDashboard.md](StatusDashboard.md) | Pointer — superseded by [v2/admin-ui.md](v2/admin-ui.md) |
|
||||||
|
|
||||||
### Client tooling
|
### Client tooling
|
||||||
@@ -79,10 +79,10 @@ For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics
|
|||||||
|-----|--------|
|
|-----|--------|
|
||||||
| [reqs/HighLevelReqs.md](reqs/HighLevelReqs.md) | HLRs — numbered system-level requirements |
|
| [reqs/HighLevelReqs.md](reqs/HighLevelReqs.md) | HLRs — numbered system-level requirements |
|
||||||
| [reqs/OpcUaServerReqs.md](reqs/OpcUaServerReqs.md) | OPC UA server-layer reqs |
|
| [reqs/OpcUaServerReqs.md](reqs/OpcUaServerReqs.md) | OPC UA server-layer reqs |
|
||||||
| [reqs/ServiceHostReqs.md](reqs/ServiceHostReqs.md) | Per-process hosting reqs |
|
| [v1/reqs/ServiceHostReqs.md](v1/reqs/ServiceHostReqs.md) | Per-process hosting reqs (v1 archive — only `OtOpcUa` server hosting remains in scope post-PR-7.2) |
|
||||||
| [reqs/ClientRequirements.md](reqs/ClientRequirements.md) | Client CLI + UI reqs |
|
| [reqs/ClientRequirements.md](reqs/ClientRequirements.md) | Client CLI + UI reqs |
|
||||||
| [reqs/GalaxyRepositoryReqs.md](reqs/GalaxyRepositoryReqs.md) | Galaxy-scoped repository reqs |
|
| [v1/reqs/GalaxyRepositoryReqs.md](v1/reqs/GalaxyRepositoryReqs.md) | Galaxy-scoped repository reqs (v1 archive — owned by mxaccessgw today) |
|
||||||
| [reqs/MxAccessClientReqs.md](reqs/MxAccessClientReqs.md) | Galaxy-scoped MXAccess reqs |
|
| [v1/reqs/MxAccessClientReqs.md](v1/reqs/MxAccessClientReqs.md) | Galaxy-scoped MXAccess reqs (v1 archive — owned by mxaccessgw today) |
|
||||||
| [reqs/StatusDashboardReqs.md](reqs/StatusDashboardReqs.md) | Pointer — superseded by Admin UI |
|
| [reqs/StatusDashboardReqs.md](reqs/StatusDashboardReqs.md) | Pointer — superseded by Admin UI |
|
||||||
|
|
||||||
## Implementation history (`docs/v2/`)
|
## Implementation history (`docs/v2/`)
|
||||||
@@ -96,4 +96,11 @@ Design decisions + phase plans + execution notes. Load-bearing cross-references
|
|||||||
- [v2/driver-specs.md](v2/driver-specs.md) — per-driver addressing + quirks for every shipped protocol
|
- [v2/driver-specs.md](v2/driver-specs.md) — per-driver addressing + quirks for every shipped protocol
|
||||||
- [v2/dev-environment.md](v2/dev-environment.md) — dev-box bootstrap
|
- [v2/dev-environment.md](v2/dev-environment.md) — dev-box bootstrap
|
||||||
- [v2/test-data-sources.md](v2/test-data-sources.md) — integration-test simulator matrix (includes the pinned libplctag `ab_server` version for AB CIP tests)
|
- [v2/test-data-sources.md](v2/test-data-sources.md) — integration-test simulator matrix (includes the pinned libplctag `ab_server` version for AB CIP tests)
|
||||||
|
- [v2/multi-host-dispatch.md](v2/multi-host-dispatch.md) — per-PLC circuit breakers (Phase 6.1 decision #144)
|
||||||
|
- [v2/v2-release-readiness.md](v2/v2-release-readiness.md) — release-readiness tracker
|
||||||
|
- [v2/phase-7-status.md](v2/phase-7-status.md) — Phase 7 reconciliation: what shipped vs. the plan, and the five remaining gaps
|
||||||
- [v2/implementation/phase-*-*.md](v2/implementation/) — per-phase execution plans with exit-gate evidence
|
- [v2/implementation/phase-*-*.md](v2/implementation/) — per-phase execution plans with exit-gate evidence
|
||||||
|
|
||||||
|
## v1 archive
|
||||||
|
|
||||||
|
The v1 in-process MXAccess architecture (Galaxy.Host + Galaxy.Proxy + Galaxy.Shared, .NET 4.8 x86 COM, the `OtOpcUaGalaxyHost` Windows service) was retired in PR 7.2 (2026-04-30, commit `ae7106d`). Docs that described that shape are kept under [v1/](v1/) as historical record — see [v1/README.md](v1/README.md) for the index.
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# Read/Write Operations
|
# Read/Write Operations
|
||||||
|
|
||||||
`DriverNodeManager` (`src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) wires the OPC UA stack's per-variable `OnReadValue` and `OnWriteValue` hooks to each driver's `IReadable` and `IWritable` capabilities. Every dispatch flows through `CapabilityInvoker` so the Polly pipeline (retry / timeout / breaker / bulkhead) applies uniformly across Galaxy, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client drivers.
|
`DriverNodeManager` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) wires the OPC UA stack's per-variable `OnReadValue` and `OnWriteValue` hooks to each driver's `IReadable` and `IWritable` capabilities. Every dispatch flows through `CapabilityInvoker` so the Polly pipeline (retry / timeout / breaker / bulkhead) applies uniformly across Galaxy, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client drivers.
|
||||||
|
|
||||||
## Driver vs virtual dispatch
|
## Driver vs virtual dispatch
|
||||||
|
|
||||||
Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), a single `DriverNodeManager` routes reads and writes across both driver-sourced and virtual (scripted) tags. At discovery time each variable registers a `NodeSourceKind` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs`) in the manager's `_sourceByFullRef` lookup; the read/write hooks pattern-match on that value to pick the backend:
|
Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), a single `DriverNodeManager` routes reads and writes across both driver-sourced and virtual (scripted) tags. At discovery time each variable registers a `NodeSourceKind` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs`) in the manager's `_sourceByFullRef` lookup; the read/write hooks pattern-match on that value to pick the backend:
|
||||||
|
|
||||||
- `NodeSourceKind.Driver` — dispatches to the driver's `IReadable` / `IWritable` through `CapabilityInvoker` (the rest of this doc).
|
- `NodeSourceKind.Driver` — dispatches to the driver's `IReadable` / `IWritable` through `CapabilityInvoker` (the rest of this doc).
|
||||||
- `NodeSourceKind.Virtual` — dispatches to `VirtualTagSource` (`src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs`), which wraps `VirtualTagEngine`. Writes are rejected with `BadUserAccessDenied` before the branch per Phase 7 decision #6 — scripts are the only write path into virtual tags.
|
- `NodeSourceKind.Virtual` — dispatches to `VirtualTagSource` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs`), which wraps `VirtualTagEngine`. Writes are rejected with `BadUserAccessDenied` before the branch per Phase 7 decision #6 — scripts are the only write path into virtual tags.
|
||||||
- `NodeSourceKind.ScriptedAlarm` — dispatches to the Phase 7 `ScriptedAlarmReadable` shim.
|
- `NodeSourceKind.ScriptedAlarm` — dispatches to the Phase 7 `ScriptedAlarmReadable` shim.
|
||||||
|
|
||||||
ACL enforcement (`WriteAuthzPolicy` + `AuthorizationGate`) runs before the source branch, so the gates below apply uniformly to all three source kinds.
|
ACL enforcement (`WriteAuthzPolicy` + `AuthorizationGate`) runs before the source branch, so the gates below apply uniformly to all three source kinds.
|
||||||
@@ -60,8 +60,8 @@ Per decision #12, exceptions in the driver's capability call are logged and conv
|
|||||||
|
|
||||||
## Key source files
|
## Key source files
|
||||||
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `OnReadValue` / `OnWriteValue` hooks
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `OnReadValue` / `OnWriteValue` hooks
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs` — classification-to-role policy
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs` — classification-to-role policy
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` — Phase 6.2 trie gate
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` — Phase 6.2 trie gate
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — `ExecuteAsync` / `ExecuteWriteAsync`
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — `ExecuteAsync` / `ExecuteWriteAsync`
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs`, `IWritable.cs`, `WriteIdempotentAttribute.cs`
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs`, `IWritable.cs`, `WriteIdempotentAttribute.cs`
|
||||||
|
|||||||
+5
-5
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
OtOpcUa supports OPC UA **non-transparent** warm/hot redundancy. Two (or more) OtOpcUa Server processes run side-by-side, share the same Config DB, the same driver backends (Galaxy ZB, MXAccess runtime, remote PLCs), and advertise the same OPC UA node tree. Each process owns a distinct `ApplicationUri`; OPC UA clients see both endpoints via the standard `ServerUriArray` and pick one based on the `ServiceLevel` that each server publishes.
|
OtOpcUa supports OPC UA **non-transparent** warm/hot redundancy. Two (or more) OtOpcUa Server processes run side-by-side, share the same Config DB, the same driver backends (Galaxy ZB, MXAccess runtime, remote PLCs), and advertise the same OPC UA node tree. Each process owns a distinct `ApplicationUri`; OPC UA clients see both endpoints via the standard `ServerUriArray` and pick one based on the `ServiceLevel` that each server publishes.
|
||||||
|
|
||||||
The redundancy surface lives in `src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`:
|
The redundancy surface lives in `src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`:
|
||||||
|
|
||||||
| Class | Role |
|
| Class | Role |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -18,7 +18,7 @@ The redundancy surface lives in `src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`:
|
|||||||
|
|
||||||
## Data model
|
## Data model
|
||||||
|
|
||||||
Per-node redundancy state lives in the Config DB `ClusterNode` table (`src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs`):
|
Per-node redundancy state lives in the Config DB `ClusterNode` table (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs`):
|
||||||
|
|
||||||
| Column | Role |
|
| Column | Role |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -64,7 +64,7 @@ Because role transitions are **operator-driven** (write `RedundancyRole` in the
|
|||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
|
|
||||||
`RedundancyMetrics` in `src/ZB.MOM.WW.OtOpcUa.Admin/Services/RedundancyMetrics.cs` registers the `ZB.MOM.WW.OtOpcUa.Redundancy` meter on the Admin process. Instruments:
|
`RedundancyMetrics` in `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/RedundancyMetrics.cs` registers the `ZB.MOM.WW.OtOpcUa.Redundancy` meter on the Admin process. Instruments:
|
||||||
|
|
||||||
| Name | Kind | Tags | Description |
|
| Name | Kind | Tags | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -77,7 +77,7 @@ Admin `Program.cs` wires OpenTelemetry to the Prometheus exporter when `Metrics:
|
|||||||
|
|
||||||
## Real-time notifications (Admin UI)
|
## Real-time notifications (Admin UI)
|
||||||
|
|
||||||
`FleetStatusPoller` in `src/ZB.MOM.WW.OtOpcUa.Admin/Hubs/` polls the `ClusterNode` table, records role transitions, updates `RedundancyMetrics.SetClusterCounts`, and pushes a `RoleChanged` SignalR event onto `FleetStatusHub` when a transition is observed. `RedundancyTab.razor` subscribes with `_hub.On<RoleChangedMessage>("RoleChanged", …)` so connected Admin sessions see role swaps the moment they happen.
|
`FleetStatusPoller` in `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Hubs/` polls the `ClusterNode` table, records role transitions, updates `RedundancyMetrics.SetClusterCounts`, and pushes a `RoleChanged` SignalR event onto `FleetStatusHub` when a transition is observed. `RedundancyTab.razor` subscribes with `_hub.On<RoleChangedMessage>("RoleChanged", …)` so connected Admin sessions see role swaps the moment they happen.
|
||||||
|
|
||||||
## Configuring a redundant pair
|
## Configuring a redundant pair
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ Role swaps, stand-alone promotions, and base-level adjustments all happen throug
|
|||||||
|
|
||||||
## Client-side failover
|
## Client-side failover
|
||||||
|
|
||||||
The OtOpcUa Client CLI at `src/ZB.MOM.WW.OtOpcUa.Client.CLI` supports `-F` / `--failover-urls` for automatic client-side failover; for long-running subscriptions the CLI monitors session KeepAlive and reconnects to the next available server, recreating the subscription on the new endpoint. See [`Client.CLI.md`](Client.CLI.md) for the command reference.
|
The OtOpcUa Client CLI at `src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI` supports `-F` / `--failover-urls` for automatic client-side failover; for long-running subscriptions the CLI monitors session KeepAlive and reconnects to the next available server, recreating the subscription on the new endpoint. See [`Client.CLI.md`](Client.CLI.md) for the command reference.
|
||||||
|
|
||||||
## Depth reference
|
## Depth reference
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# External-ID Reservations
|
||||||
|
|
||||||
|
The reservation subsystem guarantees that an asset's **external identifiers**
|
||||||
|
— its `ZTag` and `SAPID` — belong to exactly one piece of equipment across the
|
||||||
|
entire fleet, for all time. It is the mechanism that stops two pieces of
|
||||||
|
equipment (in the same cluster or different clusters, in the current generation
|
||||||
|
or an old one) from silently claiming the same plant tag.
|
||||||
|
|
||||||
|
This is **decision #124** in `docs/v2/plan.md`.
|
||||||
|
|
||||||
|
## What a reservation is
|
||||||
|
|
||||||
|
An `ExternalIdReservation` row is a permanent, fleet-wide claim on one
|
||||||
|
identifier value by one `EquipmentUuid`. There are two kinds
|
||||||
|
(`ReservationKind`):
|
||||||
|
|
||||||
|
| Kind | What it is |
|
||||||
|
|---------|------------|
|
||||||
|
| `ZTag` | The plant's tag identity for a physical asset. |
|
||||||
|
| `SAPID` | The asset's SAP record ID. |
|
||||||
|
|
||||||
|
An `Equipment` row may carry a `ZTag`, a `SAPID`, both, or neither. Whenever it
|
||||||
|
carries one and the generation is published, a reservation is created for that
|
||||||
|
value.
|
||||||
|
|
||||||
|
## Why it sits outside the generation flow
|
||||||
|
|
||||||
|
Every other part of the configuration is **generation-versioned** — authored in
|
||||||
|
a draft, promoted by publish, superseded by the next publish, and reversible by
|
||||||
|
rollback. Reservations deliberately are **not**.
|
||||||
|
|
||||||
|
The reason: a single ZTag can legitimately appear in many places at once — the
|
||||||
|
current published generation, several superseded generations, and a piece of
|
||||||
|
equipment that has since been disabled. A per-generation uniqueness index would
|
||||||
|
fail the instant you roll back to an older generation or re-enable disabled
|
||||||
|
equipment, because the "old" copy of the identifier is still on disk.
|
||||||
|
|
||||||
|
So the reservation table is a flat, fleet-wide ledger that lives *beside* the
|
||||||
|
generation flow. It is append-mostly: rows are created, their `LastPublishedAt`
|
||||||
|
is refreshed, and they are eventually *released* — but never silently deleted.
|
||||||
|
|
||||||
|
## The table
|
||||||
|
|
||||||
|
`ExternalIdReservation` (Config DB):
|
||||||
|
|
||||||
|
| Column | Notes |
|
||||||
|
|--------------------|-------|
|
||||||
|
| `ReservationId` | Surrogate PK (`NEWSEQUENTIALID()`). |
|
||||||
|
| `Kind` | `ZTag` or `SAPID`. |
|
||||||
|
| `Value` | The reserved identifier string. |
|
||||||
|
| `EquipmentUuid` | The equipment that owns the claim. Stays bound even when that equipment is disabled. |
|
||||||
|
| `ClusterId` | The first cluster to publish the reservation. |
|
||||||
|
| `FirstPublishedAt` / `FirstPublishedBy` | When and by whom the claim was first made. |
|
||||||
|
| `LastPublishedAt` | Refreshed on every subsequent publish that re-asserts the same `(Kind, Value, EquipmentUuid)`. |
|
||||||
|
| `ReleasedAt` / `ReleasedBy` / `ReleaseReason` | Non-null once a FleetAdmin explicitly releases the claim. A row with `ReleasedAt IS NULL` is *active*. |
|
||||||
|
|
||||||
|
There is no foreign key from `EquipmentUuid` / `ClusterId` to their tables — by
|
||||||
|
design, so a reservation survives the deletion or disabling of the equipment
|
||||||
|
that owns it.
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
### 1. Authoring
|
||||||
|
|
||||||
|
You give an `Equipment` row a `ZTag` and/or `SAPID` in a **draft** generation —
|
||||||
|
either by hand in the draft editor or via equipment CSV import. Nothing is
|
||||||
|
reserved yet; the draft is just a proposal.
|
||||||
|
|
||||||
|
> Equipment CSV import does **not** pre-check reservation conflicts (tracked as
|
||||||
|
> task #197). A conflict introduced by import surfaces at publish time, below.
|
||||||
|
|
||||||
|
### 2. Publish precheck
|
||||||
|
|
||||||
|
`sp_PublishGeneration` runs the draft validation first. If a `ZTag` or `SAPID`
|
||||||
|
in the draft is already reserved — `ReleasedAt IS NULL` — by a **different**
|
||||||
|
`EquipmentUuid`, the publish is rejected:
|
||||||
|
|
||||||
|
```
|
||||||
|
BadDuplicateExternalIdentifier: a ZTag in the draft is reserved by a
|
||||||
|
different EquipmentUuid
|
||||||
|
```
|
||||||
|
|
||||||
|
The same value owned by the *same* `EquipmentUuid` is fine — that is just the
|
||||||
|
asset keeping its identifier across generations.
|
||||||
|
|
||||||
|
### 3. Publish (the reservation is created)
|
||||||
|
|
||||||
|
When the publish succeeds, `sp_PublishGeneration` runs a `MERGE` into
|
||||||
|
`ExternalIdReservation` for every `ZTag`/`SAPID` in the published generation:
|
||||||
|
|
||||||
|
- **New** `(Kind, Value, EquipmentUuid)` → a reservation row is **inserted**.
|
||||||
|
`FirstPublishedBy` is the publishing user; `ClusterId` is the publishing
|
||||||
|
cluster.
|
||||||
|
- **Already present** → only `LastPublishedAt` is bumped.
|
||||||
|
|
||||||
|
So the *first* publish of an equipment carrying a ZTag is what claims that ZTag
|
||||||
|
for the fleet. After that the claim is permanent — it survives the equipment
|
||||||
|
being disabled, the generation being superseded, or a rollback.
|
||||||
|
|
||||||
|
### 4. Release
|
||||||
|
|
||||||
|
Reusing an identifier for a **different** piece of equipment requires a
|
||||||
|
FleetAdmin to explicitly release the existing claim. Release runs
|
||||||
|
`sp_ReleaseExternalIdReservation`, which:
|
||||||
|
|
||||||
|
- Requires a non-empty **reason** — a hard audit invariant; the procedure
|
||||||
|
raises an error without one.
|
||||||
|
- Stamps `ReleasedAt`, `ReleasedBy` (`SUSER_SNAME()`), and `ReleaseReason`
|
||||||
|
rather than deleting the row, so the history is preserved.
|
||||||
|
- Once released, the `(Kind, Value)` pair is free — a different
|
||||||
|
`EquipmentUuid` can claim it on a future publish.
|
||||||
|
|
||||||
|
Release the claim **only** when the physical asset is permanently retired and
|
||||||
|
its identifier genuinely needs to be reused. A reservation is meant to be
|
||||||
|
permanent for the life of the asset.
|
||||||
|
|
||||||
|
## The Admin page
|
||||||
|
|
||||||
|
`/reservations` (Admin UI) is the operator surface. It is **FleetAdmin-only**
|
||||||
|
(the `CanPublish` policy).
|
||||||
|
|
||||||
|
- **Active** table — every reservation with `ReleasedAt IS NULL`: kind, value,
|
||||||
|
owning `EquipmentUuid`, cluster, and the first/last publish stamps. Each row
|
||||||
|
has a **Release…** action.
|
||||||
|
- **Released** table — the 100 most recently released reservations, with the
|
||||||
|
releasing user and reason.
|
||||||
|
- **Release dialog** — opened from an active row; it requires a reason before
|
||||||
|
the Release button will submit, mirroring the procedure's audit invariant.
|
||||||
|
|
||||||
|
You cannot *create* a reservation from this page — reservations only ever come
|
||||||
|
into existence as a side-effect of publishing a generation. The page is for
|
||||||
|
inspection and for the release flow.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- `docs/v2/plan.md` — decision #124 (reservations outside the generation flow).
|
||||||
|
- `docs/v2/admin-ui.md` — § "Release an external-ID reservation".
|
||||||
|
- `docs/v2/config-db-schema.md` — full Config DB schema.
|
||||||
|
- `OpcUaServer.md` — generations, draft/publish flow.
|
||||||
+14
-14
@@ -6,7 +6,7 @@ This file covers the engine internals — predicate evaluation, state machine, p
|
|||||||
|
|
||||||
## Definition shape
|
## Definition shape
|
||||||
|
|
||||||
`ScriptedAlarmDefinition` (`src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs`) is the runtime contract the engine consumes. The generation-publish path materialises these from the `ScriptedAlarm` + `Script` config tables via `Phase7EngineComposer.ProjectScriptedAlarms`.
|
`ScriptedAlarmDefinition` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs`) is the runtime contract the engine consumes. The generation-publish path materialises these from the `ScriptedAlarm` + `Script` config tables via `Phase7EngineComposer.ProjectScriptedAlarms`.
|
||||||
|
|
||||||
| Field | Notes |
|
| Field | Notes |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -100,26 +100,26 @@ Emissions map into `AlarmEventArgs` as `AlarmType = Kind.ToString()`, `SourceNod
|
|||||||
|
|
||||||
## Composition
|
## Composition
|
||||||
|
|
||||||
`Phase7EngineComposer.Compose` (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs`) is the single call site that instantiates the engine. It takes the generation's `Script` / `VirtualTag` / `ScriptedAlarm` rows, the shared `CachedTagUpstreamSource`, an `IAlarmStateStore`, and an `IAlarmHistorianSink`, and returns a `Phase7ComposedSources` the caller owns. When `scriptedAlarms.Count > 0`:
|
`Phase7EngineComposer.Compose` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs`) is the single call site that instantiates the engine. It takes the generation's `Script` / `VirtualTag` / `ScriptedAlarm` rows, the shared `CachedTagUpstreamSource`, an `IAlarmStateStore`, and an `IAlarmHistorianSink`, and returns a `Phase7ComposedSources` the caller owns. When `scriptedAlarms.Count > 0`:
|
||||||
|
|
||||||
1. `ProjectScriptedAlarms` resolves each row's `PredicateScriptId` against the script dictionary and produces a `ScriptedAlarmDefinition` list. Unknown or disabled scripts throw immediately — the DB publish guarantees referential integrity but this is a belt-and-braces check.
|
1. `ProjectScriptedAlarms` resolves each row's `PredicateScriptId` against the script dictionary and produces a `ScriptedAlarmDefinition` list. Unknown or disabled scripts throw immediately — the DB publish guarantees referential integrity but this is a belt-and-braces check.
|
||||||
2. A `ScriptedAlarmEngine` is constructed with the upstream source, the store, a shared `ScriptLoggerFactory` keyed to `scripts-*.log`, and the root Serilog logger.
|
2. A `ScriptedAlarmEngine` is constructed with the upstream source, the store, a shared `ScriptLoggerFactory` keyed to `scripts-*.log`, and the root Serilog logger.
|
||||||
3. `alarmEngine.OnEvent` is wired to `RouteToHistorianAsync`, which projects each emission into an `AlarmHistorianEvent` and enqueues it on the sink. Fire-and-forget — the SQLite store-and-forward sink is already non-blocking.
|
3. `alarmEngine.OnEvent` is wired to `RouteToHistorianAsync`, which projects each emission into an `AlarmHistorianEvent` and enqueues it on the sink. Fire-and-forget — the SQLite store-and-forward sink is already non-blocking.
|
||||||
4. `LoadAsync(alarmDefs)` runs synchronously on the startup thread: it compiles every predicate, subscribes to the union of predicate inputs and message-template tokens, seeds the value cache, loads persisted state, re-derives `ActiveState` from a fresh predicate evaluation, and starts the 5s shelving timer. Compile failures are aggregated into one `InvalidOperationException` so operators see every bad predicate in one startup log line rather than one at a time.
|
4. `LoadAsync(alarmDefs)` runs synchronously on the startup thread: it compiles every predicate, subscribes to the union of predicate inputs and message-template tokens, seeds the value cache, loads persisted state, re-derives `ActiveState` from a fresh predicate evaluation, and starts the 5s shelving timer. Compile failures are aggregated into one `InvalidOperationException` so operators see every bad predicate in one startup log line rather than one at a time.
|
||||||
5. A `ScriptedAlarmSource` is created for the event stream, and a `ScriptedAlarmReadable` (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs`) is created for OPC UA variable reads on the alarm's active-state node (task #245) — unknown alarm ids return `BadNodeIdUnknown` rather than silently reading `false`.
|
5. A `ScriptedAlarmSource` is created for the event stream, and a `ScriptedAlarmReadable` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs`) is created for OPC UA variable reads on the alarm's active-state node (task #245) — unknown alarm ids return `BadNodeIdUnknown` rather than silently reading `false`.
|
||||||
|
|
||||||
Both engine and source are added to `Phase7ComposedSources.Disposables`, which `Phase7Composer` disposes on server shutdown.
|
Both engine and source are added to `Phase7ComposedSources.Disposables`, which `Phase7Composer` disposes on server shutdown.
|
||||||
|
|
||||||
## Key source files
|
## Key source files
|
||||||
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs` — orchestrator, cascade wiring, shelving timer, `OnEvent` emission
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs` — orchestrator, cascade wiring, shelving timer, `OnEvent` emission
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs` — `IAlarmSource` adapter over the engine
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs` — `IAlarmSource` adapter over the engine
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs` — runtime definition record
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs` — runtime definition record
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs` — pure-function state machine + `TransitionResult` / `EmissionKind`
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs` — pure-function state machine + `TransitionResult` / `EmissionKind`
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs` — persisted state record + `AlarmComment` audit entry + `ShelvingState`
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs` — persisted state record + `AlarmComment` audit entry + `ShelvingState`
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmPredicateContext.cs` — script-side `ScriptContext` (read-only, write rejected)
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmPredicateContext.cs` — script-side `ScriptContext` (read-only, write rejected)
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmTypes.cs` — `AlarmKind` + the four Part 9 enums
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmTypes.cs` — `AlarmKind` + the four Part 9 enums
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs` — `{path}` placeholder resolver
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs` — `{path}` placeholder resolver
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs` — persistence contract + `InMemoryAlarmStateStore` default
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs` — persistence contract + `InMemoryAlarmStateStore` default
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — composition, config-row projection, historian routing
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — composition, config-row projection, historian routing
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs` — `IReadable` adapter exposing `ActiveState` to OPC UA variable reads
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs` — `IReadable` adapter exposing `ActiveState` to OPC UA variable reads
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ with a distinct runtime and install surface:
|
|||||||
|
|
||||||
| Process | Project | Runtime | Platform | Responsibility |
|
| Process | Project | Runtime | Platform | Responsibility |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| **OtOpcUa Server** | `src/ZB.MOM.WW.OtOpcUa.Server` | .NET 10 | x64 | Hosts the OPC UA endpoint; loads every driver in-process (Modbus, S7, AbCip, AbLegacy, TwinCAT, FOCAS, OPC UA Client, Galaxy via mxaccessgw); exposes `/healthz`. |
|
| **OtOpcUa Server** | `src/Server/ZB.MOM.WW.OtOpcUa.Server` | .NET 10 | x64 | Hosts the OPC UA endpoint; loads every driver in-process (Modbus, S7, AbCip, AbLegacy, TwinCAT, FOCAS, OPC UA Client, Galaxy via mxaccessgw); exposes `/healthz`. |
|
||||||
| **OtOpcUa Admin** | `src/ZB.MOM.WW.OtOpcUa.Admin` | .NET 10 (ASP.NET Core / Blazor Server) | x64 | Operator UI for Config DB editing + fleet status, SignalR hubs (`FleetStatusHub`, `AlertHub`), Prometheus `/metrics`. |
|
| **OtOpcUa Admin** | `src/Server/ZB.MOM.WW.OtOpcUa.Admin` | .NET 10 (ASP.NET Core / Blazor Server) | x64 | Operator UI for Config DB editing + fleet status, SignalR hubs (`FleetStatusHub`, `AlertHub`), Prometheus `/metrics`. |
|
||||||
| **OtOpcUa Wonderware Historian** *(optional)* | `src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware` | .NET Framework 4.8 | x86 (32-bit) | Out-of-process sidecar exposing the Wonderware Historian SDK over a named pipe. Required only when `Historian:Wonderware:Enabled=true` in `appsettings.json`. |
|
| **OtOpcUa Wonderware Historian** *(optional)* | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware` | .NET Framework 4.8 | x86 (32-bit) | Out-of-process sidecar exposing the Wonderware Historian SDK over a named pipe. Required only when `Historian:Wonderware:Enabled=true` in `appsettings.json`. |
|
||||||
|
|
||||||
Galaxy access uses a separately-installed **mxaccessgw** running out
|
Galaxy access uses a separately-installed **mxaccessgw** running out
|
||||||
of a sibling repo (`c:\Users\dohertj2\Desktop\mxaccessgw\`) — see
|
of a sibling repo (`c:\Users\dohertj2\Desktop\mxaccessgw\`) — see
|
||||||
@@ -42,9 +42,9 @@ Reads from the same Config DB the Server writes to.
|
|||||||
When `Historian:Wonderware:Enabled=true`, the Server speaks to a
|
When `Historian:Wonderware:Enabled=true`, the Server speaks to a
|
||||||
sidecar that wraps the Wonderware Historian SDK (which is .NET
|
sidecar that wraps the Wonderware Historian SDK (which is .NET
|
||||||
Framework only). The pipe IPC contract is in
|
Framework only). The pipe IPC contract is in
|
||||||
`src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/Contracts/`
|
`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/Contracts/`
|
||||||
and the sidecar's pipe handler lives at
|
and the sidecar's pipe handler lives at
|
||||||
`src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Pipe/`.
|
`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Pipe/`.
|
||||||
|
|
||||||
Install via the `-InstallWonderwareHistorian` switch on
|
Install via the `-InstallWonderwareHistorian` switch on
|
||||||
`scripts/install/Install-Services.ps1`.
|
`scripts/install/Install-Services.ps1`.
|
||||||
|
|||||||
+26
-26
@@ -97,13 +97,13 @@ Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md) Option B,
|
|||||||
|
|
||||||
`ITagUpstreamSource` and `IHistoryWriter` are the two ports the engine requires from its host. Both live in `Core.VirtualTags`. In the Server process:
|
`ITagUpstreamSource` and `IHistoryWriter` are the two ports the engine requires from its host. Both live in `Core.VirtualTags`. In the Server process:
|
||||||
|
|
||||||
- **`CachedTagUpstreamSource`** (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/CachedTagUpstreamSource.cs`) implements the interface (and the parallel `Core.ScriptedAlarms.ITagUpstreamSource` — identical shape, distinct namespace). A `ConcurrentDictionary<path, DataValueSnapshot>` cache. `Push(path, snapshot)` updates the cache and fans out synchronously to every observer. Reads of never-pushed paths return `BadNodeIdUnknown` quality (`UpstreamNotConfigured = 0x80340000`).
|
- **`CachedTagUpstreamSource`** (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/CachedTagUpstreamSource.cs`) implements the interface (and the parallel `Core.ScriptedAlarms.ITagUpstreamSource` — identical shape, distinct namespace). A `ConcurrentDictionary<path, DataValueSnapshot>` cache. `Push(path, snapshot)` updates the cache and fans out synchronously to every observer. Reads of never-pushed paths return `BadNodeIdUnknown` quality (`UpstreamNotConfigured = 0x80340000`).
|
||||||
- **`DriverSubscriptionBridge`** (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/DriverSubscriptionBridge.cs`) feeds the cache. For each registered `ISubscribable` driver it batches a single `SubscribeAsync` for every fullRef the script graph references, installs an `OnDataChange` handler that translates driver-opaque fullRefs back to UNS paths via a reverse map, and pushes each delta into `CachedTagUpstreamSource`. Unsubscribes on dispose. The bridge suppresses `OTOPCUA0001` (the Roslyn analyzer that requires `ISubscribable` callers to go through `CapabilityInvoker`) on the documented basis that this is a lifecycle wiring, not per-evaluation hot path.
|
- **`DriverSubscriptionBridge`** (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/DriverSubscriptionBridge.cs`) feeds the cache. For each registered `ISubscribable` driver it batches a single `SubscribeAsync` for every fullRef the script graph references, installs an `OnDataChange` handler that translates driver-opaque fullRefs back to UNS paths via a reverse map, and pushes each delta into `CachedTagUpstreamSource`. Unsubscribes on dispose. The bridge suppresses `OTOPCUA0001` (the Roslyn analyzer that requires `ISubscribable` callers to go through `CapabilityInvoker`) on the documented basis that this is a lifecycle wiring, not per-evaluation hot path.
|
||||||
- **`IHistoryWriter`** — no production implementation is currently wired for virtual tags; `VirtualTagEngine` gets `NullHistoryWriter` by default from `Phase7EngineComposer`.
|
- **`IHistoryWriter`** — no production implementation is currently wired for virtual tags; `VirtualTagEngine` gets `NullHistoryWriter` by default from `Phase7EngineComposer`.
|
||||||
|
|
||||||
## Composition
|
## Composition
|
||||||
|
|
||||||
`Phase7Composer` (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs`) is an `IAsyncDisposable` injected into `OpcUaServerService`:
|
`Phase7Composer` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs`) is an `IAsyncDisposable` injected into `OpcUaServerService`:
|
||||||
|
|
||||||
1. `PrepareAsync(generationId, ct)` — called after the bootstrap generation loads and before `OpcUaApplicationHost.StartAsync`. Reads the `Script` / `VirtualTag` / `ScriptedAlarm` rows for that generation from the config DB (`OtOpcUaConfigDbContext`). Empty-config fast path returns `Phase7ComposedSources.Empty`.
|
1. `PrepareAsync(generationId, ct)` — called after the bootstrap generation loads and before `OpcUaApplicationHost.StartAsync`. Reads the `Script` / `VirtualTag` / `ScriptedAlarm` rows for that generation from the config DB (`OtOpcUaConfigDbContext`). Empty-config fast path returns `Phase7ComposedSources.Empty`.
|
||||||
2. Constructs a `CachedTagUpstreamSource` + hands it to `Phase7EngineComposer.Compose`.
|
2. Constructs a `CachedTagUpstreamSource` + hands it to `Phase7EngineComposer.Compose`.
|
||||||
@@ -117,26 +117,26 @@ Definition reload on config publish: `VirtualTagEngine.Load` is re-entrant — a
|
|||||||
|
|
||||||
## Key source files
|
## Key source files
|
||||||
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptContext.cs` — abstract `ctx` API scripts see
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptContext.cs` — abstract `ctx` API scripts see
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptGlobals.cs` — generic globals wrapper naming the field `ctx`
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptGlobals.cs` — generic globals wrapper naming the field `ctx`
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs` — assembly allow-list + imports
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs` — assembly allow-list + imports
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs` — post-compile semantic deny-list
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs` — post-compile semantic deny-list
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptEvaluator.cs` — three-step compile pipeline
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptEvaluator.cs` — three-step compile pipeline
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/TimedScriptEvaluator.cs` — 250ms default timeout wrapper
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/TimedScriptEvaluator.cs` — 250ms default timeout wrapper
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/CompiledScriptCache.cs` — SHA-256-keyed compile cache
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/CompiledScriptCache.cs` — SHA-256-keyed compile cache
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs` — static `ctx.GetTag` / `ctx.SetVirtualTag` inference
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs` — static `ctx.GetTag` / `ctx.SetVirtualTag` inference
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLoggerFactory.cs` — per-script Serilog logger
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLoggerFactory.cs` — per-script Serilog logger
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogCompanionSink.cs` — error mirror to main log
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogCompanionSink.cs` — error mirror to main log
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagDefinition.cs` — per-tag config record
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagDefinition.cs` — per-tag config record
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagContext.cs` — evaluation-scoped `ctx`
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagContext.cs` — evaluation-scoped `ctx`
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs` — Kahn topo-sort + iterative Tarjan SCC
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs` — Kahn topo-sort + iterative Tarjan SCC
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs` — load / evaluate / cascade pipeline
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs` — load / evaluate / cascade pipeline
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/TimerTriggerScheduler.cs` — periodic re-evaluation
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/TimerTriggerScheduler.cs` — periodic re-evaluation
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ITagUpstreamSource.cs` — driver-tag read + subscribe port
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ITagUpstreamSource.cs` — driver-tag read + subscribe port
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/IHistoryWriter.cs` — historize sink port + `NullHistoryWriter`
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/IHistoryWriter.cs` — historize sink port + `NullHistoryWriter`
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs` — `IReadable` + `ISubscribable` adapter
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs` — `IReadable` + `ISubscribable` adapter
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/CachedTagUpstreamSource.cs` — production `ITagUpstreamSource`
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/CachedTagUpstreamSource.cs` — production `ITagUpstreamSource`
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/DriverSubscriptionBridge.cs` — driver `ISubscribable` → cache feed
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/DriverSubscriptionBridge.cs` — driver `ISubscribable` → cache feed
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — row projection + engine instantiation
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — row projection + engine instantiation
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` — lifecycle host: load rows, compose, wire bridge
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` — lifecycle host: load rows, compose, wire bridge
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `SelectReadable` + `IsWriteAllowedBySource` dispatch kernel
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `SelectReadable` + `IsWriteAllowedBySource` dispatch kernel
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Coverage map + gap inventory for the AB Legacy (PCCC) driver — SLC 500 /
|
|||||||
MicroLogix / PLC-5 / LogixPccc-mode.
|
MicroLogix / PLC-5 / LogixPccc-mode.
|
||||||
|
|
||||||
**TL;DR:** Docker integration-test scaffolding lives at
|
**TL;DR:** Docker integration-test scaffolding lives at
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/` (task #224),
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/` (task #224),
|
||||||
reusing the AB CIP `ab_server` image in PCCC mode with per-family
|
reusing the AB CIP `ab_server` image in PCCC mode with per-family
|
||||||
compose profiles (`slc500` / `micrologix` / `plc5`). Scaffold passes
|
compose profiles (`slc500` / `micrologix` / `plc5`). Scaffold passes
|
||||||
the skip-when-absent contract cleanly. **Wire-level round-trip against
|
the skip-when-absent contract cleanly. **Wire-level round-trip against
|
||||||
@@ -19,7 +19,7 @@ via `FakeAbLegacyTag` still carry the contract coverage.
|
|||||||
|
|
||||||
**Integration layer** (task #224, scaffolded with a known ab_server
|
**Integration layer** (task #224, scaffolded with a known ab_server
|
||||||
gap):
|
gap):
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/` with
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/` with
|
||||||
`AbLegacyServerFixture` (TCP-probes `localhost:44818`) + three smoke
|
`AbLegacyServerFixture` (TCP-probes `localhost:44818`) + three smoke
|
||||||
tests (parametric read across families, SLC500 write-then-read). Reuses
|
tests (parametric read across families, SLC500 write-then-read). Reuses
|
||||||
the AB CIP `otopcua-ab-server:libplctag-release` image via a relative
|
the AB CIP `otopcua-ab-server:libplctag-release` image via a relative
|
||||||
@@ -27,7 +27,7 @@ the AB CIP `otopcua-ab-server:libplctag-release` image via a relative
|
|||||||
`--plc` flags. See `Docker/README.md` §Known limitations for the
|
`--plc` flags. See `Docker/README.md` §Known limitations for the
|
||||||
ab_server PCCC round-trip gap + resolution paths.
|
ab_server PCCC round-trip gap + resolution paths.
|
||||||
|
|
||||||
**Unit layer**: `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/` is
|
**Unit layer**: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/` is
|
||||||
still the primary coverage. All tests tagged `[Trait("Category", "Unit")]`.
|
still the primary coverage. All tests tagged `[Trait("Category", "Unit")]`.
|
||||||
The driver accepts `IAbLegacyTagFactory` via ctor DI; every test
|
The driver accepts `IAbLegacyTagFactory` via ctor DI; every test
|
||||||
supplies a `FakeAbLegacyTag`.
|
supplies a `FakeAbLegacyTag`.
|
||||||
@@ -113,16 +113,16 @@ cover the common ones but uncommon ones (`R` counters, `S` status files,
|
|||||||
|
|
||||||
## Key fixture / config files
|
## Key fixture / config files
|
||||||
|
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyServerFixture.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyServerFixture.cs`
|
||||||
— TCP probe + skip attributes + env-var parsing
|
— TCP probe + skip attributes + env-var parsing
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyReadSmokeTests.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyReadSmokeTests.cs`
|
||||||
— wire-level smoke tests; pass against the ab_server Docker fixture
|
— wire-level smoke tests; pass against the ab_server Docker fixture
|
||||||
with `AB_LEGACY_COMPOSE_PROFILE` set to the running container
|
with `AB_LEGACY_COMPOSE_PROFILE` set to the running container
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml`
|
||||||
— compose profiles reusing AB CIP Dockerfile
|
— compose profiles reusing AB CIP Dockerfile
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md`
|
||||||
— known-limitations write-up + resolution paths
|
— known-limitations write-up + resolution paths
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs` —
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs` —
|
||||||
in-process fake + factory
|
in-process fake + factory
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — scope remarks
|
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — scope remarks
|
||||||
at the top of the file
|
at the top of the file
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ behaviours from unit-only to end-to-end wire-level coverage:
|
|||||||
```powershell
|
```powershell
|
||||||
$env:AB_SERVER_PROFILE = 'emulate'
|
$env:AB_SERVER_PROFILE = 'emulate'
|
||||||
$env:AB_SERVER_ENDPOINT = '<emulate-pc-ip>:44818'
|
$env:AB_SERVER_ENDPOINT = '<emulate-pc-ip>:44818'
|
||||||
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
|
dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
|
||||||
```
|
```
|
||||||
|
|
||||||
With `AB_SERVER_PROFILE` unset or `abserver`, the Emulate-tier classes
|
With `AB_SERVER_PROFILE` unset or `abserver`, the Emulate-tier classes
|
||||||
@@ -154,7 +154,7 @@ via `AbServerProfileGate.SkipUnless`):
|
|||||||
— #177 ALMD projection, verified against the real ALMD instruction
|
— #177 ALMD projection, verified against the real ALMD instruction
|
||||||
|
|
||||||
**Required Studio 5000 project state** is documented in
|
**Required Studio 5000 project state** is documented in
|
||||||
[`tests/…/AbCip.IntegrationTests/LogixProject/README.md`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md);
|
[`tests/…/AbCip.IntegrationTests/LogixProject/README.md`](../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md);
|
||||||
the `.L5X` export lands there once the Emulate PC is on-site + the
|
the `.L5X` export lands there once the Emulate PC is on-site + the
|
||||||
project is authored.
|
project is authored.
|
||||||
|
|
||||||
@@ -201,16 +201,16 @@ options are roughly:
|
|||||||
|
|
||||||
See also:
|
See also:
|
||||||
|
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerFixture.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerFixture.cs`
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfile.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfile.cs`
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs`
|
||||||
— `AB_SERVER_PROFILE` tier gate
|
— `AB_SERVER_PROFILE` tier gate
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cs`
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/` — ab_server
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/` — ab_server
|
||||||
image + compose
|
image + compose
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/` — Logix
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/` — Logix
|
||||||
Emulate tier tests
|
Emulate tier tests
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md`
|
||||||
— L5X project state the Emulate tier expects
|
— L5X project state the Emulate tier expects
|
||||||
- `docs/v2/test-data-sources.md` §2 — the broader test-data-source picking
|
- `docs/v2/test-data-sources.md` §2 — the broader test-data-source picking
|
||||||
rationale this fixture slots into
|
rationale this fixture slots into
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Coverage map + gap inventory for the FANUC FOCAS2 CNC driver.
|
|||||||
via the pure-managed [`Focas.Wire`](https://github.com/Ladder99/focas-mock/tree/main/dotnet/Focas.Wire)
|
via the pure-managed [`Focas.Wire`](https://github.com/Ladder99/focas-mock/tree/main/dotnet/Focas.Wire)
|
||||||
client. Integration tests run the managed driver end-to-end against the
|
client. Integration tests run the managed driver end-to-end against the
|
||||||
vendored `focas-mock` Python server (at
|
vendored `focas-mock` Python server (at
|
||||||
[`tests/.../Docker/focas-mock/`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/focas-mock/VENDORED.md))
|
[`tests/.../Docker/focas-mock/`](../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/focas-mock/VENDORED.md))
|
||||||
whose native FOCAS Ethernet responder is verified PDU-by-PDU against the
|
whose native FOCAS Ethernet responder is verified PDU-by-PDU against the
|
||||||
real `fwlibe64.dll`.
|
real `fwlibe64.dll`.
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ but the mock's wire responder covers every FOCAS call OtOpcUa issues.
|
|||||||
|
|
||||||
### Unit layer (no container required)
|
### Unit layer (no container required)
|
||||||
|
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/` uses `FakeFocasClient`
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/` uses `FakeFocasClient`
|
||||||
injected via `IFocasClientFactory`:
|
injected via `IFocasClientFactory`:
|
||||||
|
|
||||||
- `FocasCapabilityTests` — data-type mapping (PMC bit / byte / word /
|
- `FocasCapabilityTests` — data-type mapping (PMC bit / byte / word /
|
||||||
@@ -48,7 +48,7 @@ message naming the CNC series + documented limit.
|
|||||||
|
|
||||||
### Integration layer (mock only, no CNC, no shim)
|
### Integration layer (mock only, no CNC, no shim)
|
||||||
|
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/` drives the
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/` drives the
|
||||||
managed `FocasDriver` end-to-end. A single gate:
|
managed `FocasDriver` end-to-end. A single gate:
|
||||||
|
|
||||||
**Docker compose up** — tests skip when the TCP probe to
|
**Docker compose up** — tests skip when the TCP probe to
|
||||||
@@ -120,10 +120,10 @@ stays as the CI quality gate.
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 1) Start the mock on a chosen profile.
|
# 1) Start the mock on a chosen profile.
|
||||||
docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml up -d
|
docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml up -d
|
||||||
|
|
||||||
# 2) Run the tests. No shim build, no DLL copy — the driver dials the mock directly.
|
# 2) Run the tests. No shim build, no DLL copy — the driver dials the mock directly.
|
||||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/
|
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use `scripts/integration/run-focas.ps1` which wraps compose up / test
|
Or use `scripts/integration/run-focas.ps1` which wraps compose up / test
|
||||||
@@ -131,20 +131,20 @@ Or use `scripts/integration/run-focas.ps1` which wraps compose up / test
|
|||||||
|
|
||||||
## Key fixture / config files
|
## Key fixture / config files
|
||||||
|
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/focas-mock/`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/focas-mock/`
|
||||||
— vendored `focas-mock` Python source + Dockerfile
|
— vendored `focas-mock` Python source + Dockerfile
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml`
|
||||||
— per-series compose profiles
|
— per-series compose profiles
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/FocasSimFixture.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/FocasSimFixture.cs`
|
||||||
— collection fixture + mock admin API client
|
— collection fixture + mock admin API client
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/FixedTreePopulatesTests.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/FixedTreePopulatesTests.cs`
|
||||||
— fixed-tree end-to-end tests
|
— fixed-tree end-to-end tests
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/WireBackendTests.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/WireBackendTests.cs`
|
||||||
— pure-wire-backend end-to-end tests
|
— pure-wire-backend end-to-end tests
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FakeFocasClient.cs` —
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FakeFocasClient.cs` —
|
||||||
in-process unit fake
|
in-process unit fake
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs` — the
|
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs` — the
|
||||||
managed wire client backing production deployments
|
managed wire client backing production deployments
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs` —
|
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs` —
|
||||||
per-series range validator
|
per-series range validator
|
||||||
- `docs/v2/focas-version-matrix.md` — authoritative range reference
|
- `docs/v2/focas-version-matrix.md` — authoritative range reference
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
Getting-started guide for the FANUC FOCAS2 driver. This is the short path — for
|
Getting-started guide for the FANUC FOCAS2 driver. This is the short path — for
|
||||||
the exhaustive per-node mapping read [`docs/v2/driver-specs.md §7`](../v2/driver-specs.md),
|
the exhaustive per-node mapping read [`docs/v2/driver-specs.md §7`](../v2/driver-specs.md),
|
||||||
for deployment details read [`docs/v2/focas-deployment.md`](../v2/focas-deployment.md),
|
|
||||||
for the test-harness map read [FOCAS-Test-Fixture.md](FOCAS-Test-Fixture.md).
|
for the test-harness map read [FOCAS-Test-Fixture.md](FOCAS-Test-Fixture.md).
|
||||||
|
|
||||||
## What it talks to
|
## What it talks to
|
||||||
@@ -20,7 +19,7 @@ protocol using the documented command IDs. Writes return
|
|||||||
|
|
||||||
| Project | Target | Role |
|
| Project | Target | Role |
|
||||||
|---------|--------|------|
|
|---------|--------|------|
|
||||||
| `src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/` | net10.0 | In-process driver — hosts `WireFocasClient` which speaks FOCAS2 over TCP directly |
|
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/` | net10.0 | In-process driver — hosts `WireFocasClient` which speaks FOCAS2 over TCP directly |
|
||||||
|
|
||||||
Previous `Driver.FOCAS.Host` / `Driver.FOCAS.Shared` Tier-C split has been
|
Previous `Driver.FOCAS.Host` / `Driver.FOCAS.Shared` Tier-C split has been
|
||||||
retired — the managed wire client removes the native-crash blast radius
|
retired — the managed wire client removes the native-crash blast radius
|
||||||
@@ -206,12 +205,13 @@ latency spike once per cadence.
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- **Unit tests** — `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/` cover the
|
- **Unit tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/` cover the
|
||||||
driver surface via `FakeFocasClient`. Includes the alarm-projection raise /
|
driver surface via `FakeFocasClient`. Includes the alarm-projection raise /
|
||||||
clear diffing tests.
|
clear diffing tests.
|
||||||
- **Integration tests** — `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/`
|
- **Integration tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/`
|
||||||
hold the Docker simulator scaffold (Stream B / C of the simulator plan —
|
hold the Docker simulator scaffold; see
|
||||||
`docs/v2/implementation/focas-simulator-plan.md`).
|
[`docs/v2/implementation/focas-wire-protocol.md`](../v2/implementation/focas-wire-protocol.md)
|
||||||
|
for what the simulator emits vs. real CNC behaviour.
|
||||||
- **E2E script** — `scripts/e2e/test-focas.ps1` stages Host + Proxy + a real
|
- **E2E script** — `scripts/e2e/test-focas.ps1` stages Host + Proxy + a real
|
||||||
CNC (or the simulator) and exercises connect → read → write → subscribe
|
CNC (or the simulator) and exercises connect → read → write → subscribe
|
||||||
round-trips. See [`docs/drivers/FOCAS-Test-Fixture.md`](FOCAS-Test-Fixture.md)
|
round-trips. See [`docs/drivers/FOCAS-Test-Fixture.md`](FOCAS-Test-Fixture.md)
|
||||||
|
|||||||
+77
-184
@@ -1,211 +1,104 @@
|
|||||||
# Galaxy Driver
|
# Galaxy Driver
|
||||||
|
|
||||||
The Galaxy driver bridges OtOpcUa to AVEVA System Platform (Wonderware) Galaxies through the `ArchestrA.MxAccess` COM API plus the Galaxy Repository SQL database. It is one driver of seven in the OtOpcUa platform (see [drivers/README.md](README.md) for the full list); all other drivers run in-process in the main Server (.NET 10 x64). Galaxy is the exception — it runs as its own Windows service and talks to the Server over a local named pipe.
|
The Galaxy driver bridges OtOpcUa to AVEVA System Platform (Wonderware) Galaxies. It is a **Tier-A in-process driver** that runs in the OtOpcUa server's .NET 10 AnyCPU process and speaks gRPC to a separately installed `mxaccessgw` server (sibling repo at `c:\Users\dohertj2\Desktop\mxaccessgw\`). The gateway owns the MXAccess COM apartment, the STA + Win32 message pump, the Galaxy Repository SQL reader, and the Historian SDK — all the bits that need x86 / .NET Framework 4.8 / COM interop. The driver itself is platform-agnostic and contains no COM, no STA thread, and no x86 bitness constraint.
|
||||||
|
|
||||||
For the decision record on why Galaxy is out-of-process and how the refactor was staged, see [docs/v2/plan.md §4 Galaxy/MXAccess as Out-of-Process Driver](../v2/plan.md). For the full driver spec (addressing, data-type map, config shape), see [docs/v2/driver-specs.md §1](../v2/driver-specs.md).
|
For the driver spec (capability surface, config shape, addressing), see [docs/v2/driver-specs.md §1](../v2/driver-specs.md). For the gateway setup recipe, see [docs/v2/Galaxy.ParityRig.md](../v2/Galaxy.ParityRig.md). For tracing, metrics, and soak profile, see [docs/v2/Galaxy.Performance.md](../v2/Galaxy.Performance.md).
|
||||||
|
|
||||||
## Project Split
|
> **Note**: the related drivers `Galaxy-Repository.md` and `Galaxy-Test-Fixture.md` describe the previous v1 / out-of-process topology and are being moved to `docs/v1/` by a parallel cleanup track. Use `Galaxy.ParityRig.md` and the `mxaccessgw` repo for current testing.
|
||||||
|
|
||||||
Galaxy ships as three projects:
|
## Architecture
|
||||||
|
|
||||||
| Project | Target | Role |
|
```
|
||||||
|---------|--------|------|
|
+---------------------------------------+
|
||||||
| `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/` | .NET Standard 2.0 | IPC contracts (MessagePack records + `MessageKind` enum) referenced by both sides |
|
| OtOpcUa.Server (.NET 10 AnyCPU) |
|
||||||
| `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/` | .NET Framework 4.8 **x86** | Separate Windows service hosting the MXAccess COM objects, STA thread + Win32 message pump, Galaxy Repository reader, Historian SDK, runtime-probe manager |
|
| GalaxyDriver (in-process) |
|
||||||
| `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/` | .NET 10 (matches Server) | `GalaxyProxyDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IAlarmSource, IHistoryProvider, IRediscoverable, IHostConnectivityProbe` — loaded in-process by the Server; every call forwards over the pipe to the Host |
|
| ITagDiscovery / IReadable / |
|
||||||
|
| IWritable / ISubscribable / |
|
||||||
The Shared assembly is the **only** contract between the two runtimes. It carries no COM or SDK references so Proxy (net10) can reference it without dragging x86 code into the Server process.
|
| IRediscoverable / |
|
||||||
|
| IHostConnectivityProbe / |
|
||||||
## Why Out-of-Process
|
| IAlarmSource |
|
||||||
|
+-------------------+-------------------+
|
||||||
Two reasons drive the split, per `docs/v2/plan.md`:
|
|
|
||||||
|
gRPC (default http://localhost:5120)
|
||||||
1. **Bitness constraint.** MXAccess is 32-bit COM only — `ArchestrA.MxAccess.dll` in `Program Files (x86)\ArchestrA\Framework\bin` has no 64-bit variant. The main OtOpcUa Server is .NET 10 x64 (the OPC Foundation stack, SqlClient, and every other non-Galaxy driver target 64-bit). In-process hosting would force the whole Server to x86, which every other driver project would then inherit.
|
|
|
||||||
2. **Tier-C stability isolation.** Galaxy is classified Tier C in [docs/v2/driver-stability.md](../v2/driver-stability.md) — the COM runtime, STA thread, Aveva Historian SDK, and SQL queries all have crash/hang modes that can take down the hosting process. Isolating the driver in its own Windows service means a COM deadlock, AccessViolation in an unmanaged Historian DLL, or a runaway SQL query never takes the Server endpoint down. The Proxy-side supervisor restarts the Host with crash-loop circuit-breaker.
|
v
|
||||||
|
+---------------------------------------+
|
||||||
The same Tier-C isolation story applies to FOCAS (decision record in `docs/v2/plan.md` §7), which is the second out-of-process driver.
|
| mxaccessgw (sibling repo) |
|
||||||
|
| +-------------------------------+ |
|
||||||
## IPC Transport
|
| | MxGateway.Worker (x86 net48) | |
|
||||||
|
| | STA + WM_APP pump | |
|
||||||
`GalaxyProxyDriver` → `GalaxyIpcClient` → named pipe → `Galaxy.Host` pipe server.
|
| | ArchestrA.MxAccess COM | |
|
||||||
|
| | Galaxy Repository SQL | |
|
||||||
- Pipe name: `otopcua-galaxy-{DriverInstanceId}` (localhost-only, no TCP surface)
|
| | Wonderware Historian SDK | |
|
||||||
- Wire format: MessagePack-CSharp, length-prefixed frames
|
| +-------------------------------+ |
|
||||||
- ACL: pipe is created with a DACL that grants `ReadWrite | Synchronize` only to the configured Server service-principal SID + denies `LocalSystem`. The per-connection SID check in `PipeServer.VerifyCaller` is the real authorization boundary — any caller whose impersonated token SID doesn't match the allowed SID is dropped before the first frame is read.
|
+---------------------------------------+
|
||||||
- Handshake: Proxy presents a shared secret at `OpenSessionRequest`; Host rejects anything else with `MessageKind.OpenSessionResponse{Success=false}`
|
|
||||||
- Heartbeat: Proxy sends a periodic ping; missed heartbeats trigger the Proxy-side crash-loop supervisor to restart the Host
|
|
||||||
|
|
||||||
Every capability call on `GalaxyProxyDriver` (Read, Write, Subscribe, HistoryRead*, etc.) serializes a `*Request`, awaits the matching `*Response` via a `CallAsync<TReq, TResp>` helper, and rehydrates the result into the `Core.Abstractions` shape the Server expects.
|
|
||||||
|
|
||||||
## STA Thread Requirement (Host-side)
|
|
||||||
|
|
||||||
MXAccess COM objects — `LMXProxyServer` instantiation, `Register`, `AddItem`, `AdviseSupervisory`, `Write`, and cleanup calls — must all execute on the same Single-Threaded Apartment. Calling a COM object from the wrong thread causes marshalling failures or silent data corruption.
|
|
||||||
|
|
||||||
`StaComThread` in the Host provides that thread with the apartment state set before the thread starts:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
_thread = new Thread(ThreadEntry) { Name = "MxAccess-STA", IsBackground = true };
|
|
||||||
_thread.SetApartmentState(ApartmentState.STA);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Work items queue via `RunAsync(Action)` or `RunAsync<T>(Func<T>)` into a `ConcurrentQueue<Action>` and post `WM_APP` to wake the pump. Each work item is wrapped in a `TaskCompletionSource` so callers can `await` the result from any thread — including the IPC handler thread that receives the inbound pipe request.
|
History reads moved server-side in PR 7.2 (`IHistoryRouter`). Galaxy no longer implements `IHistoryProvider` of its own.
|
||||||
|
|
||||||
## Win32 Message Pump (Host-side)
|
`IAlarmSource` was retired with PR 7.2 and **restored in PR B.2** of the
|
||||||
|
alarms-over-gateway epic ([docs/plans/alarms-over-gateway.md](../plans/alarms-over-gateway.md)).
|
||||||
|
Alarm transitions arrive on the same gateway `StreamEvents` channel as
|
||||||
|
data-change events under the new `MX_EVENT_FAMILY_ON_ALARM_TRANSITION`
|
||||||
|
family; acknowledgements route through the gateway's
|
||||||
|
`AcknowledgeAlarm` RPC. The previous value-driven sub-attribute path
|
||||||
|
remains as a fallback for Galaxy templates without `$Alarm*`
|
||||||
|
extensions — the server-side `AlarmConditionService` dedups when both
|
||||||
|
paths fire on the same condition. See [docs/AlarmTracking.md](../AlarmTracking.md)
|
||||||
|
for the v2-final architecture.
|
||||||
|
|
||||||
COM callbacks (`OnDataChange`, `OnWriteComplete`) are delivered through the Windows message loop. `StaComThread` runs a standard Win32 message pump via P/Invoke:
|
## Project Layout
|
||||||
|
|
||||||
1. `PeekMessage` primes the message queue (required before `PostThreadMessage` works)
|
The driver ships as a single project: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/` (.NET 10, AnyCPU). Sub-folders:
|
||||||
2. `GetMessage` blocks until a message arrives
|
|
||||||
3. `WM_APP` drains the work queue
|
|
||||||
4. `WM_APP + 1` drains the queue and posts `WM_QUIT` to exit the loop
|
|
||||||
5. All other messages go through `TranslateMessage` / `DispatchMessage` for COM callback delivery
|
|
||||||
|
|
||||||
Without this pump MXAccess callbacks never fire and the driver delivers no live data.
|
| Folder | Role |
|
||||||
|
|--------|------|
|
||||||
|
| `Browse/` | Static-side discovery: `GalaxyDiscoverer` walks the gateway's hierarchy + attribute-set RPCs, `DataTypeMap` and `SecurityMap` translate Galaxy types and security classifications into OPC UA equivalents, `AlarmRefBuilder` extracts alarm-bearing attribute references for the server-layer alarm engine. `IGalaxyHierarchySource` + `GatewayGalaxyHierarchySource` + `TracedGalaxyHierarchySource` decorate the gateway browse RPC; `IGalaxyDeployWatchSource` + `GatewayGalaxyDeployWatchSource` + `DeployWatcher` drive `IRediscoverable`. |
|
||||||
|
| `Runtime/` | Live data path: `EventPump` runs the gateway's `StreamEvents` RPC and fans out to subscribers via a bounded channel; `GalaxyMxSession` is the read-side handle; `GatewayGalaxySubscriber` + `GatewayGalaxyDataWriter` (each with a `Traced*` decorator) implement `ISubscribable` / `IWritable`; `SubscriptionRegistry` tracks subscription state for replay; `ReconnectSupervisor` owns the backoff loop and triggers `ReplaySubscriptions` on session loss; `StatusCodeMap` translates gateway StatusCodes to OPC UA; `MxValueDecoder` / `MxValueEncoder` handle scalar + array marshalling; `GalaxyTelemetry` + `GalaxySubscriptionHandle` round out the surface. |
|
||||||
|
| `Health/` | `HostStatusAggregator` rolls per-platform probe state into the driver's `IHostConnectivityProbe` view; `PerPlatformProbeWatcher` listens on the gateway's per-host status stream; `HostConnectivityForwarder` pushes transitions out to the server's connectivity bus. |
|
||||||
|
| `Config/` | `GalaxyDriverOptions` and the four nested option records (`GalaxyGatewayOptions`, `GalaxyMxAccessOptions`, `GalaxyRepositoryOptions`, `GalaxyReconnectOptions`). |
|
||||||
|
|
||||||
## LMXProxyServer COM Object
|
Project root files:
|
||||||
|
|
||||||
`MxProxyAdapter` wraps the real `ArchestrA.MxAccess.LMXProxyServer` COM object behind the `IMxProxy` interface so Host unit tests can substitute a fake proxy without requiring the ArchestrA runtime. Lifecycle:
|
- `GalaxyDriver.cs` — `IDriver` + capability-interface implementation; composes the Browse / Runtime / Health collaborators.
|
||||||
|
- `GalaxyDriverFactoryExtensions.cs` — DI registration helper used by the server's driver bootstrap.
|
||||||
|
|
||||||
1. **`Register(clientName)`** — Creates a new `LMXProxyServer` instance, wires up `OnDataChange` and `OnWriteComplete` event handlers, calls `Register` to obtain a connection handle
|
## Capability Surface
|
||||||
2. **`Unregister(handle)`** — Unwires event handlers, calls `Unregister`, releases the COM object via `Marshal.ReleaseComObject`
|
|
||||||
|
|
||||||
## Register / AddItem / AdviseSupervisory Pattern
|
`GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IRediscoverable, IHostConnectivityProbe, IDisposable`.
|
||||||
|
|
||||||
Every MXAccess data operation follows a three-step pattern, all executed on the STA thread:
|
| Capability | Implementation entry point |
|
||||||
|
|------------|---------------------------|
|
||||||
|
| `ITagDiscovery` | `Browse/GalaxyDiscoverer.cs` |
|
||||||
|
| `IRediscoverable` | `Browse/DeployWatcher.cs` |
|
||||||
|
| `IReadable` | `Runtime/GalaxyMxSession.cs` |
|
||||||
|
| `IWritable` | `Runtime/GatewayGalaxyDataWriter.cs` |
|
||||||
|
| `ISubscribable` | `Runtime/GatewayGalaxySubscriber.cs` (driven by `EventPump`) |
|
||||||
|
| `IHostConnectivityProbe` | `Health/HostStatusAggregator.cs` |
|
||||||
|
|
||||||
1. **`AddItem(handle, address)`** — Resolves a Galaxy tag reference (e.g., `TestMachine_001.MachineID`) to an integer item handle
|
## Configuration
|
||||||
2. **`AdviseSupervisory(handle, itemHandle)`** — Subscribes the item for supervisory data-change callbacks
|
|
||||||
3. The runtime begins delivering `OnDataChange` events
|
|
||||||
|
|
||||||
For writes, after `AddItem` + `AdviseSupervisory`, `Write(handle, itemHandle, value, securityClassification)` sends the value; `OnWriteComplete` confirms or rejects. Cleanup reverses: `UnAdviseSupervisory` then `RemoveItem`.
|
`DriverConfig` JSON binds to `Config/GalaxyDriverOptions.cs`. The four sections are:
|
||||||
|
|
||||||
## OnDataChange and OnWriteComplete Callbacks
|
- **`Gateway`** — endpoint, API key secret ref, TLS knobs, connect/call/stream timeouts. `StreamTimeoutSeconds = 0` keeps the long-lived `StreamEvents` RPC open for the driver's lifetime.
|
||||||
|
- **`MxAccess`** — `ClientName` (must be unique per OtOpcUa instance — redundancy pairs enforce uniqueness at install time), `PublishingIntervalMs` (forwarded as `buffered_update_interval_ms` on subscribe), `WriteUserId` for ArchestrA secured-write, `EventPumpChannelCapacity` (default 50_000 — one second of headroom at 50k tags / 1Hz; tune via the `galaxy.events.dropped` metric).
|
||||||
|
- **`Repository`** — `DiscoverPageSize`, `WatchDeployEvents`.
|
||||||
|
- **`Reconnect`** — `InitialBackoffMs`, `MaxBackoffMs`, `ReplayOnSessionLost` (calls the gateway's `ReplaySubscriptions` RPC after reconnect rather than re-issuing subscribe-bulk for every tag).
|
||||||
|
|
||||||
### OnDataChange
|
Full per-field descriptions live in `Config/GalaxyDriverOptions.cs`. The full JSON skeleton is reproduced in [docs/v2/driver-specs.md §1](../v2/driver-specs.md).
|
||||||
|
|
||||||
Fired by the COM runtime on the STA thread when a subscribed tag changes. The handler in `MxAccessClient.EventHandlers.cs`:
|
## Reconnect + Replay
|
||||||
|
|
||||||
1. Maps the integer `phItemHandle` back to a tag address via `_handleToAddress`
|
`ReconnectSupervisor` owns an exponential-backoff loop bounded by `Reconnect.InitialBackoffMs` / `MaxBackoffMs`. On session loss it tears down the gRPC channel, redials, and — when `ReplayOnSessionLost = true` — calls the gateway's `ReplaySubscriptions` RPC with the cached subscription set from `SubscriptionRegistry` instead of re-subscribing tag-by-tag. The gateway's worker then re-issues `AdviseSupervisory` server-side under the apartment lock.
|
||||||
2. Maps the MXAccess quality code to the internal `Quality` enum
|
|
||||||
3. Checks `MXSTATUS_PROXY` for error details and adjusts quality
|
|
||||||
4. Converts the timestamp to UTC
|
|
||||||
5. Constructs a `Vtq` (Value/Timestamp/Quality) and delivers it to:
|
|
||||||
- The stored per-tag subscription callback
|
|
||||||
- Any pending one-shot read completions
|
|
||||||
- The global `OnTagValueChanged` event (consumed by the Host's subscription dispatcher, which packages changes into `DataChangeEventArgs` and forwards them over the pipe to `GalaxyProxyDriver.OnDataChange`)
|
|
||||||
|
|
||||||
### OnWriteComplete
|
## Testing
|
||||||
|
|
||||||
Fired when the runtime acknowledges or rejects a write. The handler resolves the pending `TaskCompletionSource<bool>` for the item handle. If `MXSTATUS_PROXY.success == 0` the write is considered failed and the error detail is logged.
|
- **Unit tests**: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/` — fakes the gateway gRPC surface; covers Browse, Runtime, Health, and Config in isolation.
|
||||||
|
- **Parity rig + dev-rig walkthrough**: see [docs/v2/Galaxy.ParityRig.md](../v2/Galaxy.ParityRig.md). The rig stands up a real `mxaccessgw` against a live Galaxy and exercises the full read / write / subscribe / rediscover path.
|
||||||
|
- **Performance + soak**: see [docs/v2/Galaxy.Performance.md](../v2/Galaxy.Performance.md).
|
||||||
|
|
||||||
## Reconnection Logic
|
## Operational Notes
|
||||||
|
|
||||||
`MxAccessClient` implements automatic reconnection through two mechanisms.
|
- **MXAccess `ClientName` collisions**: two OtOpcUa instances sharing a `ClientName` cause the older Wonderware session to lose subscription state. Redundancy pairs (decision #149) enforce uniqueness via install scripts.
|
||||||
|
- **Channel saturation**: `galaxy.events.dropped > 0` indicates `EventPump` is back-pressured. Raise `EventPumpChannelCapacity` or investigate downstream slowness in the server-side fan-out.
|
||||||
### Monitor loop
|
- **Connectivity surface**: per-platform probe state is exposed through `IHostConnectivityProbe` and aggregated by the server's connectivity bus — there is no driver-private dashboard surface anymore. The Admin UI's Host Status panel is the consumer.
|
||||||
|
|
||||||
`StartMonitor` launches a background task that polls at `MonitorIntervalSeconds`. On each cycle:
|
|
||||||
|
|
||||||
- If the state is `Disconnected` or `Error` and `AutoReconnect` is enabled, it calls `ReconnectAsync`
|
|
||||||
- If connected and a probe tag is configured, it checks the probe staleness threshold
|
|
||||||
|
|
||||||
### Reconnect sequence
|
|
||||||
|
|
||||||
`ReconnectAsync` performs a full disconnect-then-connect cycle:
|
|
||||||
|
|
||||||
1. Increment the reconnect counter
|
|
||||||
2. `DisconnectAsync` — tear down all active subscriptions (`UnAdviseSupervisory` + `RemoveItem` for each), detach COM event handlers, call `Unregister`, clear all handle mappings
|
|
||||||
3. `ConnectAsync` — create a fresh `LMXProxyServer`, register, replay all stored subscriptions, re-subscribe the probe tag
|
|
||||||
|
|
||||||
Stored subscriptions (`_storedSubscriptions`) persist across reconnects. `ReplayStoredSubscriptionsAsync` iterates the stored entries and calls `AddItem` + `AdviseSupervisory` for each.
|
|
||||||
|
|
||||||
## Probe Tag Health Monitoring
|
|
||||||
|
|
||||||
A configurable probe tag (e.g., a frequently updating Galaxy attribute) serves as a connection health indicator. After connecting, the client subscribes to the probe tag and records `_lastProbeValueTime` on every `OnDataChange`. The monitor loop compares `DateTime.UtcNow - _lastProbeValueTime` against `ProbeStaleThresholdSeconds`; if the probe has not updated within the window, the connection is assumed stale and a reconnect is forced. This catches scenarios where the COM connection is technically alive but the runtime has stopped delivering data.
|
|
||||||
|
|
||||||
## Per-Host Runtime Status Probes (`<Host>.ScanState`)
|
|
||||||
|
|
||||||
Separate from the connection-level probe, the driver advises `<HostName>.ScanState` on every deployed `$WinPlatform` and `$AppEngine` in the Galaxy. These probes track per-host runtime state so the Admin UI dashboard can report "this specific Platform / AppEngine is off scan" and the driver can proactively invalidate every OPC UA variable hosted by the stopped object — preventing MXAccess from serving stale Good-quality cached values to clients who read those tags while the host is down.
|
|
||||||
|
|
||||||
Enabled by default via `MxAccess.RuntimeStatusProbesEnabled`; see [Configuration](../Configuration.md#mxaccess) for the two config fields.
|
|
||||||
|
|
||||||
### How it works
|
|
||||||
|
|
||||||
`GalaxyRuntimeProbeManager` lives in `Driver.Galaxy.Host` alongside the rest of the MXAccess code. It is owned by the Host's subscription dispatcher and runs a three-state machine per host (Unknown / Running / Stopped):
|
|
||||||
|
|
||||||
1. **Discovery** — After the Host completes `BuildAddressSpace`, the manager filters the hierarchy to rows where `CategoryId == 1` (`$WinPlatform`) or `CategoryId == 3` (`$AppEngine`) and issues `AdviseSupervisory` for `<TagName>.ScanState` on each one. Probes are driver-owned, not ref-counted against client subscriptions, and persist across address-space rebuilds via a `Sync` diff.
|
|
||||||
2. **Transition predicate** — A probe callback is interpreted as `isRunning = vtq.Quality.IsGood() && vtq.Value is bool b && b`. Everything else (explicit `ScanState = false`, bad quality, communication errors) means **Stopped**.
|
|
||||||
3. **On-change-only delivery** — `ScanState` is delivered only when the value actually changes. A stably Running host may go hours without a callback. `Tick()` does NOT run a starvation check on Running entries — the only time-based transition is **Unknown → Stopped** when the initial callback hasn't arrived within `RuntimeStatusUnknownTimeoutSeconds` (default 15s). This protects against a probe that fails to resolve at all without incorrectly flipping healthy long-running hosts.
|
|
||||||
4. **Transport gating** — When `IMxAccessClient.State != Connected`, `GetSnapshot()` forces every entry to `Unknown`. The dashboard shows the Connection panel as the primary signal in that case rather than misleading operators with "every host stopped".
|
|
||||||
5. **Subscribe failure rollback** — If `SubscribeAsync` throws for a new probe (SDK failure, broker rejection, transport error), the manager rolls back both `_byProbe` and `_probeByGobjectId` so the probe never appears in `GetSnapshot()`. Stability review 2026-04-13 Finding 1.
|
|
||||||
|
|
||||||
### Subtree quality invalidation on transition
|
|
||||||
|
|
||||||
When a host transitions **Running → Stopped**, the probe manager invokes a callback that walks `_hostedVariables[gobjectId]` — the set of every OPC UA variable transitively hosted by that Galaxy object — and sets each variable's `StatusCode` to `BadOutOfService`. **Stopped → Running** calls `ClearHostVariablesBadQuality` to reset each to `Good` so the next on-change MXAccess update repopulates the value.
|
|
||||||
|
|
||||||
The hosted-variables map is built once per `BuildAddressSpace` by walking each object's `HostedByGobjectId` chain up to the nearest Platform or Engine ancestor. A variable hosted by an Engine inside a Platform lands in both the Engine's list and the Platform's list, so stopping the Platform transitively invalidates every descendant Engine's variables.
|
|
||||||
|
|
||||||
### Read-path short-circuit (`IsTagUnderStoppedHost`)
|
|
||||||
|
|
||||||
The Host's Read handler checks `IsTagUnderStoppedHost(tagRef)` (a reverse-index lookup `_hostIdsByTagRef[tagRef]` → `GalaxyRuntimeProbeManager.IsHostStopped(hostId)`) before the MXAccess round-trip. When the owning host is Stopped, the handler returns a synthesized `DataValue { Value = cachedVar.Value, StatusCode = BadOutOfService }` directly without touching MXAccess. This guarantees clients see a uniform `BadOutOfService` on every descendant tag of a stopped host, regardless of whether they're reading or subscribing.
|
|
||||||
|
|
||||||
### Deferred dispatch — the STA deadlock
|
|
||||||
|
|
||||||
**Critical**: probe transition callbacks must **not** run synchronously on the STA thread that delivered the `OnDataChange`. `MarkHostVariablesBadQuality` takes the subscription dispatcher lock, which may be held by a worker thread currently inside `Read` waiting on an `_mxAccessClient.ReadAsync()` round-trip that is itself waiting for the STA thread. Classic circular wait — the first real deploy of this feature hung inside 30 seconds from exactly this pattern.
|
|
||||||
|
|
||||||
The fix is a deferred-dispatch queue: probe callbacks enqueue the transition onto `ConcurrentQueue<(int GobjectId, bool Stopped)>` and set the existing dispatch signal. The dispatch thread drains the queue inside its existing 100ms `WaitOne` loop — outside any locks held by the STA path — and then calls `MarkHostVariablesBadQuality` / `ClearHostVariablesBadQuality` under its own natural lock acquisition. No circular wait, no STA involvement.
|
|
||||||
|
|
||||||
### Dashboard and health surface
|
|
||||||
|
|
||||||
- Admin UI **Galaxy Runtime** panel shows per-host state with Name / Kind / State / Since / Last Error columns. Panel color is green (all Running), yellow (any Unknown, none Stopped), red (any Stopped), gray (MXAccess transport disconnected)
|
|
||||||
- `HealthCheckService.CheckHealth` rolls overall driver health to `Degraded` when any host is Stopped
|
|
||||||
|
|
||||||
See [Status Dashboard](../StatusDashboard.md#galaxy-runtime) for the field table and [Configuration](../Configuration.md#mxaccess) for the config fields.
|
|
||||||
|
|
||||||
## Request Timeout Safety Backstop
|
|
||||||
|
|
||||||
Every sync-over-async site on the OPC UA stack thread that calls into Galaxy (`Read`, `Write`, address-space rebuild probe sync) is wrapped in a bounded `SyncOverAsync.WaitSync(...)` helper with timeout `MxAccess.RequestTimeoutSeconds` (default 30s). Inner `ReadTimeoutSeconds` / `WriteTimeoutSeconds` bounds on the async path are the first line of defense; the outer wrapper is a backstop so a scheduler stall, slow reconnect, or any other non-returning async path cannot park the stack thread indefinitely.
|
|
||||||
|
|
||||||
On timeout, the underlying task is **not** cancelled — it runs to completion on the thread pool and is abandoned. This is acceptable because Galaxy IPC clients are shared singletons and the abandoned continuation does not capture request-scoped state. The OPC UA stack receives `StatusCodes.BadTimeout` on the affected operation.
|
|
||||||
|
|
||||||
`ConfigurationValidator` enforces `RequestTimeoutSeconds >= 1` and warns when it is set below the inner Read/Write timeouts (operator misconfiguration). Stability review 2026-04-13 Finding 3.
|
|
||||||
|
|
||||||
All capability calls at the Server dispatch layer are additionally wrapped by `CapabilityInvoker` (Core/Resilience/) which runs them through a Polly pipeline keyed on `(DriverInstanceId, HostName, DriverCapability)`. `OTOPCUA0001` analyzer enforces the wrap at build time.
|
|
||||||
|
|
||||||
## Why Marshal.ReleaseComObject Is Needed
|
|
||||||
|
|
||||||
The .NET Framework runtime's garbage collector releases COM references non-deterministically. For MXAccess, delayed release can leave stale COM connections open, preventing clean re-registration. `MxProxyAdapter.Unregister` calls `Marshal.ReleaseComObject(_lmxProxy)` in a `finally` block to immediately drive the COM reference count to zero. This ensures the underlying COM server is freed before a reconnect attempt creates a new instance.
|
|
||||||
|
|
||||||
## Tag Discovery and Historical Data
|
|
||||||
|
|
||||||
Tag discovery (the Galaxy Repository SQL reader + `LocalPlatform` scope filter) is covered in [Galaxy-Repository.md](Galaxy-Repository.md). The Galaxy driver is `ITagDiscovery` for the Server's bootstrap path and `IRediscoverable` for the on-change-redeploy path.
|
|
||||||
|
|
||||||
Historical data access (raw, processed, at-time, events) runs against the Aveva Historian via the `aahClientManaged` SDK and is exposed through the Galaxy driver's `IHistoryProvider` implementation. See [HistoricalDataAccess.md](../HistoricalDataAccess.md).
|
|
||||||
|
|
||||||
## Key source files
|
|
||||||
|
|
||||||
Host-side (`.NET 4.8 x86`, `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/`):
|
|
||||||
|
|
||||||
- `Backend/MxAccess/StaComThread.cs` — STA thread and Win32 message pump
|
|
||||||
- `Backend/MxAccess/MxAccessClient.cs` — Core client (partial)
|
|
||||||
- `Backend/MxAccess/MxAccessClient.Connection.cs` — Connect / disconnect / reconnect
|
|
||||||
- `Backend/MxAccess/MxAccessClient.Subscription.cs` — Subscribe / unsubscribe / replay
|
|
||||||
- `Backend/MxAccess/MxAccessClient.ReadWrite.cs` — Read and write operations
|
|
||||||
- `Backend/MxAccess/MxAccessClient.EventHandlers.cs` — `OnDataChange` / `OnWriteComplete` handlers
|
|
||||||
- `Backend/MxAccess/MxAccessClient.Monitor.cs` — Background health monitor
|
|
||||||
- `Backend/MxAccess/MxProxyAdapter.cs` — COM object wrapper
|
|
||||||
- `Backend/MxAccess/GalaxyRuntimeProbeManager.cs` — Per-host `ScanState` probes, state machine, `IsHostStopped` lookup
|
|
||||||
- `Backend/Historian/HistorianDataSource.cs` — `aahClientManaged` SDK wrapper (see [HistoricalDataAccess.md](../HistoricalDataAccess.md))
|
|
||||||
- `Ipc/GalaxyIpcServer.cs` — Named-pipe server, message dispatch
|
|
||||||
- `Domain/IMxAccessClient.cs` — Client interface
|
|
||||||
|
|
||||||
Shared (`.NET Standard 2.0`, `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/`):
|
|
||||||
|
|
||||||
- `Contracts/MessageKind.cs` — IPC message kinds (`ReadRequest`, `HistoryReadRequest`, `OpenSessionResponse`, …)
|
|
||||||
- `Contracts/*.cs` — MessagePack DTOs for every request/response pair
|
|
||||||
|
|
||||||
Proxy-side (`.NET 10`, `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/`):
|
|
||||||
|
|
||||||
- `GalaxyProxyDriver.cs` — `IDriver`/`ITagDiscovery`/`IReadable`/`IWritable`/`ISubscribable`/`IAlarmSource`/`IHistoryProvider`/`IRediscoverable`/`IHostConnectivityProbe` implementation; every method forwards via `GalaxyIpcClient`
|
|
||||||
- `Ipc/GalaxyIpcClient.cs` — Named-pipe client, `CallAsync<TReq, TResp>`, reconnect on broken pipe
|
|
||||||
- `GalaxyProxySupervisor.cs` — Host-process monitor, crash-loop circuit-breaker, Host relaunch
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ shaped (neither is a Modbus-side concept).
|
|||||||
|
|
||||||
- **Simulator**: `pymodbus` (Python, BSD) launched as a pinned Docker
|
- **Simulator**: `pymodbus` (Python, BSD) launched as a pinned Docker
|
||||||
container at
|
container at
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/`.
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/`.
|
||||||
Docker is the only supported launch path.
|
Docker is the only supported launch path.
|
||||||
- **Lifecycle**: `ModbusSimulatorFixture` (collection-scoped) TCP-probes
|
- **Lifecycle**: `ModbusSimulatorFixture` (collection-scoped) TCP-probes
|
||||||
`localhost:5020` on first use. `MODBUS_SIM_ENDPOINT` env var overrides the
|
`localhost:5020` on first use. `MODBUS_SIM_ENDPOINT` env var overrides the
|
||||||
@@ -115,9 +115,9 @@ Not a Modbus concept. Driver doesn't implement `IAlarmSource` or
|
|||||||
|
|
||||||
## Key fixture / config files
|
## Key fixture / config files
|
||||||
|
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs`
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs`
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Mitsubishi/MitsubishiProfile.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Mitsubishi/MitsubishiProfile.cs`
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500Profile.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500Profile.cs`
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/` —
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/` —
|
||||||
Dockerfile + compose + per-family JSON profiles
|
Dockerfile + compose + per-family JSON profiles
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ image (follow-up).
|
|||||||
## What the fixture is
|
## What the fixture is
|
||||||
|
|
||||||
**Integration layer** (task #215):
|
**Integration layer** (task #215):
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/` stands up
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/` stands up
|
||||||
`mcr.microsoft.com/iotedge/opc-plc:2.14.10` via `Docker/docker-compose.yml`
|
`mcr.microsoft.com/iotedge/opc-plc:2.14.10` via `Docker/docker-compose.yml`
|
||||||
on `opc.tcp://localhost:50000`. `OpcPlcFixture` probes the port at
|
on `opc.tcp://localhost:50000`. `OpcPlcFixture` probes the port at
|
||||||
collection init + skips tests with a clear message when the container's
|
collection init + skips tests with a clear message when the container's
|
||||||
@@ -30,7 +30,7 @@ resets on each spin-up), `--alm` (alarm simulation for IAlarmSource
|
|||||||
follow-up coverage), `--pn=50000` (port).
|
follow-up coverage), `--pn=50000` (port).
|
||||||
|
|
||||||
**Unit layer**:
|
**Unit layer**:
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` is still the primary
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` is still the primary
|
||||||
coverage. Tests inject fakes through the driver's construction path; the
|
coverage. Tests inject fakes through the driver's construction path; the
|
||||||
OPCFoundation.NetStandard `Session` surface is wrapped behind an interface
|
OPCFoundation.NetStandard `Session` surface is wrapped behind an interface
|
||||||
the tests mock.
|
the tests mock.
|
||||||
@@ -137,7 +137,7 @@ ConditionType events (non-base `BaseEventType`) is not verified.
|
|||||||
|
|
||||||
The easiest win here is to **wire the client driver tests against this
|
The easiest win here is to **wire the client driver tests against this
|
||||||
repo's own server**. The integration test project
|
repo's own server**. The integration test project
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Server.Tests/OpcUaServerIntegrationTests.cs`
|
`tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/OpcUaServerIntegrationTests.cs`
|
||||||
already stands up a real OPC UA server on a non-default port with a seeded
|
already stands up a real OPC UA server on a non-default port with a seeded
|
||||||
FakeDriver. An `OpcUaClientLiveLoopbackTests` that connects the client
|
FakeDriver. An `OpcUaClientLiveLoopbackTests` that connects the client
|
||||||
driver to that server would give:
|
driver to that server would give:
|
||||||
@@ -161,10 +161,10 @@ Beyond that:
|
|||||||
|
|
||||||
## Key fixture / config files
|
## Key fixture / config files
|
||||||
|
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` — unit tests with
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` — unit tests with
|
||||||
mocked `Session`
|
mocked `Session`
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs` — ctor +
|
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs` — ctor +
|
||||||
session-factory seam tests mock through
|
session-factory seam tests mock through
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Server.Tests/OpcUaServerIntegrationTests.cs` —
|
- `tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/OpcUaServerIntegrationTests.cs` —
|
||||||
the server-side integration harness a future loopback client test could
|
the server-side integration harness a future loopback client test could
|
||||||
piggyback on
|
piggyback on
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Drivers
|
# Drivers
|
||||||
|
|
||||||
OtOpcUa is a multi-driver OPC UA server. The Core (`ZB.MOM.WW.OtOpcUa.Core` + `Core.Abstractions` + `Server`) owns the OPC UA stack, address space, session/security/subscription machinery, resilience pipeline, and namespace kinds (Equipment + SystemPlatform). Drivers plug in through **capability interfaces** defined in `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/`:
|
OtOpcUa is a multi-driver OPC UA server. The Core (`ZB.MOM.WW.OtOpcUa.Core` + `Core.Abstractions` + `Server`) owns the OPC UA stack, address space, session/security/subscription machinery, resilience pipeline, and namespace kinds (Equipment + SystemPlatform). Drivers plug in through **capability interfaces** defined in `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/`:
|
||||||
|
|
||||||
- `IDriver` — lifecycle (`InitializeAsync`, `ReinitializeAsync`, `ShutdownAsync`, `GetHealth`)
|
- `IDriver` — lifecycle (`InitializeAsync`, `ReinitializeAsync`, `ShutdownAsync`, `GetHealth`)
|
||||||
- `IReadable` / `IWritable` — one-shot reads and writes
|
- `IReadable` / `IWritable` — one-shot reads and writes
|
||||||
@@ -14,7 +14,7 @@ OtOpcUa is a multi-driver OPC UA server. The Core (`ZB.MOM.WW.OtOpcUa.Core` + `C
|
|||||||
|
|
||||||
Each driver opts into only the capabilities it supports. Every async capability call at the Server dispatch layer goes through `CapabilityInvoker` (`Core/Resilience/CapabilityInvoker.cs`), which wraps it in a Polly pipeline keyed on `(DriverInstanceId, HostName, DriverCapability)`. The `OTOPCUA0001` analyzer enforces the wrap at build time. Drivers themselves never depend on Polly; they just implement the capability interface and let the Core wrap it.
|
Each driver opts into only the capabilities it supports. Every async capability call at the Server dispatch layer goes through `CapabilityInvoker` (`Core/Resilience/CapabilityInvoker.cs`), which wraps it in a Polly pipeline keyed on `(DriverInstanceId, HostName, DriverCapability)`. The `OTOPCUA0001` analyzer enforces the wrap at build time. Drivers themselves never depend on Polly; they just implement the capability interface and let the Core wrap it.
|
||||||
|
|
||||||
Driver type metadata is registered at startup in `DriverTypeRegistry` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs`). The registry records each type's allowed namespace kinds (`Equipment` / `SystemPlatform` / `Simulated`), its JSON Schema for `DriverConfig` / `DeviceConfig` / `TagConfig` columns, and its stability tier per [docs/v2/driver-stability.md](../v2/driver-stability.md).
|
Driver type metadata is registered at startup in `DriverTypeRegistry` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs`). The registry records each type's allowed namespace kinds (`Equipment` / `SystemPlatform` / `Simulated`), its JSON Schema for `DriverConfig` / `DeviceConfig` / `TagConfig` columns, and its stability tier per [docs/v2/driver-stability.md](../v2/driver-stability.md).
|
||||||
|
|
||||||
## Ground-truth driver list
|
## Ground-truth driver list
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ session types, PUT/GET-disabled enforcement — all need real hardware.
|
|||||||
## What the fixture is
|
## What the fixture is
|
||||||
|
|
||||||
**Integration layer** (task #216):
|
**Integration layer** (task #216):
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/` stands up a
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/` stands up a
|
||||||
python-snap7 `Server` via `Docker/docker-compose.yml --profile s7_1500`
|
python-snap7 `Server` via `Docker/docker-compose.yml --profile s7_1500`
|
||||||
on `localhost:1102` (pinned `python:3.12-slim-bookworm` base +
|
on `localhost:1102` (pinned `python:3.12-slim-bookworm` base +
|
||||||
`python-snap7>=2.0`). Docker is the only supported launch path.
|
`python-snap7>=2.0`). Docker is the only supported launch path.
|
||||||
@@ -24,7 +24,7 @@ clear message when unreachable (matches the pymodbus pattern).
|
|||||||
+ seeds DB/MB bytes at declared offsets; seeds are typed (`u16` / `i16`
|
+ seeds DB/MB bytes at declared offsets; seeds are typed (`u16` / `i16`
|
||||||
/ `i32` / `f32` / `bool` / `ascii` for S7 STRING).
|
/ `i32` / `f32` / `bool` / `ascii` for S7 STRING).
|
||||||
|
|
||||||
**Unit layer**: `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` covers
|
**Unit layer**: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` covers
|
||||||
everything the wire-level suite doesn't — address parsing, error
|
everything the wire-level suite doesn't — address parsing, error
|
||||||
branches, probe-loop contract. All tests tagged
|
branches, probe-loop contract. All tests tagged
|
||||||
`[Trait("Category", "Unit")]`.
|
`[Trait("Category", "Unit")]`.
|
||||||
@@ -115,7 +115,7 @@ from field deployments, not from the test suite.
|
|||||||
|
|
||||||
## Key fixture / config files
|
## Key fixture / config files
|
||||||
|
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` — unit tests only, no harness
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` — unit tests only, no harness
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs` — ctor takes
|
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs` — ctor takes
|
||||||
`IS7ClientFactory` which tests fake; docstring lines 8-20 note the deferred
|
`IS7ClientFactory` which tests fake; docstring lines 8-20 note the deferred
|
||||||
integration fixture
|
integration fixture
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Coverage map + gap inventory for the Beckhoff TwinCAT ADS driver.
|
Coverage map + gap inventory for the Beckhoff TwinCAT ADS driver.
|
||||||
|
|
||||||
**TL;DR:** Integration-test suite lives at
|
**TL;DR:** Integration-test suite lives at
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/`. `TwinCATXarFixture`
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/`. `TwinCATXarFixture`
|
||||||
probes TCP 48898 on an operator-supplied runtime; the suite runs **14
|
probes TCP 48898 on an operator-supplied runtime; the suite runs **14
|
||||||
`[TwinCATFact]` methods + one 16-case `[TwinCATTheory]` = 30 test cases** end-to-end
|
`[TwinCATFact]` methods + one 16-case `[TwinCATTheory]` = 30 test cases** end-to-end
|
||||||
through the real ADS stack when the runtime is reachable, skips cleanly
|
through the real ADS stack when the runtime is reachable, skips cleanly
|
||||||
@@ -18,7 +18,7 @@ also contract-tested rigorously at the unit layer.
|
|||||||
|
|
||||||
## What the fixture is
|
## What the fixture is
|
||||||
|
|
||||||
**Integration layer**: `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/`
|
**Integration layer**: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/`
|
||||||
— `TwinCATXarFixture` TCP-probes ADS port 48898 on the host supplied by
|
— `TwinCATXarFixture` TCP-probes ADS port 48898 on the host supplied by
|
||||||
`TWINCAT_TARGET_HOST` (defaults to `localhost`) + requires
|
`TWINCAT_TARGET_HOST` (defaults to `localhost`) + requires
|
||||||
`TWINCAT_TARGET_NETID` (AmsNetId of the runtime). Optionally takes
|
`TWINCAT_TARGET_NETID` (AmsNetId of the runtime). Optionally takes
|
||||||
@@ -29,7 +29,7 @@ kernel scheduler, so the runtime stays operator-managed.
|
|||||||
gate on `[TwinCATFact]` / `[TwinCATTheory]` and skip cleanly when
|
gate on `[TwinCATFact]` / `[TwinCATTheory]` and skip cleanly when
|
||||||
`TWINCAT_TARGET_NETID` is unset or the probe fails.
|
`TWINCAT_TARGET_NETID` is unset or the probe fails.
|
||||||
|
|
||||||
**Unit layer**: `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/` remains the
|
**Unit layer**: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/` remains the
|
||||||
primary contract coverage. `FakeTwinCATClient` fakes the
|
primary contract coverage. `FakeTwinCATClient` fakes the
|
||||||
`AddDeviceNotification` flow so tests can trigger callbacks without a running
|
`AddDeviceNotification` flow so tests can trigger callbacks without a running
|
||||||
runtime.
|
runtime.
|
||||||
@@ -174,13 +174,13 @@ license-rotation automation, and a dedicated lab IPC.
|
|||||||
|
|
||||||
## Key fixture / config files
|
## Key fixture / config files
|
||||||
|
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATXarFixture.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATXarFixture.cs`
|
||||||
— TCP probe + skip-attributes + env-var parsing
|
— TCP probe + skip-attributes + env-var parsing
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCAT3SmokeTests.cs`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCAT3SmokeTests.cs`
|
||||||
— wire-level test suite (14 `[TwinCATFact]` + 16-case `[TwinCATTheory]`)
|
— wire-level test suite (14 `[TwinCATFact]` + 16-case `[TwinCATTheory]`)
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCatProject/README.md`
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCatProject/README.md`
|
||||||
— project spec + VM setup + license-rotation notes
|
— project spec + VM setup + license-rotation notes
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/FakeTwinCATClient.cs` —
|
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/FakeTwinCATClient.cs` —
|
||||||
in-process fake with the notification-fire harness
|
in-process fake with the notification-fire harness
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs` — ctor is
|
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs` — ctor is
|
||||||
`(TwinCATDriverOptions, string driverInstanceId, ITwinCATClientFactory? = null)`
|
`(TwinCATDriverOptions, string driverInstanceId, ITwinCATClientFactory? = null)`
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,340 @@
|
|||||||
|
# Alarms Worker Wiring Plan
|
||||||
|
|
||||||
|
> **Context**: The alarms-over-gateway epic shipped 19 PRs across the
|
||||||
|
> `lmxopcua` and `mxaccessgw` repos (merged 2026-04-30). Contracts are live;
|
||||||
|
> the sub-attribute fallback path keeps Galaxy alarms functional today. Four
|
||||||
|
> items remain as inert scaffolds gated on a dev-rig finding. This document is
|
||||||
|
> the focused implementation plan for those four items only.
|
||||||
|
>
|
||||||
|
> **Do not duplicate `docs/plans/alarms-over-gateway.md`** — that document is
|
||||||
|
> the full historical record of all 19 PRs. This document covers only what is
|
||||||
|
> still to be done and exactly what blocks each item.
|
||||||
|
>
|
||||||
|
> **This work lives in the mxaccessgw sibling repo** at
|
||||||
|
> `C:\Users\dohertj2\Desktop\mxaccessgw\` — not in this (lmxopcua) repo,
|
||||||
|
> except where lmxopcua changes are noted explicitly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dev-rig finding that blocks everything (2026-04-30)
|
||||||
|
|
||||||
|
During PR A.2 work the following was discovered on the dev box:
|
||||||
|
|
||||||
|
> The MXAccess COM Toolkit at
|
||||||
|
> `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`
|
||||||
|
> exposes **no alarm-event family** — only `OnDataChange`, `OnWriteComplete`,
|
||||||
|
> `OperationComplete`, `OnBufferedDataChange`.
|
||||||
|
>
|
||||||
|
> AVEVA's `aaAlarmManagedClient` / `ArchestrAAlarmsAndEvents.SDK` assemblies
|
||||||
|
> are **x64-only** and incompatible with the worker's x86 net48 bitness.
|
||||||
|
|
||||||
|
The architectural decision required before any of A.2, A.3/A.4, C.1 can ship:
|
||||||
|
|
||||||
|
> **Either** accept the value-driven sub-attribute path as the production
|
||||||
|
> architecture (operator-comment fidelity is the only v1 regression), **or**
|
||||||
|
> add an x64 alarm-helper sub-process alongside the x86 worker.
|
||||||
|
|
||||||
|
Resolution drives the implementation shape of every item below. The plan
|
||||||
|
presented here assumes the x64 alarm-helper sub-process route (the higher
|
||||||
|
parity option), but notes the sub-attribute-only exit at each step.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Discovered AVEVA API surface
|
||||||
|
|
||||||
|
Before implementing, verify the following against the AVEVA SDK actually
|
||||||
|
installed on the dev box and in the mxaccessgw worker's deployment folder:
|
||||||
|
|
||||||
|
| Assembly | Bitness | Likely location | Key types |
|
||||||
|
|----------|---------|-----------------|-----------|
|
||||||
|
| `ArchestrA.MXAccess.dll` | x86 | `C:\Program Files (x86)\ArchestrA\Framework\Bin\` | `IMxAlarmEventSink`, `MxAlarmEventArgs` — **confirm exists at actual version** |
|
||||||
|
| `aaAlarmManagedClient.dll` | x64 | `C:\Program Files\ArchestrA\Framework\Bin\` | `AlarmClient`, `IAlarmConsumer`, `AlarmEventArgs` |
|
||||||
|
| `ArchestrAAlarmsAndEvents.SDK.dll` | x64 | Same or Historian SDK folder | `AlarmHistorianWriter`, `GetAlarmExtendedRec` |
|
||||||
|
|
||||||
|
The AVEVA MXAccess Toolkit reference in the mxaccessgw repo (`gateway.md`) is
|
||||||
|
the canonical API doc for the gateway worker's side. The alarm-client API is
|
||||||
|
documented separately; verify the following call shapes during PR A.2:
|
||||||
|
|
||||||
|
| Operation | Likely API | Notes |
|
||||||
|
|-----------|-----------|-------|
|
||||||
|
| Subscribe to alarm events | `AlarmClient.RegisterConsumer(IAlarmConsumer)` + `AlarmClient.Subscribe(filterSpec)` | Confirm exact method signatures against the SDK version on the dev box |
|
||||||
|
| Receive alarm event | `IAlarmConsumer.OnAlarmEvent(AlarmEventArgs)` callback | Field set: alarm name, source, type, transition kind, severity, timestamps, operator fields |
|
||||||
|
| Acknowledge alarm | `AlarmClient.AcknowledgeAlarm(alarmRef, comment, userPrincipal)` or equivalent | Confirm whether this is synchronous or returns a status |
|
||||||
|
| Query active alarms | `AlarmClient.GetAlarmExtendedRec(filter)` or `GetActiveAlarms()` | Returns current active set for ConditionRefresh |
|
||||||
|
| Get statistics | `AlarmClient.GetStatistics()` | Optional — useful for worker health checks |
|
||||||
|
|
||||||
|
Record the exact method signatures against the installed SDK before starting
|
||||||
|
A.2 — the proto field set in `OnAlarmTransitionEvent` must match the SDK's
|
||||||
|
actual payload.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency order
|
||||||
|
|
||||||
|
```
|
||||||
|
A.2 (worker: AlarmClient subscription)
|
||||||
|
└─► A.3 (gateway: dispatch OnAlarmTransition + AcknowledgeAlarm RPC handler)
|
||||||
|
└─► A.4 (gateway: QueryActiveAlarms RPC handler)
|
||||||
|
└─► lmxopcua B.2 (GalaxyDriver IAlarmSource live)
|
||||||
|
└─► C.1 (sidecar: AahClientManagedAlarmEventWriter live)
|
||||||
|
└─► D.1 (smoke artifact captured)
|
||||||
|
```
|
||||||
|
|
||||||
|
A.2 is the single blocking item. All subsequent items unblock serially once
|
||||||
|
A.2 delivers alarm events through the channel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Item A.2 — Worker: subscribe to MxAccess alarm event source
|
||||||
|
|
||||||
|
**Repo**: `mxaccessgw` — `src\MxGateway.Worker\` (net48, x86)
|
||||||
|
|
||||||
|
**What it needs**:
|
||||||
|
|
||||||
|
The worker must subscribe to AVEVA's alarm events and fan them into the same
|
||||||
|
bounded channel the data-change pump uses, translating each MxAccess alarm
|
||||||
|
event into a `WorkerEvent` proto with family `MX_EVENT_FAMILY_ON_ALARM_TRANSITION`
|
||||||
|
(defined in PR A.1, already merged).
|
||||||
|
|
||||||
|
**Architectural choice determines the implementation path**:
|
||||||
|
|
||||||
|
**Option X1 — aaAlarmManagedClient in a new x64 alarm-helper process**
|
||||||
|
|
||||||
|
Add a second worker-mode sub-process (`MxGateway.AlarmWorker`, net8.0 x64)
|
||||||
|
alongside the existing x86 worker. The AlarmWorker:
|
||||||
|
|
||||||
|
1. Loads `aaAlarmManagedClient.dll` (x64) on startup.
|
||||||
|
2. Calls `AlarmClient.RegisterConsumer` with a `WorkerAlarmConsumer` sink.
|
||||||
|
3. Calls `AlarmClient.Subscribe` with a session-level filter (all alarms for
|
||||||
|
the session's Galaxy scope).
|
||||||
|
4. Translates each `IAlarmConsumer.OnAlarmEvent` callback into a protobuf
|
||||||
|
`WorkerEvent` (family `ON_ALARM_TRANSITION`) and writes it to an IPC
|
||||||
|
channel readable by the gateway server-side multiplexer.
|
||||||
|
5. Handles session lifecycle: re-subscribes after reconnect; unsubscribes on
|
||||||
|
session close.
|
||||||
|
|
||||||
|
IPC from AlarmWorker to gateway: simplest option is a named pipe or an
|
||||||
|
in-process queue if the AlarmWorker is hosted in the same gateway process
|
||||||
|
space as a separate `IHostedService`.
|
||||||
|
|
||||||
|
**Option X2 — Accept sub-attribute fallback as production (no A.2 work)**
|
||||||
|
|
||||||
|
If the architectural decision is to accept the sub-attribute path as permanent:
|
||||||
|
|
||||||
|
- `MxAccessAlarmEventSink.Attach()` in the worker remains a no-op (as
|
||||||
|
currently coded with the architectural comment).
|
||||||
|
- The `MX_EVENT_FAMILY_ON_ALARM_TRANSITION` proto family stays defined but
|
||||||
|
the gateway never emits events on it.
|
||||||
|
- lmxopcua's `GalaxyDriver` does not implement `IAlarmSource` for the
|
||||||
|
native path; the value-driven sub-attribute path remains the production
|
||||||
|
path.
|
||||||
|
- The only regression vs. v1 is operator-comment fidelity on Galaxy alarms.
|
||||||
|
- C.1 is still needed if scripted-alarm historian write-back is required.
|
||||||
|
|
||||||
|
**What blocks it**: the architectural decision above. Once made, A.2 becomes
|
||||||
|
a 2–3 day implementation task (sub-process plumbing + proto translation +
|
||||||
|
unit tests for the consumer sink cancellation behaviour).
|
||||||
|
|
||||||
|
**Tests to write (when A.2 proceeds)**:
|
||||||
|
|
||||||
|
- `WorkerAlarmConsumerTests` — fake `IAlarmConsumer` source emits canned
|
||||||
|
transitions; assert each produces the correct `WorkerEvent` body shape.
|
||||||
|
- Cancellation/session-close test — closing the session unsubscribes from
|
||||||
|
the AlarmClient cleanly (no leaked `IAlarmConsumer` reference if the
|
||||||
|
worker is recycled mid-session).
|
||||||
|
- Re-subscribe-after-reconnect test — `ReconnectSupervisor` triggers a
|
||||||
|
reconnect; assert the alarm consumer re-attaches to the new session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Item A.3 / A.4 — Gateway: dispatch and RPC handlers
|
||||||
|
|
||||||
|
**Repo**: `mxaccessgw` — `src\MxGateway.Server\`
|
||||||
|
|
||||||
|
**Depends on**: A.2 delivering `WorkerEvent` bodies with family
|
||||||
|
`MX_EVENT_FAMILY_ON_ALARM_TRANSITION`.
|
||||||
|
|
||||||
|
**What it needs**:
|
||||||
|
|
||||||
|
### A.3 — Dispatch + AcknowledgeAlarm
|
||||||
|
|
||||||
|
1. The session-level event multiplexer (`Sessions\SessionEventStream.cs` or
|
||||||
|
equivalent — verify name in the mxaccessgw repo) must recognise the new
|
||||||
|
`WorkerEvent` body and forward it as an `MxEvent` with family
|
||||||
|
`MX_EVENT_FAMILY_ON_ALARM_TRANSITION` to every `StreamEvents` subscriber
|
||||||
|
for that session.
|
||||||
|
|
||||||
|
2. New RPC handler `AcknowledgeAlarm` builds an `AlarmAcknowledgeCommand`
|
||||||
|
worker command and forwards it to the alarm-helper process (Option X1) or
|
||||||
|
the worker's MxAccess session (Option X2 if MxAccess exposes ack). Maps
|
||||||
|
the reply status to `AcknowledgeAlarmReply.MxStatusProxy`.
|
||||||
|
|
||||||
|
3. Authorization: new API scope `invoke:alarm-ack` on the API key. Keys
|
||||||
|
without it receive `PERMISSION_DENIED`. Follow the existing scope-check
|
||||||
|
pattern used by `invoke:write`.
|
||||||
|
|
||||||
|
### A.4 — QueryActiveAlarms
|
||||||
|
|
||||||
|
1. New RPC handler `QueryActiveAlarms` calls `AlarmClient.GetAlarmExtendedRec`
|
||||||
|
(or `GetActiveAlarms` — confirm the method name during implementation)
|
||||||
|
on the alarm-helper process, batches results into `ActiveAlarmSnapshot`
|
||||||
|
proto messages, and streams them back to the caller.
|
||||||
|
|
||||||
|
2. New API scope `invoke:alarm-query` (separate from ack so read-only clients
|
||||||
|
can refresh without ack rights).
|
||||||
|
|
||||||
|
**What blocks A.3/A.4**: A.2 must deliver `WorkerEvent` bodies on the channel.
|
||||||
|
A.3/A.4 are pure dispatch wiring once the events arrive.
|
||||||
|
|
||||||
|
**Tests to write**:
|
||||||
|
|
||||||
|
- A.3 dispatch test — fake worker emits an `AlarmTransition` event; assert
|
||||||
|
the gateway forwards it on the `StreamEvents` channel of every subscribed
|
||||||
|
session (mirrors existing `OnDataChange` dispatch tests).
|
||||||
|
- A.3 AcknowledgeAlarm auth test — existing key without `invoke:alarm-ack`
|
||||||
|
scope returns `PERMISSION_DENIED`.
|
||||||
|
- A.4 pagination test — synthetic active-alarm set of 0 / 1 / 100 entries;
|
||||||
|
assert each streams back as separate `ActiveAlarmSnapshot` messages.
|
||||||
|
- Integration (parity rig — requires dev box with AVEVA platform):
|
||||||
|
trigger a real Galaxy alarm, call `QueryActiveAlarms`, assert the alarm
|
||||||
|
appears in the stream; call `AcknowledgeAlarm`, assert the alarm transitions
|
||||||
|
to `ActiveAcked` and a `Acknowledge` transition event appears on
|
||||||
|
`StreamEvents`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Item C.1 — Historian sidecar: AahClientManagedAlarmEventWriter
|
||||||
|
|
||||||
|
**Repo**: `lmxopcua` — `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\`
|
||||||
|
|
||||||
|
**Depends on**: Architectural decision (the sidecar uses `aahClientManaged`
|
||||||
|
x64, which is not bitness-constrained like the worker). C.1 is independently
|
||||||
|
unblockable from A.2 if the goal is to wire up the scripted-alarm historian
|
||||||
|
path.
|
||||||
|
|
||||||
|
**Current state**:
|
||||||
|
|
||||||
|
`SdkAlarmHistorianWriteBackend` in `src\MxGateway.Worker\MxAccess\` is a
|
||||||
|
placeholder returning `RetryPlease`. The lmxopcua sidecar's `WriteAlarmEvents`
|
||||||
|
IPC slot is defined in `Ipc\Contracts.cs` but `Program.cs` constructs
|
||||||
|
`HistorianFrameHandler` without an `alarmWriter` (line 57 per the alarms plan).
|
||||||
|
The `IAlarmEventWriter` interface exists; only the production implementation
|
||||||
|
and the consumer wiring are missing.
|
||||||
|
|
||||||
|
**What it needs**:
|
||||||
|
|
||||||
|
1. New `AahClientManagedAlarmEventWriter.cs` implementing `IAlarmEventWriter`
|
||||||
|
(defined in `Ipc\HistorianFrameHandler.cs`). Calls `aahClientManaged`'s
|
||||||
|
alarm-event write API — same path v1's `GalaxyHistorianWriter` used.
|
||||||
|
Uses `HistorianClusterEndpointPicker` for multi-node routing.
|
||||||
|
Maps `MxStatus` write outcomes to `HistorianWriteOutcome` enum
|
||||||
|
(Ack / PermanentFail / RetryPlease).
|
||||||
|
|
||||||
|
2. `Program.cs` — build `AahClientManagedAlarmEventWriter` next to the
|
||||||
|
existing `BuildHistorian()` call; pass it to `HistorianFrameHandler`.
|
||||||
|
Gate behind `OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED` env var (default `true`
|
||||||
|
when `OTOPCUA_HISTORIAN_ENABLED=true`).
|
||||||
|
|
||||||
|
3. `Install-Services.ps1` — add the new env var to the install-time block.
|
||||||
|
|
||||||
|
**What blocks C.1**: access to the `aahClientManaged` SDK on the dev box
|
||||||
|
(confirmed available per `project_aveva_platform_installed.md` — AVEVA
|
||||||
|
Historian SDK is present). C.1 can proceed without A.2 since the sidecar's
|
||||||
|
`aahClientManaged` is x64 and does not share the worker's x86 bitness
|
||||||
|
constraint.
|
||||||
|
|
||||||
|
**Tests to write**:
|
||||||
|
|
||||||
|
- Outcome-mapping table: every `MxStatus` on alarm-write → expected
|
||||||
|
`HistorianWriteOutcome`.
|
||||||
|
- Batch test: 1 / 100 / 1000 events through a fake `aahClientManaged`
|
||||||
|
writer; assert per-row outcome list parallel to input order.
|
||||||
|
- Cluster failover: primary Historian node returns `BadCommunicationError`;
|
||||||
|
picker rotates to secondary; eventual success.
|
||||||
|
- `Program.cs` seam: assert handler constructed with alarm writer when env
|
||||||
|
var enabled; without it when disabled.
|
||||||
|
- Live integration (parity rig): write a synthetic alarm event through the
|
||||||
|
IPC; query it back via `ReadEvents`; assert round-trip fidelity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Item D.1 — Smoke artifact
|
||||||
|
|
||||||
|
**Repo**: `lmxopcua` (deployment refresh) + `mxaccessgw` (rig verification)
|
||||||
|
|
||||||
|
**Depends on**: A.2, A.3, A.4, and C.1 all passing on the dev rig with a live
|
||||||
|
Galaxy and live Historian.
|
||||||
|
|
||||||
|
**Current state**: The deployment script `Refresh-Services.ps1` (task D.1) has
|
||||||
|
shipped as PR #417 (merged 2026-04-30). What was NOT captured at that time was
|
||||||
|
a smoke artifact — a log snippet or test output confirming that:
|
||||||
|
|
||||||
|
1. An alarm transition event from a live Galaxy alarm reaches lmxopcua's
|
||||||
|
`AlarmConditionService` via the new `IAlarmSource` path (not the fallback).
|
||||||
|
2. A scripted-alarm historian write-back reaches AVEVA Historian via the
|
||||||
|
sidecar `IAlarmEventWriter`.
|
||||||
|
|
||||||
|
**What it needs**:
|
||||||
|
|
||||||
|
Once A.2, A.3, C.1 are wired on the parity rig:
|
||||||
|
|
||||||
|
1. Deploy the updated mxaccessgw (with A.2 / A.3 / A.4 changes).
|
||||||
|
2. Deploy the updated sidecar (with C.1 changes).
|
||||||
|
3. Run `Refresh-Services.ps1` to confirm clean service restarts.
|
||||||
|
4. Trigger a Galaxy alarm (e.g. set an AnalogLimitAlarm attribute out of
|
||||||
|
range in Galaxy IDE).
|
||||||
|
5. Observe the lmxopcua OPC UA alarm surface via the Client CLI:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
alarms -u opc.tcp://localhost:4840 --subscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: the alarm condition appears on the OPC UA A&E surface within
|
||||||
|
2 × publishing interval.
|
||||||
|
|
||||||
|
6. Trigger a scripted alarm via the lmxopcua `ScriptedAlarmEngine`
|
||||||
|
(or an OPC UA method call if one is wired).
|
||||||
|
7. Confirm in the AVEVA Historian that the scripted alarm event is stored
|
||||||
|
(query via the Historian client or HistorianWatch tool).
|
||||||
|
|
||||||
|
8. Capture log snippets:
|
||||||
|
- mxaccessgw log: `[INF] AlarmTransition dispatched sessionId=<> alarmRef=<>`
|
||||||
|
- lmxopcua log: `[INF] AlarmConditionService: IAlarmSource event alarmRef=<> origin=Driver`
|
||||||
|
- Sidecar log: `[INF] AahClientManagedAlarmEventWriter: Wrote <n> alarm events`
|
||||||
|
|
||||||
|
9. Commit the log snippets as `docs/plans/alarms-d1-smoke-artifact.md`
|
||||||
|
(a new doc, not this one).
|
||||||
|
|
||||||
|
**What blocks D.1**: all of A.2, A.3, C.1, plus the operator decision on the
|
||||||
|
x64 alarm-helper architecture (or explicit acceptance of the sub-attribute
|
||||||
|
fallback as production).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary of blocks
|
||||||
|
|
||||||
|
| Item | Blocked by | Estimated effort once unblocked |
|
||||||
|
|------|-----------|--------------------------------|
|
||||||
|
| A.2 | Architectural decision (x64 alarm-helper vs. sub-attribute fallback as production) | 2–3 days implementation; 1 day tests |
|
||||||
|
| A.3 | A.2 delivering WorkerEvent bodies | 1–2 days |
|
||||||
|
| A.4 | A.2 (active-alarm query needs AlarmClient session) | 1 day |
|
||||||
|
| C.1 | aahClientManaged SDK access (available on dev box); NOT blocked by A.2 | 1–2 days |
|
||||||
|
| D.1 | A.2 + A.3 + C.1 all passing on parity rig | 0.5 day (smoke + artifact capture) |
|
||||||
|
|
||||||
|
C.1 can proceed in parallel with A.2 / A.3 since the sidecar's `aahClientManaged`
|
||||||
|
is x64 and does not share the worker bitness constraint.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this plan does NOT cover
|
||||||
|
|
||||||
|
- The value-driven sub-attribute fallback path — already shipped and
|
||||||
|
functional (not being changed).
|
||||||
|
- Track B (lmxopcua EventPump, GalaxyDriver IAlarmSource re-implementation)
|
||||||
|
and Track E (client SDK surface refresh) from the alarms-over-gateway plan —
|
||||||
|
those are in `lmxopcua` and depend on A.3 being live; they follow naturally
|
||||||
|
once A.3 ships.
|
||||||
|
- Galaxy-native alarm historian path — System Platform's own `HistorizeToAveva`
|
||||||
|
toggle on the Galaxy template; not in scope.
|
||||||
|
- Alarm ACL / role-grant surface — already shipped in Phase 6.2.
|
||||||
@@ -0,0 +1,497 @@
|
|||||||
|
# Live-Hardware Driver Validation Runbooks
|
||||||
|
|
||||||
|
> **Scope**: These runbooks cover the three driver validation tasks that
|
||||||
|
> require physical hardware or a hardware-equivalent live environment and
|
||||||
|
> cannot be satisfied by the Docker-based simulator fixtures or unit tests
|
||||||
|
> alone.
|
||||||
|
>
|
||||||
|
> Driver implementation is complete. The runbooks document the preconditions,
|
||||||
|
> step-by-step procedure, expected results, and how to record the outcome for
|
||||||
|
> each driver that has an open live-hardware gap.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. FANUC FOCAS — Live CNC Smoke (task #54)
|
||||||
|
|
||||||
|
### Background
|
||||||
|
|
||||||
|
The FOCAS driver (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/`) uses the
|
||||||
|
pure-managed `WireFocasClient` that speaks FOCAS2 over TCP directly (no
|
||||||
|
`Fwlib64.dll`, no P/Invoke). The integration test suite at
|
||||||
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/` runs against
|
||||||
|
the `focas-mock` Python server (PDU-verified against `fwlibe64.dll` upstream)
|
||||||
|
and covers all call-shapes the driver issues. What the mock cannot cover:
|
||||||
|
|
||||||
|
- Series-specific firmware quirks (e.g. 0i-F vs 30i-B parameter range limits)
|
||||||
|
- Real CNC Ethernet stack behaviour (TCP keep-alive, session-close edge cases)
|
||||||
|
- Series gating: some driver nodes are conditionally emitted based on
|
||||||
|
`CncSeries` — only a physical CNC can confirm the suppression works
|
||||||
|
|
||||||
|
### Preconditions
|
||||||
|
|
||||||
|
| Item | Requirement |
|
||||||
|
|------|-------------|
|
||||||
|
| CNC hardware | FANUC CNC with Ethernet option enabled; TCP port 8193 reachable from the dev box or from the host running OtOpcUa |
|
||||||
|
| CNC series | Any of: 0i-D, 0i-F, 0i-MF, 0i-TF, 16i, 30i-B, 31i, 32i, Power Motion i |
|
||||||
|
| CNC state | Running state (not E-stop, not alarm) for live axis-data reads |
|
||||||
|
| Network | TCP reachability from OtOpcUa server host to CNC port 8193 |
|
||||||
|
| OtOpcUa | Server built and deployed (`dotnet publish` or running via `dotnet run`) |
|
||||||
|
| Config | DriverInstance row for FOCAS in Config DB (`Type="FOCAS"`, `Backend="wire"`, `Devices[0].HostAddress="focas://<cnc-ip>:8193"`, `Devices[0].Series="<series>"`) |
|
||||||
|
|
||||||
|
### Procedure
|
||||||
|
|
||||||
|
**Step 1 — Verify TCP reachability**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Test-NetConnection -ComputerName <cnc-ip> -Port 8193
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: `TcpTestSucceeded: True`.
|
||||||
|
|
||||||
|
**Step 2 — Start OtOpcUa with FOCAS driver configured**
|
||||||
|
|
||||||
|
Ensure the Config DB has the DriverInstance row. Start the server:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
sc start OtOpcUa
|
||||||
|
# or for a dev run:
|
||||||
|
dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server
|
||||||
|
```
|
||||||
|
|
||||||
|
Watch the Serilog log for:
|
||||||
|
|
||||||
|
```
|
||||||
|
[INF] FocasDriver initializing device focas://<cnc-ip>:8193 series=<series>
|
||||||
|
[INF] FocasDriver device <cnc-ip>:8193 Connected
|
||||||
|
```
|
||||||
|
|
||||||
|
If `EW_SOCKET (-1)` appears, the TCP endpoint is unreachable or the CNC
|
||||||
|
Ethernet option is not active.
|
||||||
|
|
||||||
|
**Step 3 — Browse the address space**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
browse -u opc.tcp://localhost:4840 -r -d 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: a node tree containing at minimum:
|
||||||
|
|
||||||
|
```
|
||||||
|
FOCAS/
|
||||||
|
<device>/
|
||||||
|
Identity/
|
||||||
|
SeriesNumber
|
||||||
|
Version
|
||||||
|
MaxAxes
|
||||||
|
Status/
|
||||||
|
RunState
|
||||||
|
Mode
|
||||||
|
EmergencyStop
|
||||||
|
Axes/
|
||||||
|
<X|Y|Z>/
|
||||||
|
AbsolutePosition
|
||||||
|
MachinePosition
|
||||||
|
```
|
||||||
|
|
||||||
|
Nodes suppressed by the `Series` capability gate will be absent — this is
|
||||||
|
correct behaviour.
|
||||||
|
|
||||||
|
**Step 4 — Read identity nodes**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
read -u opc.tcp://localhost:4840 -n "ns=2;s=FOCAS/<device>/Identity/SeriesNumber"
|
||||||
|
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
read -u opc.tcp://localhost:4840 -n "ns=2;s=FOCAS/<device>/Identity/MaxAxes"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: `Good` quality; `SeriesNumber` matches the string printed on the CNC
|
||||||
|
control panel (e.g. `"0i-F"`); `MaxAxes` is a non-zero integer.
|
||||||
|
|
||||||
|
**Step 5 — Read live status and axis data**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
read -u opc.tcp://localhost:4840 -n "ns=2;s=FOCAS/<device>/Status/RunState"
|
||||||
|
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
read -u opc.tcp://localhost:4840 -n "ns=2;s=FOCAS/<device>/Axes/X/AbsolutePosition"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: both return `Good` quality. `AbsolutePosition` is a `Double` (e.g.
|
||||||
|
`-12.3456` mm). Manually compare against the machine's position display.
|
||||||
|
|
||||||
|
**Step 6 — Subscribe and observe polling**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
subscribe -u opc.tcp://localhost:4840 `
|
||||||
|
-n "ns=2;s=FOCAS/<device>/Status/RunState" -i 500
|
||||||
|
```
|
||||||
|
|
||||||
|
Let run for 30 s while jogging an axis or changing mode on the CNC operator
|
||||||
|
panel. Pass: at least one data-change event received within 5 s; events
|
||||||
|
continue arriving every ~500 ms.
|
||||||
|
|
||||||
|
**Step 7 — 2-minute soak**
|
||||||
|
|
||||||
|
Let the server run for 2 minutes with the subscription active. Pass: no
|
||||||
|
`EW_SOCKET`, `EW_HANDLE`, `EW_BUSY` errors in the Serilog output; subscribed
|
||||||
|
node continues delivering updates.
|
||||||
|
|
||||||
|
**Step 8 — Run the FOCAS e2e script**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pwsh scripts/e2e/test-focas.ps1 -ServerUrl opc.tcp://localhost:4840 `
|
||||||
|
-DriverInstance "<device>" -Series "<series>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: script exits 0.
|
||||||
|
|
||||||
|
### Expected results
|
||||||
|
|
||||||
|
| Check | Expected |
|
||||||
|
|-------|----------|
|
||||||
|
| TCP connect to CNC port 8193 | Success |
|
||||||
|
| FOCAS session open (`cnc_allclibhndl3`) | EW_OK (0) in driver log |
|
||||||
|
| `Identity/SeriesNumber` | Matches CNC panel, `Good` quality |
|
||||||
|
| `Identity/MaxAxes` | Non-zero integer, `Good` quality |
|
||||||
|
| `Status/RunState` | Integer 0–3, `Good` quality |
|
||||||
|
| `Axes/X/AbsolutePosition` | Double, `Good` quality, matches display |
|
||||||
|
| Subscribe: events delivered | >= 3 events in 5 s soak |
|
||||||
|
| 2-minute soak: no FOCAS errors | Clean Serilog log |
|
||||||
|
|
||||||
|
### Recording the outcome
|
||||||
|
|
||||||
|
```
|
||||||
|
FOCAS live-CNC smoke — task #54
|
||||||
|
Date: YYYY-MM-DD
|
||||||
|
CNC: <manufacturer> <model> series=<series> firmware=<version>
|
||||||
|
IP: <cnc-ip>:8193
|
||||||
|
OtOpcUa SHA: <git sha>
|
||||||
|
|
||||||
|
TCP connect: PASS
|
||||||
|
Session open: PASS
|
||||||
|
Identity reads: PASS SeriesNumber="<>" MaxAxes=<n>
|
||||||
|
Status read: PASS RunState=<n>
|
||||||
|
Axis read: PASS X/AbsolutePosition=<value>
|
||||||
|
Subscribe: PASS <n> events in 30s
|
||||||
|
2-min soak: PASS no errors
|
||||||
|
e2e script: PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Allen-Bradley CIP — Live Boot (ControlLogix)
|
||||||
|
|
||||||
|
### Background
|
||||||
|
|
||||||
|
The AB CIP driver (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/`) uses
|
||||||
|
`libplctag` 1.6.x. The Docker `ab_server` simulator covers connectivity and
|
||||||
|
atomic type reads (7 integration tests). Live-boot validation is needed to
|
||||||
|
confirm UDT shape-reading, array tag access, and the CIP packing behaviour on
|
||||||
|
a real ControlLogix backplane — all gaps acknowledged in
|
||||||
|
`docs/drivers/AbServer-Test-Fixture.md`.
|
||||||
|
|
||||||
|
AB CIP live-boot was first verified against a ControlLogix rig at PR #222.
|
||||||
|
Continue running before each release.
|
||||||
|
|
||||||
|
### Preconditions
|
||||||
|
|
||||||
|
| Item | Requirement |
|
||||||
|
|------|-------------|
|
||||||
|
| PLC hardware | ControlLogix (preferred) or CompactLogix; firmware 20+ for request packing |
|
||||||
|
| Network | TCP port 44818 reachable from OtOpcUa server host |
|
||||||
|
| PLC state | Running; at least one DINT / REAL / BOOL / STRING controller-scoped tag defined |
|
||||||
|
| OtOpcUa | Server built and deployed |
|
||||||
|
| Config | DriverInstance row: `Type="AbCip"`, `Host="<plc-ip>"`, `Path="1,0"`, `PlcType="ControlLogix"` |
|
||||||
|
|
||||||
|
### Procedure
|
||||||
|
|
||||||
|
**Step 1 — Verify TCP reachability**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Test-NetConnection -ComputerName <plc-ip> -Port 44818
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: `TcpTestSucceeded: True`.
|
||||||
|
|
||||||
|
**Step 2 — Start OtOpcUa and watch driver log**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
sc start OtOpcUa
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for:
|
||||||
|
|
||||||
|
```
|
||||||
|
[INF] AbCipDriver device <plc-ip> Connected path=1,0 plcType=ControlLogix
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3 — Browse the address space**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
browse -u opc.tcp://localhost:4840 -r -d 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: node tree shows the tags defined in the ControlLogix project (controller-
|
||||||
|
and program-scoped). UDT members appear as child nodes.
|
||||||
|
|
||||||
|
**Step 4 — Read atomic tags**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Read a DINT tag
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
read -u opc.tcp://localhost:4840 -n "ns=2;s=AbCip/<device>/<TagName>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: `Good` quality; value type matches the PLC tag type.
|
||||||
|
|
||||||
|
**Step 5 — Read a UDT member**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
read -u opc.tcp://localhost:4840 -n "ns=2;s=AbCip/<device>/<UDT>/<MemberName>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: `Good` quality; value matches the live PLC value.
|
||||||
|
|
||||||
|
**Step 6 — Write a DINT tag (if in ReadWrite mode)**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
write -u opc.tcp://localhost:4840 `
|
||||||
|
-n "ns=2;s=AbCip/<device>/<TagName>" -v 42 -t Int32
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the new value via a subsequent read or on the PLC HMI.
|
||||||
|
|
||||||
|
Pass: read back returns 42 with `Good` quality.
|
||||||
|
|
||||||
|
**Step 7 — Subscribe to a tag that changes**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
subscribe -u opc.tcp://localhost:4840 `
|
||||||
|
-n "ns=2;s=AbCip/<device>/<ChangingTag>" -i 500
|
||||||
|
```
|
||||||
|
|
||||||
|
Jog or trigger a value change on the PLC. Pass: events received within 2 s.
|
||||||
|
|
||||||
|
**Step 8 — Override endpoint to docker sim and confirm parity**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:AB_SERVER_ENDPOINT = "<plc-ip>:44818"
|
||||||
|
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests `
|
||||||
|
--filter "AbServerFact"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: all 7 integration tests pass against the live PLC.
|
||||||
|
|
||||||
|
### Expected results
|
||||||
|
|
||||||
|
| Check | Expected |
|
||||||
|
|-------|----------|
|
||||||
|
| TCP connect | Success |
|
||||||
|
| Driver log `Connected` | Present, no error |
|
||||||
|
| Browse | Node tree mirrors PLC tag list |
|
||||||
|
| Atomic read | `Good` quality, correct type |
|
||||||
|
| UDT member read | `Good` quality, correct value |
|
||||||
|
| Write round-trip | Written value reads back |
|
||||||
|
| Subscribe | Events delivered on value change |
|
||||||
|
| Integration tests with live PLC | 7/7 pass |
|
||||||
|
|
||||||
|
### Recording the outcome
|
||||||
|
|
||||||
|
```
|
||||||
|
AB CIP live-boot
|
||||||
|
Date: YYYY-MM-DD
|
||||||
|
PLC: Allen-Bradley <model> firmware=<version>
|
||||||
|
IP: <plc-ip>:44818 path=1,0
|
||||||
|
OtOpcUa SHA: <git sha>
|
||||||
|
|
||||||
|
TCP connect: PASS
|
||||||
|
Driver connected: PASS
|
||||||
|
Browse: PASS <n> tags visible
|
||||||
|
Atomic read: PASS
|
||||||
|
UDT read: PASS
|
||||||
|
Write round-trip: PASS
|
||||||
|
Subscribe: PASS
|
||||||
|
Integration tests: 7/7 PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Beckhoff TwinCAT — Wire-Live Validation
|
||||||
|
|
||||||
|
### Background
|
||||||
|
|
||||||
|
The TwinCAT driver (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/`) uses the
|
||||||
|
Beckhoff `TwinCAT.Ads` .NET SDK v6. The integration test suite at
|
||||||
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/`
|
||||||
|
(`TwinCAT3SmokeTests.cs`) covers 14 `[TwinCATFact]` methods + one 16-case
|
||||||
|
`[TwinCATTheory]` (30 cases total) against a live ADS runtime. The TCBSD ESXi
|
||||||
|
VM at `10.100.0.128` (AmsNetId `41.169.163.43.1.1`) is the primary fixture
|
||||||
|
runtime (project memory `project_tcbsd_fixture.md`) and bypasses the
|
||||||
|
TwinCAT/Hyper-V conflict on the dev box.
|
||||||
|
|
||||||
|
Live-hardware validation extends beyond the TCBSD VM to confirm the driver
|
||||||
|
works against a production PLC (not just the ESXi test VM) and that the three
|
||||||
|
defects found during original integration testing do not regress on newer
|
||||||
|
firmware:
|
||||||
|
|
||||||
|
1. Notification cycle time unit (250 ms was being set to ~41 min — fixed).
|
||||||
|
2. `STRING(N)` / `WSTRING(N)` type mapper (fixed).
|
||||||
|
3. Bit-indexed BOOL path (fixed).
|
||||||
|
|
||||||
|
### Preconditions
|
||||||
|
|
||||||
|
**TCBSD ESXi fixture (primary — no physical hardware needed)**
|
||||||
|
|
||||||
|
| Item | Requirement |
|
||||||
|
|------|-------------|
|
||||||
|
| TCBSD VM | Running on ESXi at `10.100.0.128` |
|
||||||
|
| AMS Net ID | `41.169.163.43.1.1` |
|
||||||
|
| ADS port | `851` (TwinCAT 3 PLC runtime 1) |
|
||||||
|
| PLC project | TwinCAT project from `tests/.../TwinCatProject/` loaded and in Run state |
|
||||||
|
| Network | TCP port 48898 reachable from dev box to `10.100.0.128` |
|
||||||
|
|
||||||
|
**Production PLC (for true wire-live validation)**
|
||||||
|
|
||||||
|
| Item | Requirement |
|
||||||
|
|------|-------------|
|
||||||
|
| TwinCAT hardware | Beckhoff IPC or CX series, TwinCAT 3 (TC3); TC2 is a known gap per fixture doc |
|
||||||
|
| AMS route | Route configured on TwinCAT device back to the OtOpcUa host |
|
||||||
|
| PLC state | Run state |
|
||||||
|
| GVL | At least a `GVL_Fixture.nCounter` DINT and `GVL_Fixture.rSetpoint` REAL present |
|
||||||
|
|
||||||
|
### Procedure — TCBSD ESXi fixture
|
||||||
|
|
||||||
|
**Step 1 — Verify TCBSD VM is reachable**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Test-NetConnection -ComputerName 10.100.0.128 -Port 48898
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: `TcpTestSucceeded: True`.
|
||||||
|
|
||||||
|
**Step 2 — Run the integration test suite**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:TWINCAT_TARGET_HOST = "10.100.0.128"
|
||||||
|
$env:TWINCAT_TARGET_NETID = "41.169.163.43.1.1"
|
||||||
|
|
||||||
|
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests `
|
||||||
|
--logger "console;verbosity=normal"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: all 30 test cases pass (14 `[TwinCATFact]` + 16-case `[TwinCATTheory]`).
|
||||||
|
No `[TwinCATFact]` / `[TwinCATTheory]` skips — the env var is set, so the
|
||||||
|
runtime probe is expected to succeed.
|
||||||
|
|
||||||
|
Key tests to watch:
|
||||||
|
|
||||||
|
| Test | Validates |
|
||||||
|
|------|-----------|
|
||||||
|
| `Driver_subscribe_receives_native_ADS_notifications_on_counter_changes` | Native ADS notification path (the cycle-time-unit bug regression) |
|
||||||
|
| `Driver_reads_every_primitive_type_with_correct_mapping` | 16-type theory incl. `STRING(N)` |
|
||||||
|
| `Driver_reads_bit_indexed_BOOL_from_word` | Bit-indexed BOOL fix regression |
|
||||||
|
| `Driver_auto_reconnects_after_underlying_client_is_disposed` | Reconnect on ADS client dispose |
|
||||||
|
| `Driver_routes_reads_per_device_and_isolates_unreachable_peers` | Multi-device isolation |
|
||||||
|
|
||||||
|
**Step 3 — OtOpcUa server browse/read via Client CLI**
|
||||||
|
|
||||||
|
Start OtOpcUa with a TwinCAT DriverInstance pointing at the TCBSD VM:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# appsettings.json DriverInstance: Type=TwinCAT, AmsNetId=41.169.163.43.1.1, AmsPort=851
|
||||||
|
sc start OtOpcUa
|
||||||
|
# or dev run
|
||||||
|
dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
browse -u opc.tcp://localhost:4840 -r -d 4
|
||||||
|
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
read -u opc.tcp://localhost:4840 -n "ns=2;s=TwinCAT/<device>/GVL_Fixture/nCounter"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: browse shows the PLC symbol tree; read returns `Good` quality with an
|
||||||
|
integer value.
|
||||||
|
|
||||||
|
### Procedure — Production PLC (optional, for full wire-live signoff)
|
||||||
|
|
||||||
|
If a Beckhoff production IPC is available in the lab:
|
||||||
|
|
||||||
|
**Step 1** — Configure the AMS route on the TwinCAT device (TwinCAT System
|
||||||
|
Manager → Routes → Add static route from the TwinCAT device back to the
|
||||||
|
OtOpcUa server machine).
|
||||||
|
|
||||||
|
**Step 2** — Set env vars and run the integration suite against the production
|
||||||
|
target:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:TWINCAT_TARGET_HOST = "<production-plc-ip>"
|
||||||
|
$env:TWINCAT_TARGET_NETID = "<production-ams-net-id>"
|
||||||
|
$env:TWINCAT_TARGET_PORT = "851"
|
||||||
|
|
||||||
|
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3** — Subscribe to a counter tag for 30 s to confirm native
|
||||||
|
notifications arrive:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
subscribe -u opc.tcp://localhost:4840 `
|
||||||
|
-n "ns=2;s=TwinCAT/<device>/GVL_Fixture/nCounter" -i 100
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: events arrive every ~100 ms driven by the PLC's ADS notification, not
|
||||||
|
by polling.
|
||||||
|
|
||||||
|
### Expected results
|
||||||
|
|
||||||
|
| Check | TCBSD VM | Production PLC |
|
||||||
|
|-------|----------|----------------|
|
||||||
|
| ADS port 48898 reachable | Required | Required |
|
||||||
|
| Integration tests: all 30 pass | Required | Optional (same 30) |
|
||||||
|
| Notification cycle-time test passes | Required | Required |
|
||||||
|
| Server browse shows symbol tree | Required | Optional |
|
||||||
|
| Read `Good` quality | Required | Optional |
|
||||||
|
| Native ADS notifications deliver in subscribe | Required | Recommended |
|
||||||
|
|
||||||
|
### Known gaps (documented — not blockers for v2 GA)
|
||||||
|
|
||||||
|
Per `docs/drivers/TwinCAT-Test-Fixture.md` §"What it does NOT cover":
|
||||||
|
|
||||||
|
- Multi-hop AMS routing — single-hop only.
|
||||||
|
- TC2 (ADS v1) compatibility — TC3 only.
|
||||||
|
- Notification coalescing under sustained CPU load.
|
||||||
|
- `Symbol version changed (0x0702)` storm handling under rapid PLC re-downloads.
|
||||||
|
|
||||||
|
These are deferred to v3 per `docs/v3/twincat-backlog.md`.
|
||||||
|
|
||||||
|
### Recording the outcome
|
||||||
|
|
||||||
|
```
|
||||||
|
TwinCAT wire-live validation
|
||||||
|
Date: YYYY-MM-DD
|
||||||
|
Target: TCBSD VM 10.100.0.128 AmsNetId=41.169.163.43.1.1 (and/or production PLC details)
|
||||||
|
TwinCAT version: <version>
|
||||||
|
OtOpcUa SHA: <git sha>
|
||||||
|
|
||||||
|
ADS port reachable: PASS
|
||||||
|
Integration tests: 30/30 PASS
|
||||||
|
notification-cycle-time test: PASS (regression check)
|
||||||
|
STRING(N) type test: PASS (regression check)
|
||||||
|
bit-indexed BOOL test: PASS (regression check)
|
||||||
|
Server browse: PASS
|
||||||
|
Read Good quality: PASS
|
||||||
|
Native subscription delivery: PASS <n> events in 30s
|
||||||
|
```
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
# Phase 6.3 Redundancy — Client Interop Matrix and Cutover Validation Plan
|
||||||
|
|
||||||
|
> **Scope**: Phase 6.3 redundancy runtime core shipped (PRs #89-90, #98-99,
|
||||||
|
> #24-peerprobe, Stream C node wiring, Stream D lease wrap). What remains is
|
||||||
|
> Stream F (task #150): validating that third-party OPC UA clients honour
|
||||||
|
> our `ServiceLevel` / `ServerUriArray` / `RedundancySupport` signals and
|
||||||
|
> fail over correctly when the Primary drops. This document defines what is
|
||||||
|
> automatable as integration tests, what requires two live instances plus a
|
||||||
|
> real client, and a step-by-step cutover-validation runbook.
|
||||||
|
>
|
||||||
|
> **Source of truth**: `docs/Redundancy.md`, `docs/v2/redundancy-interop-playbook.md`,
|
||||||
|
> `docs/v2/implementation/phase-6-3-redundancy-runtime.md`,
|
||||||
|
> `scripts/compliance/phase-6-3-compliance.ps1`.
|
||||||
|
|
||||||
|
## What is already tested (no live cluster needed)
|
||||||
|
|
||||||
|
The following are covered by existing automated tests that run in ordinary
|
||||||
|
`dotnet test`:
|
||||||
|
|
||||||
|
| Area | Test class(es) | What it asserts |
|
||||||
|
|---|---|---|
|
||||||
|
| `ServiceLevelCalculator` — 8-state matrix | `ServiceLevelCalculatorTests` | All 10 band values; role × self-health × peer-http × peer-ua × apply × recovery × topology combinations |
|
||||||
|
| `RecoveryStateManager` — dwell + witness | `RecoveryStateManagerTests` | 60 s dwell default; premature-exit rejection; witness-required gate |
|
||||||
|
| `ApplyLeaseRegistry` — lease lifecycle | `ApplyLeaseRegistryTests` | Disposal on success / exception / cancellation; watchdog force-close at 10 min |
|
||||||
|
| `ServerRedundancyNodeWriter` — OPC UA variable binding | `ServerRedundancyNodeWriterTests` | `ServiceLevel` byte push; `RedundancySupport` enum; `ServerUriArray` skip-log when node absent |
|
||||||
|
| `RedundancyStatePublisher` — orchestration | `RedundancyStatePublisherTests` | Edge-triggered `OnStateChanged`; idempotent dedup |
|
||||||
|
| `ClusterTopologyLoader` | `ClusterTopologyLoaderTests` | Two-node seed; one-node degenerate; duplicate-URI rejection |
|
||||||
|
| `DraftValidator.ValidateClusterTopology` | `DraftValidatorTests` (8 cases) | NodeCount/mode pairs; Enabled-count vs NodeCount; multiple-Primary rejection |
|
||||||
|
|
||||||
|
Run with:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet test ZB.MOM.WW.OtOpcUa.slnx --filter "FullyQualifiedName~Redundancy"
|
||||||
|
```
|
||||||
|
|
||||||
|
Compliance gate (every Phase 6.3 static check):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pwsh ./scripts/compliance/phase-6-3-compliance.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass criteria: exit 0; all `[PASS]` lines green; `[DEFERRED]` lines are
|
||||||
|
known-deferred surfaces, not failures.
|
||||||
|
|
||||||
|
## What cannot be automated — requires two live instances
|
||||||
|
|
||||||
|
The scenarios below require two running `OtOpcUa.Server` processes in the
|
||||||
|
same `ServerCluster`, a real SQL Server Config DB, and at least one driver
|
||||||
|
instance with a reachable endpoint (simulator or real PLC).
|
||||||
|
|
||||||
|
### Why it cannot be unit/integration-tested in-process
|
||||||
|
|
||||||
|
- UaExpert, Kepware KEPServerEX, and AVEVA OI Gateway are closed-source
|
||||||
|
Windows GUI binaries with no headless CLI interface for the
|
||||||
|
subscribe/browse flows.
|
||||||
|
- The AVEVA MXAccess failover leg (`IAlarmSource` reconnect, `$MxAccessClient`
|
||||||
|
quality transition) involves the Galaxy runtime's own client-redundancy
|
||||||
|
policy and the COM-layer session model — both live outside this repo.
|
||||||
|
- Even the automatable sub-set (our own `otopcua-cli` as the client) needs
|
||||||
|
two distinct listening TCP endpoints; that requires two live processes,
|
||||||
|
which is out of scope for `dotnet test` integration fixtures.
|
||||||
|
|
||||||
|
## Test matrix
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. Two `OtOpcUa.Server` processes on separate Windows hosts (or separate
|
||||||
|
ports on the same host for dev) sharing one Config DB (`ServerCluster`
|
||||||
|
with `NodeCount=2`, `RedundancyMode=Warm` or `Hot`).
|
||||||
|
2. Each node registered in `ClusterNode`:
|
||||||
|
- Node A: `RedundancyRole=Primary`, `ServiceLevelBase=255`,
|
||||||
|
`ApplicationUri=urn:node-a:OtOpcUa`
|
||||||
|
- Node B: `RedundancyRole=Secondary`, `ServiceLevelBase=100`,
|
||||||
|
`ApplicationUri=urn:node-b:OtOpcUa`
|
||||||
|
3. `PeerHttpProbeLoop` and `PeerUaProbeLoop` HostedServices running on both
|
||||||
|
nodes (registered via `AddHostedService<PeerHttpProbeLoop>` +
|
||||||
|
`AddHostedService<PeerUaProbeLoop>` in `Program.cs`).
|
||||||
|
4. At least one `DriverInstance` in the cluster with a reachable PLC or
|
||||||
|
simulator (e.g. Modbus sim at `10.100.0.35:5020`).
|
||||||
|
5. Client machine with UaExpert >= 1.7 installed.
|
||||||
|
6. Optional second client: Kepware KEPServerEX 6.x QuickClient or AVEVA
|
||||||
|
OI Gateway 2020R2+.
|
||||||
|
|
||||||
|
### Block A — OPC UA protocol signals (UaExpert, no failover yet)
|
||||||
|
|
||||||
|
| ID | Scenario | Procedure | Pass criterion | Automatable? |
|
||||||
|
|----|----------|-----------|----------------|--------------|
|
||||||
|
| A1 | ServiceLevel published on Primary | Connect UaExpert to Node A. Browse `Server/ServerStatus/ServiceLevel`. | Value = 255 (`AuthoritativePrimary`) | No — requires UaExpert GUI |
|
||||||
|
| A2 | ServiceLevel published on Backup | Connect UaExpert to Node B. Read same node. | Value = 100 (`AuthoritativeBackup`) | No |
|
||||||
|
| A3 | ServiceLevel updates when peer drops | Node A connected. Stop Node B (`sc stop OtOpcUa`). Watch `ServiceLevel` on Node A. | Transitions 255 → 230 (`IsolatedPrimary`) within ~6 s (3 × 2 s HTTP probe interval) | No |
|
||||||
|
| A4 | RedundancySupport | Browse `Server/ServerRedundancy/RedundancySupport` on either node. | Value = `Warm` or `Hot` matching the cluster `RedundancyMode` | No |
|
||||||
|
| A5 | ServerUriArray | Browse `Server/ServerRedundancy/ServerUriArray` on either node. | Array contains both `ApplicationUri` values; self listed first. Note: requires non-transparent redundancy-type upgrade (currently logs-and-skips — see known limitation A5 below). | No |
|
||||||
|
| A6 | Mid-apply ServiceLevel dip | Trigger a `sp_PublishGeneration` apply (via Admin UI draft → publish) while watching Node A `ServiceLevel`. | Drops to 200 (`PrimaryMidApply`) for the apply duration; returns to 255 after `RefreshAsync`. | No |
|
||||||
|
| A7 | Client.CLI reads correct ServiceLevel | `dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://<node-a>:4840 -n "i=2267"` | Prints current byte value matching expected band. | **Yes** — scriptable with the Client CLI |
|
||||||
|
| A8 | otopcua-cli failover reconnect | `dotnet run ... -- connect -u opc.tcp://<node-a>:4840 -F opc.tcp://<node-b>:4840` — then kill Node A. | CLI session reconnects to Node B within the session keep-alive timeout. | **Yes** — scriptable with the Client CLI |
|
||||||
|
|
||||||
|
### Block B — Third-party client failover
|
||||||
|
|
||||||
|
| ID | Scenario | Procedure | Pass criterion |
|
||||||
|
|----|----------|-----------|----------------|
|
||||||
|
| B1 | UaExpert picks Primary by ServiceLevel | Configure a Redundancy Group in UaExpert with both endpoint URLs. | Client connects to Node A (higher ServiceLevel) |
|
||||||
|
| B2 | UaExpert cuts over on Primary kill | Kill Node A `OtOpcUa` service. | Client session reconnects to Node B within UaExpert's reconnect timeout (default 5 s). Data-change monitored items resume. |
|
||||||
|
| B3 | UaExpert returns when Primary restores | Start Node A. Wait >= 60 s recovery dwell. | `ServiceLevel` on Node A progresses: 180 (`RecoveringPrimary`) → 255 (`AuthoritativePrimary`). UaExpert may or may not switch back (client-policy-dependent; both outcomes accepted). |
|
||||||
|
| B4 | Kepware QuickClient failover | Repeat B1–B3 with Kepware configured for the same two endpoints. | Same pass criteria; establishes no UaExpert-specific behaviour. |
|
||||||
|
| B5 | AVEVA OI Gateway | Configure OI Gateway OPC DA/UA client object against the cluster. Kill Primary. | OI Gateway data quality recovers within `ReconnectInterval` (default 20 s); no permanent data-loss alert. |
|
||||||
|
|
||||||
|
### Block C — Galaxy MXAccess failover
|
||||||
|
|
||||||
|
This block requires a running Galaxy and `$MxAccessClient` object (AVEVA
|
||||||
|
System Platform installed, Galaxy deployed on dev box — see project memory
|
||||||
|
`project_aveva_platform_installed.md`).
|
||||||
|
|
||||||
|
| ID | Scenario | Procedure | Pass criterion |
|
||||||
|
|----|----------|-----------|----------------|
|
||||||
|
| C1 | Galaxy binds to Primary on first connect | Bring cluster up. Start a Galaxy `$MxAccessClient` with both node URLs configured. | Galaxy reports `QUALITY = Good`; initial values stream from Node A. |
|
||||||
|
| C2 | Galaxy redirects on Primary drop | Stop Node A. | Galaxy `QUALITY` briefly goes `Uncertain`, then returns to `Good`; values continue streaming from Node B within MXAccess's `ReconnectInterval` (default 20 s). |
|
||||||
|
| C3 | Galaxy tolerates mid-apply dip | Trigger generation apply on Node A. | Galaxy remains bound — mid-apply dip (200) is advisory, not a session drop. No quality interruption. |
|
||||||
|
|
||||||
|
Note: A negative result on C1–C3 does not necessarily indicate an OtOpcUa
|
||||||
|
defect. Cross-check with Block A / B first to confirm our `ServiceLevel`
|
||||||
|
signal is correct before debugging the MXAccess client layer.
|
||||||
|
|
||||||
|
## Step-by-step cutover-validation runbook
|
||||||
|
|
||||||
|
This is the minimum procedure to satisfy the v2 GA exit criterion:
|
||||||
|
"Non-transparent redundancy cutover validated with at least one production
|
||||||
|
client (Ignition 8.3 recommended — see decision #85)."
|
||||||
|
|
||||||
|
### Step 1 — Provision the cluster
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# On the Config DB host, seed or verify cluster rows:
|
||||||
|
# ServerCluster: Id=<id>, Name="test-cluster", NodeCount=2, RedundancyMode=Warm
|
||||||
|
# ClusterNode A: NodeId="node-a", ClusterId=<id>, RedundancyRole=Primary,
|
||||||
|
# ServiceLevelBase=255, ApplicationUri="urn:node-a:OtOpcUa"
|
||||||
|
# ClusterNode B: NodeId="node-b", ClusterId=<id>, RedundancyRole=Secondary,
|
||||||
|
# ServiceLevelBase=100, ApplicationUri="urn:node-b:OtOpcUa"
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify uniqueness constraint: no two `ClusterNode` rows share the same
|
||||||
|
`ApplicationUri` (unique index on `ApplicationUri`).
|
||||||
|
|
||||||
|
### Step 2 — Start both server instances
|
||||||
|
|
||||||
|
On Node A host:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# appsettings.json: Node:NodeId = "node-a"
|
||||||
|
sc start OtOpcUa
|
||||||
|
```
|
||||||
|
|
||||||
|
On Node B host:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# appsettings.json: Node:NodeId = "node-b"
|
||||||
|
sc start OtOpcUa
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait 10 s for HostedServices to complete first probe cycle.
|
||||||
|
|
||||||
|
### Step 3 — Verify baseline ServiceLevel via Client CLI
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Node A should report 255
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
|
||||||
|
-u opc.tcp://<node-a-host>:4840 -n "i=2267"
|
||||||
|
|
||||||
|
# Node B should report 100
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
|
||||||
|
-u opc.tcp://<node-b-host>:4840 -n "i=2267"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: Node A = 255, Node B = 100.
|
||||||
|
|
||||||
|
### Step 4 — Verify ServerUriArray
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
|
||||||
|
-u opc.tcp://<node-a-host>:4840 -n "i=2271"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass: array returned contains both `ApplicationUri` strings. If
|
||||||
|
`ServerUriArray` node returns empty or an error, the non-transparent
|
||||||
|
redundancy-type upgrade follow-up is still pending (known limitation —
|
||||||
|
`ServerRedundancyNodeWriter.ApplyServerUriArray` logs-and-skips on the
|
||||||
|
base `ServerRedundancyState` object type).
|
||||||
|
|
||||||
|
### Step 5 — Execute Primary kill + failover (B2 scenario)
|
||||||
|
|
||||||
|
1. Connect UaExpert (or Kepware) Redundancy Group to both endpoints.
|
||||||
|
2. Confirm client is subscribed to at least one variable node.
|
||||||
|
3. Kill Node A: `sc stop OtOpcUa` on Node A host.
|
||||||
|
4. Observe:
|
||||||
|
- Node B `ServiceLevel` should transition: 100 (`AuthoritativeBackup`)
|
||||||
|
→ 80 (`IsolatedBackup`) within ~6 s.
|
||||||
|
- Client should reconnect to Node B and resume data-change events.
|
||||||
|
5. Record: time from kill to client reconnect; whether data gaps occurred.
|
||||||
|
|
||||||
|
### Step 6 — Verify Primary recovery (B3 scenario)
|
||||||
|
|
||||||
|
1. Restart Node A: `sc start OtOpcUa` on Node A host.
|
||||||
|
2. Observe Node A `ServiceLevel` progression:
|
||||||
|
- ~0 s: 1 (`NoData`) briefly while HostedServices start.
|
||||||
|
- Startup: 180 (`RecoveringPrimary`) — recovery dwell gate active.
|
||||||
|
- After >= 60 s dwell + one positive publish witness: 255 (`AuthoritativePrimary`).
|
||||||
|
3. Observe Node B:
|
||||||
|
- Returns to 100 (`AuthoritativeBackup`) once it sees Node A peer probe succeed.
|
||||||
|
4. Record dwell duration and whether the client (UaExpert/Kepware) switches back.
|
||||||
|
|
||||||
|
### Step 7 — Execute mid-apply dip (A6 scenario)
|
||||||
|
|
||||||
|
1. Via Admin UI, create a trivial draft change and publish.
|
||||||
|
2. Watch Node A `ServiceLevel` during apply.
|
||||||
|
3. Expected: drops to 200 (`PrimaryMidApply`) for the apply duration
|
||||||
|
(typically seconds); returns to 255 when `GenerationRefreshHostedService`
|
||||||
|
releases the lease.
|
||||||
|
|
||||||
|
### Step 8 — Record results
|
||||||
|
|
||||||
|
Copy the following block into a tracking doc:
|
||||||
|
|
||||||
|
```
|
||||||
|
Run date: YYYY-MM-DD
|
||||||
|
Release SHA: <git sha>
|
||||||
|
Cluster: <cluster-id> Primary: node-a Backup: node-b
|
||||||
|
Config DB: 10.100.0.35,14330
|
||||||
|
|
||||||
|
A1: [PASS/FAIL] evidence: <screenshot or CLI output>
|
||||||
|
A2: [PASS/FAIL]
|
||||||
|
A3: [PASS/FAIL] time-to-IsolatedPrimary: <N>s
|
||||||
|
A4: [PASS/FAIL]
|
||||||
|
A5: [PASS/FAIL/DEFERRED - ServerUriArray upgrade pending]
|
||||||
|
A6: [PASS/FAIL] mid-apply duration: <N>s
|
||||||
|
A7: [PASS/FAIL] CLI output attached
|
||||||
|
A8: [PASS/FAIL] CLI reconnect observed
|
||||||
|
B1: [PASS/FAIL]
|
||||||
|
B2: [PASS/FAIL] reconnect time: <N>s
|
||||||
|
B3: [PASS/FAIL] dwell observed: <N>s
|
||||||
|
B4: [PASS/FAIL] (Kepware)
|
||||||
|
B5: [PASS/FAIL] (OI Gateway — if available)
|
||||||
|
C1: [PASS/FAIL/SKIP - Galaxy not available]
|
||||||
|
C2: [PASS/FAIL/SKIP]
|
||||||
|
C3: [PASS/FAIL/SKIP]
|
||||||
|
```
|
||||||
|
|
||||||
|
One pass of every non-SKIP row is the v2 GA acceptance criterion.
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
### A5 — ServerUriArray node not yet writable
|
||||||
|
|
||||||
|
The OPC UA .NET Standard SDK's default `Server.ServerRedundancy` object is the
|
||||||
|
base `ServerRedundancyState`, which has no `ServerUriArray` child node.
|
||||||
|
`ServerRedundancyNodeWriter.ApplyServerUriArray` currently logs a warning and
|
||||||
|
skips. The operator obtains `ServerUriArray` by reading `ClusterNode` rows
|
||||||
|
directly until the non-transparent redundancy-type upgrade follow-up ships.
|
||||||
|
|
||||||
|
### Recovery dwell is 60 s by default
|
||||||
|
|
||||||
|
`RecoveryStateManager.DwellTime` defaults to `TimeSpan.FromSeconds(60)` in
|
||||||
|
`Program.cs`. Step 6 of the runbook will block for at least 60 s waiting for
|
||||||
|
Node A to return to `AuthoritativePrimary`. This is intentional per
|
||||||
|
decision #154 (thrash prevention) — do not lower it for the test run.
|
||||||
|
|
||||||
|
### IsolatedBackup (80) does not auto-promote
|
||||||
|
|
||||||
|
Per decision #154, the Backup at band 80 does not self-elevate. If the operator
|
||||||
|
needs authoritative service from Node B while Node A is down, they must write
|
||||||
|
`RedundancyRole=Primary` on the `ClusterNode` row for Node B and publish a
|
||||||
|
draft generation. The Admin UI `RedundancyTab` exposes this flow.
|
||||||
|
|
||||||
|
## Dependency on existing tests
|
||||||
|
|
||||||
|
The cutover runbook validates the end-to-end wire path. The math and edge cases
|
||||||
|
are already locked by the unit/integration tests enumerated in the first section.
|
||||||
|
A failing runbook step that contradicts a passing unit test indicates a
|
||||||
|
deployment configuration error or an SDK version mismatch — not a logic bug.
|
||||||
|
Check `PeerHttpProbeLoop` logs first (look for `PeerProbe` Serilog events).
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
# v2 GA Lab Gates Plan
|
||||||
|
|
||||||
|
> **Canonical tracker**: `docs/v2/v2-release-readiness.md` — all code-path
|
||||||
|
> release blockers are closed as of 2026-04-24. This document maps the
|
||||||
|
> remaining exit-criteria from that tracker to concrete commands, automation
|
||||||
|
> boundaries, operator procedures, and pass criteria.
|
||||||
|
>
|
||||||
|
> **Status**: RELEASE-READY (code-path). Manual/lab gates remain open.
|
||||||
|
|
||||||
|
## The gate list
|
||||||
|
|
||||||
|
From `docs/v2/v2-release-readiness.md` §"Release-readiness exit criteria":
|
||||||
|
|
||||||
|
| # | Gate | Kind | Automatable here |
|
||||||
|
|---|------|------|-----------------|
|
||||||
|
| G1 | All four Phase 6.N compliance scripts exit 0 | Script | Yes — run on this box |
|
||||||
|
| G2 | `dotnet test ZB.MOM.WW.OtOpcUa.slnx` passes with <= 1 known-flake failure | Script | Yes — run on this box |
|
||||||
|
| G3 | Release blockers closed | Audit | Already closed (code-path) |
|
||||||
|
| G4 | Phase 5 driver complement shipped | Audit | Already closed |
|
||||||
|
| G5 | Production deployment checklist signed off by Fleet Admin | Operator | No — separate doc, human signoff |
|
||||||
|
| G6 | At least one end-to-end integration run against live Galaxy succeeds | Dev rig | No — requires AVEVA platform |
|
||||||
|
| G7 | FOCAS live-CNC wire-level smoke (#54) passes against a real FANUC control | Lab hardware | No — requires FANUC CNC |
|
||||||
|
| G8 | OPC UA CTT / UA Compliance Test Tool passes against the live endpoint | Operator tool | No — requires CTT binary + live endpoint |
|
||||||
|
| G9 | Non-transparent redundancy cutover validated with >= 1 production client | Lab | No — see `docs/plans/phase-6-3-redundancy-interop-plan.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G1 — Phase 6 compliance scripts
|
||||||
|
|
||||||
|
### Command
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pwsh ./scripts/compliance/phase-6-all.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
This meta-runner at `scripts/compliance/phase-6-all.ps1` invokes each
|
||||||
|
sub-script in a separate `powershell.exe` process to isolate exit codes:
|
||||||
|
|
||||||
|
| Sub-script | Phase | What it checks |
|
||||||
|
|-----------|-------|---------------|
|
||||||
|
| `phase-6-1-compliance.ps1` | 6.1 Resilience & Observability | Polly resilience classes, health endpoints, LiteDB sealed cache, observability sinks |
|
||||||
|
| `phase-6-2-compliance.ps1` | 6.2 Authorization runtime | `AuthorizationGate`, `TriePermissionEvaluator`, `NodeScopeResolver`, dispatch wiring in `DriverNodeManager` |
|
||||||
|
| `phase-6-3-compliance.ps1` | 6.3 Redundancy runtime | `ServiceLevelCalculator` 8-state band values, `RecoveryStateManager`, `ApplyLeaseRegistry`, `ServerRedundancyNodeWriter`; also invokes `dotnet test` with a baseline of 1097 |
|
||||||
|
| `phase-6-4-compliance.ps1` | 6.4 Admin UI completion | Data-layer types, Identification folder, deferred Blazor items marked `[DEFERRED]` |
|
||||||
|
|
||||||
|
### Pass criterion
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 6 aggregate: PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit code 0. Any `[FAIL]` line is a blocker. `[DEFERRED]` lines are expected
|
||||||
|
for the known-deferred surfaces listed in the implementation docs; they do not
|
||||||
|
fail the run.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- SQL Server `10.100.0.35,14330` reachable (Config DB tests use it).
|
||||||
|
- `dotnet` SDK on PATH (`.NET 10`).
|
||||||
|
- Run from repo root.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G2 — Full solution test suite
|
||||||
|
|
||||||
|
### Command
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet test ZB.MOM.WW.OtOpcUa.slnx --logger "console;verbosity=minimal"
|
||||||
|
```
|
||||||
|
|
||||||
|
For a more targeted run of integration suites that need their fixtures up:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# bring modbus fixture up first
|
||||||
|
lmxopcua-fix up modbus standard
|
||||||
|
|
||||||
|
dotnet test ZB.MOM.WW.OtOpcUa.slnx --logger "console;verbosity=minimal"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pass criterion
|
||||||
|
|
||||||
|
- Passed count >= 1159 (2026-04-19 baseline after Phase 5 driver complement).
|
||||||
|
- Failed count <= 1 (the pre-existing
|
||||||
|
`SubscribeCommandTests.Execute_PrintsSubscriptionMessage` flake in
|
||||||
|
`Client.CLI` is the only tolerated failure).
|
||||||
|
- No new `[FAILED]` tests relative to the baseline.
|
||||||
|
|
||||||
|
### Known flake
|
||||||
|
|
||||||
|
`ZB.MOM.WW.OtOpcUa.Client.CLI.Tests::SubscribeCommandTests.Execute_PrintsSubscriptionMessage`
|
||||||
|
is a timing-sensitive subscribe-then-cancel test. Rerun the specific project
|
||||||
|
if it appears:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests `
|
||||||
|
--filter "FullyQualifiedName~SubscribeCommandTests.Execute_PrintsSubscriptionMessage" `
|
||||||
|
--count 3
|
||||||
|
```
|
||||||
|
|
||||||
|
If it fails all three runs, investigate; otherwise treat as flake.
|
||||||
|
|
||||||
|
### Docker fixtures needed for integration suites
|
||||||
|
|
||||||
|
| Driver | Command | Endpoint used |
|
||||||
|
|--------|---------|---------------|
|
||||||
|
| Modbus | `lmxopcua-fix up modbus standard` | `10.100.0.35:5020` |
|
||||||
|
| AB CIP | `lmxopcua-fix up abcip controllogix` | `10.100.0.35:44818` |
|
||||||
|
| S7 | `lmxopcua-fix up s7 s7_1500` | `10.100.0.35:1102` |
|
||||||
|
| OPC UA Client | `lmxopcua-fix up opcuaclient` | `opc.tcp://10.100.0.35:50000` |
|
||||||
|
| FOCAS | `lmxopcua-fix up focas` (mock server) | `10.100.0.35:8193` |
|
||||||
|
|
||||||
|
TwinCAT integration tests require the TCBSD ESXi VM at `10.100.0.128`
|
||||||
|
(AmsNetId `41.169.163.43.1.1`). Set env var before running:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:TWINCAT_TARGET_HOST = "10.100.0.128"
|
||||||
|
$env:TWINCAT_TARGET_NETID = "41.169.163.43.1.1"
|
||||||
|
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests
|
||||||
|
```
|
||||||
|
|
||||||
|
Galaxy integration tests run against the live mxaccessgw on the dev box
|
||||||
|
(gate G6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G3 — Release blockers closed (audit, already satisfied)
|
||||||
|
|
||||||
|
All three code-path release blockers are closed per `v2-release-readiness.md`:
|
||||||
|
|
||||||
|
- Authorization dispatch wiring (task #143, PR #94) — CLOSED.
|
||||||
|
- Config fallback Phase 6.1 Stream D (task #136, PR #96) — CLOSED.
|
||||||
|
- Redundancy Phase 6.3 Streams A/C core (tasks #145/#147, PRs #98-99) — CLOSED.
|
||||||
|
|
||||||
|
No action required. Record the PR numbers in the release notes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G4 — Driver complement (audit, already satisfied)
|
||||||
|
|
||||||
|
All eight drivers shipped:
|
||||||
|
|
||||||
|
Galaxy, Modbus (+ DL205/S7/MELSEC profiles), S7 native, OPC UA Client, AB CIP,
|
||||||
|
AB Legacy, TwinCAT ADS, FOCAS (managed wire client — Tier-C isolation retired,
|
||||||
|
FOCAS is now Tier A in-process via `WireFocasClient`).
|
||||||
|
|
||||||
|
No action required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G5 — Production deployment checklist (operator action)
|
||||||
|
|
||||||
|
The deployment checklist is a separate document covering:
|
||||||
|
|
||||||
|
- Windows service install (`scripts/install/Install-Services.ps1`)
|
||||||
|
- Config DB migration (`scripts/db/Apply-Migrations.ps1`)
|
||||||
|
- Certificate provisioning and trust
|
||||||
|
- LDAP / GLAuth configuration for production AD target
|
||||||
|
- mxaccessgw API key provisioning (`apikey create-key` in the sibling repo)
|
||||||
|
- Service account permissions
|
||||||
|
- Prometheus / OpenTelemetry export configuration
|
||||||
|
- Firewall rules (port 4840 OPC UA, port 5120 gRPC to mxaccessgw,
|
||||||
|
Admin port 5000/5001)
|
||||||
|
|
||||||
|
**Sign-off party**: Fleet Admin (operator). Not automatable.
|
||||||
|
|
||||||
|
Record sign-off as a comment on the v2 release issue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G6 — Live Galaxy end-to-end integration run
|
||||||
|
|
||||||
|
**Requires**: AVEVA System Platform installed on dev box (confirmed available
|
||||||
|
per project memory `project_aveva_platform_installed.md`); mxaccessgw running
|
||||||
|
with a provisioned API key; at least one Galaxy object deployed.
|
||||||
|
|
||||||
|
### Procedure
|
||||||
|
|
||||||
|
1. Start mxaccessgw:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# in sibling repo C:\Users\dohertj2\Desktop\mxaccessgw\
|
||||||
|
dotnet run --project src/MxGateway.Server -- --apikey-path .local/api-key.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start OtOpcUa server with Galaxy driver instance configured:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
sc start OtOpcUa
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Browse via Client CLI:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
browse -u opc.tcp://localhost:4840 -r -d 3
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Read a known Galaxy tag (e.g. a deployed `$UserDefined` object attribute):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
read -u opc.tcp://localhost:4840 -n "ns=2;s=<tag_name.AttributeName>"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Subscribe and verify live updates:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||||
|
subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=<tag_name.AttributeName>" -i 1000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pass criterion
|
||||||
|
|
||||||
|
- Browse returns a non-empty node tree mirroring the Galaxy hierarchy.
|
||||||
|
- Read returns `Good` quality with a non-null value.
|
||||||
|
- Subscribe receives at least one data-change notification within 5 s
|
||||||
|
(or within the configured publishing interval).
|
||||||
|
- No `BadNoCommunication` or `BadTimeout` errors in the server log.
|
||||||
|
|
||||||
|
Record: Galaxy version, deployed object count, OtOpcUa git SHA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G7 — FOCAS live-CNC smoke (task #54)
|
||||||
|
|
||||||
|
**Requires**: real FANUC CNC with Ethernet option, accessible on TCP port 8193
|
||||||
|
from the dev box; CNC series known (e.g. 0i-F, 30i-B).
|
||||||
|
|
||||||
|
See `docs/plans/live-hardware-validation-runbooks.md` §FOCAS for the full
|
||||||
|
runbook.
|
||||||
|
|
||||||
|
### Pass criterion
|
||||||
|
|
||||||
|
- `WireFocasClient` opens a FOCAS2 session (`cnc_allclibhndl3` succeeds).
|
||||||
|
- Identity nodes (`Identity/SeriesNumber`, `Identity/MaxAxes`) return non-null
|
||||||
|
values matching the physical control panel display.
|
||||||
|
- At least one axis position (`Axes/X/AbsolutePosition` or similar) returns
|
||||||
|
`Good` quality with a plausible double value.
|
||||||
|
- Subscribe on a polled tag delivers at least three updates within 5 s.
|
||||||
|
- No `EW_SOCKET` (-1) or `EW_HANDLE` (-7) errors in the server log during a
|
||||||
|
2-minute soak.
|
||||||
|
|
||||||
|
Record: CNC series, firmware version, test date, OtOpcUa git SHA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G8 — OPC UA Conformance Test Tool (CTT) pass
|
||||||
|
|
||||||
|
**Requires**: OPC Foundation OPC UA Compliance Test Tool (CTT) or the
|
||||||
|
open-source UA Compliance Test Tool installed on the client machine;
|
||||||
|
live OtOpcUa server endpoint.
|
||||||
|
|
||||||
|
### Recommended minimum profile set
|
||||||
|
|
||||||
|
- `Attribute Read`
|
||||||
|
- `Attribute Write`
|
||||||
|
- `Browse`
|
||||||
|
- `Subscription` (DataChange)
|
||||||
|
- `Server-side monitoring`
|
||||||
|
- `Security — None profile` (if server configured with `Security:Profiles=[None]`)
|
||||||
|
|
||||||
|
### Procedure
|
||||||
|
|
||||||
|
1. Launch CTT. Add server endpoint: `opc.tcp://localhost:4840`.
|
||||||
|
2. Run the profile set above.
|
||||||
|
3. Capture the CTT report HTML/XML.
|
||||||
|
|
||||||
|
### Pass criterion
|
||||||
|
|
||||||
|
All mandatory test cases in each profile: **PASS** or **NOT APPLICABLE**.
|
||||||
|
|
||||||
|
Zero mandatory failures. Advisory failures may be documented with rationale
|
||||||
|
(e.g. optional capability not implemented).
|
||||||
|
|
||||||
|
Record: CTT version, profile set, OtOpcUa git SHA, report artifact.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G9 — Non-transparent redundancy cutover with production client
|
||||||
|
|
||||||
|
See `docs/plans/phase-6-3-redundancy-interop-plan.md` for the full runbook.
|
||||||
|
|
||||||
|
**Minimum acceptable result**: one complete pass of the A-block (UaExpert
|
||||||
|
OPC UA signal verification) plus scenario B2 (UaExpert failover on Primary
|
||||||
|
kill).
|
||||||
|
|
||||||
|
Ignition 8.3 is the recommended production client per decision #85. If
|
||||||
|
Ignition is not available on the lab machine, UaExpert is accepted for v2 GA.
|
||||||
|
|
||||||
|
Record: client name + version, OtOpcUa git SHA, test date.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gate summary table
|
||||||
|
|
||||||
|
| Gate | Command / Procedure | Pass criterion | Owner |
|
||||||
|
|------|---------------------|----------------|-------|
|
||||||
|
| G1 | `pwsh ./scripts/compliance/phase-6-all.ps1` | Exit 0, no `[FAIL]` | Dev |
|
||||||
|
| G2 | `dotnet test ZB.MOM.WW.OtOpcUa.slnx` | >= 1159 passing, <= 1 failure | Dev |
|
||||||
|
| G3 | Audit PR list in release-readiness.md | All blockers show CLOSED | Dev |
|
||||||
|
| G4 | Audit driver table | All 8 drivers listed as shipped | Dev |
|
||||||
|
| G5 | Run deployment checklist doc | All items checked; Fleet Admin signs off | Fleet Admin |
|
||||||
|
| G6 | Browse/read/subscribe against live Galaxy | Good quality, non-empty tree | Dev (dev box) |
|
||||||
|
| G7 | FOCAS CNC smoke — see live-hardware runbook | Session open, Good quality reads | Dev + lab hardware |
|
||||||
|
| G8 | CTT profile run against live endpoint | Zero mandatory failures | Dev + CTT tool |
|
||||||
|
| G9 | Redundancy cutover runbook | A-block + B2 pass with >= 1 client | Dev + two instances |
|
||||||
+10
-10
@@ -95,7 +95,7 @@ The Server accepts three OPC UA identity-token types:
|
|||||||
| Token | Handler | Notes |
|
| Token | Handler | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Anonymous | `IUserAuthenticator.AuthenticateAsync(username: "", password: "")` | Refused in strict mode unless explicit anonymous grants exist; allowed in lax mode for backward compatibility. |
|
| Anonymous | `IUserAuthenticator.AuthenticateAsync(username: "", password: "")` | Refused in strict mode unless explicit anonymous grants exist; allowed in lax mode for backward compatibility. |
|
||||||
| UserName/Password | `LdapUserAuthenticator` (`src/ZB.MOM.WW.OtOpcUa.Server/Security/LdapUserAuthenticator.cs`) | LDAP bind + group lookup; resolved `LdapGroups` flow into the session's identity bearer (`ILdapGroupsBearer`). |
|
| UserName/Password | `LdapUserAuthenticator` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/LdapUserAuthenticator.cs`) | LDAP bind + group lookup; resolved `LdapGroups` flow into the session's identity bearer (`ILdapGroupsBearer`). |
|
||||||
| X.509 Certificate | Stack-level acceptance + role mapping via CN | X.509 identity carries `AuthenticatedUser` + read roles; finer-grain authorization happens through the data-plane ACLs. |
|
| X.509 Certificate | Stack-level acceptance + role mapping via CN | X.509 identity carries `AuthenticatedUser` + read roles; finer-grain authorization happens through the data-plane ACLs. |
|
||||||
|
|
||||||
### LDAP bind flow (`LdapUserAuthenticator`)
|
### LDAP bind flow (`LdapUserAuthenticator`)
|
||||||
@@ -164,7 +164,7 @@ ACLs are evaluated against the UNS path:
|
|||||||
ClusterId → Namespace → UnsArea → UnsLine → Equipment → Tag
|
ClusterId → Namespace → UnsArea → UnsLine → Equipment → Tag
|
||||||
```
|
```
|
||||||
|
|
||||||
Each level can carry `NodeAcl` rows (`src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs`) that grant a permission bundle to a set of `LdapGroups`.
|
Each level can carry `NodeAcl` rows (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs`) that grant a permission bundle to a set of `LdapGroups`.
|
||||||
|
|
||||||
### Permission flags
|
### Permission flags
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ The three Write tiers map to Galaxy's v1 `SecurityClassification` — `FreeAcces
|
|||||||
|
|
||||||
### Evaluator — `PermissionTrie`
|
### Evaluator — `PermissionTrie`
|
||||||
|
|
||||||
`src/ZB.MOM.WW.OtOpcUa.Core/Authorization/`:
|
`src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/`:
|
||||||
|
|
||||||
| Class | Role |
|
| Class | Role |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -209,7 +209,7 @@ The three Write tiers map to Galaxy's v1 `SecurityClassification` — `FreeAcces
|
|||||||
|
|
||||||
### Dispatch gate — `AuthorizationGate`
|
### Dispatch gate — `AuthorizationGate`
|
||||||
|
|
||||||
`src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` bridges the OPC UA stack's `ISystemContext.UserIdentity` to the evaluator. `DriverNodeManager` holds exactly one reference to it and calls `IsAllowed(identity, OpcUaOperation.*, NodeScope)` on every Read, Write, HistoryRead, Browse, Subscribe, AckAlarm, Call path. A false return short-circuits the dispatch with `BadUserAccessDenied`.
|
`src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` bridges the OPC UA stack's `ISystemContext.UserIdentity` to the evaluator. `DriverNodeManager` holds exactly one reference to it and calls `IsAllowed(identity, OpcUaOperation.*, NodeScope)` on every Read, Write, HistoryRead, Browse, Subscribe, AckAlarm, Call path. A false return short-circuits the dispatch with `BadUserAccessDenied`.
|
||||||
|
|
||||||
Key properties:
|
Key properties:
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ Key properties:
|
|||||||
|
|
||||||
### Probe-this-permission (Admin UI)
|
### Probe-this-permission (Admin UI)
|
||||||
|
|
||||||
`PermissionProbeService` (`src/ZB.MOM.WW.OtOpcUa.Admin/Services/PermissionProbeService.cs`) lets an operator ask "if a user with groups X, Y, Z asked to do operation O on node N, would it succeed?" The answer is rendered in the AclsTab "Probe" dialog — same evaluator, same trie, so the Admin UI answer and the live Server answer cannot disagree.
|
`PermissionProbeService` (`src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/PermissionProbeService.cs`) lets an operator ask "if a user with groups X, Y, Z asked to do operation O on node N, would it succeed?" The answer is rendered in the AclsTab "Probe" dialog — same evaluator, same trie, so the Admin UI answer and the live Server answer cannot disagree.
|
||||||
|
|
||||||
### Full model
|
### Full model
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ Per decision #150 control-plane roles are **deliberately independent of data-pla
|
|||||||
|
|
||||||
### Roles
|
### Roles
|
||||||
|
|
||||||
`src/ZB.MOM.WW.OtOpcUa.Admin/Services/AdminRoles.cs`:
|
`src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/AdminRoles.cs`:
|
||||||
|
|
||||||
| Role | Capabilities |
|
| Role | Capabilities |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -255,17 +255,17 @@ Razor pages and API endpoints gate with `[Authorize(Policy = "CanEdit")]` / `"Ca
|
|||||||
|
|
||||||
### Role grant source
|
### Role grant source
|
||||||
|
|
||||||
Admin reads `LdapGroupRoleMapping` rows from the Config DB (`src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs`) — the same pattern as the data-plane `NodeAcl` but scoped to Admin roles + (optionally) cluster scope for multi-site fleets. The `RoleGrants.razor` page lets FleetAdmins edit these mappings without leaving the UI.
|
Admin reads `LdapGroupRoleMapping` rows from the Config DB (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs`) — the same pattern as the data-plane `NodeAcl` but scoped to Admin roles + (optionally) cluster scope for multi-site fleets. The `RoleGrants.razor` page lets FleetAdmins edit these mappings without leaving the UI.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## OTOPCUA0001 Analyzer — Compile-Time Guard
|
## OTOPCUA0001 Analyzer — Compile-Time Guard
|
||||||
|
|
||||||
Per-capability resilience (retry, timeout, circuit-breaker, bulkhead) is applied by `CapabilityInvoker` in `src/ZB.MOM.WW.OtOpcUa.Core/Resilience/`. A driver-capability call made **outside** the invoker bypasses resilience entirely — which in production looks like inconsistent timeouts, un-wrapped retries, and unbounded blocking.
|
Per-capability resilience (retry, timeout, circuit-breaker, bulkhead) is applied by `CapabilityInvoker` in `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/`. A driver-capability call made **outside** the invoker bypasses resilience entirely — which in production looks like inconsistent timeouts, un-wrapped retries, and unbounded blocking.
|
||||||
|
|
||||||
`OTOPCUA0001` (Roslyn analyzer at `src/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs`) fires as a compile-time **warning** when an `async`/`Task`-returning method on one of the seven guarded capability interfaces (`IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider`) is invoked **outside** a lambda passed to `CapabilityInvoker.ExecuteAsync` / `ExecuteWriteAsync` / `AlarmSurfaceInvoker.*`. The analyzer walks up the syntax tree from the call site, finds any enclosing invoker invocation, and verifies the call lives transitively inside that invocation's anonymous-function argument — a sibling pattern (do the call, then invoke `ExecuteAsync` on something unrelated nearby) does not satisfy the rule.
|
`OTOPCUA0001` (Roslyn analyzer at `src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs`) fires as a compile-time **warning** when an `async`/`Task`-returning method on one of the seven guarded capability interfaces (`IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider`) is invoked **outside** a lambda passed to `CapabilityInvoker.ExecuteAsync` / `ExecuteWriteAsync` / `AlarmSurfaceInvoker.*`. The analyzer walks up the syntax tree from the call site, finds any enclosing invoker invocation, and verifies the call lives transitively inside that invocation's anonymous-function argument — a sibling pattern (do the call, then invoke `ExecuteAsync` on something unrelated nearby) does not satisfy the rule.
|
||||||
|
|
||||||
Five xUnit-v3 + Shouldly tests at `tests/ZB.MOM.WW.OtOpcUa.Analyzers.Tests` cover the common fail/pass shapes + the sibling-pattern regression guard.
|
Five xUnit-v3 + Shouldly tests at `tests/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers.Tests` cover the common fail/pass shapes + the sibling-pattern regression guard.
|
||||||
|
|
||||||
The rule is intentionally scoped to async surfaces — pure in-memory accessors like `IHostConnectivityProbe.GetHostStatuses()` return synchronously and do not require the invoker wrap.
|
The rule is intentionally scoped to async surfaces — pure in-memory accessors like `IHostConnectivityProbe.GetHostStatuses()` return synchronously and do not require the invoker wrap.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# Alarm Tracking — v1 archive
|
||||||
|
|
||||||
|
> **Historical record.** This document describes the v1 / pre-PR-7.2
|
||||||
|
> Galaxy alarm path that ran inside `Galaxy.Host`'s STA pump as
|
||||||
|
> `GalaxyAlarmTracker`. PR 7.2 retired the in-process Galaxy stack; the
|
||||||
|
> alarms-over-gateway epic (B.2 / B.3 / E.7) restored Galaxy's
|
||||||
|
> `IAlarmSource` capability against the new gateway-mediated transport.
|
||||||
|
> See [docs/AlarmTracking.md](../AlarmTracking.md) for the v2 final
|
||||||
|
> architecture — that is the document to read for current behaviour.
|
||||||
|
|
||||||
|
Alarm surfacing is an optional driver capability exposed via `IAlarmSource` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs`). Drivers whose backends have an alarm concept implement it — today: Galaxy (MXAccess alarms), FOCAS (CNC alarms), OPC UA Client (A&C events from the upstream server). Modbus / S7 / AB CIP / AB Legacy / TwinCAT do not implement the interface and the feature is simply absent from their subtrees.
|
||||||
|
|
||||||
|
## IAlarmSource surface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||||
|
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken);
|
||||||
|
Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken);
|
||||||
|
Task AcknowledgeAsync(IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
event EventHandler<AlarmEventArgs>? OnAlarmEvent;
|
||||||
|
```
|
||||||
|
|
||||||
|
The driver fires `OnAlarmEvent` for every transition (`Active`, `Acknowledged`, `Inactive`) with an `AlarmEventArgs` carrying the source node id, condition id, alarm type, message, severity (`AlarmSeverity` enum), and source timestamp.
|
||||||
|
|
||||||
|
## AlarmSurfaceInvoker
|
||||||
|
|
||||||
|
`AlarmSurfaceInvoker` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs`) wraps the three mutating surfaces through `CapabilityInvoker`:
|
||||||
|
|
||||||
|
- `SubscribeAlarmsAsync` / `UnsubscribeAlarmsAsync` run through the `DriverCapability.AlarmSubscribe` pipeline — retries apply under the tier configuration.
|
||||||
|
- `AcknowledgeAsync` runs through `DriverCapability.AlarmAcknowledge` which does NOT retry per decision #143. A timed-out ack may have already registered at the plant floor; replay would silently double-acknowledge.
|
||||||
|
|
||||||
|
Multi-host fan-out: when the driver implements `IPerCallHostResolver`, each source node id is resolved individually and batches are grouped by host so a dead PLC inside a multi-device driver doesn't poison sibling breakers. Single-host drivers fall back to `IDriver.DriverInstanceId` as the pipeline-key host.
|
||||||
|
|
||||||
|
## Condition-node creation via CapturingBuilder
|
||||||
|
|
||||||
|
Alarm-condition nodes are materialized at address-space build time. During `GenericDriverNodeManager.BuildAddressSpaceAsync` the builder is wrapped in a `CapturingBuilder` that observes every `Variable()` call. When a driver calls `IVariableHandle.MarkAsAlarmCondition(AlarmConditionInfo)` on a returned handle, the server-side `DriverNodeManager.VariableHandle` creates a sibling `AlarmConditionState` node and returns an `IAlarmConditionSink`. The wrapper stores the sink in `_alarmSinks` keyed by the variable's full reference, then `GenericDriverNodeManager` registers a forwarder on `IAlarmSource.OnAlarmEvent` that routes each push to the matching sink by `SourceNodeId`. Unknown source ids are dropped silently — they may belong to another driver.
|
||||||
|
|
||||||
|
The `AlarmConditionState` layout matches OPC UA Part 9:
|
||||||
|
|
||||||
|
- `SourceNode` → the originating variable
|
||||||
|
- `SourceName` / `ConditionName` → from `AlarmConditionInfo.SourceName`
|
||||||
|
- Initial state: enabled, inactive, acknowledged, severity per `InitialSeverity`, retain false
|
||||||
|
- `HasCondition` references wire the source variable ↔ the condition node bidirectionally
|
||||||
|
|
||||||
|
Drivers flag alarm-bearing variables at discovery time via `DriverAttributeInfo.IsAlarm = true`. The Galaxy driver, for example, sets this on attributes that have an `AlarmExtension` primitive in the Galaxy repository DB; FOCAS sets it on the CNC alarm register.
|
||||||
|
|
||||||
|
## State transitions
|
||||||
|
|
||||||
|
`ConditionSink.OnTransition` runs under the node manager's `Lock` and maps the `AlarmEventArgs.AlarmType` string to Part 9 state:
|
||||||
|
|
||||||
|
| AlarmType | Action |
|
||||||
|
|---|---|
|
||||||
|
| `Active` | `SetActiveState(true)`, `SetAcknowledgedState(false)`, `Retain = true` |
|
||||||
|
| `Acknowledged` | `SetAcknowledgedState(true)` |
|
||||||
|
| `Inactive` | `SetActiveState(false)`; `Retain = false` once both inactive and acknowledged |
|
||||||
|
|
||||||
|
Severity is remapped: `AlarmSeverity.Low/Medium/High/Critical` → OPC UA numeric 250 / 500 / 700 / 900. `Message.Value` is set from `AlarmEventArgs.Message` on every transition. `ClearChangeMasks(true)` and `ReportEvent(condition)` fire the OPC UA event notification for clients subscribed to any ancestor notifier.
|
||||||
|
|
||||||
|
## Acknowledge dispatch
|
||||||
|
|
||||||
|
Alarm acknowledgement initiated by an OPC UA client flows:
|
||||||
|
|
||||||
|
1. The SDK invokes the `AlarmConditionState.OnAcknowledge` method delegate.
|
||||||
|
2. The handler checks the session's roles for `AlarmAck` — drivers never see a request the session wasn't entitled to make.
|
||||||
|
3. `AlarmSurfaceInvoker.AcknowledgeAsync` is called with the source / condition / comment tuple. The invoker groups by host and runs each batch through the no-retry `AlarmAcknowledge` pipeline.
|
||||||
|
|
||||||
|
Drivers return normally for success or throw to signal the ack failed at the backend.
|
||||||
|
|
||||||
|
## EventNotifier propagation
|
||||||
|
|
||||||
|
Drivers that want hierarchical alarm subscriptions propagate `EventNotifier.SubscribeToEvents` up the containment chain during discovery — the Galaxy driver flips the flag on every ancestor of an alarm-bearing object up to the driver root, mirroring v1 behavior. Clients subscribed at the driver root, a mid-level folder, or the `Objects/` root see alarm events from every descendant with an `AlarmConditionState` sibling. The driver-root `FolderState` is created in `DriverNodeManager.CreateAddressSpace` with `EventNotifier = SubscribeToEvents | HistoryRead` so alarm event subscriptions and alarm history both have a single natural target.
|
||||||
|
|
||||||
|
## ConditionRefresh
|
||||||
|
|
||||||
|
The OPC UA `ConditionRefresh` service queues the current state of every retained condition back to the requesting monitored items. `DriverNodeManager` iterates the node manager's `AlarmConditionState` collection and queues each condition whose `Retain.Value == true` — matching the Part 9 requirement.
|
||||||
|
|
||||||
|
## Alarm historian sink
|
||||||
|
|
||||||
|
Distinct from the live `IAlarmSource` stream and the Part 9 `AlarmConditionState` materialization above, qualifying alarm transitions are **also** persisted to a durable event log for downstream AVEVA Historian ingestion. This is a separate subsystem from the `IHistoryProvider` capability used by `HistoryReadEvents` (see [HistoricalDataAccess.md](HistoricalDataAccess.md#alarm-event-history-vs-ihistoryprovider)): the sink is a *producer* path (server → Historian) that runs independently of any client HistoryRead call.
|
||||||
|
|
||||||
|
### `IAlarmHistorianSink`
|
||||||
|
|
||||||
|
`src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs` defines the intake contract:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken);
|
||||||
|
HistorianSinkStatus GetStatus();
|
||||||
|
```
|
||||||
|
|
||||||
|
`EnqueueAsync` is fire-and-forget from the producer's perspective — it must never block the emitting thread. The event payload (`AlarmHistorianEvent` — same file) is source-agnostic: `AlarmId`, `EquipmentPath`, `AlarmName`, `AlarmTypeName` (Part 9 subtype name), `Severity`, `EventKind` (free-form transition string — `Activated` / `Cleared` / `Acknowledged` / `Confirmed` / `Shelved` / …), `Message`, `User`, `Comment`, `TimestampUtc`.
|
||||||
|
|
||||||
|
The sink scope is defined to span every alarm source (plan decision #15: scripted, Galaxy-native, AB CIP ALMD, any future `IAlarmSource`), gated per-alarm by a `HistorizeToAveva` toggle on the producer. Today only `Phase7EngineComposer.RouteToHistorianAsync` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs`) is wired — it subscribes to `ScriptedAlarmEngine.OnEvent` and marshals each emission into `AlarmHistorianEvent`. Galaxy-native alarms continue to reach AVEVA Historian via the driver's direct `aahClientManaged` path and do not flow through the sink; the AB CIP ALMD path remains unwired pending a producer-side integration.
|
||||||
|
|
||||||
|
### `SqliteStoreAndForwardSink`
|
||||||
|
|
||||||
|
Default production implementation (`src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs`). A local SQLite queue absorbs every `EnqueueAsync` synchronously; a background `Timer` drains batches asynchronously to an `IAlarmHistorianWriter` so operator actions are never blocked on historian reachability.
|
||||||
|
|
||||||
|
Queue schema (single table `Queue`): `RowId PK autoincrement`, `AlarmId`, `EnqueuedUtc`, `PayloadJson` (serialized `AlarmHistorianEvent`), `AttemptCount`, `LastAttemptUtc`, `LastError`, `DeadLettered` (bool), plus `IX_Queue_Drain (DeadLettered, RowId)`. Default capacity `1_000_000` non-dead-lettered rows; oldest rows evict with a WARN log past the cap.
|
||||||
|
|
||||||
|
Drain cadence: `StartDrainLoop(tickInterval)` arms a periodic timer. `DrainOnceAsync` reads up to `batchSize` rows (default 100) in `RowId` order and forwards them through `IAlarmHistorianWriter.WriteBatchAsync`, which returns one `HistorianWriteOutcome` per row:
|
||||||
|
|
||||||
|
| Outcome | Action |
|
||||||
|
|---|---|
|
||||||
|
| `Ack` | Row deleted. |
|
||||||
|
| `PermanentFail` | Row flipped to `DeadLettered = 1` with reason. Peers in the batch retry independently. |
|
||||||
|
| `RetryPlease` | `AttemptCount` bumped; row stays queued. Drain worker enters `BackingOff`. |
|
||||||
|
|
||||||
|
Writer-side exceptions treat the whole batch as `RetryPlease`.
|
||||||
|
|
||||||
|
Backoff ladder on `RetryPlease` (hard-coded): 1s → 2s → 5s → 15s → 60s cap. Reset to 0 on any batch with no retries. `CurrentBackoff` exposes the current step for instrumentation; the drain timer itself fires on `tickInterval`, so the ladder governs write cadence rather than timer period.
|
||||||
|
|
||||||
|
Dead-letter retention defaults to 30 days (plan decision #21). `PurgeAgedDeadLetters` runs each drain pass and deletes rows whose `LastAttemptUtc` is past the cutoff. `RetryDeadLettered()` is an operator action that clears `DeadLettered` + resets `AttemptCount` on every dead-lettered row so they rejoin the main queue.
|
||||||
|
|
||||||
|
### Composition and writer resolution
|
||||||
|
|
||||||
|
`Phase7Composer.ResolveHistorianSink` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs`) scans the registered drivers for one that implements `IAlarmHistorianWriter`. Today that is `GalaxyProxyDriver` via `GalaxyHistorianWriter` (`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/Ipc/GalaxyHistorianWriter.cs`), which forwards batches over the Galaxy.Host pipe to the `aahClientManaged` alarm schema. When a writer is found, a `SqliteStoreAndForwardSink` is instantiated against `%ProgramData%/OtOpcUa/alarm-historian-queue.db` with a 2 s drain tick and the writer attached. When no driver provides a writer the fallback is the DI-registered `NullAlarmHistorianSink` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Program.cs`), which silently discards and reports `HistorianDrainState.Disabled`.
|
||||||
|
|
||||||
|
### Status and observability
|
||||||
|
|
||||||
|
`GetStatus()` returns `HistorianSinkStatus(QueueDepth, DeadLetterDepth, LastDrainUtc, LastSuccessUtc, LastError, DrainState)` — two `COUNT(*)` scalars plus last-drain telemetry. `DrainState` is one of `Disabled` / `Idle` / `Draining` / `BackingOff`.
|
||||||
|
|
||||||
|
The Admin UI `/alarms/historian` page surfaces this through `HistorianDiagnosticsService` (`src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/HistorianDiagnosticsService.cs`), which also exposes `TryRetryDeadLettered` — it calls through to `SqliteStoreAndForwardSink.RetryDeadLettered` when the live sink is the SQLite implementation and returns 0 otherwise.
|
||||||
|
|
||||||
|
## Key source files
|
||||||
|
|
||||||
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs` — capability contract + `AlarmEventArgs`
|
||||||
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs` — per-host fan-out + no-retry ack
|
||||||
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — `CapturingBuilder` + alarm forwarder
|
||||||
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `VariableHandle.MarkAsAlarmCondition` + `ConditionSink`
|
||||||
|
- `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/Alarms/GalaxyAlarmTracker.cs` — Galaxy-specific alarm-event production
|
||||||
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs` — historian sink intake contract + `AlarmHistorianEvent` + `HistorianSinkStatus` + `IAlarmHistorianWriter`
|
||||||
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs` — durable queue + drain worker + backoff ladder + dead-letter retention
|
||||||
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — `RouteToHistorianAsync` wires scripted-alarm emissions into the sink
|
||||||
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` — `ResolveHistorianSink` selects `SqliteStoreAndForwardSink` vs `NullAlarmHistorianSink`
|
||||||
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/HistorianDiagnosticsService.cs` — Admin UI `/alarms/historian` status + retry-dead-lettered operator action
|
||||||
@@ -17,7 +17,7 @@ The rule: if the setting describes *how the process connects to the rest of the
|
|||||||
|
|
||||||
Each of the three processes (Server, Admin, Galaxy.Host) reads its own `appsettings.json` plus environment overrides.
|
Each of the three processes (Server, Admin, Galaxy.Host) reads its own `appsettings.json` plus environment overrides.
|
||||||
|
|
||||||
### OtOpcUa Server — `src/ZB.MOM.WW.OtOpcUa.Server/appsettings.json`
|
### OtOpcUa Server — `src/Server/ZB.MOM.WW.OtOpcUa.Server/appsettings.json`
|
||||||
|
|
||||||
Bootstrap-only. `Program.cs` reads four top-level sections:
|
Bootstrap-only. `Program.cs` reads four top-level sections:
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ Minimal example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### OtOpcUa Admin — `src/ZB.MOM.WW.OtOpcUa.Admin/appsettings.json`
|
### OtOpcUa Admin — `src/Server/ZB.MOM.WW.OtOpcUa.Admin/appsettings.json`
|
||||||
|
|
||||||
| Section | Purpose |
|
| Section | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -73,7 +73,7 @@ Standard .NET config layering applies: `appsettings.{Environment}.json`, then en
|
|||||||
|
|
||||||
## Authoritative configuration (Config DB)
|
## Authoritative configuration (Config DB)
|
||||||
|
|
||||||
The Config DB is the single source of truth for every setting that a v1 deployment used to carry in `appsettings.json` as driver-specific state. `OtOpcUaConfigDbContext` (`src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs`) is the EF Core context used by both the Admin writer and every Server reader.
|
The Config DB is the single source of truth for every setting that a v1 deployment used to carry in `appsettings.json` as driver-specific state. `OtOpcUaConfigDbContext` (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs`) is the EF Core context used by both the Admin writer and every Server reader.
|
||||||
|
|
||||||
### Top-level sections operators touch
|
### Top-level sections operators touch
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ Old generations are retained; rollback is "publish older generation as new". `Co
|
|||||||
|
|
||||||
### Offline cache
|
### Offline cache
|
||||||
|
|
||||||
Each Server process caches the last-seen published generation in `Node:LocalCachePath` via LiteDB (`LiteDbConfigCache` in `src/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/`). The cache lets a node start without the central DB reachable; once the DB comes back, `NodeBootstrap` syncs to the current generation.
|
Each Server process caches the last-seen published generation in `Node:LocalCachePath` via LiteDB (`LiteDbConfigCache` in `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/`). The cache lets a node start without the central DB reachable; once the DB comes back, `NodeBootstrap` syncs to the current generation.
|
||||||
|
|
||||||
### Full schema reference
|
### Full schema reference
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# Data Type Mapping
|
# Data Type Mapping
|
||||||
|
|
||||||
Data-type mapping is driver-defined. Each driver translates its native attribute metadata into two driver-agnostic enums from `Core.Abstractions` — `DriverDataType` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs`) and `SecurityClassification` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/SecurityClassification.cs`) — and populates the `DriverAttributeInfo` record it hands to `IAddressSpaceBuilder.Variable(...)`. Core doesn't interpret the native types; it trusts the driver's translation.
|
Data-type mapping is driver-defined. Each driver translates its native attribute metadata into two driver-agnostic enums from `Core.Abstractions` — `DriverDataType` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs`) and `SecurityClassification` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/SecurityClassification.cs`) — and populates the `DriverAttributeInfo` record it hands to `IAddressSpaceBuilder.Variable(...)`. Core doesn't interpret the native types; it trusts the driver's translation.
|
||||||
|
|
||||||
## DriverDataType → OPC UA built-in type
|
## DriverDataType → OPC UA built-in type
|
||||||
|
|
||||||
`DriverNodeManager.MapDataType` (`src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) is the single translation table for every driver:
|
`DriverNodeManager.MapDataType` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) is the single translation table for every driver:
|
||||||
|
|
||||||
| DriverDataType | OPC UA NodeId |
|
| DriverDataType | OPC UA NodeId |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -23,8 +23,8 @@ The enum also carries `Int16 / Int64 / UInt16 / UInt32 / UInt64 / Reference` mem
|
|||||||
Each driver owns its native → `DriverDataType` translation:
|
Each driver owns its native → `DriverDataType` translation:
|
||||||
|
|
||||||
- **Galaxy Proxy** — `GalaxyProxyDriver.MapDataType(int mxDataType)` and `MapSecurity(int mxSec)` (inline in `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/GalaxyProxyDriver.cs`). The Galaxy `mx_data_type` integer is sent across the Host↔Proxy pipe and mapped on the Proxy side. Galaxy's full classic 16-entry table (Boolean / Integer / Float / Double / String / Time / ElapsedTime / Reference / Enumeration / Custom / InternationalizedString) is preserved but compressed into the seven-entry `DriverDataType` enum — `ElapsedTime` → `Float64`, `InternationalizedString` → `String`, `Reference` → `Reference`, enumerations → `Int32`.
|
- **Galaxy Proxy** — `GalaxyProxyDriver.MapDataType(int mxDataType)` and `MapSecurity(int mxSec)` (inline in `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/GalaxyProxyDriver.cs`). The Galaxy `mx_data_type` integer is sent across the Host↔Proxy pipe and mapped on the Proxy side. Galaxy's full classic 16-entry table (Boolean / Integer / Float / Double / String / Time / ElapsedTime / Reference / Enumeration / Custom / InternationalizedString) is preserved but compressed into the seven-entry `DriverDataType` enum — `ElapsedTime` → `Float64`, `InternationalizedString` → `String`, `Reference` → `Reference`, enumerations → `Int32`.
|
||||||
- **AB CIP** — `src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDataType.cs` maps CIP tag type codes.
|
- **AB CIP** — `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDataType.cs` maps CIP tag type codes.
|
||||||
- **Modbus** — `src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs` maps register shapes (16-bit signed, 16-bit unsigned, 32-bit float, etc.) including the DirectLogic quirk table in `DirectLogicAddress.cs`.
|
- **Modbus** — `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs` maps register shapes (16-bit signed, 16-bit unsigned, 32-bit float, etc.) including the DirectLogic quirk table in `DirectLogicAddress.cs`.
|
||||||
- **S7 / AB Legacy / TwinCAT / FOCAS / OPC UA Client** — each has its own inline mapper or `*DataType.cs` file per the same pattern.
|
- **S7 / AB Legacy / TwinCAT / FOCAS / OPC UA Client** — each has its own inline mapper or `*DataType.cs` file per the same pattern.
|
||||||
|
|
||||||
The driver's mapping is authoritative — when a field type is ambiguous (a `LREAL` that could be bit-reinterpreted, a BCD counter, a string of a particular encoding), the driver decides the exposed OPC UA shape.
|
The driver's mapping is authoritative — when a field type is ambiguous (a `LREAL` that could be bit-reinterpreted, a BCD counter, a string of a particular encoding), the driver decides the exposed OPC UA shape.
|
||||||
@@ -35,7 +35,7 @@ The driver's mapping is authoritative — when a field type is ambiguous (a `LRE
|
|||||||
|
|
||||||
## SecurityClassification — metadata, not ACL
|
## SecurityClassification — metadata, not ACL
|
||||||
|
|
||||||
`SecurityClassification` is driver-reported metadata only. Drivers never enforce write permissions themselves — the classification flows into the Server project where `WriteAuthzPolicy.IsAllowed(classification, userRoles)` (`src/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs`) gates the write against the session's LDAP-derived roles, and (Phase 6.2) the `AuthorizationGate` + permission trie apply on top. This is the "ACL at server layer" invariant documented in `docs/security.md`.
|
`SecurityClassification` is driver-reported metadata only. Drivers never enforce write permissions themselves — the classification flows into the Server project where `WriteAuthzPolicy.IsAllowed(classification, userRoles)` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs`) gates the write against the session's LDAP-derived roles, and (Phase 6.2) the `AuthorizationGate` + permission trie apply on top. This is the "ACL at server layer" invariant documented in `docs/security.md`.
|
||||||
|
|
||||||
The classification values mirror the v1 Galaxy model so existing Galaxy galaxies keep their published semantics:
|
The classification values mirror the v1 Galaxy model so existing Galaxy galaxies keep their published semantics:
|
||||||
|
|
||||||
@@ -57,9 +57,9 @@ Drivers whose backend has no notion of classification (Modbus, most PLCs) defaul
|
|||||||
|
|
||||||
## Key source files
|
## Key source files
|
||||||
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs` — driver-agnostic type enum
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs` — driver-agnostic type enum
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/SecurityClassification.cs` — write-authz tier metadata
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/SecurityClassification.cs` — write-authz tier metadata
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs` — per-attribute descriptor
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs` — per-attribute descriptor
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `MapDataType` translation
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `MapDataType` translation
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs` — classification-to-role policy
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs` — classification-to-role policy
|
||||||
- Per-driver mappers in each `Driver.*` project
|
- Per-driver mappers in each `Driver.*` project
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Historical Data Access
|
# Historical Data Access
|
||||||
|
|
||||||
OPC UA HistoryRead is a **per-driver optional capability** in OtOpcUa. The Core dispatches HistoryRead service calls to the owning driver through the `IHistoryProvider` capability interface (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs`). Drivers that don't implement the interface return `BadHistoryOperationUnsupported` for every history call on their nodes; that is the expected behavior for protocol drivers (Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS) whose wire protocols carry no time-series data.
|
OPC UA HistoryRead is a **per-driver optional capability** in OtOpcUa. The Core dispatches HistoryRead service calls to the owning driver through the `IHistoryProvider` capability interface (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs`). Drivers that don't implement the interface return `BadHistoryOperationUnsupported` for every history call on their nodes; that is the expected behavior for protocol drivers (Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS) whose wire protocols carry no time-series data.
|
||||||
|
|
||||||
Historian integration is no longer a separate bolt-on assembly, as it was in v1 (`ZB.MOM.WW.LmxOpcUa.Historian.Aveva` plugin). It is now one optional capability any driver can implement. The first implementation is the Galaxy driver's Wonderware Historian integration; OPC UA Client forwards HistoryRead to the upstream server. Every other driver leaves the capability unimplemented and the Core short-circuits history calls on nodes that belong to those drivers.
|
Historian integration is no longer a separate bolt-on assembly, as it was in v1 (`ZB.MOM.WW.LmxOpcUa.Historian.Aveva` plugin). It is now one optional capability any driver can implement. The first implementation is the Galaxy driver's Wonderware Historian integration; OPC UA Client forwards HistoryRead to the upstream server. Every other driver leaves the capability unimplemented and the Core short-circuits history calls on nodes that belong to those drivers.
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ Supporting DTOs live alongside the interface in `Core.Abstractions`:
|
|||||||
|
|
||||||
`IHistoryProvider.ReadEventsAsync` is the **pull** path: an OPC UA client calls `HistoryReadEvents` against a notifier node and the driver walks its own backend event store to satisfy the request. The Galaxy driver's implementation reads from AVEVA Historian's event schema via `aahClientManaged`; every other driver leaves the default `NotSupportedException` in place.
|
`IHistoryProvider.ReadEventsAsync` is the **pull** path: an OPC UA client calls `HistoryReadEvents` against a notifier node and the driver walks its own backend event store to satisfy the request. The Galaxy driver's implementation reads from AVEVA Historian's event schema via `aahClientManaged`; every other driver leaves the default `NotSupportedException` in place.
|
||||||
|
|
||||||
There is also a separate **push** path for persisting alarm transitions from any `IAlarmSource` (and the Phase 7 scripted-alarm engine) into a durable event log, independent of any client HistoryRead call. That path is covered by `IAlarmHistorianSink` + `SqliteStoreAndForwardSink` in `src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/` and is documented in [AlarmTracking.md#alarm-historian-sink](AlarmTracking.md#alarm-historian-sink). The two paths are complementary — the sink populates an external historian's alarm schema; `ReadEventsAsync` reads from whatever event store the driver owns — and share neither interface nor dispatch.
|
There is also a separate **push** path for persisting alarm transitions from any `IAlarmSource` (and the Phase 7 scripted-alarm engine) into a durable event log, independent of any client HistoryRead call. That path is covered by `IAlarmHistorianSink` + `SqliteStoreAndForwardSink` in `src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/` and is documented in [AlarmTracking.md#alarm-historian-sink](AlarmTracking.md#alarm-historian-sink). The two paths are complementary — the sink populates an external historian's alarm schema; `ReadEventsAsync` reads from whatever event store the driver owns — and share neither interface nor dispatch.
|
||||||
|
|
||||||
## Dispatch through `CapabilityInvoker`
|
## Dispatch through `CapabilityInvoker`
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# v1 documentation archive
|
||||||
|
|
||||||
|
This folder contains documentation that described the original v1
|
||||||
|
in-process MXAccess architecture (`Galaxy.Host` + `Galaxy.Proxy` +
|
||||||
|
`Galaxy.Shared` three-project split, .NET 4.8 x86 + COM apartment, the
|
||||||
|
`OtOpcUaGalaxyHost` Windows service). That architecture was retired in
|
||||||
|
PR 7.2 (merged 2026-04-30 at commit `ae7106d`). These docs are kept as
|
||||||
|
the historical record of how the system worked before the v2-mxgw
|
||||||
|
migration; treat their content as accurate at the time of writing, NOT
|
||||||
|
as current state.
|
||||||
|
|
||||||
|
For current architecture see:
|
||||||
|
|
||||||
|
- `CLAUDE.md` — agent-facing v2 overview
|
||||||
|
- `docs/drivers/Galaxy.md` — current Galaxy driver doc
|
||||||
|
- `docs/v2/Galaxy.ParityRig.md` — current testing setup
|
||||||
|
- `docs/v2/Galaxy.Performance.md` — observability + perf
|
||||||
|
|
||||||
|
| File | What it covered |
|
||||||
|
|---|---|
|
||||||
|
| `AlarmTracking.md` | v1 alarm-tracking flow through the in-process MXAccess client |
|
||||||
|
| `Configuration.md` | v1 server configuration (`OTOPCUA_GALAXY_*` env vars now live in mxaccessgw config) |
|
||||||
|
| `DataTypeMapping.md` | Galaxy `mx_data_type` → OPC UA type mapping (still accurate as a reference; the live mapping logic is in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`) |
|
||||||
|
| `HistoricalDataAccess.md` | v1 IHistoryProvider on the Host side; current path is the server-level HistoryRouter + Wonderware sidecar |
|
||||||
|
| `Subscriptions.md` | v1 MXAccess subscription mechanics; current path uses gateway StreamEvents |
|
||||||
|
| `drivers/Galaxy-Repository.md` | v1 Host-side ZB SQL repository client; the gateway owns this path now |
|
||||||
|
| `drivers/Galaxy-Test-Fixture.md` | v1 test-fixture setup (parity tests + Galaxy.Host EXE spawn) |
|
||||||
|
| `reqs/GalaxyRepositoryReqs.md`, `reqs/MxAccessClientReqs.md` | Original Phase 0 requirements; satisfied in mxaccessgw repo today |
|
||||||
|
| `reqs/ServiceHostReqs.md` | Service-hosting requirements including `OtOpcUaGalaxyHost` (GHX-* section); only `OtOpcUa` server hosting remains in scope post-7.2 |
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
# Subscriptions
|
# Subscriptions
|
||||||
|
|
||||||
Driver-side data-change subscriptions live behind `ISubscribable` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs`). The interface is deliberately mechanism-agnostic: it covers native subscriptions (Galaxy MXAccess advisory, OPC UA monitored items on an upstream server, TwinCAT ADS notifications) and driver-internal polled subscriptions (Modbus, AB CIP, S7, FOCAS). Core sees the same event shape regardless — drivers fire `OnDataChange` and Core dispatches to the matching OPC UA monitored items.
|
Driver-side data-change subscriptions live behind `ISubscribable` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs`). The interface is deliberately mechanism-agnostic: it covers native subscriptions (Galaxy MXAccess advisory, OPC UA monitored items on an upstream server, TwinCAT ADS notifications) and driver-internal polled subscriptions (Modbus, AB CIP, S7, FOCAS). Core sees the same event shape regardless — drivers fire `OnDataChange` and Core dispatches to the matching OPC UA monitored items.
|
||||||
|
|
||||||
## Driver vs virtual dispatch
|
## Driver vs virtual dispatch
|
||||||
|
|
||||||
Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), `DriverNodeManager` routes subscriptions across both driver tags and virtual (scripted) tags through the same `ISubscribable` contract. The per-variable `NodeSourceKind` (registered from `DriverAttributeInfo` at discovery) selects the backend:
|
Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), `DriverNodeManager` routes subscriptions across both driver tags and virtual (scripted) tags through the same `ISubscribable` contract. The per-variable `NodeSourceKind` (registered from `DriverAttributeInfo` at discovery) selects the backend:
|
||||||
|
|
||||||
- `NodeSourceKind.Driver` — subscribes via the driver's `ISubscribable`, wrapped by `CapabilityInvoker` (the rest of this doc).
|
- `NodeSourceKind.Driver` — subscribes via the driver's `ISubscribable`, wrapped by `CapabilityInvoker` (the rest of this doc).
|
||||||
- `NodeSourceKind.Virtual` — subscribes via `VirtualTagSource` (`src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs`), which forwards change events emitted by `VirtualTagEngine` as `OnDataChange`. The ref-counting, initial-value, and transfer-restoration behaviour below applies identically.
|
- `NodeSourceKind.Virtual` — subscribes via `VirtualTagSource` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs`), which forwards change events emitted by `VirtualTagEngine` as `OnDataChange`. The ref-counting, initial-value, and transfer-restoration behaviour below applies identically.
|
||||||
|
|
||||||
Because both kinds expose `ISubscribable`, Core's dispatch, ref-count map, and monitored-item fan-out are unchanged across the source branch.
|
Because both kinds expose `ISubscribable`, Core's dispatch, ref-count map, and monitored-item fan-out are unchanged across the source branch.
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ When an OPC UA session is resumed (client reconnect with `TransferSubscriptions`
|
|||||||
|
|
||||||
## Key source files
|
## Key source files
|
||||||
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs` — capability contract
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs` — capability contract
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — pipeline wrapping
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — pipeline wrapping
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Sta/StaPump.cs` — Galaxy STA thread + message pump
|
- `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Sta/StaPump.cs` — Galaxy STA thread + message pump
|
||||||
- Per-driver subscribe implementations in each `Driver.*` project
|
- Per-driver subscribe implementations in each `Driver.*` project
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
|
> **✅ Completed 2026-04-30 — historical record of the parity-rig validation gate for PR 7.2.**
|
||||||
|
>
|
||||||
|
> The matrix below was the go/no-go gate for retiring the legacy
|
||||||
|
> Galaxy.Host backend (PR 7.2). Final run on the dev rig 2026-04-30
|
||||||
|
> returned 14 passed / 1 skipped / 0 failed; PR 7.2 (commit `fe91d42`)
|
||||||
|
> deleted the legacy projects + service the next day. The "Running
|
||||||
|
> the matrix" section is preserved for historical reproducibility but
|
||||||
|
> the test projects it references (`Driver.Galaxy.ParityTests`) were
|
||||||
|
> deleted alongside the legacy backend; this matrix is no longer
|
||||||
|
> runnable. Current Galaxy testing flows through the gateway's own
|
||||||
|
> test suite (sibling mxaccessgw repo).
|
||||||
|
|
||||||
# Galaxy backend parity matrix
|
# Galaxy backend parity matrix
|
||||||
|
|
||||||
This document tracks the scenario × result matrix that the
|
This document tracks the scenario × result matrix that the
|
||||||
|
|||||||
+33
-13
@@ -1,15 +1,30 @@
|
|||||||
# Galaxy parity rig — runbook
|
# Galaxy parity rig — runbook
|
||||||
|
|
||||||
|
> ✅ **Completed 2026-04-30 — historical record.** This runbook is the
|
||||||
|
> recipe that produced the green parity matrix that gated PR 7.2
|
||||||
|
> (retire legacy Galaxy projects, merged at commit `ae7106d`). The
|
||||||
|
> matrix it produced is captured in
|
||||||
|
> [`Galaxy.ParityMatrix.md`](Galaxy.ParityMatrix.md), also marked
|
||||||
|
> historical. The test project this doc drove
|
||||||
|
> (`Driver.Galaxy.ParityTests`) was deleted in PR 7.2, along with
|
||||||
|
> `Driver.Galaxy.{Host,Proxy,Shared}` and the `OtOpcUaGalaxyHost`
|
||||||
|
> Windows service. **You cannot re-run this rig today.** Current
|
||||||
|
> Galaxy testing flows through the gateway's own test suite in the
|
||||||
|
> sibling `mxaccessgw` repo.
|
||||||
|
>
|
||||||
|
> The text below is preserved as-written so the migration trail (what
|
||||||
|
> was tested, against what shape, with what env vars) stays auditable.
|
||||||
|
|
||||||
Brings up both Galaxy backends side-by-side against a single live Galaxy
|
Brings up both Galaxy backends side-by-side against a single live Galaxy
|
||||||
so the parity matrix in `docs/v2/Galaxy.ParityMatrix.md` and the soak
|
so the parity matrix in `docs/v2/Galaxy.ParityMatrix.md` and the soak
|
||||||
scenario in `tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/SoakScenarioTests.cs`
|
scenario in `tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/SoakScenarioTests.cs`
|
||||||
can run for real. Closing the parity matrix is the gate for PR 7.2
|
can run for real. Closing the parity matrix was the gate for PR 7.2
|
||||||
(retire legacy Galaxy projects).
|
(retire legacy Galaxy projects).
|
||||||
|
|
||||||
## Conceptual layout
|
## Conceptual layout
|
||||||
|
|
||||||
```
|
```
|
||||||
Galaxy ZB SQL ──┬── OtOpcUaGalaxyHost (NSSM service, net48 x86)
|
Galaxy ZB SQL ──┬── OtOpcUaGalaxyHost (NSSM service, net48 x86) [DELETED in PR 7.2]
|
||||||
│ └── MxAccess COM, ClientName "OtOpcUa-Galaxy.Host"
|
│ └── MxAccess COM, ClientName "OtOpcUa-Galaxy.Host"
|
||||||
│ └── named pipe "OtOpcUaGalaxy"
|
│ └── named pipe "OtOpcUaGalaxy"
|
||||||
│ ▲
|
│ ▲
|
||||||
@@ -29,17 +44,19 @@ Galaxy ZB SQL ──┬── OtOpcUaGalaxyHost (NSSM service, net48 x86)
|
|||||||
Both halves talk to the **same Galaxy** through **two distinct MxAccess
|
Both halves talk to the **same Galaxy** through **two distinct MxAccess
|
||||||
sessions** (different ClientNames so they don't evict each other).
|
sessions** (different ClientNames so they don't evict each other).
|
||||||
|
|
||||||
## What's already on this dev box
|
## What was on the dev box at the time
|
||||||
|
|
||||||
Per `~/.claude/projects/.../memory/`:
|
Per `~/.claude/projects/.../memory/` *as of the rig run*:
|
||||||
|
|
||||||
- **AVEVA System Platform + Galaxy + MXAccess runtime** — `project_aveva_platform_installed.md`.
|
- **AVEVA System Platform + Galaxy + MXAccess runtime** — `project_aveva_platform_installed.md`.
|
||||||
- **`OtOpcUaGalaxyHost`** Windows service running as `dohertj2`, NSSM-wrapped,
|
- **`OtOpcUaGalaxyHost`** Windows service running as `dohertj2`, NSSM-wrapped,
|
||||||
binary at `C:\publish\OtOpcUaGalaxyHost\OtOpcUa.Driver.Galaxy.Host.exe`,
|
binary at `C:\publish\OtOpcUaGalaxyHost\OtOpcUa.Driver.Galaxy.Host.exe`,
|
||||||
shared secret at `.local/galaxy-host-secret.txt`, ZB SQL on `localhost:1433`
|
shared secret at `.local/galaxy-host-secret.txt`, ZB SQL on `localhost:1433`
|
||||||
— `project_galaxy_host_installed.md`.
|
— `project_galaxy_host_installed.md`. **(Service uninstalled and binary
|
||||||
- **Parity test project** (`Driver.Galaxy.ParityTests`) committed and
|
retired as part of PR 7.2; the host source project no longer exists in
|
||||||
skip-clean — runs as soon as the mxgw half resolves.
|
this repo.)**
|
||||||
|
- **Parity test project** (`Driver.Galaxy.ParityTests`) — committed and
|
||||||
|
skip-clean at the time of the rig run. **Deleted in PR 7.2.**
|
||||||
|
|
||||||
## Setup steps (one-time)
|
## Setup steps (one-time)
|
||||||
|
|
||||||
@@ -282,7 +299,7 @@ sees the change:
|
|||||||
```powershell
|
```powershell
|
||||||
graccess object deploy --galaxy ZB --name OtOpcUaParityTest_001 `
|
graccess object deploy --galaxy ZB --name OtOpcUaParityTest_001 `
|
||||||
--confirm --confirm-target OtOpcUaParityTest_001
|
--confirm --confirm-target OtOpcUaParityTest_001
|
||||||
sc.exe restart OtOpcUaGalaxyHost
|
sc.exe restart OtOpcUaGalaxyHost # service no longer exists post-PR-7.2; in the modern shape, restart mxaccessgw instead
|
||||||
```
|
```
|
||||||
|
|
||||||
Then re-run the parity matrix. The previously-skipped scenarios should
|
Then re-run the parity matrix. The previously-skipped scenarios should
|
||||||
@@ -343,11 +360,14 @@ Galaxy with a script that imports 50k attributes onto a generated UDO
|
|||||||
- **`LegacySkipReason` says "Galaxy ZB SQL not reachable on
|
- **`LegacySkipReason` says "Galaxy ZB SQL not reachable on
|
||||||
localhost:1433"** — SQL Server isn't running, or its TCP listener is
|
localhost:1433"** — SQL Server isn't running, or its TCP listener is
|
||||||
off. Check `services.msc` for the SQL Server (default) instance.
|
off. Check `services.msc` for the SQL Server (default) instance.
|
||||||
- **`LegacySkipReason` says "Galaxy.Host EXE not built"** — the parity
|
- **`LegacySkipReason` says "Galaxy.Host EXE not built"** — at rig time
|
||||||
harness looks under `src/.../bin/Debug/net48/`. Build it once:
|
the parity harness looked under
|
||||||
`dotnet build src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host`. Note the
|
`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/bin/Debug/net48/` for the
|
||||||
separately-published copy at `C:\publish\OtOpcUaGalaxyHost\` is for
|
EXE it spawned as a subprocess, separate from the published copy at
|
||||||
the Windows service; the parity harness spawns its own subprocess.
|
`C:\publish\OtOpcUaGalaxyHost\` used by the Windows service. **Both
|
||||||
|
the source project and the published binary were removed in PR 7.2,
|
||||||
|
so this troubleshooting branch no longer applies — the legacy half
|
||||||
|
cannot be brought up at all.**
|
||||||
- **Both halves resolve but parity scenarios assert deltas** — that's
|
- **Both halves resolve but parity scenarios assert deltas** — that's
|
||||||
the expected outcome the rig exists to surface. Review each delta
|
the expected outcome the rig exists to surface. Review each delta
|
||||||
against `docs/v2/Galaxy.ParityMatrix.md`'s "Accepted deltas" section
|
against `docs/v2/Galaxy.ParityMatrix.md`'s "Accepted deltas" section
|
||||||
|
|||||||
@@ -147,10 +147,10 @@ Dev credentials in this inventory are convenience defaults, not secrets. Change
|
|||||||
| Resource | Purpose | Type | Default port | Default credentials | Owner |
|
| Resource | Purpose | Type | Default port | Default credentials | Owner |
|
||||||
|----------|---------|------|--------------|---------------------|-------|
|
|----------|---------|------|--------------|---------------------|-------|
|
||||||
| **Docker Desktop for Windows** | Host for every driver test-fixture simulator (Modbus / AB CIP / S7 / OpcUaClient) + SQL Server | Install | (Hyper-V required; not compatible with TwinCAT runtime — see TwinCAT row below for the workaround) | n/a | Integration host admin |
|
| **Docker Desktop for Windows** | Host for every driver test-fixture simulator (Modbus / AB CIP / S7 / OpcUaClient) + SQL Server | Install | (Hyper-V required; not compatible with TwinCAT runtime — see TwinCAT row below for the workaround) | n/a | Integration host admin |
|
||||||
| **Modbus fixture — `otopcua-pymodbus:3.13.0`** | Modbus driver integration tests | Docker image (local build, see `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/`); 4 compose profiles: `standard` / `dl205` / `mitsubishi` / `s7_1500` | 5020 (non-privileged) | n/a (no auth in protocol) | Developer (per machine) |
|
| **Modbus fixture — `otopcua-pymodbus:3.13.0`** | Modbus driver integration tests | Docker image (local build, see `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/`); 4 compose profiles: `standard` / `dl205` / `mitsubishi` / `s7_1500` | 5020 (non-privileged) | n/a (no auth in protocol) | Developer (per machine) |
|
||||||
| **AB CIP fixture — `otopcua-ab-server:libplctag-release`** | AB CIP driver integration tests | Docker image (multi-stage build of libplctag's `ab_server` from source, pinned to the `release` tag; see `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/`); 4 compose profiles: `controllogix` / `compactlogix` / `micro800` / `guardlogix` | 44818 (CIP / EtherNet/IP) | n/a | Developer (per machine) |
|
| **AB CIP fixture — `otopcua-ab-server:libplctag-release`** | AB CIP driver integration tests | Docker image (multi-stage build of libplctag's `ab_server` from source, pinned to the `release` tag; see `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/`); 4 compose profiles: `controllogix` / `compactlogix` / `micro800` / `guardlogix` | 44818 (CIP / EtherNet/IP) | n/a | Developer (per machine) |
|
||||||
| **S7 fixture — `otopcua-python-snap7:1.0`** | S7 driver integration tests | Docker image (local build, `python-snap7>=2.0`; see `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/`); 1 compose profile: `s7_1500` | 1102 (non-privileged; driver honours `S7DriverOptions.Port`) | n/a | Developer (per machine) |
|
| **S7 fixture — `otopcua-python-snap7:1.0`** | S7 driver integration tests | Docker image (local build, `python-snap7>=2.0`; see `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/`); 1 compose profile: `s7_1500` | 1102 (non-privileged; driver honours `S7DriverOptions.Port`) | n/a | Developer (per machine) |
|
||||||
| **OPC UA Client fixture — `mcr.microsoft.com/iotedge/opc-plc:2.14.10`** | OpcUaClient driver integration tests | Docker image (Microsoft-maintained, pinned; see `tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/`) | 50000 (OPC UA) | Anonymous (`--daa` off); auto-accept certs (`--aa`) | Developer (per machine) |
|
| **OPC UA Client fixture — `mcr.microsoft.com/iotedge/opc-plc:2.14.10`** | OpcUaClient driver integration tests | Docker image (Microsoft-maintained, pinned; see `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/`) | 50000 (OPC UA) | Anonymous (`--daa` off); auto-accept certs (`--aa`) | Developer (per machine) |
|
||||||
| **TwinCAT XAR runtime VM** | TwinCAT ADS testing (per `test-data-sources.md` §5; Beckhoff XAR cannot coexist with Hyper-V on the same OS) | Hyper-V VM with Windows + TwinCAT XAR installed under 7-day renewable trial | 48898 (ADS over TCP) | TwinCAT default route credentials configured per Beckhoff docs | Integration host admin |
|
| **TwinCAT XAR runtime VM** | TwinCAT ADS testing (per `test-data-sources.md` §5; Beckhoff XAR cannot coexist with Hyper-V on the same OS) | Hyper-V VM with Windows + TwinCAT XAR installed under 7-day renewable trial | 48898 (ADS over TCP) | TwinCAT default route credentials configured per Beckhoff docs | Integration host admin |
|
||||||
| **Rockwell Studio 5000 Logix Emulate** | AB CIP golden-box tier — closes UDT / ALMD / AOI / GuardLogix-safety / CompactLogix-ConnectionSize gaps the ab_server simulator can't cover. Loads the L5X project documented at `tests/.../AbCip.IntegrationTests/LogixProject/README.md`. Tests gated on `AB_SERVER_PROFILE=emulate` + `AB_SERVER_ENDPOINT=<ip>:44818`; see `docs/drivers/AbServer-Test-Fixture.md` §Logix Emulate golden-box tier | Windows-only install; **Hyper-V conflict** — can't coexist with Docker Desktop's WSL 2 backend on the same OS, same story as TwinCAT XAR. Runs on a dedicated Windows PC reachable on the LAN | 44818 (CIP / EtherNet/IP) | None required at the CIP layer; Studio 5000 project credentials per Rockwell install | Integration host admin (license + install); Developer (per session — open Emulate, load L5X, click Run) |
|
| **Rockwell Studio 5000 Logix Emulate** | AB CIP golden-box tier — closes UDT / ALMD / AOI / GuardLogix-safety / CompactLogix-ConnectionSize gaps the ab_server simulator can't cover. Loads the L5X project documented at `tests/.../AbCip.IntegrationTests/LogixProject/README.md`. Tests gated on `AB_SERVER_PROFILE=emulate` + `AB_SERVER_ENDPOINT=<ip>:44818`; see `docs/drivers/AbServer-Test-Fixture.md` §Logix Emulate golden-box tier | Windows-only install; **Hyper-V conflict** — can't coexist with Docker Desktop's WSL 2 backend on the same OS, same story as TwinCAT XAR. Runs on a dedicated Windows PC reachable on the LAN | 44818 (CIP / EtherNet/IP) | None required at the CIP layer; Studio 5000 project credentials per Rockwell install | Integration host admin (license + install); Developer (per session — open Emulate, load L5X, click Run) |
|
||||||
| **FOCAS TCP stub** (`Driver.Focas.TestStub`) | FOCAS functional testing (per `test-data-sources.md` §6) | Local .NET 10 console app from this repo | 8193 (FOCAS) | n/a | Developer / integration host (run on demand) |
|
| **FOCAS TCP stub** (`Driver.Focas.TestStub`) | FOCAS functional testing (per `test-data-sources.md` §6) | Local .NET 10 console app from this repo | 8193 (FOCAS) | n/a | Developer / integration host (run on demand) |
|
||||||
@@ -165,10 +165,10 @@ init + skip cleanly when nothing's running.
|
|||||||
|
|
||||||
| Driver | Fixture image | Compose file | Bring up |
|
| Driver | Fixture image | Compose file | Bring up |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Modbus | local-build `otopcua-pymodbus:3.13.0` | `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile <standard\|dl205\|mitsubishi\|s7_1500> up -d` |
|
| Modbus | local-build `otopcua-pymodbus:3.13.0` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile <standard\|dl205\|mitsubishi\|s7_1500> up -d` |
|
||||||
| AB CIP | local-build `otopcua-ab-server:libplctag-release` | `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile <controllogix\|compactlogix\|micro800\|guardlogix> up -d` |
|
| AB CIP | local-build `otopcua-ab-server:libplctag-release` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile <controllogix\|compactlogix\|micro800\|guardlogix> up -d` |
|
||||||
| S7 | local-build `otopcua-python-snap7:1.0` | `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile s7_1500 up -d` |
|
| S7 | local-build `otopcua-python-snap7:1.0` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile s7_1500 up -d` |
|
||||||
| OpcUaClient | `mcr.microsoft.com/iotedge/opc-plc:2.14.10` (pinned) | `tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> up -d` |
|
| OpcUaClient | `mcr.microsoft.com/iotedge/opc-plc:2.14.10` (pinned) | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> up -d` |
|
||||||
|
|
||||||
First build of a local-build image takes 1–5 minutes; subsequent runs use
|
First build of a local-build image takes 1–5 minutes; subsequent runs use
|
||||||
layer cache. `ab_server` is the slowest (multi-stage build clones
|
layer cache. `ab_server` is the slowest (multi-stage build clones
|
||||||
@@ -408,6 +408,49 @@ For production:
|
|||||||
- Per-NodeId credentials in `ClusterNodeCredential` table (per decision #83)
|
- Per-NodeId credentials in `ClusterNodeCredential` table (per decision #83)
|
||||||
- Admin app uses LDAP (no SQL credential at all on the user-facing side)
|
- Admin app uses LDAP (no SQL credential at all on the user-facing side)
|
||||||
|
|
||||||
|
## Service Refresh — `Refresh-Services.ps1`
|
||||||
|
|
||||||
|
The deploy host hosts three NSSM-wrapped services (`MxAccessGw`,
|
||||||
|
`OtOpcUaWonderwareHistorian`, `OtOpcUa`) that consume binaries from
|
||||||
|
`C:\publish\`. After landing changes in either repo, refresh the
|
||||||
|
deployed bits with `scripts\install\Refresh-Services.ps1`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Default invocation (dev rig).
|
||||||
|
& C:\Users\dohertj2\Desktop\lmxopcua\scripts\install\Refresh-Services.ps1
|
||||||
|
|
||||||
|
# Skip the timestamped backup (faster on iterative dev cycles).
|
||||||
|
& Refresh-Services.ps1 -SkipBackup
|
||||||
|
|
||||||
|
# Dry-run — print the actions without doing them.
|
||||||
|
& Refresh-Services.ps1 -WhatIf
|
||||||
|
```
|
||||||
|
|
||||||
|
The script:
|
||||||
|
|
||||||
|
1. Stops services in reverse-dependency order (`OtOpcUa` →
|
||||||
|
`OtOpcUaWonderwareHistorian` → `MxAccessGw`) and force-kills
|
||||||
|
any residual processes.
|
||||||
|
2. Snapshots the existing `C:\publish\mxaccessgw\` and
|
||||||
|
`C:\publish\lmxopcua\` trees to `C:\publish\.backup-<timestamp>\`
|
||||||
|
for rollback (skip with `-SkipBackup`).
|
||||||
|
3. Builds + copies mxaccessgw worker (x86 net48) + server (net10.0)
|
||||||
|
binaries from the sibling repo.
|
||||||
|
4. `dotnet publish`-es the OtOpcUa server + Wonderware historian
|
||||||
|
sidecar from this repo.
|
||||||
|
5. Ensures `OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true` is set on
|
||||||
|
the historian service env block (PR C.2 toggle).
|
||||||
|
6. Starts services in forward-dependency order (`MxAccessGw` →
|
||||||
|
`OtOpcUaWonderwareHistorian` → `OtOpcUa`).
|
||||||
|
7. Smoke-verifies — service status, listening ports (5120 / 4840 /
|
||||||
|
4841), recent log tails.
|
||||||
|
|
||||||
|
Functional verification (alarm raise / scripted alarm historian
|
||||||
|
round-trip / sub-attribute fallback) is the operator's next step
|
||||||
|
after the refresh; see
|
||||||
|
[docs/plans/alarms-over-gateway.md](../plans/alarms-over-gateway.md)
|
||||||
|
§Track D for the scenarios.
|
||||||
|
|
||||||
## Test Data Seed
|
## Test Data Seed
|
||||||
|
|
||||||
Each environment needs a baseline data set so cross-developer tests are reproducible. Lives in `tests/ZB.MOM.WW.OtOpcUa.IntegrationTests/SeedData/`:
|
Each environment needs a baseline data set so cross-developer tests are reproducible. Lives in `tests/ZB.MOM.WW.OtOpcUa.IntegrationTests/SeedData/`:
|
||||||
|
|||||||
+59
-283
@@ -10,289 +10,65 @@
|
|||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
Out-of-process **Tier C** driver bridging AVEVA System Platform (Wonderware) Galaxies. The existing v1 implementation is refactored behind the new driver capability interfaces and hosted in a separate Windows service (.NET 4.8 x86) that communicates with the main OtOpcUa server (.NET 10 x64) via named pipes + MessagePack. Hosted out-of-process for **two reasons**: COM/.NET 4.8 x86 bitness constraint **and** Tier C stability isolation (per `driver-stability.md`). FOCAS is the second Tier C driver, also out-of-process — see §7.
|
Galaxy (MXAccess) is a **Tier-A in-process driver** that runs in the OtOpcUa server's .NET 10 AnyCPU process and speaks gRPC to a separately installed `mxaccessgw` (sibling repo at `c:\Users\dohertj2\Desktop\mxaccessgw\`). The gateway owns the MXAccess COM apartment, the STA pump, and the Galaxy Repository / Historian SDK on its own host; the driver itself is platform-agnostic and carries no COM or x86 bitness constraint. Project lives at `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/`.
|
||||||
|
|
||||||
### Library & Dependencies
|
### Capability Surface
|
||||||
|
|
||||||
| Component | Package / Source | Version | Target | Notes |
|
`GalaxyDriver` (in `GalaxyDriver.cs`) implements `IDriver`, `IDisposable`, plus six driver capabilities — eight interfaces total.
|
||||||
|-----------|------------------|---------|--------|-------|
|
|
||||||
| **MXAccess COM** | `ArchestrA.MxAccess` (GAC / `lib/ArchestrA.MxAccess.dll`) | version-neutral late-bound | .NET 4.8 x86 | Pinned via `<Reference Include="ArchestrA.MxAccess">` with `EmbedInteropTypes=false`; interfaces: `LMXProxyServer`, `ILMXProxyServerEvents`, `MXSTATUS_PROXY` |
|
| Capability | Source files |
|
||||||
| **Galaxy DB client** | `System.Data.SqlClient` (BCL) | BCL | .NET 4.8 x86 | Direct SQL for hierarchy/attribute/change-detection queries |
|
|------------|--------------|
|
||||||
| **Wonderware Historian SDK** | `aahClientManaged`, `aahClientCommon` | Historian-shipped | .NET 4.8 x86 | Optional — loaded only when `Historian.Enabled=true` |
|
| `ITagDiscovery` | `Browse/GalaxyDiscoverer.cs`, `Browse/GatewayGalaxyHierarchySource.cs`, `Browse/DataTypeMap.cs`, `Browse/SecurityMap.cs`, `Browse/AlarmRefBuilder.cs` |
|
||||||
| **MessagePack-CSharp** | `MessagePack` NuGet | 2.x | .NET Standard 2.0 (Shared) | IPC serialization; shared contract between Proxy and Host |
|
| `IRediscoverable` | `Browse/DeployWatcher.cs`, `Browse/GatewayGalaxyDeployWatchSource.cs` |
|
||||||
| **Named pipes** | `System.IO.Pipes` (BCL) | BCL | both sides | IPC transport, localhost only |
|
| `IReadable` | `Runtime/GalaxyMxSession.cs`, `Runtime/MxValueDecoder.cs`, `Runtime/StatusCodeMap.cs` |
|
||||||
|
| `IWritable` | `Runtime/GatewayGalaxyDataWriter.cs` (+ `TracedGalaxyDataWriter.cs`), `Runtime/MxValueEncoder.cs` |
|
||||||
### Required Components
|
| `ISubscribable` | `Runtime/GatewayGalaxySubscriber.cs` (+ `TracedGalaxySubscriber.cs`), `Runtime/EventPump.cs`, `Runtime/SubscriptionRegistry.cs`, `Runtime/ReconnectSupervisor.cs` |
|
||||||
|
| `IHostConnectivityProbe` | `Health/HostStatusAggregator.cs`, `Health/HostConnectivityForwarder.cs`, `Health/PerPlatformProbeWatcher.cs` |
|
||||||
- **AVEVA System Platform / ArchestrA Platform** deployed on the same machine as `Galaxy.Host` (installs MXAccess COM objects into the GAC)
|
|
||||||
- A **deployed Galaxy** with at least one $WinPlatform object hosting $AppEngine(s) hosting AutomationObjects
|
History reads + alarm condition tracking now live in the server-layer `IHistoryRouter` and `AlarmConditionService` (PR 7.2). Galaxy no longer carries `IHistoryProvider` or `IAlarmSource` of its own.
|
||||||
- **SQL Server** reachable from `Galaxy.Host` with the Galaxy repository database (default `ZB`); Windows Auth by default
|
|
||||||
- **32-bit .NET Framework 4.8** runtime on the Host machine (MXAccess is 32-bit COM, no 64-bit variant)
|
### DriverConfig JSON shape
|
||||||
- **STA thread + Win32 message pump** inside the Host process for all COM calls and event callbacks (see §13)
|
|
||||||
- **Wonderware Historian** installed on-box or reachable via aah SDK — *only* if HDA is enabled
|
Per `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Config/GalaxyDriverOptions.cs`:
|
||||||
- **No external firewall ports** — MXAccess is local-machine COM/IPC; pipe is localhost-only. Galaxy DB port (default SQL 1433) if the ZB database is remote.
|
|
||||||
|
```jsonc
|
||||||
### Connection Settings (per driver instance, from central config DB)
|
{
|
||||||
|
"Gateway": {
|
||||||
All settings live under a schemaless `DriverConfig` JSON blob on the `DriverInstance` row. Current v1 equivalents (defaults and source file references in parentheses):
|
"Endpoint": "http://localhost:5120",
|
||||||
|
"ApiKeySecretRef": "secret:galaxy-gw-api-key",
|
||||||
**MXAccess** (`MxAccessConfiguration.cs`):
|
"UseTls": true,
|
||||||
|
"CaCertificatePath": null,
|
||||||
| Setting | Type | Default | Description |
|
"ConnectTimeoutSeconds": 10,
|
||||||
|---------|------|---------|-------------|
|
"DefaultCallTimeoutSeconds": 30,
|
||||||
| `ClientName` | string | `"LmxOpcUa"` | Registration name passed to `LMXProxyServer.Register()` |
|
"StreamTimeoutSeconds": 0
|
||||||
| `NodeName` | string? | `null` | Optional ArchestrA node override (null = local) |
|
},
|
||||||
| `GalaxyName` | string? | `null` | Optional Galaxy name override |
|
"MxAccess": {
|
||||||
| `ReadTimeoutSeconds` | int | `5` | Per-read timeout |
|
"ClientName": "OtOpcUa",
|
||||||
| `WriteTimeoutSeconds` | int | `5` | Per-write timeout |
|
"PublishingIntervalMs": 1000,
|
||||||
| `RequestTimeoutSeconds` | int | `30` | Outer safety timeout around any MXAccess request |
|
"WriteUserId": 0,
|
||||||
| `MaxConcurrentOperations` | int | `10` | Pool bound on in-flight MXAccess work items |
|
"EventPumpChannelCapacity": 50000
|
||||||
| `MonitorIntervalSeconds` | int | `5` | Connectivity heartbeat probe interval |
|
},
|
||||||
| `AutoReconnect` | bool | `true` | Replay stored subscriptions on COM reconnect |
|
"Repository": {
|
||||||
| `ProbeTag` | string? | `null` | Optional heartbeat tag for health monitoring |
|
"DiscoverPageSize": 5000,
|
||||||
| `ProbeStaleThresholdSeconds` | int | `60` | Mark connection stale if no probe callback within |
|
"WatchDeployEvents": true
|
||||||
| `RuntimeStatusProbesEnabled` | bool | `true` | Auto-subscribe `ScanState` for $WinPlatform / $AppEngine |
|
},
|
||||||
| `RuntimeStatusUnknownTimeoutSeconds` | int | `15` | Grace period before an un-probed host is assumed Stopped |
|
"Reconnect": {
|
||||||
|
"InitialBackoffMs": 500,
|
||||||
**Galaxy repository** (`GalaxyRepositoryConfiguration.cs`):
|
"MaxBackoffMs": 30000,
|
||||||
|
"ReplayOnSessionLost": true
|
||||||
| Setting | Type | Default | Description |
|
}
|
||||||
|---------|------|---------|-------------|
|
}
|
||||||
| `ConnectionString` | string | `Server=localhost;Database=ZB;Integrated Security=true;` | ZB SQL Server connection |
|
```
|
||||||
| `ChangeDetectionIntervalSeconds` | int | `30` | Poll interval for `galaxy.time_of_last_deploy` |
|
|
||||||
| `CommandTimeoutSeconds` | int | `30` | SQL command timeout |
|
`Gateway.ApiKeySecretRef` resolves through the server-side secret store (DPAPI in production, env override in dev) — the API key never appears in cleartext config. `MxAccess.ClientName` MUST be unique per OtOpcUa instance; redundancy pairs enforce uniqueness at install time. `StreamTimeoutSeconds = 0` keeps the `StreamEvents` RPC alive for the lifetime of the driver.
|
||||||
| `ExtendedAttributes` | bool | `false` | Include extended attribute metadata in discovery |
|
|
||||||
| `Scope` | enum (`Galaxy` \| `LocalPlatform`) | `Galaxy` | Address-space scope filter (commit bc282b6) |
|
### Performance, tracing, soak
|
||||||
| `PlatformName` | string? | `Environment.MachineName` | Platform to scope to when `Scope=LocalPlatform` |
|
|
||||||
|
See [Galaxy.Performance.md](Galaxy.Performance.md) for the OpenTelemetry trace map, the per-RPC metric set (`galaxy.events.dropped`, channel headroom, reconnect backoff distribution), and the soak-run profile.
|
||||||
**IPC** (new for v2):
|
|
||||||
|
### Parity rig + gateway setup
|
||||||
| Setting | Type | Default | Description |
|
|
||||||
|---------|------|---------|-------------|
|
See [Galaxy.ParityRig.md](Galaxy.ParityRig.md) and the `mxaccessgw` repo for the gateway worker layout and the dev-rig recipe.
|
||||||
| `PipeName` | string | `otopcua-galaxy-{InstanceId}` | Named pipe name |
|
|
||||||
| `HostStartupTimeoutMs` | int | `30000` | Proxy wait for Host `Ready` handshake |
|
|
||||||
| `IpcCallTimeoutMs` | int | `15000` | Per-call RPC timeout |
|
|
||||||
|
|
||||||
### Addressing
|
|
||||||
|
|
||||||
Galaxy objects carry two names:
|
|
||||||
|
|
||||||
- **`contained_name`** — human-readable, scoped to parent; used for OPC UA browse tree
|
|
||||||
- **`tag_name`** — globally unique system identifier; used for MXAccess runtime references
|
|
||||||
|
|
||||||
| Layer | Example |
|
|
||||||
|-------|---------|
|
|
||||||
| OPC UA browse path | `TestMachine_001/DelmiaReceiver/DownloadPath` |
|
|
||||||
| OPC UA NodeId | `ns=<galaxyNs>;s=<tagName>.<AttributeName>` |
|
|
||||||
| MXAccess reference | `DelmiaReceiver_001.DownloadPath` (passed to `AddItem()`) |
|
|
||||||
|
|
||||||
Tag discovery is **dynamic** — driven by the Galaxy repository DB (`gobject`, `dynamic_attribute`, `primitive_instance`, `template_definition`). Optional `Scope=LocalPlatform` filters the hierarchy via the `hosted_by_gobject_id` chain to the subtree rooted at the local $WinPlatform (on a dev Galaxy: 49→3 objects, 4206→386 attributes).
|
|
||||||
|
|
||||||
### Data Type Mapping (`MxDataTypeMapper.cs`, `gr/data_type_mapping.md`)
|
|
||||||
|
|
||||||
| mx_data_type | Galaxy Type | OPC UA BuiltInType | CLR Type |
|
|
||||||
|--------------|-------------|--------------------|----------|
|
|
||||||
| 1 | Boolean | Boolean (i=1) | `bool` |
|
|
||||||
| 2 | Integer | Int32 (i=6) | `int` |
|
|
||||||
| 3 | Float | Float (i=10) | `float` |
|
|
||||||
| 4 | Double | Double (i=11) | `double` |
|
|
||||||
| 5 | String | String (i=12) | `string` |
|
|
||||||
| 6 | Time | DateTime (i=13) | `DateTime` |
|
|
||||||
| 7 | ElapsedTime | Double (i=11) | `double` (seconds) |
|
|
||||||
| 8 | Reference | String (i=12) | `string` |
|
|
||||||
| 13 | Enumeration | Int32 (i=6) | `int` |
|
|
||||||
| 14 / 16 | Custom | String (i=12) | `string` |
|
|
||||||
| 15 | InternationalizedString | LocalizedText (i=21) | `string` |
|
|
||||||
| (default) | Unknown | String (i=12) | `string` |
|
|
||||||
|
|
||||||
**Arrays**: `is_array=0` → ValueRank `-1` (Scalar); `is_array=1` → ValueRank `1` (OneDimension), ArrayDimensions = `[array_dimension]`.
|
|
||||||
|
|
||||||
### Security Classification Mapping (`SecurityClassificationMapper.cs`)
|
|
||||||
|
|
||||||
| security_classification | Galaxy Level | OPC UA Write Permission |
|
|
||||||
|-------------------------|--------------|-------------------------|
|
|
||||||
| 0 | FreeAccess | `WriteOperate` |
|
|
||||||
| 1 | Operate | `WriteOperate` |
|
|
||||||
| 2 | SecuredWrite | — (read-only in v1) |
|
|
||||||
| 3 | VerifiedWrite | — (read-only in v1) |
|
|
||||||
| 4 | Tune | `WriteTune` |
|
|
||||||
| 5 | Configure | `WriteConfigure` |
|
|
||||||
| 6 | ViewOnly | — (read-only) |
|
|
||||||
|
|
||||||
Maps to the OPC UA roles `ReadOnly` / `WriteOperate` / `WriteTune` / `WriteConfigure` defined in the LDAP role provider (see `docs/security.md`).
|
|
||||||
|
|
||||||
### Subscription Model — Native MXAccess Advisories
|
|
||||||
|
|
||||||
**Galaxy is one of three drivers with native subscriptions (Galaxy, TwinCAT, OPC UA Client).** No polling.
|
|
||||||
|
|
||||||
- Mechanism: `LMXProxyServer.AddItem()` → `AdviseSupervisory(handle, itemHandle)`; callbacks delivered through the `ILMXProxyServerEvents.OnDataChange` COM event
|
|
||||||
- Callback signature: `MxDataChangeHandler(itemHandle, MXSTATUS_PROXY, value, quality, timestamp)`
|
|
||||||
- Dispatch: STA COM event → dispatch-thread queue → OPC UA `ClearChangeMasks` fan-out (decouples COM thread from UA stack lock — commit c76ab8f)
|
|
||||||
- **Stored subscriptions** replayed on reconnect via `ReplayStoredSubscriptionsAsync()`
|
|
||||||
- **Probe tag** + runtime-status probes provide connection-health visibility (see §14)
|
|
||||||
- **Bad-quality fan-out**: when a host ($WinPlatform or $AppEngine) ScanState transitions to Stopped, every attribute under that host is immediately published as `BadOutOfService` (commits 7310925, c76ab8f)
|
|
||||||
|
|
||||||
### Alarm Model
|
|
||||||
|
|
||||||
In-process alarm-condition tracking (v1 baseline; extended in v2 to match `IAlarmSource`):
|
|
||||||
|
|
||||||
- **Auto-subscribed attributes per alarm-eligible object**: `InAlarm`, `Priority`, `Description` (cached for severity and message)
|
|
||||||
- **Filtering**: `AlarmFilterConfiguration.ObjectFilters[]` — include/exclude by template chain (empty = all eligible)
|
|
||||||
- **Transitions**: `InAlarm` change → OPC UA A&C `AlarmConditionState` event (Active / Return to Normal)
|
|
||||||
- **Severity**: Galaxy `Priority` (1 = highest) mapped to OPC UA 1–1000 severity (higher = more severe)
|
|
||||||
- **Acknowledgment**: local OPC UA ack forwards to MXAccess write on the `Ack` attribute of the alarm-bearing object
|
|
||||||
|
|
||||||
### History Model — Wonderware Historian (optional plugin)
|
|
||||||
|
|
||||||
- Loaded **at runtime** from `ZB.MOM.WW.LmxOpcUa.Historian.Aveva.dll` when `Historian.Enabled=true`; compile-time optional
|
|
||||||
- SDK: `aahClientManaged` / `aahClientCommon`
|
|
||||||
- Supported OPC UA HDA calls:
|
|
||||||
- `HistoryReadRawModified` (raw values with bounds)
|
|
||||||
- `HistoryReadProcessed` (Historian aggregates: AVG, MIN, MAX, TIMEAVG, etc. — mapped to OPC UA aggregates)
|
|
||||||
- Continuation points for paged reads
|
|
||||||
- Only attributes flagged `historize=1` in the Galaxy DB expose `AccessLevel.HistoryRead`
|
|
||||||
|
|
||||||
### Error Mapping — MXAccess → Quality → OPC UA StatusCode
|
|
||||||
|
|
||||||
**Byte quality (OPC DA convention)** — `QualityMapper.cs`:
|
|
||||||
|
|
||||||
| OPC DA Quality | Category |
|
|
||||||
|----------------|----------|
|
|
||||||
| `>= 192` | Good |
|
|
||||||
| `64–191` | Uncertain |
|
|
||||||
| `< 64` | Bad |
|
|
||||||
|
|
||||||
**MXAccess error codes → Quality** (`MxErrorCodes.cs`):
|
|
||||||
|
|
||||||
| Code | Name | Quality |
|
|
||||||
|------|------|---------|
|
|
||||||
| 1008 | `MX_E_InvalidReference` | `BadConfigError` |
|
|
||||||
| 1012 | `MX_E_WrongDataType` | `BadConfigError` |
|
|
||||||
| 1013 | `MX_E_NotWritable` | `BadOutOfService` |
|
|
||||||
| 1014 | `MX_E_RequestTimedOut` | `BadCommFailure` |
|
|
||||||
| 1015 | `MX_E_CommFailure` | `BadCommFailure` |
|
|
||||||
| 1016 | `MX_E_NotConnected` | `BadNotConnected` |
|
|
||||||
|
|
||||||
**Quality → OPC UA StatusCode** (`QualityMapper.cs`):
|
|
||||||
|
|
||||||
| Quality | StatusCode |
|
|
||||||
|---------|-----------|
|
|
||||||
| Good | `0x00000000` |
|
|
||||||
| GoodLocalOverride | `0x00D80000` |
|
|
||||||
| Uncertain | `0x40000000` |
|
|
||||||
| Bad (generic) | `0x80000000` |
|
|
||||||
| BadCommFailure | `0x80050000` |
|
|
||||||
| BadNotConnected | `0x808A0000` |
|
|
||||||
| BadOutOfService | `0x808D0000` |
|
|
||||||
|
|
||||||
### Change Detection
|
|
||||||
|
|
||||||
- `ChangeDetectionService` polls `galaxy.time_of_last_deploy` at `ChangeDetectionIntervalSeconds` (default 30s)
|
|
||||||
- On timestamp change, `OnGalaxyChanged` fires → Host re-queries hierarchy/attributes → emits `TagSetChanged` over IPC → Proxy implements `IRediscoverable` and rebuilds the affected subtree in the address space
|
|
||||||
- Platform-scope filter (commit bc282b6) applied during hierarchy load when `Scope=LocalPlatform`
|
|
||||||
|
|
||||||
### IPC Contract (Proxy ↔ Host) — `Galaxy.Shared`
|
|
||||||
|
|
||||||
.NET Standard 2.0 MessagePack contracts. Every request carries a correlation ID; responses carry the same ID plus success/error.
|
|
||||||
|
|
||||||
**Lifecycle / handshake**:
|
|
||||||
|
|
||||||
| Message | Direction | Payload |
|
|
||||||
|---------|-----------|---------|
|
|
||||||
| `ClientHello` | Proxy → Host | InstanceId, expected protocol version |
|
|
||||||
| `HostReady` | Host → Proxy | Host version, Galaxy name, capabilities |
|
|
||||||
| `Shutdown` | Proxy → Host | Graceful stop |
|
|
||||||
|
|
||||||
**Tag discovery** (`ITagDiscovery`):
|
|
||||||
|
|
||||||
| Message | Direction | Payload |
|
|
||||||
|---------|-----------|---------|
|
|
||||||
| `DiscoverHierarchyRequest` | Proxy → Host | `Scope`, `PlatformName` |
|
|
||||||
| `DiscoverHierarchyResponse` | Host → Proxy | `GalaxyObjectInfo[]` (TagName, ContainedName, ParentTagName, TemplateChain, category) |
|
|
||||||
| `DiscoverAttributesRequest` | Proxy → Host | `TagName[]` |
|
|
||||||
| `DiscoverAttributesResponse` | Host → Proxy | `GalaxyAttributeInfo[]` (Name, MxDataType, IsArray, ArrayDim, SecurityClass, Historized, WriteableRuntimeChecked) |
|
|
||||||
| `TagSetChangedNotification` | Host → Proxy | New deploy timestamp; triggers re-discover |
|
|
||||||
|
|
||||||
**Read / Write** (`IReadable`, `IWritable`):
|
|
||||||
|
|
||||||
| Message | Direction | Payload |
|
|
||||||
|---------|-----------|---------|
|
|
||||||
| `ReadRequest` | Proxy → Host | `TagRef[]` (tag_name + attribute) |
|
|
||||||
| `ReadResponse` | Host → Proxy | `VtqPayload[]` (value, quality, timestamp, statusCode) |
|
|
||||||
| `WriteRequest` | Proxy → Host | `(TagRef, Value, ExpectedDataType)[]` |
|
|
||||||
| `WriteResponse` | Host → Proxy | `(TagRef, StatusCode)[]` |
|
|
||||||
|
|
||||||
**Subscription** (`ISubscribable`):
|
|
||||||
|
|
||||||
| Message | Direction | Payload |
|
|
||||||
|---------|-----------|---------|
|
|
||||||
| `SubscribeRequest` | Proxy → Host | `TagRef[]` + Proxy-generated subscription ID |
|
|
||||||
| `SubscribeResponse` | Host → Proxy | Per-tag subscribe ack + handle |
|
|
||||||
| `UnsubscribeRequest` | Proxy → Host | handles |
|
|
||||||
| `DataChangeNotification` | Host → Proxy (push) | handle, VTQ, sequence number |
|
|
||||||
| `ProbeHealthNotification` | Host → Proxy (push) | probe tag staleness, `ScanState` transitions, overall connected/disconnected |
|
|
||||||
|
|
||||||
**Alarms** (`IAlarmSource`):
|
|
||||||
|
|
||||||
| Message | Direction | Payload |
|
|
||||||
|---------|-----------|---------|
|
|
||||||
| `AlarmEventNotification` | Host → Proxy (push) | source tag, InAlarm, Priority, Description, severity, transition type |
|
|
||||||
| `AlarmAckRequest` | Proxy → Host | source tag, user, comment |
|
|
||||||
|
|
||||||
**History** (`IHistoryProvider`):
|
|
||||||
|
|
||||||
| Message | Direction | Payload |
|
|
||||||
|---------|-----------|---------|
|
|
||||||
| `HistoryReadRawRequest` | Proxy → Host | TagRef, start, end, numValues, returnBounds, continuationPoint |
|
|
||||||
| `HistoryReadRawResponse` | Host → Proxy | values + next continuation point |
|
|
||||||
| `HistoryReadProcessedRequest` | Proxy → Host | TagRef, aggregateId, start, end, resampleInterval |
|
|
||||||
| `HistoryReadProcessedResponse` | Host → Proxy | aggregated values |
|
|
||||||
|
|
||||||
**Framing**: length-prefixed MessagePack frames over a single `NamedPipeServerStream` in `PipeTransmissionMode.Byte`. Separate outgoing pipe for push notifications or multiplex via message type tag.
|
|
||||||
|
|
||||||
### Threading / COM Constraints
|
|
||||||
|
|
||||||
- **STA thread** (`StaComThread.cs`) hosts MXAccess: `ApartmentState.STA`, raw Win32 `GetMessage` / `DispatchMessage` loop
|
|
||||||
- Work items marshaled in via `PostThreadMessage(WM_APP=0x8000)`
|
|
||||||
- **Per-handle serialization**: LMXProxyServer is not thread-safe — all Read/Write/Subscribe calls on one handle run serially via the STA queue
|
|
||||||
- **Dispatch thread** (separate from STA thread) drains `_pendingDataChanges` to the OPC UA framework; decouples the STA pump from UA stack locks so a slow subscriber can't back up COM event delivery
|
|
||||||
- **Reentrancy guards** — event unwiring must precede `Marshal.ReleaseComObject()` on disconnect
|
|
||||||
|
|
||||||
### Runtime Status (recent commits bc282b6 / 4b209f6 / 7310925 / c76ab8f / 0003984)
|
|
||||||
|
|
||||||
- `GalaxyRuntimeProbeManager` auto-subscribes `<ObjectName>.ScanState` for every $WinPlatform (category 1) and $AppEngine (category 3) in scope
|
|
||||||
- Per-host state machine: `Unknown → Running | Stopped`; transitions fire `_onHostStopped` / `_onHostRunning` callbacks on the dispatch thread
|
|
||||||
- **Synthetic OPC UA nodes** expose `ScanState` per host as read-only variables so clients see runtime topology without the dashboard
|
|
||||||
- **HealthCheck Rule 2e** monitors probe subscription health; a failed probe can no longer leave phantom entries that fan out false `BadOutOfService`
|
|
||||||
- Generalizes to the driver-agnostic `IHostConnectivityProbe` capability interface in v2 (see `plan.md` §5a)
|
|
||||||
|
|
||||||
### Implementation Notes
|
|
||||||
|
|
||||||
- **First Tier C out-of-process driver** — uses the `Galaxy.Proxy` / `Galaxy.Host` / `Galaxy.Shared` three-project split. The pattern is reusable; FOCAS is the second adopter (see §7), and any future driver with bitness, licensing, or stability-isolation needs reuses the same template. See `driver-stability.md` for the generalized contract
|
|
||||||
- `Galaxy.Proxy` (in the main server) implements `IDriver`, `ITagDiscovery`, `IRediscoverable`, `IReadable`, `IWritable`, `ISubscribable`, `IAlarmSource`, `IHistoryProvider`, `IHostConnectivityProbe`
|
|
||||||
- `Galaxy.Host` owns `MxAccessBridge`, `GalaxyRepository`, alarm tracking, `GalaxyRuntimeProbeManager`, and the Historian plugin — no reference to `Core.Abstractions`
|
|
||||||
- `Galaxy.Shared` is .NET Standard 2.0, referenced by both sides
|
|
||||||
- Existing v1 code is the implementation — **refactor in place** (extract capability interfaces first, then move behind IPC — see `plan.md` Decision #55)
|
|
||||||
- **Parity gate**: v2 driver must pass v1 `IntegrationTests` suite + scripted Client.CLI walkthrough before Phase 3 begins
|
|
||||||
|
|
||||||
### Operational Stability Notes
|
|
||||||
|
|
||||||
Galaxy has a Tier C deep dive in `driver-stability.md` covering the STA pump, COM object lifetime, subscription replay, recycle policy, and post-mortem contents. Driver-instance specifics:
|
|
||||||
|
|
||||||
- **Memory baseline scales with Galaxy size**. Watchdog floor of 200 MB above baseline + 1.5 GB hard ceiling — higher than FOCAS because legitimate Galaxy footprints are larger.
|
|
||||||
- **Slope tolerance is 5 MB/min** (more permissive than FOCAS) because address-space rebuild on redeploy can transiently allocate large amounts.
|
|
||||||
- **Known regression-prone failure modes** (closed in commits `c76ab8f` and `7310925`, must remain closed): phantom probe subscription flipping Tick() to Stopped; cross-host quality clear wiping sibling state during recovery; sync-over-async on the OPC UA stack thread; fire-and-forget alarm tasks racing shutdown. Each should have a regression test in the v2 parity suite.
|
|
||||||
- **STA pump health probe** every 10 s (separate from the proxy↔host heartbeat). A wedged pump is the most likely Tier C failure mode for Galaxy.
|
|
||||||
- **Recycle preserves cached `time_of_last_deploy` watermark** — the common case (crash unrelated to redeploy) skips full DB rediscovery for faster recovery.
|
|
||||||
|
|
||||||
### Namespace Assignment
|
|
||||||
|
|
||||||
Galaxy is the canonical **SystemPlatform-kind namespace** driver. It exposes Aveva System Platform / Galaxy objects as OPC UA — these are *processed* values with business meaning attached at Layer 3, not raw equipment signals. Per `plan.md` §4:
|
|
||||||
|
|
||||||
- The Galaxy driver's `DriverInstance.NamespaceId` must reference a `Namespace` row with `Kind = 'SystemPlatform'`.
|
|
||||||
- **UNS naming rules do NOT apply** to the Galaxy hierarchy. Tags belong to `DriverInstanceId + FolderPath` (v1 LmxOpcUa pattern preserved); `Tag.EquipmentId` is NULL.
|
|
||||||
- The Galaxy hierarchy reflects the gobject parent chain as v1 has always done — no migration to UNS path conventions in v2.
|
|
||||||
- If a future need arises to expose raw Galaxy gobject data alongside processed (e.g. an Aveva-Wonderware Historian raw signal feed), that becomes a *separate* driver instance assigned to an Equipment-kind namespace, with its own per-equipment mapping.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# FOCAS version / capability matrix
|
# FOCAS version / capability matrix
|
||||||
|
|
||||||
Authoritative source for the per-CNC-series ranges that
|
Authoritative source for the per-CNC-series ranges that
|
||||||
[`FocasCapabilityMatrix`](../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs)
|
[`FocasCapabilityMatrix`](../../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs)
|
||||||
enforces at driver init time. Every row cites the Fanuc FOCAS Developer
|
enforces at driver init time. Every row cites the Fanuc FOCAS Developer
|
||||||
Kit function whose documented input range determines the ceiling.
|
Kit function whose documented input range determines the ceiling.
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ matrix: Macro variable #50000 is outside the documented range
|
|||||||
## How this matrix stays honest
|
## How this matrix stays honest
|
||||||
|
|
||||||
- Every row is covered by a parameterized test in
|
- Every row is covered by a parameterized test in
|
||||||
[`FocasCapabilityMatrixTests.cs`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasCapabilityMatrixTests.cs)
|
[`FocasCapabilityMatrixTests.cs`](../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasCapabilityMatrixTests.cs)
|
||||||
— 46 cases across macro / parameter / PMC-letter / PMC-number
|
— 46 cases across macro / parameter / PMC-letter / PMC-number
|
||||||
boundaries + unknown-series permissiveness + rejection-message
|
boundaries + unknown-series permissiveness + rejection-message
|
||||||
content + case-insensitivity.
|
content + case-insensitivity.
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ takes the form of the per-driver test suites + e2e scripts:
|
|||||||
- [x] **Integration tests** — `Driver.*.IntegrationTests` stands up Docker-hosted simulators (pymodbus, ab_server, python-snap7, opc-plc) at collection init and exercises real wire-level read/write/subscribe/probe per driver.
|
- [x] **Integration tests** — `Driver.*.IntegrationTests` stands up Docker-hosted simulators (pymodbus, ab_server, python-snap7, opc-plc) at collection init and exercises real wire-level read/write/subscribe/probe per driver.
|
||||||
- [x] **CLI tests** — `Driver.*.Cli.Tests` covers the per-driver test-client CLIs (#249–#251).
|
- [x] **CLI tests** — `Driver.*.Cli.Tests` covers the per-driver test-client CLIs (#249–#251).
|
||||||
- [x] **E2E scripts** — `scripts/e2e/test-<driver>.ps1` covers the driver-CLI → PLC → OtOpcUa server → OPC UA client round-trip for all seven drivers + Galaxy; `test-all.ps1` aggregates; README status section (rewritten this session) summarises live-boot evidence.
|
- [x] **E2E scripts** — `scripts/e2e/test-<driver>.ps1` covers the driver-CLI → PLC → OtOpcUa server → OPC UA client round-trip for all seven drivers + Galaxy; `test-all.ps1` aggregates; README status section (rewritten this session) summarises live-boot evidence.
|
||||||
- [x] **Factory registration** — all seven factories plus Galaxy register in `src/ZB.MOM.WW.OtOpcUa.Server/Program.cs` inside the `DriverFactoryRegistry` composition; the `DriverInstanceBootstrapper` can materialise any configured row.
|
- [x] **Factory registration** — all seven factories plus Galaxy register in `src/Server/ZB.MOM.WW.OtOpcUa.Server/Program.cs` inside the `DriverFactoryRegistry` composition; the `DriverInstanceBootstrapper` can materialise any configured row.
|
||||||
- [x] **Seed SQL** — #210–#213 provide per-driver Config DB seed scripts so a fresh Config DB is populatable without Admin UI interaction.
|
- [x] **Seed SQL** — #210–#213 provide per-driver Config DB seed scripts so a fresh Config DB is populatable without Admin UI interaction.
|
||||||
|
|
||||||
### Live-boot verification
|
### Live-boot verification
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ Covered by `scripts/compliance/phase-7-compliance.ps1`:
|
|||||||
|
|
||||||
Originally kept out of the capstone so the gate could close cleanly. Each landed as a targeted follow-up PR; audit this session verified them against the repo:
|
Originally kept out of the capstone so the gate could close cleanly. Each landed as a targeted follow-up PR; audit this session verified them against the repo:
|
||||||
|
|
||||||
- [x] **SealedBootstrap composition root** (task #239) — **CLOSED**. `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` instantiates `VirtualTagEngine` + `ScriptedAlarmEngine` via `Phase7EngineComposer.Compose`, and `SqliteStoreAndForwardSink` in `ResolveHistorianSink` when a registered driver provides `IAlarmHistorianWriter` (today: `GalaxyProxyDriver`). `OpcUaServerService.ExecuteAsync` calls `Phase7Composer.PrepareAsync` then `OpcUaApplicationHost.SetPhase7Sources` **before** `applicationHost.StartAsync` so `OtOpcUaServer` + `DriverNodeManager` capture the `VirtualReadable` / `ScriptedAlarmReadable` at construction. 38 tests green under `tests/ZB.MOM.WW.OtOpcUa.Server.Tests/Phase7/` + `SealedBootstrapIntegrationTests`. The work landed under the label "Phase 7 follow-up #246" and was never re-labelled against #239.
|
- [x] **SealedBootstrap composition root** (task #239) — **CLOSED**. `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` instantiates `VirtualTagEngine` + `ScriptedAlarmEngine` via `Phase7EngineComposer.Compose`, and `SqliteStoreAndForwardSink` in `ResolveHistorianSink` when a registered driver provides `IAlarmHistorianWriter` (today: `GalaxyProxyDriver`). `OpcUaServerService.ExecuteAsync` calls `Phase7Composer.PrepareAsync` then `OpcUaApplicationHost.SetPhase7Sources` **before** `applicationHost.StartAsync` so `OtOpcUaServer` + `DriverNodeManager` capture the `VirtualReadable` / `ScriptedAlarmReadable` at construction. 38 tests green under `tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/Phase7/` + `SealedBootstrapIntegrationTests`. The work landed under the label "Phase 7 follow-up #246" and was never re-labelled against #239.
|
||||||
- [x] **Live OPC UA end-to-end smoke** (task #240) — **CLOSED**. `scripts/e2e/test-phase7-virtualtags.ps1` drives a full Client.CLI read of a driver-sourced input, reads the VirtualTag computed off it, triggers a scripted alarm by writing the trigger value, and subscribes to the alarm condition — all through a running OtOpcUa server. Covered in `scripts/e2e/test-all.ps1` + `scripts/e2e/README.md` matrix.
|
- [x] **Live OPC UA end-to-end smoke** (task #240) — **CLOSED**. `scripts/e2e/test-phase7-virtualtags.ps1` drives a full Client.CLI read of a driver-sourced input, reads the VirtualTag computed off it, triggers a scripted alarm by writing the trigger value, and subscribes to the alarm condition — all through a running OtOpcUa server. Covered in `scripts/e2e/test-all.ps1` + `scripts/e2e/README.md` matrix.
|
||||||
- [x] **sp_ComputeGenerationDiff extension** (task #241) — **CLOSED**. Migration `20260420232000_ExtendComputeGenerationDiffWithPhase7.cs` extends the stored proc to emit Script / VirtualTag / ScriptedAlarm sections alongside the existing NodeAcl / Tag / Equipment / DriverInstance / Namespace output. Admin DiffViewer picks them up through its existing section-plugin architecture (Phase 6.4 Stream C).
|
- [x] **sp_ComputeGenerationDiff extension** (task #241) — **CLOSED**. Migration `20260420232000_ExtendComputeGenerationDiffWithPhase7.cs` extends the stored proc to emit Script / VirtualTag / ScriptedAlarm sections alongside the existing NodeAcl / Tag / Equipment / DriverInstance / Namespace output. Admin DiffViewer picks them up through its existing section-plugin architecture (Phase 6.4 Stream C).
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ itself is verifiable without Fwlib32 actually being called:
|
|||||||
assert rejection.
|
assert rejection.
|
||||||
- **Fwlib32 integration itself**: still untestable without hardware.
|
- **Fwlib32 integration itself**: still untestable without hardware.
|
||||||
When a real CNC becomes available, the smoke tests already
|
When a real CNC becomes available, the smoke tests already
|
||||||
scaffolded in `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/`
|
scaffolded in `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/`
|
||||||
run against it via `FOCAS_ENDPOINT`.
|
run against it via `FOCAS_ENDPOINT`.
|
||||||
|
|
||||||
## Decisions to confirm before starting
|
## Decisions to confirm before starting
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
# FOCAS wire protocol — what's authoritative vs. what's guessed
|
# FOCAS wire protocol — what's authoritative vs. what's guessed
|
||||||
|
|
||||||
Companion to [`focas-simulator-plan.md`](focas-simulator-plan.md). Written during
|
Written during Stream B on 2026-04-23 after a research pass through `strangesast/fwlib` +
|
||||||
Stream B on 2026-04-23 after a research pass through `strangesast/fwlib` +
|
|
||||||
public FOCAS documentation. Purpose: separate what we *know* about the FOCAS
|
public FOCAS documentation. Purpose: separate what we *know* about the FOCAS
|
||||||
wire protocol (can quote with confidence) from what we're *guessing* (will need
|
wire protocol (can quote with confidence) from what we're *guessing* (will need
|
||||||
Wireshark traces to validate in Stream C).
|
Wireshark traces to validate in Stream C).
|
||||||
|
|
||||||
This document directly informs `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/server/`.
|
This document directly informs `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/server/`.
|
||||||
|
|
||||||
## Authoritative — from Fanuc's public `fwlib32.h`
|
## Authoritative — from Fanuc's public `fwlib32.h`
|
||||||
|
|
||||||
@@ -270,7 +269,7 @@ mock is already correct. Only the framing layer needs iteration.
|
|||||||
This is the iterative Wireshark loop — no point starting until the Windows rig
|
This is the iterative Wireshark loop — no point starting until the Windows rig
|
||||||
+ licensed Fwlib64.dll + real CNC are all available. See the implementer's
|
+ licensed Fwlib64.dll + real CNC are all available. See the implementer's
|
||||||
checklist in
|
checklist in
|
||||||
[`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/README.md`](../../../tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/README.md).
|
[`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/README.md`](../../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/README.md).
|
||||||
|
|
||||||
### Phase 3 — flip the C# test gate
|
### Phase 3 — flip the C# test gate
|
||||||
|
|
||||||
@@ -284,8 +283,8 @@ Once Phase 2 proves Fwlib64 can talk to the mock:
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [`src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibNative.cs`](../../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibNative.cs) — P/Invoke surface, authoritative struct layouts
|
- [`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibNative.cs`](../../../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibNative.cs) — P/Invoke surface, authoritative struct layouts
|
||||||
- [`src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibFocasClient.cs`](../../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibFocasClient.cs) — reference C# implementation of each FWLIB call
|
- [`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibFocasClient.cs`](../../../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibFocasClient.cs) — reference C# implementation of each FWLIB call
|
||||||
- [`src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs`](../../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs) — EW_* → OPC UA status mapping
|
- [`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs`](../../../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs) — EW_* → OPC UA status mapping
|
||||||
- Fanuc FOCAS Developer Kit (licensed, not in repo) — ultimate source of truth
|
- Fanuc FOCAS Developer Kit (licensed, not in repo) — ultimate source of truth
|
||||||
- `strangesast/fwlib` on GitHub — redistributes `fwlib32.h` + runtime binaries; no wire protocol docs
|
- `strangesast/fwlib` on GitHub — redistributes `fwlib32.h` + runtime binaries; no wire protocol docs
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ Save the result to `docs/v2/implementation/phase-0-rename-inventory.md` (gitigno
|
|||||||
Per project (11 projects total — 5 src + 6 tests):
|
Per project (11 projects total — 5 src + 6 tests):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git mv src/ZB.MOM.WW.LmxOpcUa.Client.CLI src/ZB.MOM.WW.OtOpcUa.Client.CLI
|
git mv src/ZB.MOM.WW.LmxOpcUa.Client.CLI src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI
|
||||||
git mv src/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.LmxOpcUa.Client.CLI.csproj \
|
git mv src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.LmxOpcUa.Client.CLI.csproj \
|
||||||
src/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj
|
src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj
|
||||||
```
|
```
|
||||||
|
|
||||||
Repeat for: `Client.Shared`, `Client.UI`, `Historian.Aveva`, `Host`, and all 6 test projects.
|
Repeat for: `Client.Shared`, `Client.UI`, `Historian.Aveva`, `Host`, and all 6 test projects.
|
||||||
@@ -156,8 +156,8 @@ dotnet test ZB.MOM.WW.OtOpcUa.slnx
|
|||||||
Plus manual smoke test of Client.CLI against a running v1 OPC UA server:
|
Plus manual smoke test of Client.CLI against a running v1 OPC UA server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 2
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 2
|
||||||
```
|
```
|
||||||
|
|
||||||
**Acceptance**:
|
**Acceptance**:
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Phase 1 is large — broken into 5 work streams (A–E) that can partly overlap.
|
|||||||
|
|
||||||
#### Task A.1 — Define driver capability interfaces
|
#### Task A.1 — Define driver capability interfaces
|
||||||
|
|
||||||
Create `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/` (.NET 10, no dependencies). Define:
|
Create `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/` (.NET 10, no dependencies). Define:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public interface IDriver { /* lifecycle, metadata, health */ }
|
public interface IDriver { /* lifecycle, metadata, health */ }
|
||||||
@@ -131,7 +131,7 @@ In v2.0 v1 only registers the `Galaxy` type (`AllowedNamespaceKinds = SystemPlat
|
|||||||
|
|
||||||
#### Task B.1 — EF Core schema + initial migration
|
#### Task B.1 — EF Core schema + initial migration
|
||||||
|
|
||||||
Create `src/ZB.MOM.WW.OtOpcUa.Configuration/` (.NET 10, EF Core 10).
|
Create `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/` (.NET 10, EF Core 10).
|
||||||
|
|
||||||
Implement DbContext with entities matching `config-db-schema.md` exactly:
|
Implement DbContext with entities matching `config-db-schema.md` exactly:
|
||||||
- `ServerCluster`, `ClusterNode`, `ClusterNodeCredential`
|
- `ServerCluster`, `ClusterNode`, `ClusterNodeCredential`
|
||||||
@@ -146,7 +146,7 @@ Implement DbContext with entities matching `config-db-schema.md` exactly:
|
|||||||
Generate the initial migration:
|
Generate the initial migration:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet ef migrations add InitialSchema --project src/ZB.MOM.WW.OtOpcUa.Configuration
|
dotnet ef migrations add InitialSchema --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
**Acceptance**:
|
**Acceptance**:
|
||||||
@@ -338,7 +338,7 @@ If the central DB is unreachable at startup, load the most recent cached generat
|
|||||||
#### Task E.1 — Project scaffold mirroring ScadaLink CentralUI (decision #102)
|
#### Task E.1 — Project scaffold mirroring ScadaLink CentralUI (decision #102)
|
||||||
|
|
||||||
Copy the project layout from `scadalink-design/src/ScadaLink.CentralUI/` (decision #104):
|
Copy the project layout from `scadalink-design/src/ScadaLink.CentralUI/` (decision #104):
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Admin/`: Razor Components project, .NET 10, `AddInteractiveServerComponents`
|
- `src/Server/ZB.MOM.WW.OtOpcUa.Admin/`: Razor Components project, .NET 10, `AddInteractiveServerComponents`
|
||||||
- `Auth/AuthEndpoints.cs`, `Auth/CookieAuthenticationStateProvider.cs`
|
- `Auth/AuthEndpoints.cs`, `Auth/CookieAuthenticationStateProvider.cs`
|
||||||
- `Components/Layout/MainLayout.razor`, `Components/Layout/NavMenu.razor`
|
- `Components/Layout/MainLayout.razor`, `Components/Layout/NavMenu.razor`
|
||||||
- `Components/Pages/Login.razor`, `Components/Pages/Dashboard.razor`
|
- `Components/Pages/Login.razor`, `Components/Pages/Dashboard.razor`
|
||||||
@@ -496,10 +496,10 @@ A `phase-1-compliance.ps1` script that exits non-zero on any failure:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Run all migrations against a clean SQL Server instance
|
# Run all migrations against a clean SQL Server instance
|
||||||
dotnet ef database update --project src/ZB.MOM.WW.OtOpcUa.Configuration --connection "Server=...;Database=OtOpcUaConfig_Test_$(date +%s);..."
|
dotnet ef database update --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration --connection "Server=...;Database=OtOpcUaConfig_Test_$(date +%s);..."
|
||||||
|
|
||||||
# Run schema-introspection tests
|
# Run schema-introspection tests
|
||||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests --filter "Category=SchemaCompliance"
|
dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests --filter "Category=SchemaCompliance"
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: every table, column, index, FK, CHECK, and stored procedure in `config-db-schema.md` is present and matches.
|
Expected: every table, column, index, FK, CHECK, and stored procedure in `config-db-schema.md` is present and matches.
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
> **✅ Completed 2026-04-30 — historical record of Phase 2 (Galaxy out-of-process split).**
|
||||||
|
>
|
||||||
|
> Phase 2 produced the `Galaxy.Host` / `Galaxy.Proxy` / `Galaxy.Shared`
|
||||||
|
> three-project split as a stepping stone toward the eventual mxaccessgw
|
||||||
|
> architecture. Those projects shipped, served their purpose for
|
||||||
|
> roughly a year, then retired in PR 7.2 alongside the
|
||||||
|
> `OtOpcUaGalaxyHost` Windows service. This file is preserved as the
|
||||||
|
> phase-exit evidence; do not treat it as live architecture
|
||||||
|
> documentation. See `docs/drivers/Galaxy.md` for the current
|
||||||
|
> in-process driver.
|
||||||
|
|
||||||
# Phase 2 — Galaxy Out-of-Process Refactor (Tier C)
|
# Phase 2 — Galaxy Out-of-Process Refactor (Tier C)
|
||||||
|
|
||||||
> **Status**: DRAFT — implementation plan for Phase 2 of the v2 build (`plan.md` §6, `driver-stability.md` §"Galaxy — Deep Dive").
|
> **Status**: DRAFT — implementation plan for Phase 2 of the v2 build (`plan.md` §6, `driver-stability.md` §"Galaxy — Deep Dive").
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
> **Status**: **SHIPPED (core + Stream C)** — original body merged 2026-04-19; audit 2026-04-23 promoted **Stream C (task #147)** into shipped state.
|
> **Status**: **SHIPPED (core + Stream C)** — original body merged 2026-04-19; audit 2026-04-23 promoted **Stream C (task #147)** into shipped state.
|
||||||
>
|
>
|
||||||
> **In** (verified in repo):
|
> **In** (verified in repo):
|
||||||
> - Stream A — `ClusterTopologyLoader`, `RedundancyCoordinator`, `RedundancyTopology`, `PeerReachability` all present under `src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`. Coordinator is now also hosted by `Program.cs` via the new `RedundancyPublisherHostedService`, which calls `RefreshAsync` on startup.
|
> - Stream A — `ClusterTopologyLoader`, `RedundancyCoordinator`, `RedundancyTopology`, `PeerReachability` all present under `src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`. Coordinator is now also hosted by `Program.cs` via the new `RedundancyPublisherHostedService`, which calls `RefreshAsync` on startup.
|
||||||
> - Stream B — `ServiceLevelCalculator` + `RecoveryStateManager`.
|
> - Stream B — `ServiceLevelCalculator` + `RecoveryStateManager`.
|
||||||
> - **Stream C (task #147) — OPC UA node wiring**. `ServerRedundancyNodeWriter` maintains `Server.ServiceLevel` (i=2267), `Server.ServerRedundancy.RedundancySupport` (i=2994), and `Server.ServerRedundancy.ServerUriArray` (non-transparent subtype) by writing the `PropertyState.Value` + calling `ClearChangeMasks`. `RedundancyPublisherHostedService` drives the publisher on a 1 s tick and fans `OnStateChanged` / `OnServerUriArrayChanged` into the writer. Mapping of `Configuration.RedundancyMode` → Part 4 `RedundancySupport` is Warm/Hot/None (v2 clusters don't enumerate Cold / HotAndMirrored per decision #85). Idempotent per-value dedupe prevents spurious OPC UA notifications. Unit coverage: `ServerRedundancyNodeWriterTests` (4 tests, green).
|
> - **Stream C (task #147) — OPC UA node wiring**. `ServerRedundancyNodeWriter` maintains `Server.ServiceLevel` (i=2267), `Server.ServerRedundancy.RedundancySupport` (i=2994), and `Server.ServerRedundancy.ServerUriArray` (non-transparent subtype) by writing the `PropertyState.Value` + calling `ClearChangeMasks`. `RedundancyPublisherHostedService` drives the publisher on a 1 s tick and fans `OnStateChanged` / `OnServerUriArrayChanged` into the writer. Mapping of `Configuration.RedundancyMode` → Part 4 `RedundancySupport` is Warm/Hot/None (v2 clusters don't enumerate Cold / HotAndMirrored per decision #85). Idempotent per-value dedupe prevents spurious OPC UA notifications. Unit coverage: `ServerRedundancyNodeWriterTests` (4 tests, green).
|
||||||
> - Stream D — `ApplyLeaseRegistry`.
|
> - Stream D — `ApplyLeaseRegistry`.
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
> **Status**: **SHIPPED (mostly)** 2026-04-19; audit 2026-04-23 confirms what landed separately after the data-layer PR #91:
|
> **Status**: **SHIPPED (mostly)** 2026-04-19; audit 2026-04-23 confirms what landed separately after the data-layer PR #91:
|
||||||
>
|
>
|
||||||
> **In** (verified in repo):
|
> **In** (verified in repo):
|
||||||
> - **Task #153 Stream A UI** — `UnsTab.razor` with drag/drop handlers + concurrent-edit via `DraftRevisionToken` + `UnsImpactAnalyzer`; Playwright smoke test in `tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/UnsTabDragDropE2ETests.cs`.
|
> - **Task #153 Stream A UI** — `UnsTab.razor` with drag/drop handlers + concurrent-edit via `DraftRevisionToken` + `UnsImpactAnalyzer`; Playwright smoke test in `tests/Server/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/UnsTabDragDropE2ETests.cs`.
|
||||||
> - **Task #155 Stream B** — `EquipmentImportBatch` entity + migration, `EquipmentImportBatchService.CreateBatchAsync` / `FinaliseBatchAsync` / `DropBatchAsync` / `ListByUserAsync`, `ImportEquipment.razor` UI.
|
> - **Task #155 Stream B** — `EquipmentImportBatch` entity + migration, `EquipmentImportBatchService.CreateBatchAsync` / `FinaliseBatchAsync` / `DropBatchAsync` / `ListByUserAsync`, `ImportEquipment.razor` UI.
|
||||||
> - **Task #156 Stream C** — `DiffViewer.razor` + `DiffSection.razor` refactor in place.
|
> - **Task #156 Stream C** — `DiffViewer.razor` + `DiffSection.razor` refactor in place.
|
||||||
> - Admin UI `IdentificationFields.razor` surface shipped (part of #157).
|
> - Admin UI `IdentificationFields.razor` surface shipped (part of #157).
|
||||||
>
|
>
|
||||||
> **Closed this session (2026-04-23)**:
|
> **Closed this session (2026-04-23)**:
|
||||||
> - **Task #157 Stream D server-side half** was a stale audit claim. `src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/IdentificationFolderBuilder.cs` ships the OPC 40010 Identification sub-folder materializer (Manufacturer / Model / SerialNumber / HardwareRevision / SoftwareRevision / YearOfConstruction / AssetLocation / ManufacturerUri / DeviceManualUri); `EquipmentNodeWalker.Walk` calls it per equipment; `IdentificationFolderBuilderTests` (158 lines) + two walker-level tests (`Walk_Materializes_Identification_Subfolder_When_AnyFieldPresent`, `Walk_Omits_Identification_Subfolder_When_AllFieldsNull`) cover the null-handling branches. The initial audit grepped only `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/`; the builder lives in `Core/OpcUa/`.
|
> - **Task #157 Stream D server-side half** was a stale audit claim. `src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/IdentificationFolderBuilder.cs` ships the OPC 40010 Identification sub-folder materializer (Manufacturer / Model / SerialNumber / HardwareRevision / SoftwareRevision / YearOfConstruction / AssetLocation / ManufacturerUri / DeviceManualUri); `EquipmentNodeWalker.Walk` calls it per equipment; `IdentificationFolderBuilderTests` (158 lines) + two walker-level tests (`Walk_Materializes_Identification_Subfolder_When_AnyFieldPresent`, `Walk_Omits_Identification_Subfolder_When_AllFieldsNull`) cover the null-handling branches. The initial audit grepped only `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/`; the builder lives in `Core/OpcUa/`.
|
||||||
>
|
>
|
||||||
> **Phase 6.4 is now FULLY SHIPPED — no deferred surfaces remain.**
|
> **Phase 6.4 is now FULLY SHIPPED — no deferred surfaces remain.**
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ End-to-end validation that the Phase 7 production wiring chain (#243 / #244 / #2
|
|||||||
| `OtOpcUaGalaxyHost` Windows service running | `sc query OtOpcUaGalaxyHost` → `STATE: 4 RUNNING` |
|
| `OtOpcUaGalaxyHost` Windows service running | `sc query OtOpcUaGalaxyHost` → `STATE: 4 RUNNING` |
|
||||||
| Galaxy.Host shared secret matches `.local/galaxy-host-secret.txt` | Set during NSSM install — see `docs/ServiceHosting.md` |
|
| Galaxy.Host shared secret matches `.local/galaxy-host-secret.txt` | Set during NSSM install — see `docs/ServiceHosting.md` |
|
||||||
| SQL Server reachable, `OtOpcUaConfig` DB exists with all migrations applied | `sqlcmd -S "localhost,14330" -d OtOpcUaConfig -U sa -P "..." -Q "SELECT COUNT(*) FROM dbo.__EFMigrationsHistory"` returns ≥ 11 |
|
| SQL Server reachable, `OtOpcUaConfig` DB exists with all migrations applied | `sqlcmd -S "localhost,14330" -d OtOpcUaConfig -U sa -P "..." -Q "SELECT COUNT(*) FROM dbo.__EFMigrationsHistory"` returns ≥ 11 |
|
||||||
| Server's `appsettings.json` `Node:ConfigDbConnectionString` matches your SQL Server | `cat src/ZB.MOM.WW.OtOpcUa.Server/appsettings.json` |
|
| Server's `appsettings.json` `Node:ConfigDbConnectionString` matches your SQL Server | `cat src/Server/ZB.MOM.WW.OtOpcUa.Server/appsettings.json` |
|
||||||
|
|
||||||
> **Galaxy.Host pipe ACL.** The pipe allows the configured `OTOPCUA_ALLOWED_SID` (typically the user that runs `OtOpcUaGalaxyHost` — `dohertj2` on the dev box). Run the Server under the same user; elevation doesn't matter — `PipeAcl.cs` no longer denies `BUILTIN\Administrators` since UAC's deny-only Admins SID would have blocked non-elevated dev-box admins too.
|
> **Galaxy.Host pipe ACL.** The pipe allows the configured `OTOPCUA_ALLOWED_SID` (typically the user that runs `OtOpcUaGalaxyHost` — `dohertj2` on the dev box). Run the Server under the same user; elevation doesn't matter — `PipeAcl.cs` no longer denies `BUILTIN\Administrators` since UAC's deny-only Admins SID would have blocked non-elevated dev-box admins too.
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ End-to-end validation that the Phase 7 production wiring chain (#243 / #244 / #2
|
|||||||
### 1. Migrate the Config DB
|
### 1. Migrate the Config DB
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd src/ZB.MOM.WW.OtOpcUa.Configuration
|
cd src/Core/ZB.MOM.WW.OtOpcUa.Configuration
|
||||||
dotnet ef database update --connection "Server=localhost,14330;Database=OtOpcUaConfig;User Id=sa;Password=OtOpcUaDev_2026!;TrustServerCertificate=True;Encrypt=False;"
|
dotnet ef database update --connection "Server=localhost,14330;Database=OtOpcUaConfig;User Id=sa;Password=OtOpcUaDev_2026!;TrustServerCertificate=True;Encrypt=False;"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ Dev-box GLAuth ships `writeop` / `writeop123` in the `WriteOperate` group, `admi
|
|||||||
### 5. Start the Server
|
### 5. Start the Server
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Server
|
dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected log markers (in order):
|
Expected log markers (in order):
|
||||||
@@ -146,7 +146,7 @@ Any line missing = follow up the failure surface (each step has its own log sign
|
|||||||
### 6. Validate via Client.CLI
|
### 6. Validate via Client.CLI
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840/OtOpcUa -r -d 5
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840/OtOpcUa -r -d 5
|
||||||
```
|
```
|
||||||
|
|
||||||
Expect to see under the namespace root: `lab-floor → galaxy-line → reactor-1` with three child variables: `Source` (driver-sourced Int32), `MachineStatus` (virtual tag Boolean, `Source > 0`), and `OverTemp` (scripted alarm Boolean, `Source > 50`). NodeIds are path-based per OPC UA Part 3 §5.2.2 — the walker mints them from `{driverId}/{folder-path}/{browseName}` and stores the driver-side FullReference in an internal NodeId→FullRef map, so client subscriptions survive backend address renames.
|
Expect to see under the namespace root: `lab-floor → galaxy-line → reactor-1` with three child variables: `Source` (driver-sourced Int32), `MachineStatus` (virtual tag Boolean, `Source > 0`), and `OverTemp` (scripted alarm Boolean, `Source > 50`). NodeIds are path-based per OPC UA Part 3 §5.2.2 — the walker mints them from `{driverId}/{folder-path}/{browseName}` and stores the driver-side FullReference in an internal NodeId→FullRef map, so client subscriptions survive backend address renames.
|
||||||
@@ -154,7 +154,7 @@ Expect to see under the namespace root: `lab-floor → galaxy-line → reactor-1
|
|||||||
#### Read the virtual tag
|
#### Read the virtual tag
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
|
||||||
-u opc.tcp://localhost:4840/OtOpcUa `
|
-u opc.tcp://localhost:4840/OtOpcUa `
|
||||||
-n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/MachineStatus"
|
-n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/MachineStatus"
|
||||||
```
|
```
|
||||||
@@ -164,7 +164,7 @@ Expected: `Boolean`. Push a value change into the Source Galaxy attribute and re
|
|||||||
#### Read the scripted alarm
|
#### Read the scripted alarm
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
|
||||||
-u opc.tcp://localhost:4840/OtOpcUa `
|
-u opc.tcp://localhost:4840/OtOpcUa `
|
||||||
-n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/OverTemp"
|
-n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/OverTemp"
|
||||||
```
|
```
|
||||||
@@ -177,7 +177,7 @@ Push a Source value above 50 — either from Galaxy itself, or via the Server's
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# OPC UA write path — requires LDAP from step 4a + a writeop-class user.
|
# OPC UA write path — requires LDAP from step 4a + a writeop-class user.
|
||||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- write `
|
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- write `
|
||||||
-u opc.tcp://localhost:4840/OtOpcUa -S sign `
|
-u opc.tcp://localhost:4840/OtOpcUa -S sign `
|
||||||
-n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/Source" `
|
-n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/Source" `
|
||||||
-v 75 -U writeop -P writeop123
|
-v 75 -U writeop -P writeop123
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
# PR 1 — Phase 1 + Phase 2 A/B/C → v2
|
|
||||||
|
|
||||||
**Source**: `phase-1-configuration` (commits `980ea51..7403b92`, 11 commits)
|
|
||||||
**Target**: `v2`
|
|
||||||
**URL**: https://gitea.dohertylan.com/dohertj2/lmxopcua/pulls/new/phase-1-configuration
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- **Phase 1 complete** — Configuration project with 16 entities + 3 EF migrations
|
|
||||||
(InitialSchema + 8 stored procs + AuthorizationGrants), Core + Server + full Admin UI
|
|
||||||
(Blazor Server with cluster CRUD, draft → diff → publish → rollback, equipment with
|
|
||||||
OPC 40010, UNS, namespaces, drivers, ACLs, reservations, audit), LDAP via GLAuth
|
|
||||||
(`localhost:3893`), SignalR real-time fleet status + alerts.
|
|
||||||
- **Phase 2 Streams A + B + C feature-complete** — full IPC contract surface
|
|
||||||
(Galaxy.Shared, netstandard2.0, MessagePack), Galaxy.Host with real Win32 STA pump,
|
|
||||||
ACL + caller-SID + per-process-secret IPC, Galaxy-specific MemoryWatchdog +
|
|
||||||
RecyclePolicy + PostMortemMmf + MxAccessHandle, three `IGalaxyBackend`
|
|
||||||
implementations (Stub / DbBacked / **MxAccess** — real ArchestrA.MxAccess.dll
|
|
||||||
reference, x86, smoke-tested live against `LMXProxyServer`), Galaxy.Proxy with all
|
|
||||||
9 capability interfaces (`IDriver` / `ITagDiscovery` / `IReadable` / `IWritable` /
|
|
||||||
`ISubscribable` / `IAlarmSource` / `IHistoryProvider` / `IRediscoverable` /
|
|
||||||
`IHostConnectivityProbe`) + supervisor (Backoff + CircuitBreaker +
|
|
||||||
HeartbeatMonitor).
|
|
||||||
- **Phase 2 Stream D non-destructive deliverables** — appsettings.json → 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.
|
|
||||||
|
|
||||||
## What's NOT in this PR
|
|
||||||
|
|
||||||
- Legacy `OtOpcUa.Host` deletion (Stream D.1) — reserved for a follow-up PR after
|
|
||||||
Option B's E2E suite is green. The 494 v1 tests still pass against the unchanged
|
|
||||||
legacy Host.
|
|
||||||
- Live-Galaxy parity validation (Stream E) — needs the iterative debug cycle the
|
|
||||||
removal-procedure doc describes.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
**964 pass / 1 pre-existing Phase 0 baseline failure**, across 14 test projects:
|
|
||||||
|
|
||||||
| Project | Pass | Notes |
|
|
||||||
|---|---:|---|
|
|
||||||
| Core.Abstractions.Tests | 24 | |
|
|
||||||
| Configuration.Tests | 42 | incl. 7 schema compliance, 8 stored-proc, 3 SQL-role auth, 13 validator, 6 LiteDB cache, 5 generation-applier |
|
|
||||||
| Core.Tests | 4 | DriverHost lifecycle |
|
|
||||||
| Server.Tests | 2 | NodeBootstrap + LiteDB cache fallback |
|
|
||||||
| Admin.Tests | 21 | incl. 5 RoleMapper, 6 LdapAuth, 3 LiveLdap, 2 FleetStatusPoller, 2 services-integration |
|
|
||||||
| Driver.Galaxy.Shared.Tests | 6 | Round-trip + framing |
|
|
||||||
| Driver.Galaxy.Host.Tests | 30 | incl. 5 GalaxyRepository live ZB, 3 live MXAccess COM, 5 EndToEndIpc, 2 IpcHandshake, 4 MemoryWatchdog, 3 RecyclePolicy, 3 PostMortemMmf, 3 StaPump, 2 service-installer dry-run |
|
|
||||||
| Driver.Galaxy.Proxy.Tests | 10 | 9 unit + 1 process-spawn parity |
|
|
||||||
| Client.Shared.Tests | 131 | unchanged |
|
|
||||||
| Client.UI.Tests | 98 | unchanged |
|
|
||||||
| Client.CLI.Tests | 51 / 1 fail | pre-existing baseline failure |
|
|
||||||
| Historian.Aveva.Tests | 41 | unchanged |
|
|
||||||
| IntegrationTests (net48) | 6 | unchanged — v1 parity baseline |
|
|
||||||
| **OtOpcUa.Tests (net48)** | **494** | **unchanged — v1 parity baseline** |
|
|
||||||
|
|
||||||
## Test plan for reviewers
|
|
||||||
|
|
||||||
- [ ] `dotnet build ZB.MOM.WW.OtOpcUa.slnx` succeeds with no warnings beyond the
|
|
||||||
known NuGetAuditSuppress + xUnit1051 warnings
|
|
||||||
- [ ] `dotnet test ZB.MOM.WW.OtOpcUa.slnx` shows the same 964/1 result
|
|
||||||
- [ ] `Get-Service aaGR, aaBootstrap` reports Running on the merger's box
|
|
||||||
- [ ] `docker ps --filter name=otopcua-mssql` shows the SQL container Up
|
|
||||||
- [ ] Admin UI boots (`dotnet run --project src/ZB.MOM.WW.OtOpcUa.Admin`); home page
|
|
||||||
renders at http://localhost:5123/; LDAP sign-in with GLAuth `readonly` /
|
|
||||||
`readonly123` succeeds
|
|
||||||
- [ ] Migration script dry-run: `powershell -File
|
|
||||||
scripts/migration/Migrate-AppSettings-To-DriverConfig.ps1 -DryRun` produces
|
|
||||||
a well-formed DriverConfig JSON
|
|
||||||
- [ ] Spot-read three commit messages to confirm the deferred-with-rationale items
|
|
||||||
are explicitly documented (`549cd36`, `a7126ba`, `7403b92` are the most
|
|
||||||
recent and most detailed)
|
|
||||||
|
|
||||||
## Follow-up tracking
|
|
||||||
|
|
||||||
PR 2 (next session) will execute Stream D Option B — archive `OtOpcUa.Tests` as
|
|
||||||
`OtOpcUa.Tests.v1Archive`, build the new `OtOpcUa.Driver.Galaxy.E2E` test project,
|
|
||||||
delete legacy `OtOpcUa.Host`, and run the parity-validation cycle. See
|
|
||||||
`docs/v2/implementation/stream-d-removal-procedure.md`.
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# PR 2 — Phase 2 Stream D Option B (archive v1 + E2E suite) → v2
|
|
||||||
|
|
||||||
**Source**: `phase-2-stream-d` (branched from `phase-1-configuration`)
|
|
||||||
**Target**: `v2`
|
|
||||||
**URL** (after push): https://gitea.dohertylan.com/dohertj2/lmxopcua/pulls/new/phase-2-stream-d
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Phase 2 Stream D Option B per `docs/v2/implementation/stream-d-removal-procedure.md`:
|
|
||||||
|
|
||||||
- **Archived the v1 surface** without deleting:
|
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.Tests/` → `tests/ZB.MOM.WW.OtOpcUa.Tests.v1Archive/`
|
|
||||||
(`<AssemblyName>` kept as `ZB.MOM.WW.OtOpcUa.Tests` so v1 Host's `InternalsVisibleTo`
|
|
||||||
still matches; `<IsTestProject>false</IsTestProject>` so solution test runs skip it).
|
|
||||||
- `tests/ZB.MOM.WW.OtOpcUa.IntegrationTests/` — `<IsTestProject>false</IsTestProject>`
|
|
||||||
+ archive comment.
|
|
||||||
- `src/ZB.MOM.WW.OtOpcUa.Host/` + `src/ZB.MOM.WW.OtOpcUa.Historian.Aveva/` — archive
|
|
||||||
PropertyGroup comments. Both still build (Historian plugin + 41 historian tests still
|
|
||||||
pass) so Phase 2 PR 3 can delete them in a focused, reviewable destructive change.
|
|
||||||
- **New `tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E/`** test project (.NET 10):
|
|
||||||
- `ParityFixture` spawns `OtOpcUa.Driver.Galaxy.Host.exe` (net48 x86) as a subprocess via
|
|
||||||
`Process.Start`, connects via real named pipe, exposes a connected `GalaxyProxyDriver`.
|
|
||||||
Skips when Galaxy ZB unreachable / Host EXE not built / Administrator shell.
|
|
||||||
- `HierarchyParityTests` (3) and `StabilityFindingsRegressionTests` (4) — one test per
|
|
||||||
2026-04-13 stability finding (phantom probe, cross-host quality clear, sync-over-async,
|
|
||||||
fire-and-forget alarm shutdown race).
|
|
||||||
- **`docs/v2/V1_ARCHIVE_STATUS.md`** — inventory + deletion plan for PR 3.
|
|
||||||
- **`docs/v2/implementation/exit-gate-phase-2-final.md`** — supersedes the two partial-exit
|
|
||||||
docs with the as-built state, adversarial review of PR 2 deltas (4 new findings), and the
|
|
||||||
recommended PR sequence (1 → 2 → 3 → 4).
|
|
||||||
|
|
||||||
## What's NOT in this PR
|
|
||||||
|
|
||||||
- Deletion of the v1 archive — saved for PR 3 with explicit operator review (destructive change).
|
|
||||||
- Wonderware Historian SDK plugin port — Task B.1.h, follow-up to enable real `HistoryRead`.
|
|
||||||
- MxAccess subscription push-frames — Task B.1.s, follow-up to enable real-time
|
|
||||||
data-change push from Host → Proxy.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
**`dotnet test ZB.MOM.WW.OtOpcUa.slnx`**: **470 pass / 7 skip / 1 pre-existing baseline**.
|
|
||||||
|
|
||||||
The 7 skips are the new E2E tests, all skipping with the documented reason
|
|
||||||
"PipeAcl denies Administrators on dev shells" — the production install runs as a non-admin
|
|
||||||
service account and these tests will execute there.
|
|
||||||
|
|
||||||
Run the archived v1 suites explicitly:
|
|
||||||
```powershell
|
|
||||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.Tests.v1Archive # → 494 pass
|
|
||||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.IntegrationTests # → 6 pass
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test plan for reviewers
|
|
||||||
|
|
||||||
- [ ] `dotnet build ZB.MOM.WW.OtOpcUa.slnx` succeeds with no warnings beyond the known
|
|
||||||
NuGetAuditSuppress + NU1702 cross-FX
|
|
||||||
- [ ] `dotnet test ZB.MOM.WW.OtOpcUa.slnx` shows the 470/7-skip/1-baseline result
|
|
||||||
- [ ] Both archived suites pass when run explicitly
|
|
||||||
- [ ] Build the Galaxy.Host EXE (`dotnet build src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host`),
|
|
||||||
then run E2E tests on a non-admin shell — they should actually execute and pass
|
|
||||||
against live Galaxy ZB
|
|
||||||
- [ ] Spot-read `docs/v2/V1_ARCHIVE_STATUS.md` and confirm the deletion plan is acceptable
|
|
||||||
|
|
||||||
## Follow-up tracking
|
|
||||||
|
|
||||||
- **PR 3** (next session, when ready): execute the deletion plan in `V1_ARCHIVE_STATUS.md`.
|
|
||||||
4 projects removed, .slnx updated, full solution test confirms parity.
|
|
||||||
- **PR 4** (Phase 2 follow-up): port Historian plugin + wire MxAccess subscription pushes +
|
|
||||||
close the high/medium open findings from `exit-gate-phase-2-final.md`.
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# PR 4 — Phase 2 follow-up: close the 4 open MXAccess findings
|
|
||||||
|
|
||||||
**Source**: `phase-2-pr4-findings` (branched from `phase-2-stream-d`)
|
|
||||||
**Target**: `v2`
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Closes the 4 high/medium open findings carried forward in `exit-gate-phase-2-final.md`:
|
|
||||||
|
|
||||||
- **High 1 — `ReadAsync` subscription-leak on cancel.** One-shot read now wraps the
|
|
||||||
subscribe→first-OnDataChange→unsubscribe pattern in a `try/finally` so the per-tag
|
|
||||||
callback is always detached, and if the read installed the underlying MXAccess
|
|
||||||
subscription itself (no other caller had it), it tears it down on the way out.
|
|
||||||
- **High 2 — No reconnect loop on the MXAccess COM connection.** New
|
|
||||||
`MxAccessClientOptions { AutoReconnect, MonitorInterval, StaleThreshold }` + a background
|
|
||||||
`MonitorLoopAsync` that watches a stale-activity threshold + probes the proxy via a
|
|
||||||
no-op COM call, then reconnects-with-replay (re-Register, re-AddItem every active
|
|
||||||
subscription) when the proxy is dead. Liveness signal: every `OnDataChange` callback bumps
|
|
||||||
`_lastObservedActivityUtc`. Defaults match v1 monitor cadence (5s poll, 60s stale).
|
|
||||||
`ReconnectCount` exposed for diagnostics; `ConnectionStateChanged` event for downstream
|
|
||||||
consumers (the supervisor on the Proxy side already surfaces this through its
|
|
||||||
HeartbeatMonitor, but the Host-side event lets local logging/metrics hook in).
|
|
||||||
- **Medium 3 — `MxAccessGalaxyBackend.SubscribeAsync` doesn't push OnDataChange frames back to
|
|
||||||
the Proxy.** New `IGalaxyBackend.OnDataChange` / `OnAlarmEvent` / `OnHostStatusChanged`
|
|
||||||
events that the new `GalaxyFrameHandler.AttachConnection` subscribes per-connection and
|
|
||||||
forwards as outbound `OnDataChangeNotification` / `AlarmEvent` /
|
|
||||||
`RuntimeStatusChange` frames through the connection's `FrameWriter`. `MxAccessGalaxyBackend`
|
|
||||||
fans out per-tag value changes to every `SubscriptionId` that's listening to that tag
|
|
||||||
(multiple Proxy subs may share a Galaxy attribute — single COM subscription, multi-fan-out
|
|
||||||
on the wire). Stub + DbBacked backends declare the events with `#pragma warning disable
|
|
||||||
CS0067` (treat-warnings-as-errors would otherwise fail on never-raised events that exist
|
|
||||||
only to satisfy the interface).
|
|
||||||
- **Medium 4 — `WriteValuesAsync` doesn't await `OnWriteComplete`.** New
|
|
||||||
`WriteAsync(...)` overload returns `bool` after awaiting the OnWriteComplete callback via
|
|
||||||
the v1-style `TaskCompletionSource`-keyed-by-item-handle pattern in `_pendingWrites`.
|
|
||||||
`MxAccessGalaxyBackend.WriteValuesAsync` now reports per-tag `Bad_InternalError` when the
|
|
||||||
runtime rejected the write, instead of false-positive `Good`.
|
|
||||||
|
|
||||||
## Pipe server change
|
|
||||||
|
|
||||||
`IFrameHandler` gains `AttachConnection(FrameWriter writer): IDisposable` so the handler can
|
|
||||||
register backend event sinks on each accepted connection and detach them at disconnect. The
|
|
||||||
`PipeServer.RunOneConnectionAsync` calls it after the Hello handshake and disposes it in the
|
|
||||||
finally of the per-connection scope. `StubFrameHandler` returns `IFrameHandler.NoopAttachment.Instance`
|
|
||||||
(net48 doesn't support default interface methods, so the empty-attach lives as a public nested
|
|
||||||
class).
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
**`dotnet test ZB.MOM.WW.OtOpcUa.slnx`**: **460 pass / 7 skip (E2E on admin shell) / 1
|
|
||||||
pre-existing baseline failure**. No regressions. The Driver.Galaxy.Host unit tests + 5 live
|
|
||||||
ZB smoke + 3 live MXAccess COM smoke all pass unchanged.
|
|
||||||
|
|
||||||
## Test plan for reviewers
|
|
||||||
|
|
||||||
- [ ] `dotnet build` clean
|
|
||||||
- [ ] `dotnet test` shows 460/7-skip/1-baseline
|
|
||||||
- [ ] Spot-check `MxAccessClient.MonitorLoopAsync` against v1's `MxAccessClient.Monitor`
|
|
||||||
partial (`src/ZB.MOM.WW.OtOpcUa.Host/MxAccess/MxAccessClient.Monitor.cs`) — same
|
|
||||||
polling cadence, same probe-then-reconnect-with-replay shape
|
|
||||||
- [ ] Read `GalaxyFrameHandler.ConnectionSink.Dispose` and confirm event handlers are
|
|
||||||
detached on connection close (no leaked invocation list refs)
|
|
||||||
- [ ] `WriteValuesAsync` returning `Bad_InternalError` on a runtime-rejected write is the
|
|
||||||
correct shape — confirm against the v1 `MxAccessClient.ReadWrite.cs` pattern
|
|
||||||
|
|
||||||
## What's NOT in this PR
|
|
||||||
|
|
||||||
- Wonderware Historian SDK plugin port (Task B.1.h) — separate PR, larger scope.
|
|
||||||
- Alarm subsystem wire-up (`MxAccessGalaxyBackend.SubscribeAlarmsAsync` is still a no-op).
|
|
||||||
`OnAlarmEvent` is declared on the backend interface and pushed by the frame handler when
|
|
||||||
raised; `MxAccessGalaxyBackend` just doesn't raise it yet (waits for the alarm-tracking
|
|
||||||
port from v1's `AlarmObjectFilter` + Galaxy alarm primitives).
|
|
||||||
- Host-status push (`OnHostStatusChanged`) — declared on the interface and pushed by the
|
|
||||||
frame handler; `MxAccessGalaxyBackend` doesn't raise it (the Galaxy.Host's
|
|
||||||
`HostConnectivityProbe` from v1 needs porting too, scoped under the Historian PR).
|
|
||||||
|
|
||||||
## Adversarial review
|
|
||||||
|
|
||||||
Quick pass over the PR 4 deltas. No new findings beyond:
|
|
||||||
|
|
||||||
- **Low 1** — `MonitorLoopAsync`'s `$Heartbeat` probe item-handle is leaked
|
|
||||||
(`AddItem` succeeds, never `RemoveItem`'d). Cosmetic — the probe item is internal to
|
|
||||||
the COM connection, dies with `Unregister` at disconnect/recycle. Worth a follow-up
|
|
||||||
to call `RemoveItem` after the probe succeeds.
|
|
||||||
- **Low 2** — Replay loop in `MonitorLoopAsync` swallows per-subscription failures. If
|
|
||||||
Galaxy permanently rejects a previously-valid reference (rare but possible after a
|
|
||||||
re-deploy), the user gets silent data loss for that one subscription. The stub-handler-
|
|
||||||
unaware operator wouldn't notice. Worth surfacing as a `ConnectionStateChanged(false)
|
|
||||||
→ ConnectionStateChanged(true)` payload that includes the replay-failures list.
|
|
||||||
|
|
||||||
Both are low-priority follow-ups, not PR 4 blockers.
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
# LMX Galaxy bridge — remaining follow-ups
|
|
||||||
|
|
||||||
State after PR 19: the Galaxy driver is functionally at v1 parity through the
|
|
||||||
`IDriver` abstraction; the OPC UA server runs with LDAP-authenticated
|
|
||||||
Basic256Sha256 endpoints and alarms are observable through
|
|
||||||
`AlarmConditionState.ReportEvent`. The items below are what remains LMX-
|
|
||||||
specific before the stack can fully replace the v1 deployment, in
|
|
||||||
rough priority order.
|
|
||||||
|
|
||||||
## 1. Proxy-side `IHistoryProvider` for `ReadAtTime` / `ReadEvents` — **DONE (PRs 35 + 38)**
|
|
||||||
|
|
||||||
PR 35 extended `IHistoryProvider` with `ReadAtTimeAsync` + `ReadEventsAsync`
|
|
||||||
(default throwing implementations so existing impls keep compiling), added the
|
|
||||||
`HistoricalEvent` + `HistoricalEventsResult` records to `Core.Abstractions`,
|
|
||||||
and implemented both methods in `GalaxyProxyDriver` on top of the PR 10 / PR 11
|
|
||||||
IPC messages.
|
|
||||||
|
|
||||||
PR 38 wired the OPC UA HistoryRead service-handler through
|
|
||||||
`DriverNodeManager` by overriding `CustomNodeManager2`'s four per-kind hooks —
|
|
||||||
`HistoryReadRawModified` / `HistoryReadProcessed` / `HistoryReadAtTime` /
|
|
||||||
`HistoryReadEvents`. Each walks `nodesToProcess`, resolves the driver-side
|
|
||||||
full reference from `NodeId.Identifier`, dispatches to the right
|
|
||||||
`IHistoryProvider` method, and populates the paired results + errors lists
|
|
||||||
(both must be set — the MasterNodeManager merges them and a Good result with
|
|
||||||
an unset error slot serializes as `BadHistoryOperationUnsupported` on the
|
|
||||||
wire). Historized variables gain `AccessLevels.HistoryRead` so the stack
|
|
||||||
dispatches; the driver root folder gains `EventNotifiers.HistoryRead` so
|
|
||||||
`HistoryReadEvents` can target it.
|
|
||||||
|
|
||||||
Aggregate translation uses a small `MapAggregate` helper that handles
|
|
||||||
`Average` / `Minimum` / `Maximum` / `Total` / `Count` (the enum surface the
|
|
||||||
driver exposes) and returns null for unsupported aggregates so the handler
|
|
||||||
can surface `BadAggregateNotSupported`. Raw+Processed+AtTime wrap driver
|
|
||||||
samples as `HistoryData` in an `ExtensionObject`; Events emits a
|
|
||||||
`HistoryEvent` with the standard BaseEventType field list (EventId /
|
|
||||||
SourceName / Message / Severity / Time / ReceiveTime) — custom
|
|
||||||
`SelectClause` evaluation is an explicit follow-up.
|
|
||||||
|
|
||||||
**Tests**:
|
|
||||||
|
|
||||||
- `DriverNodeManagerHistoryMappingTests` — 12 unit cases pinning
|
|
||||||
`MapAggregate`, `BuildHistoryData`, `BuildHistoryEvent`, `ToDataValue`.
|
|
||||||
- `HistoryReadIntegrationTests` — 5 end-to-end cases drive a real OPC UA
|
|
||||||
client (`Session.HistoryRead`) against a fake `IHistoryProvider` driver
|
|
||||||
through the running stack. Covers raw round-trip, processed with Average
|
|
||||||
aggregate, unsupported aggregate → `BadAggregateNotSupported`, at-time
|
|
||||||
timestamp forwarding, and events field-list shape.
|
|
||||||
|
|
||||||
**Deferred**:
|
|
||||||
- Continuation-point plumbing via `Session.Save/RestoreHistoryContinuationPoint`.
|
|
||||||
Driver returns null continuations today so the pass-through is fine.
|
|
||||||
- Per-`SelectClause` evaluation in HistoryReadEvents — clients that send a
|
|
||||||
custom field selection currently get the standard BaseEventType layout.
|
|
||||||
|
|
||||||
## 2. Write-gating by role — **DONE (PR 26)**
|
|
||||||
|
|
||||||
Landed in PR 26. `WriteAuthzPolicy` in `Server/Security/` maps
|
|
||||||
`SecurityClassification` → required role (`FreeAccess` → no role required,
|
|
||||||
`Operate`/`SecuredWrite` → `WriteOperate`, `Tune` → `WriteTune`,
|
|
||||||
`Configure`/`VerifiedWrite` → `WriteConfigure`, `ViewOnly` → deny regardless).
|
|
||||||
`DriverNodeManager` caches the classification per variable during discovery and
|
|
||||||
checks the session's roles (via `IRoleBearer`) in `OnWriteValue` before calling
|
|
||||||
`IWritable.WriteAsync`. Roles do not cascade — a session with `WriteOperate`
|
|
||||||
can't write a `Tune` attribute unless it also carries `WriteTune`.
|
|
||||||
|
|
||||||
See `feedback_acl_at_server_layer.md` in memory for the architectural directive
|
|
||||||
that authz stays at the server layer and never delegates to driver-specific auth.
|
|
||||||
|
|
||||||
## 3. Admin UI client-cert trust management — **DONE (PR 28)**
|
|
||||||
|
|
||||||
PR 28 shipped `/certificates` in the Admin UI. `CertTrustService` reads the OPC
|
|
||||||
UA server's PKI store root (`OpcUaServerOptions.PkiStoreRoot` — default
|
|
||||||
`%ProgramData%\OtOpcUa\pki`) and lists rejected + trusted certs by parsing the
|
|
||||||
`.der` files directly, so it has no `Opc.Ua` dependency and runs on any
|
|
||||||
Admin host that can reach the shared PKI directory.
|
|
||||||
|
|
||||||
Operator actions: Trust (moves `rejected/certs/*.der` → `trusted/certs/*.der`),
|
|
||||||
Delete rejected, Revoke trust. The OPC UA stack re-reads the trusted store on
|
|
||||||
each new client handshake, so no explicit reload signal is needed —
|
|
||||||
operators retry the rejected client's connection after trusting.
|
|
||||||
|
|
||||||
Deferred: flipping `AutoAcceptUntrustedClientCertificates` to `false` as the
|
|
||||||
deployment default. That's a production-hardening config change, not a code
|
|
||||||
gap — the Admin UI is now ready to be the trust gate.
|
|
||||||
|
|
||||||
## 4. Live-LDAP integration test — **DONE (PR 31)**
|
|
||||||
|
|
||||||
PR 31 shipped `Server.Tests/LdapUserAuthenticatorLiveTests.cs` — 6 live-bind
|
|
||||||
tests against the dev GLAuth instance at `localhost:3893`, skipped cleanly
|
|
||||||
when the port is unreachable. Covers: valid bind, wrong password, unknown
|
|
||||||
user, empty credentials, single-group → WriteOperate mapping, multi-group
|
|
||||||
admin user surfacing all mapped roles.
|
|
||||||
|
|
||||||
Also added `UserNameAttribute` to `LdapOptions` (default `uid` for RFC 2307
|
|
||||||
compat) so Active Directory deployments can configure `sAMAccountName` /
|
|
||||||
`userPrincipalName` without code changes. `LdapUserAuthenticatorAdCompatTests`
|
|
||||||
(5 unit guards) pins the AD-shape DN parsing + filter escape behaviors. See
|
|
||||||
`docs/security.md` §"Active Directory configuration" for the AD appsettings
|
|
||||||
snippet.
|
|
||||||
|
|
||||||
Deferred: asserting `session.Identity` end-to-end on the server side (i.e.
|
|
||||||
drive a full OPC UA session with username/password, then read an
|
|
||||||
`IHostConnectivityProbe`-style "whoami" node to verify the role surfaced).
|
|
||||||
That needs a test-only address-space node and is a separate PR.
|
|
||||||
|
|
||||||
## 5. Full Galaxy live-service smoke test against the merged v2 stack — **IN PROGRESS (PRs 36 + 37)**
|
|
||||||
|
|
||||||
PR 36 shipped the prerequisites helper (`AvevaPrerequisites`) that probes
|
|
||||||
every dependency a live smoke test needs and produces actionable skip
|
|
||||||
messages.
|
|
||||||
|
|
||||||
PR 37 shipped the live-stack smoke test project structure:
|
|
||||||
`tests/Driver.Galaxy.Proxy.Tests/LiveStack/` with `LiveStackFixture` (connects
|
|
||||||
to the *already-running* `OtOpcUaGalaxyHost` Windows service via named pipe;
|
|
||||||
never spawns the Host process) and `LiveStackSmokeTests` covering:
|
|
||||||
|
|
||||||
- Fixture initializes successfully (IPC handshake succeeds end-to-end).
|
|
||||||
- Driver reports `DriverState.Healthy` post-handshake.
|
|
||||||
- `DiscoverAsync` returns at least one variable from the live Galaxy.
|
|
||||||
- `GetHostStatuses` reports at least one Platform/AppEngine host.
|
|
||||||
- `ReadAsync` on a discovered variable round-trips through
|
|
||||||
Proxy → Host pipe → MXAccess → back without a BadInternalError.
|
|
||||||
|
|
||||||
Shared secret + pipe name resolve from `OTOPCUA_GALAXY_SECRET` /
|
|
||||||
`OTOPCUA_GALAXY_PIPE` env vars, falling back to reading the service's
|
|
||||||
registry-stored Environment values (requires elevated test host).
|
|
||||||
|
|
||||||
**PR 40** added the write + subscribe facts targeting
|
|
||||||
`DelmiaReceiver_001.TestAttribute` (the writable Boolean UDA the dev Galaxy
|
|
||||||
ships under TestMachine_001) — write-then-read with a 5s scan-window poll +
|
|
||||||
restore-on-finally, and subscribe-then-write asserting both an initial-value
|
|
||||||
OnDataChange and a post-write OnDataChange. PR 39 added the elevated-shell
|
|
||||||
short-circuit so a developer running from an admin window gets an actionable
|
|
||||||
skip instead of `UnauthorizedAccessException`.
|
|
||||||
|
|
||||||
**Run the live tests** (from a NORMAL non-admin PowerShell):
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:OTOPCUA_GALAXY_SECRET = Get-Content C:\Users\dohertj2\Desktop\lmxopcua\.local\galaxy-host-secret.txt
|
|
||||||
cd C:\Users\dohertj2\Desktop\lmxopcua
|
|
||||||
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests --filter "FullyQualifiedName~LiveStackSmokeTests"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: 7/7 pass against the running `OtOpcUaGalaxyHost` service.
|
|
||||||
|
|
||||||
**Remaining for #5 in production-grade form**:
|
|
||||||
- Confirm the suite passes from a non-elevated shell (operator action).
|
|
||||||
- Add similar facts for an alarm-source attribute once `TestMachine_001` (or
|
|
||||||
a sibling) carries a deployed alarm condition — the current dev Galaxy's
|
|
||||||
TestAttribute isn't alarm-flagged.
|
|
||||||
|
|
||||||
## 6. Second driver instance on the same server — **DONE (PR 32)**
|
|
||||||
|
|
||||||
`Server.Tests/MultipleDriverInstancesIntegrationTests.cs` registers two
|
|
||||||
drivers with distinct `DriverInstanceId`s on one `DriverHost`, spins up the
|
|
||||||
full OPC UA server, and asserts three behaviors: (1) each driver's namespace
|
|
||||||
URI (`urn:OtOpcUa:{id}`) resolves to a distinct index in the client's
|
|
||||||
NamespaceUris, (2) browsing one subtree returns that driver's folder and
|
|
||||||
does NOT leak the other driver's folder, (3) reads route to the correct
|
|
||||||
driver — the alpha instance returns 42 while beta returns 99, so a misroute
|
|
||||||
would surface at the assertion layer.
|
|
||||||
|
|
||||||
Deferred: the alarm-event multi-driver parity case (two drivers each raising
|
|
||||||
a `GalaxyAlarmEvent`, assert each condition lands on its owning instance's
|
|
||||||
condition node). Alarm tracking already has its own integration test
|
|
||||||
(`AlarmSubscription*`); the multi-driver alarm case would need a stub
|
|
||||||
`IAlarmSource` that's worth its own focused PR.
|
|
||||||
|
|
||||||
## 7. Host-status per-AppEngine granularity → Admin UI dashboard — **DONE (PRs 33 + 34)**
|
|
||||||
|
|
||||||
**PR 33** landed the data layer: `DriverHostStatus` entity + migration with
|
|
||||||
composite key `(NodeId, DriverInstanceId, HostName)` and two query-supporting
|
|
||||||
indexes (per-cluster drill-down on `NodeId`, stale-row detection on
|
|
||||||
`LastSeenUtc`).
|
|
||||||
|
|
||||||
**PR 34** wired the publisher + consumer. `HostStatusPublisher` is a
|
|
||||||
`BackgroundService` in the Server process that walks every registered
|
|
||||||
`IHostConnectivityProbe`-capable driver every 10s, calls
|
|
||||||
`GetHostStatuses()`, and upserts rows (`LastSeenUtc` advances each tick;
|
|
||||||
`State` + `StateChangedUtc` update on transitions). Admin UI `/hosts` page
|
|
||||||
groups by cluster, shows four summary cards (Hosts / Running / Stale /
|
|
||||||
Faulted), and flags rows whose `LastSeenUtc` is older than 30s as Stale so
|
|
||||||
operators see crashed Servers without waiting for a state change.
|
|
||||||
|
|
||||||
Deferred as follow-ups:
|
|
||||||
|
|
||||||
- Event-driven push (subscribe to `OnHostStatusChanged` per driver for
|
|
||||||
sub-heartbeat latency). Adds DriverHost lifecycle-event plumbing;
|
|
||||||
10s polling is fine for operator-scale use.
|
|
||||||
- Failure-count column — needs the publisher to track a transition history
|
|
||||||
per host, not just current-state.
|
|
||||||
- SignalR fan-out to the Admin page (currently the page polls the DB, not
|
|
||||||
a hub). The DB-polled version is fine at current cadence but a hub push
|
|
||||||
would eliminate the 10s race where a new row sits in the DB before the
|
|
||||||
Admin page notices.
|
|
||||||
@@ -7,7 +7,7 @@ populations disagree with the spec in small, device-specific ways, and a driver
|
|||||||
passes textbook tests can still misbehave against actual equipment.
|
passes textbook tests can still misbehave against actual equipment.
|
||||||
|
|
||||||
This doc is the harness-and-quirks playbook. The project it describes lives at
|
This doc is the harness-and-quirks playbook. The project it describes lives at
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/` — scaffolded in PR 30 with
|
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/` — scaffolded in PR 30 with
|
||||||
the simulator fixture, DL205 profile stub, and one write/read smoke test. Each
|
the simulator fixture, DL205 profile stub, and one write/read smoke test. Each
|
||||||
confirmed DL205 quirk lands in a follow-up PR as a named test in that project.
|
confirmed DL205 quirk lands in a follow-up PR as a named test in that project.
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ under `tests/.../Modbus.IntegrationTests/Docker/`. See that folder's
|
|||||||
|
|
||||||
**Setup pattern**:
|
**Setup pattern**:
|
||||||
1. `docker compose -f tests\...\Modbus.IntegrationTests\Docker\docker-compose.yml --profile <standard|dl205|mitsubishi|s7_1500> up -d`.
|
1. `docker compose -f tests\...\Modbus.IntegrationTests\Docker\docker-compose.yml --profile <standard|dl205|mitsubishi|s7_1500> up -d`.
|
||||||
2. `dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` —
|
2. `dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` —
|
||||||
tests auto-skip when the endpoint is unreachable. Default endpoint is
|
tests auto-skip when the endpoint is unreachable. Default endpoint is
|
||||||
`localhost:5020`; override via `MODBUS_SIM_ENDPOINT` for a real PLC on its
|
`localhost:5020`; override via `MODBUS_SIM_ENDPOINT` for a real PLC on its
|
||||||
native port 502.
|
native port 502.
|
||||||
@@ -70,6 +70,17 @@ integration tests until reproduced on hardware:
|
|||||||
- TxId drop under load (forum rumour; not reproduced).
|
- TxId drop under load (forum rumour; not reproduced).
|
||||||
- Pre-2004 firmware ABCD word order (every shipped DL205/DL260 since 2004 is CDAB).
|
- Pre-2004 firmware ABCD word order (every shipped DL205/DL260 since 2004 is CDAB).
|
||||||
|
|
||||||
|
### Siemens SIMATIC S7
|
||||||
|
|
||||||
|
Quirk catalog at [`s7.md`](s7.md) — covers S7-1200 / S7-1500 / S7-300 / S7-400 /
|
||||||
|
ET 200SP. Modbus TCP isn't native; each platform exposes it via a different
|
||||||
|
add-on module with its own register-mapping conventions.
|
||||||
|
|
||||||
|
### Mitsubishi MELSEC
|
||||||
|
|
||||||
|
Quirk catalog at [`mitsubishi.md`](mitsubishi.md) — Modbus TCP via add-on modules
|
||||||
|
across the MELSEC family.
|
||||||
|
|
||||||
### Future devices
|
### Future devices
|
||||||
|
|
||||||
One section per device class, same shape as DL205. Quirks that apply across
|
One section per device class, same shape as DL205. Quirks that apply across
|
||||||
@@ -105,7 +116,7 @@ vendors get promoted into driver defaults or opt-in options:
|
|||||||
## Next concrete PRs
|
## Next concrete PRs
|
||||||
|
|
||||||
- **PR 30 — Integration test project + DL205 profile scaffold** — **DONE**.
|
- **PR 30 — Integration test project + DL205 profile scaffold** — **DONE**.
|
||||||
Shipped `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` with
|
Shipped `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` with
|
||||||
`ModbusSimulatorFixture` (TCP-probe, skips with a clear `SkipReason` when the
|
`ModbusSimulatorFixture` (TCP-probe, skips with a clear `SkipReason` when the
|
||||||
endpoint is unreachable), `DL205/DL205Profile.cs` (tag map stub), and
|
endpoint is unreachable), `DL205/DL205Profile.cs` (tag map stub), and
|
||||||
`DL205/DL205SmokeTests.cs` (write-then-read round-trip).
|
`DL205/DL205SmokeTests.cs` (write-then-read round-trip).
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# Phase 7 Status — Scripting Runtime, Virtual Tags, Scripted Alarms, Historian Sink
|
||||||
|
|
||||||
|
> **Reconciliation date**: 2026-05-18
|
||||||
|
> **Based on**: `docs/v2/implementation/phase-7-scripting-and-alarming.md` (the plan) and
|
||||||
|
> `docs/v2/implementation/exit-gate-phase-7.md` (the exit-gate audit) cross-checked against
|
||||||
|
> the actual repository files. See "Evidence sources" at the bottom.
|
||||||
|
|
||||||
|
## Summary verdict
|
||||||
|
|
||||||
|
**Phase 7 core is fully shipped and the exit gate is closed.** All eight plan streams
|
||||||
|
(A–H, where H = exit gate) plus the three deferred follow-ups (tasks #239 / #240 / #241)
|
||||||
|
landed before the 2026-04-23 exit-gate audit. The `v2-release-readiness.md` note
|
||||||
|
"Phase 7 — out of scope for v2 GA" is a stale label: the work shipped after that doc
|
||||||
|
was last updated. The four `Core.*` Phase 7 projects exist, have tests, and are wired
|
||||||
|
into the running server. Five targeted gaps remain open (see section below).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Work-item status by plan stream
|
||||||
|
|
||||||
|
### Stream A — `Core.Scripting` (Roslyn engine, sandbox, AST inference, logger)
|
||||||
|
|
||||||
|
| Plan item | Status | Evidence |
|
||||||
|
|-----------|--------|----------|
|
||||||
|
| A.1 — Project scaffold + `ScriptContext` base class (`GetTag` / `SetVirtualTag` / `Logger` / `Now` / `Deadband`) | **Done** | `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptContext.cs`, `ScriptGlobals.cs` |
|
||||||
|
| A.2 — `DependencyExtractor : CSharpSyntaxWalker` — literal-only path check, `Inputs` + `Outputs` sets | **Done** | `DependencyExtractor.cs`; literal-reject logic exercised by 7 test files in `Core.Scripting.Tests` |
|
||||||
|
| A.3 — Compile cache keyed on `SHA-256(source)` | **Done** | `CompiledScriptCache.cs` (`ConcurrentDictionary<string, Lazy<ScriptEvaluator<...>>>`) |
|
||||||
|
| A.4 — Per-evaluation timeout (250 ms default) | **Done** | `TimedScriptEvaluator.cs`; `TimedScriptEvaluatorTests.cs` |
|
||||||
|
| A.5 — Serilog sink wiring; `scripts-*.log` companion mirror to main log at WARN on ERROR | **Done** | `ScriptLoggerFactory.cs`, `ScriptLogCompanionSink.cs`; `ScriptLogCompanionSinkTests.cs` |
|
||||||
|
| A.6 — Tests (AST extraction, sandbox escape, exception isolation, timeout, logger binding) | **Done** | `ScriptSandboxTests.cs`, `DependencyExtractorTests.cs`, `CompiledScriptCacheTests.cs`, `ScriptLoggerFactoryTests.cs`, `TimedScriptEvaluatorTests.cs` — 7 test files |
|
||||||
|
|
||||||
|
Shipped as PRs #177–#179 (63 tests).
|
||||||
|
|
||||||
|
### Stream B — Virtual tag engine
|
||||||
|
|
||||||
|
| Plan item | Status | Evidence |
|
||||||
|
|-----------|--------|----------|
|
||||||
|
| B.1 — `VirtualTagEngine` + `DependencyGraph` | **Done** | `VirtualTagEngine.cs`, `DependencyGraph.cs` |
|
||||||
|
| B.2 — `ChangeTriggerDispatcher` (subscribe to referenced driver tags via `ITagUpstreamSource`) | **Done** | `VirtualTagEngine.OnUpstreamChange` internal subscriber path |
|
||||||
|
| B.3 — `TimerTriggerDispatcher` (per-tag `IntervalMs` via timer-wheel) | **Done** | `TimerTriggerScheduler.cs` |
|
||||||
|
| B.4 — `EvaluationPipeline` (serial, per-tag isolation, `_evalGate` semaphore) | **Done** | `VirtualTagEngine.EvaluateInternalAsync`; `_evalGate SemaphoreSlim(1,1)` |
|
||||||
|
| B.5 — `IVirtualTagSource` implementing `IReadable` + `ISubscribable` | **Done** | `VirtualTagSource.cs` |
|
||||||
|
| B.6 — History routing (`IHistoryWriter.Record` when `Historize=true`) | **Partial** | `IHistoryWriter.cs` + `NullHistoryWriter` present; no production writer is wired into the virtual-tag path. `docs/VirtualTags.md` §"Upstream reads + history" explicitly notes: "no production writer is currently wired for virtual tags". Virtual-tag historization is functional at the engine level but has no live sink. |
|
||||||
|
| B.7 — Tests: dependency graph, cascade, timer, change+timer combined, error propagation, historize | **Done** | `DependencyGraphTests.cs`, `VirtualTagEngineTests.cs`, `TimerTriggerSchedulerTests.cs`, `VirtualTagSourceTests.cs` — 5 test files |
|
||||||
|
|
||||||
|
Shipped as PR #180 (36 tests).
|
||||||
|
|
||||||
|
### Stream C — Scripted alarm engine + Part 9 state machine + template messages
|
||||||
|
|
||||||
|
| Plan item | Status | Evidence |
|
||||||
|
|-----------|--------|----------|
|
||||||
|
| C.1 — `ScriptedAlarmEngine` skeleton + alarm config model | **Done** | `ScriptedAlarmEngine.cs`, `ScriptedAlarmDefinition.cs` |
|
||||||
|
| C.2 — `Part9StateMachine` (Enable/Disable/Active/Ack/Confirm/Shelve/Unshelve/Comment/ShelvingCheck) | **Done** | `Part9StateMachine.cs`; `Part9StateMachineTests.cs` |
|
||||||
|
| C.3 — Predicate evaluation on input change; activate/clear transitions | **Done** | `ScriptedAlarmEngine.ReevaluateAsync`; `_alarmsReferencing` inverse index |
|
||||||
|
| C.4 — Startup recovery (`ActiveState` re-derived; Enabled/Ack/Confirm/Shelve loaded from store) | **Done** | `ScriptedAlarmEngine.LoadAsync`; `IAlarmStateStore.LoadAsync` |
|
||||||
|
| C.5 — Template substitution (`{TagPath}` tokens resolved at emission time) | **Done** | `MessageTemplate.cs`; `MessageTemplateTests.cs` |
|
||||||
|
| C.6 — OPC UA method binding (Acknowledge / Confirm / AddComment / OneShotShelve / TimedShelve / Unshelve) | **Partial** | Engine methods exist and are tested. `ScriptedAlarmSource.AcknowledgeAsync` defaults the user to `"opcua-client"`. The plan's Stream G wiring of these methods to OPC UA `MethodCall` dispatch on the condition nodes (so OPC UA client method calls reach the engine with the authenticated principal) is noted in the e2e smoke doc as "not yet wired through `DriverNodeManager.MethodCall` dispatch." Operators acknowledge through Admin UI today; the Part 9 method-call path is a follow-up. |
|
||||||
|
| C.7 — `IAlarmSource` implementation / fan-out registration | **Done** | `ScriptedAlarmSource.cs` |
|
||||||
|
| C.8 — Tests: all state transitions, startup recovery, template substitution, shelving timer expiry | **Done** | `Part9StateMachineTests.cs`, `ScriptedAlarmEngineTests.cs`, `ScriptedAlarmSourceTests.cs`, `MessageTemplateTests.cs` — 5 test files |
|
||||||
|
|
||||||
|
Shipped as PR #181 (47 tests).
|
||||||
|
|
||||||
|
### Stream D — Historian alarm sink (SQLite store-and-forward + Wonderware IPC)
|
||||||
|
|
||||||
|
| Plan item | Status | Evidence |
|
||||||
|
|-----------|--------|----------|
|
||||||
|
| D.1 — `Core.AlarmHistorian` project; `IAlarmHistorianSink`; `SqliteStoreAndForwardSink` (backoff, dead-letter, capacity) | **Done** | `IAlarmHistorianSink.cs`, `SqliteStoreAndForwardSink.cs`; `SqliteStoreAndForwardSinkTests.cs` |
|
||||||
|
| D.2 — Live-historian smoke against dev box Aveva Historian; document the exact SDK entry point | **Partial** | The smoke (`docs/v2/implementation/phase-7-e2e-smoke.md`) ran but the IPC path via Galaxy.Host to `aahClientManaged` was the original plan. That path changed: the production implementation uses `Driver.Historian.Wonderware.Client` (`WonderwareHistorianClient.WriteBatchAsync`) over a named-pipe sidecar, not Galaxy.Host. There is no separate `docs/v2/historian-alarm-api.md` artifact documenting the SDK entry point as the plan called for; the implementation detail is in `WonderwareHistorianClient.cs` inline. |
|
||||||
|
| D.3 — `Driver.Galaxy.Shared` contract additions (`HistorianAlarmEventRequest` / `Response` / `ConnectivityStatusNotification`) | **Changed** | The plan routed alarm writes through Galaxy.Host IPC. The shipped implementation uses `Driver.Historian.Wonderware.Client` (a standalone sidecar project) instead. `HistorianAlarmEventRequest` / `HistorianAlarmEventResponse` as named protos never shipped; the equivalent contract is the `AlarmHistorianEventDto` / `WriteAlarmEventsRequest` / `WriteAlarmEventsReply` MessagePack DTOs in `Driver.Historian.Wonderware.Client/Ipc/`. Galaxy.Host still exists as the mxaccessgw entry point but does not carry historian writes. |
|
||||||
|
| D.4 — `Driver.Galaxy.Host` handler for alarm writes | **Changed** | Not shipped via Galaxy.Host. The sidecar (`Driver.Historian.Wonderware.Client`) is the production path. `IAlarmHistorianWriter` is implemented by `WonderwareHistorianClient`, not by a Galaxy.Host frame handler. |
|
||||||
|
| D.5 — Drain worker in main server (poll SQLite queue, batch 100 events, exponential backoff) | **Done** | `SqliteStoreAndForwardSink.StartDrainLoop`; backoff ladder 1s → 2s → 5s → 15s → 60s; `Phase7Composer.ResolveHistorianSink` starts it with a 2-second drain cadence |
|
||||||
|
| D.6 — Per-alarm `HistorizeToAveva` toggle; `AlarmHistorizationPolicy` per source | **Done** | `ScriptedAlarm.HistorizeToAveva` column (default `true`); `Phase7EngineComposer.RouteToHistorianAsync` checks it; Galaxy defaults `false` |
|
||||||
|
| D.7 — `/alarms/historian` diagnostics view in Admin (queue depth, drain rate, last error, retry dead-lettered) | **Done** | `AlarmsHistorian.razor`; `HistorianDiagnosticsService.cs` |
|
||||||
|
| D.8 — Tests | **Done** | `SqliteStoreAndForwardSinkTests.cs`; `Phase7ComposerWriterSelectionTests.cs` covers historian-writer resolution |
|
||||||
|
|
||||||
|
Shipped as PR #182 (14 tests). Architecture deviated from the plan (Wonderware sidecar instead of Galaxy.Host IPC) but the functional goals are met.
|
||||||
|
|
||||||
|
### Stream E — Config DB schema + generation-sealed cache extensions
|
||||||
|
|
||||||
|
| Plan item | Status | Evidence |
|
||||||
|
|-----------|--------|----------|
|
||||||
|
| E.1 — EF migration for `Script` / `VirtualTag` / `ScriptedAlarm` / `ScriptedAlarmState` tables | **Done** | Migration `20260420231641_AddPhase7ScriptingTables.cs`; entities in `Configuration/Entities/` |
|
||||||
|
| E.2 — `sp_PublishGeneration` extension (sealed-cache snapshot includes Phase 7 rows) | **Done** | Migration `20260420232000_ExtendComputeGenerationDiffWithPhase7.cs` |
|
||||||
|
| E.3 — CRUD services: `VirtualTagService`, `ScriptedAlarmService`, `ScriptService`, `ScriptedAlarmStateService` | **Done** | All four exist in `Admin/Services/`; `GetStateAsync` on `ScriptedAlarmService` serves the state query |
|
||||||
|
| E.4 — Tests: migration up/down; publish atomicity; audit trail | **Done** | `Phase7ServicesTests.cs` (13 tests covering CRUD + hash behavior + harness) |
|
||||||
|
|
||||||
|
Shipped as PR #183 (12 tests in configuration; 13 more in Admin.Tests).
|
||||||
|
|
||||||
|
### Stream F — Admin UI scripting tab
|
||||||
|
|
||||||
|
| Plan item | Status | Evidence |
|
||||||
|
|-----------|--------|----------|
|
||||||
|
| F.1 — Monaco editor Razor component (CDN bundle + textarea fallback) | **Done** | `ScriptEditor.razor` (textarea with Monaco JS interop, `otOpcUaScriptEditor.attach`) |
|
||||||
|
| F.2 — `/virtual-tags` tab (list view, edit pane, dependency preview, publish gate) | **Partial** | The `ScriptsTab.razor` is the single tab covering script CRUD, dependency preview, and harness. There is no separate `/virtual-tags` tab UI — virtual tags are managed through the script service alone; no VirtualTag list/edit form exists in the Admin UI. The per-tag fields (`EquipmentId`, `DataType`, `ChangeTriggered`, `TimerIntervalMs`, `Historize`) are accessible via the `VirtualTagService` backend but have no corresponding UI form. |
|
||||||
|
| F.3 — `/scripted-alarms` tab (alarm type, severity, message template, `HistorizeToAveva`, detail page with shelve/ack state read-only) | **Partial** | No dedicated scripted-alarms tab razor page exists (confirmed by Glob + Grep searches). Scripted alarm CRUD (`ScriptedAlarmService`) exists as a service but has no Admin UI page. |
|
||||||
|
| F.4 — Test harness (modal, synthetic inputs, output + logger display) | **Partial** | `ScriptTestHarnessService.cs` is complete and tested. `ScriptsTab.razor` calls `Harness.RunVirtualTagAsync` with zero-value synthetic inputs derived from the extractor. A full interactive input-form modal was not shipped — the harness zeroes all inputs automatically rather than prompting the operator per-tag. |
|
||||||
|
| F.5 — Script log viewer (SignalR tail of `scripts-*.log` filtered by `ScriptName`, load-more) | **Not started** | No SignalR stream of the scripts log is wired in the Admin UI. The `AlertHub` / `FleetStatusHub` exist but there is no `ScriptLogHub`. |
|
||||||
|
| F.6 — `/alarms/historian` diagnostics view | **Done** | `AlarmsHistorian.razor` + `HistorianDiagnosticsService.cs` |
|
||||||
|
| F.7 — Playwright smoke (author calc tag, verify in equipment tree; author alarm, verify in `AlarmsAndConditions`) | **Not started** | `tests/Server/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/` exists but its `UnsTabDragDropE2ETests.cs` is the only Playwright test; no Phase 7 Admin UI playwright scenario. |
|
||||||
|
|
||||||
|
Shipped as PR #185 (13 Admin service tests; UI completeness is partial — see gaps section).
|
||||||
|
|
||||||
|
### Stream G — Address-space integration
|
||||||
|
|
||||||
|
| Plan item | Status | Evidence |
|
||||||
|
|-----------|--------|----------|
|
||||||
|
| G.1 — `EquipmentNodeWalker` extension emits `NodeSourceKind.Virtual` + `NodeSourceKind.ScriptedAlarm` variables | **Done** | PR #184; `NodeSourceKind` discriminator confirmed in exit gate |
|
||||||
|
| G.2 — `DriverNodeManager` dispatch routes reads by source; writes to non-Driver rejected with `BadUserAccessDenied` | **Done** | PR #186 follow-up; `OpcUaApplicationHost.SetPhase7Sources` threads `_virtualReadable` + `_scriptedAlarmReadable` into the node manager |
|
||||||
|
| G.3 — `AlarmTracker` composition (`ScriptedAlarmEngine` registers as additional `IAlarmSource`) | **Done** | `ScriptedAlarmSource` adapts engine to `IAlarmSource`; `Phase7EngineComposer.Compose` wires it |
|
||||||
|
| G.4 — Tests: mixed equipment folder browsable via Client.CLI; read/subscribe round-trip; alarm transitions in event stream | **Done** | `Phase7ComposerMappingTests.cs`, `Phase7EngineComposerTests.cs`, `ScriptedAlarmReadableTests.cs`, `CachedTagUpstreamSourceTests.cs`, `DriverSubscriptionBridgeTests.cs` — 6 test files in `Server.Tests/Phase7/` |
|
||||||
|
| OPC UA method binding for alarm Ack/Confirm/Shelve | **Not started** | Noted explicitly in `phase-7-e2e-smoke.md` §"Known limitations": `DriverNodeManager.MethodCall` dispatch for scripted alarm methods is not wired. Engine has the methods; the OPC UA call path does not reach them. |
|
||||||
|
|
||||||
|
Shipped across PRs #184 + #186 (5 + 7 tests).
|
||||||
|
|
||||||
|
### Stream H — Exit gate
|
||||||
|
|
||||||
|
| Plan item | Status | Evidence |
|
||||||
|
|-----------|--------|----------|
|
||||||
|
| H.1 — Compliance script real-checks | **Done** | `scripts/compliance/phase-7-compliance.ps1` |
|
||||||
|
| H.2 — Full solution `dotnet test` baseline | **Done** | Exit gate records ~197 new tests + solution baseline |
|
||||||
|
| H.3 — `plan.md` Migration Strategy §6 update | **Not verified** | Not explicitly confirmed; minor — the plan doc is not the primary status artifact |
|
||||||
|
| H.4 — Phase-status memory update | **Done** | Memory updated (see `project_alarms_over_gateway_epic.md` + `project_server_history_alarm_subsystems.md`) |
|
||||||
|
| H.5 — Merge `v2/phase-7-scripting-and-alarming` → `v2` | **Done** | All PRs (#177–#186) merged |
|
||||||
|
|
||||||
|
### Post-gate follow-ups (tasks #239 / #240 / #241)
|
||||||
|
|
||||||
|
All three are verified closed in the 2026-04-23 exit-gate audit:
|
||||||
|
|
||||||
|
| Task | Item | Status |
|
||||||
|
|------|------|--------|
|
||||||
|
| #239 | `SealedBootstrap` composition root — `Phase7Composer.PrepareAsync` + `OpcUaServerService` wiring | **Done** |
|
||||||
|
| #240 | Live OPC UA e2e smoke — `scripts/e2e/test-phase7-virtualtags.ps1` | **Done** (partial pass: 3/7 stages reach `PASS`; writer/subscribe/alarm stages blocked by live Galaxy attribute activity + Historian SDK environment) |
|
||||||
|
| #241 | `sp_ComputeGenerationDiff` extension for Script / VirtualTag / ScriptedAlarm diff sections | **Done** — migration `20260420232000_ExtendComputeGenerationDiffWithPhase7` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What genuinely remains
|
||||||
|
|
||||||
|
These are real open items, not issues with the plan reconciliation.
|
||||||
|
|
||||||
|
### Gap 1 — OPC UA method-call dispatch for scripted alarm Ack/Confirm/Shelve (Stream G / C.6)
|
||||||
|
|
||||||
|
`DriverNodeManager.MethodCall` does not route OPC UA `Acknowledge` / `Confirm` / `OneShotShelve` / `TimedShelve` / `Unshelve` / `AddComment` method invocations to the `ScriptedAlarmEngine`. Operators can acknowledge scripted alarms through the Admin UI today; OPC UA HMI clients expecting to use Part 9 method nodes directly cannot. Explicit in `phase-7-e2e-smoke.md` §"Known limitations".
|
||||||
|
|
||||||
|
### Gap 2 — Admin UI: no `/virtual-tags` tab or form (Stream F.2)
|
||||||
|
|
||||||
|
`VirtualTagService` CRUD is fully tested but no razor page exposes it. Operators must author virtual tags through direct SQL or Admin API calls. `ScriptsTab.razor` covers script CRUD only; virtual-tag fields (`EquipmentId`, `DataType`, trigger config, `Historize`) have no UI form.
|
||||||
|
|
||||||
|
### Gap 3 — Admin UI: no `/scripted-alarms` tab or form (Stream F.3)
|
||||||
|
|
||||||
|
`ScriptedAlarmService` CRUD is fully tested but no razor page exists. Only `ScriptsTab.razor` under the cluster detail view is present; there is no `ScriptedAlarmsTab.razor` or equivalent.
|
||||||
|
|
||||||
|
### Gap 4 — Script log viewer not shipped (Stream F.5)
|
||||||
|
|
||||||
|
The SignalR tail of `scripts-*.log` filtered by `ScriptName` was not implemented. `ScriptsTab.razor` shows script output from the in-process harness but has no live-log panel for production emissions.
|
||||||
|
|
||||||
|
### Gap 5 — Virtual-tag historization has no production sink (Stream B.6)
|
||||||
|
|
||||||
|
`IHistoryWriter` + `NullHistoryWriter` are present; `VirtualTagEngine` calls `IHistoryWriter.Record` per evaluation when `Historize=true`. `Phase7EngineComposer.Compose` passes `NullHistoryWriter` — no live writer is wired. Virtual-tag values are computed and served correctly but never persisted to any historian. Explicitly documented in `docs/VirtualTags.md` §"Upstream reads + history".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What is definitely done
|
||||||
|
|
||||||
|
- All four `Core.*` projects (`Core.Scripting`, `Core.VirtualTags`, `Core.ScriptedAlarms`, `Core.AlarmHistorian`) ship with full implementation and test coverage.
|
||||||
|
- Roslyn sandbox (allow-list + `ForbiddenTypeAnalyzer` defense-in-depth + 250 ms timeout + per-script Serilog sink + compile cache) is complete.
|
||||||
|
- Virtual tag engine: dependency graph with iterative Tarjan SCC, topo-sort, change-trigger cascade, timer trigger, `IReadable` + `ISubscribable` adapter, per-tag error isolation.
|
||||||
|
- Scripted alarm engine: full Part 9 state machine, startup recovery, template substitution, `IAlarmSource` fan-out, 5-second shelving timer, `IAlarmStateStore` (in-memory default; DB-backed via Config DB entities).
|
||||||
|
- SQLite store-and-forward historian sink: drain loop with exponential backoff, dead-letter retention, bounded capacity, `RetryDeadLettered` operator action.
|
||||||
|
- Config DB schema: `Script`, `VirtualTag`, `ScriptedAlarm`, `ScriptedAlarmState` tables with EF migrations and generation-diff extension.
|
||||||
|
- Admin services: `ScriptService`, `VirtualTagService`, `ScriptedAlarmService`, `ScriptTestHarnessService`, `HistorianDiagnosticsService` — all backed by unit tests.
|
||||||
|
- Admin UI: `ScriptsTab.razor` (Monaco-backed editor, dependency preview, test harness), `AlarmsHistorian.razor` (queue depth, drain state, retry dead-lettered).
|
||||||
|
- Server-side composition: `Phase7Composer`, `Phase7EngineComposer`, `CachedTagUpstreamSource`, `DriverSubscriptionBridge`, `ScriptedAlarmReadable` — fully wired into `OpcUaServerService` startup sequence before `OpcUaApplicationHost.StartAsync`.
|
||||||
|
- `EquipmentNodeWalker` emits `NodeSourceKind.Virtual` and `NodeSourceKind.ScriptedAlarm` variables; `DriverNodeManager` dispatches reads and rejects writes to virtual nodes.
|
||||||
|
- `WonderwareHistorianClient.WriteBatchAsync` implements `IAlarmHistorianWriter` as the alarm-event write path (deviation from plan's Galaxy.Host route, but functionally equivalent).
|
||||||
|
- Compliance script `scripts/compliance/phase-7-compliance.ps1` and e2e smoke `scripts/e2e/test-phase7-virtualtags.ps1` both present.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Evidence sources
|
||||||
|
|
||||||
|
| Source | Path |
|
||||||
|
|--------|------|
|
||||||
|
| Phase 7 plan | `docs/v2/implementation/phase-7-scripting-and-alarming.md` |
|
||||||
|
| Phase 7 exit gate | `docs/v2/implementation/exit-gate-phase-7.md` |
|
||||||
|
| E2E smoke runbook | `docs/v2/implementation/phase-7-e2e-smoke.md` |
|
||||||
|
| Virtual tags reference doc | `docs/VirtualTags.md` |
|
||||||
|
| Scripted alarms reference doc | `docs/ScriptedAlarms.md` |
|
||||||
|
| `Core.Scripting` sources | `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/` |
|
||||||
|
| `Core.VirtualTags` sources | `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/` |
|
||||||
|
| `Core.ScriptedAlarms` sources | `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/` |
|
||||||
|
| `Core.AlarmHistorian` sources | `src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/` |
|
||||||
|
| Server Phase7 composition | `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/` |
|
||||||
|
| Admin services | `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/Script*.cs`, `VirtualTagService.cs`, `HistorianDiagnosticsService.cs` |
|
||||||
|
| Admin UI pages | `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptsTab.razor`, `AlarmsHistorian.razor` |
|
||||||
|
| Historian sidecar writer | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/WonderwareHistorianClient.cs` |
|
||||||
|
| EF migrations | `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260420231641_AddPhase7ScriptingTables.cs`, `20260420232000_ExtendComputeGenerationDiffWithPhase7.cs` |
|
||||||
@@ -121,7 +121,7 @@ flips A4 from "deferred" to "expected pass").
|
|||||||
redundancy implementations we don't control.
|
redundancy implementations we don't control.
|
||||||
- For the sub-set of scenarios that *can* be automated — the self-loopback
|
- For the sub-set of scenarios that *can* be automated — the self-loopback
|
||||||
case where our own `otopcua-cli` drives Primary + Backup — the existing
|
case where our own `otopcua-cli` drives Primary + Backup — the existing
|
||||||
`tests/ZB.MOM.WW.OtOpcUa.Server.Tests/RedundancyStatePublisherTests` +
|
`tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/RedundancyStatePublisherTests` +
|
||||||
`ServiceLevelCalculatorTests` (unit) + `ClusterTopologyLoaderTests`
|
`ServiceLevelCalculatorTests` (unit) + `ClusterTopologyLoaderTests`
|
||||||
(integration) already cover the math + data path. The wire-level assertion
|
(integration) already cover the math + data path. The wire-level assertion
|
||||||
that the values actually land on the right OPC UA nodes is covered by
|
that the values actually land on the right OPC UA nodes is covered by
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ Modbus has no native String, DateTime, or Int64 — those rows are skipped on th
|
|||||||
|
|
||||||
### CI fixture (task #180)
|
### CI fixture (task #180)
|
||||||
|
|
||||||
The integration harness at `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/` is Docker-only — `ab_server` is a source-only tool under libplctag's `src/tools/ab_server/`, and the fixture's multi-stage `Docker/Dockerfile` is the only supported reproducible build path.
|
The integration harness at `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/` is Docker-only — `ab_server` is a source-only tool under libplctag's `src/tools/ab_server/`, and the fixture's multi-stage `Docker/Dockerfile` is the only supported reproducible build path.
|
||||||
|
|
||||||
- **`AbServerFixture(AbServerProfile)`** — thin TCP probe against `127.0.0.1:44818` (or `AB_SERVER_ENDPOINT` override). Does not spawn the simulator; the operator brings up the compose service for whichever family the test class targets (`controllogix` / `compactlogix` / `micro800` / `guardlogix`).
|
- **`AbServerFixture(AbServerProfile)`** — thin TCP probe against `127.0.0.1:44818` (or `AB_SERVER_ENDPOINT` override). Does not spawn the simulator; the operator brings up the compose service for whichever family the test class targets (`controllogix` / `compactlogix` / `micro800` / `guardlogix`).
|
||||||
- **`KnownProfiles.{ControlLogix, CompactLogix, Micro800, GuardLogix}`** — thin `(Family, ComposeProfile, Notes)` records. The compose file (`Docker/docker-compose.yml`) is the canonical source of truth for which tags each family seeds + which `--plc` mode the simulator boots in. `Micro800` uses the dedicated `--plc=Micro800` mode; `GuardLogix` uses `ControlLogix` emulation because ab_server has no safety subsystem (the `_S`-suffixed seed tag triggers driver-side ViewOnly classification only).
|
- **`KnownProfiles.{ControlLogix, CompactLogix, Micro800, GuardLogix}`** — thin `(Family, ComposeProfile, Notes)` records. The compose file (`Docker/docker-compose.yml`) is the canonical source of truth for which tags each family seeds + which `--plc` mode the simulator boots in. `Micro800` uses the dedicated `--plc=Micro800` mode; `GuardLogix` uses `ControlLogix` emulation because ab_server has no safety subsystem (the `_S`-suffixed seed tag triggers driver-side ViewOnly classification only).
|
||||||
@@ -205,7 +205,7 @@ The integration harness at `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTest
|
|||||||
- name: Start ab_server Docker container
|
- name: Start ab_server Docker container
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml `
|
docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml `
|
||||||
--profile controllogix up -d --build
|
--profile controllogix up -d --build
|
||||||
# Wait for :44818 to accept connections (compose healthcheck-equivalent)
|
# Wait for :44818 to accept connections (compose healthcheck-equivalent)
|
||||||
for ($i = 0; $i -lt 30; $i++) {
|
for ($i = 0; $i -lt 30; $i++) {
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
The goal of this project is to identify and develop SQL queries that extract the Galaxy object hierarchy from the **System Platform Galaxy Repository** database in order to build a tag structure for an OPC UA server.
|
|
||||||
|
|
||||||
Specifically, we need to:
|
|
||||||
- Build the hierarchy of **areas** and **automation objects** (using contained names for human-readable browsing)
|
|
||||||
- Translate contained names to **tag_names** for read/write operations (e.g., `TestMachine_001.DelmiaReceiver` in the hierarchy becomes `DelmiaReceiver_001` when addressing tag values)
|
|
||||||
|
|
||||||
See `layout.md` for details on the hierarchy vs tag name relationship.
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- `connectioninfo.md` — Database connection details and sqlcmd usage
|
|
||||||
- `layout.md` — Galaxy object hierarchy, contained_name vs tag_name translation, and target OPC UA structure
|
|
||||||
- `build_layout_plan.md` — Step-by-step plan for extracting hierarchy, attaching attributes, and monitoring for changes
|
|
||||||
- `data_type_mapping.md` — Galaxy mx_data_type to OPC UA DataType mapping, including array handling (ValueRank, ArrayDimensions)
|
|
||||||
|
|
||||||
### Queries
|
|
||||||
- `queries/hierarchy.sql` — Deployed object hierarchy with browse names and parent relationships
|
|
||||||
- `queries/attributes.sql` — User-defined (dynamic) attributes with data types and array dimensions
|
|
||||||
- `queries/attributes_extended.sql` — All attributes (system + user-defined) with data types and array dimensions
|
|
||||||
- `queries/change_detection.sql` — Poll `galaxy.time_of_last_deploy` to detect deployment changes
|
|
||||||
|
|
||||||
### Schema Reference
|
|
||||||
- `schema.md` — Full schema reference for all tables and views in the ZB database
|
|
||||||
- `ddl/tables/` — Individual CREATE TABLE definitions
|
|
||||||
- `ddl/views/` — Individual view definitions
|
|
||||||
|
|
||||||
## Working with the Galaxy Repository Database
|
|
||||||
|
|
||||||
The Galaxy Repository is the backing SQL Server database for Wonderware/AVEVA System Platform (Galaxy: ZB, localhost, Windows Auth). Key tables used by the queries:
|
|
||||||
|
|
||||||
- **gobject** — Object instances, hierarchy (contained_by_gobject_id, area_gobject_id), deployment state (deployed_package_id)
|
|
||||||
- **template_definition** — Object type categories (category_id distinguishes areas, engines, user-defined objects, etc.)
|
|
||||||
- **dynamic_attribute** — User-defined attributes on templates, inherited by instances via derived_from_gobject_id chain
|
|
||||||
- **attribute_definition** — System/primitive attributes
|
|
||||||
- **primitive_instance** — Links objects to their primitive components and attribute definitions
|
|
||||||
- **galaxy** — Single-row table with time_of_last_deploy for change detection
|
|
||||||
|
|
||||||
Use `sqlcmd -S localhost -d ZB -E -Q "..."` to run queries. See `connectioninfo.md` for details.
|
|
||||||
|
|
||||||
## Conventions
|
|
||||||
|
|
||||||
- Store all connection parameters in `connectioninfo.md`, not scattered across scripts.
|
|
||||||
- Keep SQL query examples and extraction notes as Markdown files in this repo.
|
|
||||||
- If scripts are added (Python, PowerShell, etc.), document their usage and dependencies alongside them.
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# OPC UA Server Layout — Build Plan
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Extract the Galaxy object hierarchy and tag definitions from the ZB (Galaxy Repository) database to construct an OPC UA server address space. The root node is hardcoded as **ZB**.
|
|
||||||
|
|
||||||
## Step 1: Build the Browse Tree
|
|
||||||
|
|
||||||
Run `queries/hierarchy.sql` to get all deployed automation objects and their parent-child relationships.
|
|
||||||
|
|
||||||
For each row returned:
|
|
||||||
- `parent_gobject_id = 0` → child of the root ZB node
|
|
||||||
- `is_area = 1` → create as an OPC UA folder node (organizational)
|
|
||||||
- `is_area = 0` → create as an OPC UA object node (container for tags)
|
|
||||||
- Use `browse_name` as the OPC UA BrowseName/DisplayName
|
|
||||||
- Store `gobject_id` and `tag_name` for attribute lookup and tag reference translation
|
|
||||||
|
|
||||||
Build the tree by matching each row's `parent_gobject_id` to another row's `gobject_id`. The result is:
|
|
||||||
|
|
||||||
```
|
|
||||||
ZB (root, hardcoded)
|
|
||||||
└── DEV (folder, is_area=1)
|
|
||||||
├── DevAppEngine (object)
|
|
||||||
├── DevPlatform (object)
|
|
||||||
└── TestArea (folder, is_area=1)
|
|
||||||
├── DevTestObject (object)
|
|
||||||
└── TestMachine_001 (object)
|
|
||||||
├── DelmiaReceiver (object, browse_name from contained_name)
|
|
||||||
└── MESReceiver (object, browse_name from contained_name)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 2: Attach Attributes as Tag Nodes
|
|
||||||
|
|
||||||
Run `queries/attributes.sql` to get all user-defined attributes for deployed objects.
|
|
||||||
|
|
||||||
For each attribute row:
|
|
||||||
- Match to the browse tree via `gobject_id`
|
|
||||||
- Create an OPC UA variable node under the matching object node
|
|
||||||
- Use `attribute_name` as the BrowseName/DisplayName
|
|
||||||
- Use `full_tag_reference` as the runtime tag path for read/write operations
|
|
||||||
- Map `mx_data_type` to OPC UA built-in types:
|
|
||||||
|
|
||||||
| mx_data_type | Description | OPC UA Type |
|
|
||||||
|--------------|-------------|-------------|
|
|
||||||
| 1 | Boolean | Boolean |
|
|
||||||
| 2 | Integer | Int32 |
|
|
||||||
| 3 | Float | Float |
|
|
||||||
| 4 | Double | Double |
|
|
||||||
| 5 | String | String |
|
|
||||||
| 6 | Time | DateTime |
|
|
||||||
| 7 | ElapsedTime | Double (seconds) or Duration |
|
|
||||||
|
|
||||||
- If `is_array = 1`, create the variable as an array with rank 1 and dimension from `array_dimension`
|
|
||||||
|
|
||||||
## Step 3: Monitor for Changes
|
|
||||||
|
|
||||||
Poll `queries/change_detection.sql` on a regular interval (e.g., every 30 seconds).
|
|
||||||
|
|
||||||
```
|
|
||||||
SELECT time_of_last_deploy FROM galaxy;
|
|
||||||
```
|
|
||||||
|
|
||||||
Compare the returned `time_of_last_deploy` to the last known value:
|
|
||||||
- **No change** → do nothing
|
|
||||||
- **Changed** → a deployment occurred; re-run Steps 1 and 2 to rebuild the address space
|
|
||||||
|
|
||||||
This handles objects being deployed, undeployed, added, or removed.
|
|
||||||
|
|
||||||
## Connection Details
|
|
||||||
|
|
||||||
See `connectioninfo.md` for database connection parameters and sqlcmd usage.
|
|
||||||
|
|
||||||
```
|
|
||||||
sqlcmd -S localhost -d ZB -E -Q "YOUR QUERY HERE"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Query Files
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `queries/hierarchy.sql` | Deployed object hierarchy with browse names and parent relationships |
|
|
||||||
| `queries/attributes.sql` | User-defined attributes with data types and array dimensions |
|
|
||||||
| `queries/attributes_extended.sql` | All attributes (system + user-defined) with data types and array dimensions |
|
|
||||||
| `queries/change_detection.sql` | Poll galaxy.time_of_last_deploy for deployment changes |
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Galaxy Repository — Connection Information
|
|
||||||
|
|
||||||
## Database Connection
|
|
||||||
|
|
||||||
| Parameter | Value |
|
|
||||||
|-----------------|----------------|
|
|
||||||
| Server | localhost (default instance) |
|
|
||||||
| Database Name | ZB |
|
|
||||||
| Port | 1433 (default) |
|
|
||||||
| Authentication | Windows Auth |
|
|
||||||
| Username | dohertj2 |
|
|
||||||
|
|
||||||
## sqlcmd Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
sqlcmd -S localhost -d ZB -E -Q "YOUR QUERY HERE"
|
|
||||||
```
|
|
||||||
|
|
||||||
- `-S localhost` — default instance
|
|
||||||
- `-d ZB` — database name
|
|
||||||
- `-E` — Windows Authentication (dohertj2)
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- The Galaxy Repository is a SQL Server database created and managed by AVEVA System Platform (formerly Wonderware).
|
|
||||||
- Typically accessed via SQL Server Management Studio (SSMS), `sqlcmd`, or programmatically via ODBC/ADO.NET/pyodbc.
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# Data Type Mapping — Galaxy Repository to OPC UA
|
|
||||||
|
|
||||||
## Scalar Type Mapping
|
|
||||||
|
|
||||||
| mx_data_type | Galaxy Description | OPC UA DataType | OPC UA NodeId | Notes |
|
|
||||||
|--------------|--------------------|-----------------|---------------|-------|
|
|
||||||
| 1 | Boolean | Boolean | i=1 | Direct mapping |
|
|
||||||
| 2 | Integer (Int32) | Int32 | i=6 | Galaxy integers are 32-bit signed |
|
|
||||||
| 3 | Float (Single) | Float | i=10 | 32-bit IEEE 754 |
|
|
||||||
| 4 | Double | Double | i=11 | 64-bit IEEE 754 |
|
|
||||||
| 5 | String | String | i=12 | Unicode string |
|
|
||||||
| 6 | Time (DateTime) | DateTime | i=13 | Galaxy DateTime to OPC UA DateTime (100ns ticks since 1601-01-01) |
|
|
||||||
| 7 | ElapsedTime (TimeSpan) | Double | i=11 | No native OPC UA TimeSpan; map to Double representing seconds (or use Duration type alias, NodeId i=290) |
|
|
||||||
| 8 | (reference) | String | i=12 | Object reference; expose as string representation |
|
|
||||||
| 13 | (enumeration) | Int32 | i=6 | Enum backing value is integer |
|
|
||||||
| 14 | (custom) | String | i=12 | Fallback to string |
|
|
||||||
| 15 | InternationalizedString | LocalizedText | i=21 | OPC UA LocalizedText supports locale + text pairs |
|
|
||||||
| 16 | (custom) | String | i=12 | Fallback to string |
|
|
||||||
|
|
||||||
## OPC UA Built-in Type Reference
|
|
||||||
|
|
||||||
For context, the full set of OPC UA built-in types and their NodeIds:
|
|
||||||
|
|
||||||
| NodeId | Type | Description |
|
|
||||||
|--------|------|-------------|
|
|
||||||
| i=1 | Boolean | True/false |
|
|
||||||
| i=2 | SByte | Signed 8-bit integer |
|
|
||||||
| i=3 | Byte | Unsigned 8-bit integer |
|
|
||||||
| i=4 | Int16 | Signed 16-bit integer |
|
|
||||||
| i=5 | UInt16 | Unsigned 16-bit integer |
|
|
||||||
| i=6 | Int32 | Signed 32-bit integer |
|
|
||||||
| i=7 | UInt32 | Unsigned 32-bit integer |
|
|
||||||
| i=8 | Int64 | Signed 64-bit integer |
|
|
||||||
| i=9 | UInt64 | Unsigned 64-bit integer |
|
|
||||||
| i=10 | Float | 32-bit IEEE 754 |
|
|
||||||
| i=11 | Double | 64-bit IEEE 754 |
|
|
||||||
| i=12 | String | Unicode string |
|
|
||||||
| i=13 | DateTime | Date and time (100ns ticks since 1601-01-01) |
|
|
||||||
| i=14 | Guid | 128-bit globally unique identifier |
|
|
||||||
| i=15 | ByteString | Sequence of bytes |
|
|
||||||
| i=21 | LocalizedText | Locale + text pair |
|
|
||||||
|
|
||||||
## Array Handling
|
|
||||||
|
|
||||||
When `is_array = 1` in the attributes query, the OPC UA variable node must be configured as an array.
|
|
||||||
|
|
||||||
### ValueRank
|
|
||||||
|
|
||||||
Set on the OPC UA variable node to indicate scalar vs array:
|
|
||||||
|
|
||||||
| is_array | ValueRank | Meaning |
|
|
||||||
|----------|-----------|---------|
|
|
||||||
| 0 | -1 (Scalar) | Value is not an array |
|
|
||||||
| 1 | 1 (OneDimension) | Value is a one-dimensional array |
|
|
||||||
|
|
||||||
### ArrayDimensions
|
|
||||||
|
|
||||||
When `ValueRank = 1`, set the `ArrayDimensions` attribute to a single-element array containing the `array_dimension` value from the attributes query.
|
|
||||||
|
|
||||||
Example for `MESReceiver_001.MoveInPartNumbers` (`is_array=1`, `array_dimension=50`):
|
|
||||||
- DataType: String (i=12)
|
|
||||||
- ValueRank: 1
|
|
||||||
- ArrayDimensions: [50]
|
|
||||||
|
|
||||||
Example for `TestMachine_001.MachineID` (`is_array=0`):
|
|
||||||
- DataType: String (i=12)
|
|
||||||
- ValueRank: -1
|
|
||||||
- ArrayDimensions: (not set)
|
|
||||||
|
|
||||||
## Security Classification
|
|
||||||
|
|
||||||
Galaxy attributes have a `security_classification` column that controls the access level required for writes. The attributes query returns this value for each attribute.
|
|
||||||
|
|
||||||
| security_classification | Galaxy Level | OPC UA Access | Description |
|
|
||||||
|-------------------------|--------------|---------------|-------------|
|
|
||||||
| 0 | FreeAccess | ReadWrite | No security restrictions |
|
|
||||||
| 1 | Operate | ReadWrite | Normal operating level (default) |
|
|
||||||
| 2 | SecuredWrite | ReadOnly | Requires elevated write access |
|
|
||||||
| 3 | VerifiedWrite | ReadOnly | Requires verified/confirmed write access |
|
|
||||||
| 4 | Tune | ReadWrite | Tuning-level access |
|
|
||||||
| 5 | Configure | ReadWrite | Configuration-level access |
|
|
||||||
| 6 | ViewOnly | ReadOnly | Read-only, no writes permitted |
|
|
||||||
|
|
||||||
Most attributes default to `Operate` (1). Higher values indicate more restrictive write access. `ViewOnly` (6) attributes should be exposed as read-only in OPC UA (`AccessLevel = CurrentRead` only, no `CurrentWrite`).
|
|
||||||
|
|
||||||
## DateTime Conversion
|
|
||||||
|
|
||||||
Galaxy `Time` (mx_data_type=6) stores DateTime values. OPC UA DateTime is defined as the number of 100-nanosecond intervals since January 1, 1601 (UTC). Ensure the conversion accounts for:
|
|
||||||
- Timezone: Galaxy may store local time; OPC UA expects UTC
|
|
||||||
- Epoch difference: adjust if Galaxy uses a different epoch (e.g., Unix epoch 1970-01-01)
|
|
||||||
|
|
||||||
## ElapsedTime Handling
|
|
||||||
|
|
||||||
Galaxy `ElapsedTime` (mx_data_type=7) represents a duration/timespan. OPC UA has no native TimeSpan type. Options:
|
|
||||||
- **Double (i=11)**: Store as seconds (recommended for simplicity)
|
|
||||||
- **Duration (i=290)**: OPC UA type alias for Double, semantically represents milliseconds — use if the OPC UA SDK supports it
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- Table: ConversionQueue
|
|
||||||
CREATE TABLE [ConversionQueue] (
|
|
||||||
[id] int NULL,
|
|
||||||
[Name] nvarchar(329) NULL,
|
|
||||||
[IsCheckedOut] bit NOT NULL,
|
|
||||||
[Status] bit NOT NULL DEFAULT ((0)),
|
|
||||||
[MetaData] nchar(256) NULL,
|
|
||||||
[OperationType] nchar(20) NOT NULL,
|
|
||||||
[timestamp_of_last_change] bigint NULL,
|
|
||||||
[change_type] int NULL
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
-- Table: CurrentSessionContainedName
|
|
||||||
CREATE TABLE [CurrentSessionContainedName] (
|
|
||||||
[Uniqeid] int NOT NULL,
|
|
||||||
[obj_id] int NULL,
|
|
||||||
[containedname] nvarchar(32) NULL,
|
|
||||||
CONSTRAINT [PK_CurrentSessionContainedName] PRIMARY KEY ([Uniqeid])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
-- Table: ImportTransaction
|
|
||||||
CREATE TABLE [ImportTransaction] (
|
|
||||||
[ImportOperationId] nvarchar(329) NULL,
|
|
||||||
[Status] bit NOT NULL DEFAULT ((1))
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-- Table: aa_sql_objects
|
|
||||||
CREATE TABLE [aa_sql_objects] (
|
|
||||||
[object_name] nvarchar(128) NOT NULL,
|
|
||||||
[object_type] nvarchar(10) NOT NULL,
|
|
||||||
CONSTRAINT [PK_aa_sql_objects] PRIMARY KEY ([object_name])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
-- Table: affected_overview_symbols
|
|
||||||
CREATE TABLE [affected_overview_symbols] (
|
|
||||||
[gobject_id] int NOT NULL,
|
|
||||||
[package_id] int NOT NULL,
|
|
||||||
[mx_primitive_id] smallint NOT NULL,
|
|
||||||
[visual_element_id] int NOT NULL
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-- Table: alarm_message_defaults
|
|
||||||
CREATE TABLE [alarm_message_defaults] (
|
|
||||||
[phrase_id] int NOT NULL,
|
|
||||||
[default_message] nvarchar(1024) NOT NULL,
|
|
||||||
CONSTRAINT [PK_alarm_message_defaults] PRIMARY KEY ([phrase_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-- Table: alarm_message_timestamps
|
|
||||||
CREATE TABLE [alarm_message_timestamps] (
|
|
||||||
[gobject_id] int NOT NULL,
|
|
||||||
[timestamp_of_populate] bigint NOT NULL DEFAULT ((0)),
|
|
||||||
CONSTRAINT [PK_alarm_message_timestamps] PRIMARY KEY ([gobject_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
-- Table: alarm_message_translations
|
|
||||||
CREATE TABLE [alarm_message_translations] (
|
|
||||||
[phrase_id] int NOT NULL,
|
|
||||||
[locale_id] smallint NOT NULL,
|
|
||||||
[translated_message] nvarchar(1024) NOT NULL,
|
|
||||||
CONSTRAINT [PK_alarm_message_translations] PRIMARY KEY ([phrase_id], [locale_id], [phrase_id], [locale_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
ALTER TABLE [alarm_message_translations] ADD FOREIGN KEY ([locale_id]) REFERENCES [supported_locales] ([locale_id]);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- Table: alarm_messages
|
|
||||||
CREATE TABLE [alarm_messages] (
|
|
||||||
[gobject_id] int NOT NULL,
|
|
||||||
[package_id] int NOT NULL,
|
|
||||||
[mx_primitive_id] smallint NOT NULL,
|
|
||||||
[phrase_id] int NOT NULL,
|
|
||||||
CONSTRAINT [PK_alarm_messages] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [phrase_id], [gobject_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id], [gobject_id], [mx_primitive_id], [package_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
ALTER TABLE [alarm_messages] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
-- Table: attribute_definition
|
|
||||||
CREATE TABLE [attribute_definition] (
|
|
||||||
[attribute_definition_id] int NOT NULL,
|
|
||||||
[primitive_definition_id] int NOT NULL,
|
|
||||||
[attribute_name] nvarchar(329) NOT NULL,
|
|
||||||
[mx_attribute_id] smallint NOT NULL,
|
|
||||||
[has_config_set_handler] bit NOT NULL,
|
|
||||||
[mx_data_type] smallint NOT NULL,
|
|
||||||
[is_array] bit NOT NULL,
|
|
||||||
[security_classification] smallint NOT NULL,
|
|
||||||
[security_classification_needs_deployed] bit NOT NULL,
|
|
||||||
[mx_attribute_category] int NOT NULL,
|
|
||||||
[is_frequently_accessed] bit NOT NULL,
|
|
||||||
[is_locked] bit NOT NULL,
|
|
||||||
[is_locked_needs_deployed] bit NOT NULL,
|
|
||||||
[mx_value] text(2147483647) NOT NULL,
|
|
||||||
[mx_value_needs_deployed] bit NOT NULL,
|
|
||||||
CONSTRAINT [PK_attribute_definition] PRIMARY KEY ([primitive_definition_id], [mx_attribute_id], [primitive_definition_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
ALTER TABLE [attribute_definition] ADD FOREIGN KEY ([primitive_definition_id]) REFERENCES [primitive_definition] ([primitive_definition_id]);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
-- Table: attribute_reference
|
|
||||||
CREATE TABLE [attribute_reference] (
|
|
||||||
[gobject_id] int NOT NULL,
|
|
||||||
[package_id] int NOT NULL,
|
|
||||||
[referring_mx_primitive_id] smallint NOT NULL DEFAULT ((0)),
|
|
||||||
[referring_mx_attribute_id] smallint NOT NULL DEFAULT ((0)),
|
|
||||||
[element_index] smallint NOT NULL DEFAULT ((0)),
|
|
||||||
[resolved_gobject_id] int NOT NULL DEFAULT ((0)),
|
|
||||||
[reference_string] nvarchar(700) NOT NULL DEFAULT (''),
|
|
||||||
[context_string] nvarchar(329) NOT NULL DEFAULT (''),
|
|
||||||
[object_signature] int NOT NULL DEFAULT ((0)),
|
|
||||||
[resolved_mx_primitive_id] smallint NOT NULL DEFAULT ((0)),
|
|
||||||
[resolved_mx_attribute_id] smallint NOT NULL DEFAULT ((0)),
|
|
||||||
[resolved_mx_property_id] smallint NOT NULL DEFAULT ((0)),
|
|
||||||
[attribute_signature] int NOT NULL DEFAULT ((0)),
|
|
||||||
[lock_type] int NOT NULL DEFAULT ((0)),
|
|
||||||
[is_valid] bit NOT NULL DEFAULT ((0)),
|
|
||||||
[attr_res_status] int NOT NULL DEFAULT ((0)),
|
|
||||||
[attribute_index] smallint NULL DEFAULT ((-1)),
|
|
||||||
CONSTRAINT [PK_attribute_reference] PRIMARY KEY ([gobject_id], [package_id], [referring_mx_primitive_id], [referring_mx_attribute_id], [element_index], [gobject_id], [package_id], [referring_mx_primitive_id], [gobject_id], [package_id], [referring_mx_primitive_id], [gobject_id], [package_id], [referring_mx_primitive_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
ALTER TABLE [attribute_reference] ADD FOREIGN KEY ([referring_mx_primitive_id]) REFERENCES [primitive_instance] ([package_id]);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
-- Table: attributes_translation_table
|
|
||||||
CREATE TABLE [attributes_translation_table] (
|
|
||||||
[gobject_id] int NULL,
|
|
||||||
[attribute_name] nvarchar(329) NOT NULL,
|
|
||||||
[new_primitive_id] int NULL,
|
|
||||||
[new_attribute_id] int NULL,
|
|
||||||
[old_primitive_id] int NULL,
|
|
||||||
[old_attribute_id] int NULL
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
-- Table: autobind_device
|
|
||||||
CREATE TABLE [autobind_device] (
|
|
||||||
[dio_id] int NOT NULL,
|
|
||||||
[overridden_naming_rule_id] int NULL,
|
|
||||||
CONSTRAINT [PK_autobind_device] PRIMARY KEY ([dio_id], [overridden_naming_rule_id], [dio_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
ALTER TABLE [autobind_device] ADD FOREIGN KEY ([dio_id]) REFERENCES [gobject] ([gobject_id]);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
-- Table: autobind_device_category
|
|
||||||
CREATE TABLE [autobind_device_category] (
|
|
||||||
[category_id] smallint NOT NULL,
|
|
||||||
[rule_id] int NULL DEFAULT ((0)),
|
|
||||||
CONSTRAINT [PK_autobind_device_category] PRIMARY KEY ([category_id], [rule_id], [category_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
ALTER TABLE [autobind_device_category] ADD FOREIGN KEY ([category_id]) REFERENCES [lookup_category] ([category_id]);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
-- Table: autobind_device_template
|
|
||||||
CREATE TABLE [autobind_device_template] (
|
|
||||||
[template_definition_id] int NOT NULL,
|
|
||||||
[rule_id] int NULL,
|
|
||||||
CONSTRAINT [PK_autobind_device_template] PRIMARY KEY ([template_definition_id], [rule_id], [template_definition_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
ALTER TABLE [autobind_device_template] ADD FOREIGN KEY ([template_definition_id]) REFERENCES [template_definition] ([template_definition_id]);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- Table: autobind_device_topic
|
|
||||||
CREATE TABLE [autobind_device_topic] (
|
|
||||||
[dio_id] int NOT NULL,
|
|
||||||
[sg_mx_primitive_id] smallint NOT NULL DEFAULT ((0)),
|
|
||||||
[overridden_naming_rule_id] int NULL,
|
|
||||||
[default_xlate_rule_id] int NOT NULL DEFAULT ((0)),
|
|
||||||
CONSTRAINT [PK_autobind_device_topic] PRIMARY KEY ([dio_id], [sg_mx_primitive_id], [overridden_naming_rule_id], [dio_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
ALTER TABLE [autobind_device_topic] ADD FOREIGN KEY ([dio_id]) REFERENCES [autobind_device] ([dio_id]);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-- Table: autobind_naming_rule
|
|
||||||
CREATE TABLE [autobind_naming_rule] (
|
|
||||||
[rule_id] int NOT NULL,
|
|
||||||
[rule_name] nvarchar(329) NOT NULL,
|
|
||||||
CONSTRAINT [PK_autobind_naming_rule] PRIMARY KEY ([rule_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
-- Table: autobind_naming_rule_spec
|
|
||||||
CREATE TABLE [autobind_naming_rule_spec] (
|
|
||||||
[rule_id] int NOT NULL,
|
|
||||||
[io_type] nchar(1) NOT NULL,
|
|
||||||
[rule_spec] nvarchar(512) NOT NULL,
|
|
||||||
CONSTRAINT [PK_autobind_naming_rule_spec] PRIMARY KEY ([rule_id], [io_type], [rule_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
ALTER TABLE [autobind_naming_rule_spec] ADD FOREIGN KEY ([rule_id]) REFERENCES [autobind_naming_rule] ([rule_id]);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
-- Table: autobind_translation_rule
|
|
||||||
CREATE TABLE [autobind_translation_rule] (
|
|
||||||
[xlate_rule_id] int NOT NULL,
|
|
||||||
[xlate_rule_name] nvarchar(329) NOT NULL,
|
|
||||||
[xlate_rule_gsub_str] nvarchar(1000) NULL,
|
|
||||||
[xlate_rule_scope_global] bit NOT NULL DEFAULT ((0)),
|
|
||||||
CONSTRAINT [PK_autobind_translation_rule] PRIMARY KEY ([xlate_rule_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
-- Table: autobound_attribute
|
|
||||||
CREATE TABLE [autobound_attribute] (
|
|
||||||
[dio_id] int NOT NULL,
|
|
||||||
[sg_mx_primitive_id] smallint NOT NULL DEFAULT ((0)),
|
|
||||||
[gobject_id] int NOT NULL,
|
|
||||||
[mx_primitive_id] smallint NOT NULL,
|
|
||||||
[mx_attribute_id] smallint NOT NULL,
|
|
||||||
[element_index] smallint NOT NULL DEFAULT ((0)),
|
|
||||||
[attr_alias] nvarchar(329) NULL,
|
|
||||||
[xlate_rule_id] int NOT NULL DEFAULT ((0)),
|
|
||||||
CONSTRAINT [PK_autobound_attribute] PRIMARY KEY ([gobject_id], [mx_primitive_id], [mx_attribute_id], [element_index], [dio_id], [sg_mx_primitive_id], [dio_id], [sg_mx_primitive_id], [xlate_rule_id])
|
|
||||||
);
|
|
||||||
GO
|
|
||||||
|
|
||||||
ALTER TABLE [autobound_attribute] ADD FOREIGN KEY ([xlate_rule_id]) REFERENCES [autobind_translation_rule] ([xlate_rule_id]);
|
|
||||||
GO
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user