Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64d8838e18 | |||
| 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 | |||
| ae7106dfce | |||
| 1bd8a1875b | |||
| fe91d42927 | |||
| 6bf147a113 | |||
| 9db2edcbb5 | |||
| 5e890ec9d6 | |||
| 580c45f494 | |||
| da277a843a | |||
| c55da145ec | |||
| 42f41fbe50 | |||
| d5a87c7467 | |||
| 6f4cbf8449 | |||
| edee47d77f | |||
| 22ef2eb5ba | |||
| 698bdef572 | |||
| 2fdad81af3 | |||
| 7b21c3b428 | |||
| 619207e7f5 | |||
| 78fe3e8a45 |
@@ -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)
|
||||
scripts/e2e/e2e-config.json
|
||||
config_cache*.db
|
||||
|
||||
# Client CLI/UI runtime scratch (last-connected endpoint cache)
|
||||
session.dat
|
||||
|
||||
@@ -4,15 +4,38 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Goal
|
||||
|
||||
Build an OPC UA server on .NET Framework 4.8 (32-bit) that exposes AVEVA System Platform (Wonderware) Galaxy tags via the MXAccess toolkit. The server mirrors the Galaxy object hierarchy as an OPC UA address space, translating between contained-name browse paths and tag-name runtime references.
|
||||
Build an OPC UA server (.NET 10) that exposes AVEVA System Platform
|
||||
(Wonderware) Galaxy tags. The server mirrors the Galaxy object
|
||||
hierarchy as an OPC UA address space, translating between
|
||||
contained-name browse paths and tag-name runtime references. Galaxy
|
||||
access flows through the in-process `GalaxyDriver`
|
||||
(`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/`) talking gRPC to a separately
|
||||
installed **mxaccessgw** gateway process. The gateway owns the
|
||||
MXAccess COM bitness constraint (its worker is x86 net48); everything
|
||||
in this repo is .NET 10. PR 7.2 retired the legacy in-process
|
||||
`Galaxy.Host` / `Galaxy.Proxy` / `Galaxy.Shared` projects + the
|
||||
`OtOpcUaGalaxyHost` Windows service.
|
||||
|
||||
See `docs/v2/Galaxy.Performance.md` for the runtime perf surface
|
||||
(tracing, metrics, soak harness).
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Galaxy Repository DB (ZB)** — SQL Server database holding the deployed object hierarchy and attribute definitions. Queried at startup and on change detection to build/rebuild the OPC UA address space.
|
||||
2. **MXAccess COM API** — Runtime data access layer. Subscribes to Galaxy tag attributes for live read/write. Requires a dedicated STA thread with a Win32 message pump for COM callbacks.
|
||||
3. **OPC UA Server** — Exposes the hierarchy as browse nodes and attributes as variable nodes. Clients browse via contained names but reads/writes are translated to `tag_name.AttributeName` format for MXAccess.
|
||||
1. **Galaxy Repository DB (ZB)** — SQL Server database holding the
|
||||
deployed object hierarchy and attribute definitions. The
|
||||
mxaccessgw's `GalaxyRepositoryClient` queries it via gRPC; the
|
||||
driver consumes the materialised hierarchy through
|
||||
`IGalaxyHierarchySource`.
|
||||
2. **MXAccess (via mxaccessgw)** — Live read/write/subscribe over a
|
||||
gRPC session. The gateway owns the COM apartment + STA pump
|
||||
server-side; the driver speaks `MxCommand` / `MxEvent` protos
|
||||
exclusively.
|
||||
3. **OPC UA Server** — Exposes the hierarchy as browse nodes and
|
||||
attributes as variable nodes. Clients browse via contained names
|
||||
but reads/writes are translated to `tag_name.AttributeName` format
|
||||
for MXAccess.
|
||||
|
||||
### Key Concept: Contained Name vs Tag Name
|
||||
|
||||
@@ -22,43 +45,17 @@ Galaxy objects have two names:
|
||||
|
||||
Example: browsing `TestMachine_001/DelmiaReceiver/DownloadPath` translates to MXAccess reference `DelmiaReceiver_001.DownloadPath`.
|
||||
|
||||
See `gr/layout.md` for the full mapping and target OPC UA structure.
|
||||
|
||||
### 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. Full mapping in `gr/data_type_mapping.md`.
|
||||
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`.
|
||||
|
||||
### Change Detection
|
||||
|
||||
Poll `galaxy.time_of_last_deploy` in the ZB database to detect redeployments, then rebuild the address space. See `gr/build_layout_plan.md` for the step-by-step plan.
|
||||
`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.
|
||||
|
||||
## Reference Implementation
|
||||
## mxaccessgw
|
||||
|
||||
An existing MXAccess client implementation is at:
|
||||
`C:\Users\dohertj2\Desktop\scadalink-design\lmxproxy\src\ZB.MOM.WW.LmxProxy.Host`
|
||||
|
||||
Key patterns from that codebase:
|
||||
- **StaComThread** — Dedicated STA thread with Win32 message pump (`GetMessage`/`DispatchMessage` loop). All MXAccess COM objects must be created and called on this thread. Uses `PostThreadMessage(WM_APP)` to marshal work items.
|
||||
- **LMXProxyServer COM object** — `Register(clientName)` returns a connection handle. `AddItem(handle, address)` + `AdviseSupervisory(handle, itemHandle)` for subscriptions. `OnDataChange`/`OnWriteComplete` events for callbacks.
|
||||
- **Reconnect** — Stored subscriptions are replayed after reconnect. A probe tag subscription monitors connection health.
|
||||
- **COM cleanup** — `Marshal.ReleaseComObject()` on disconnect. Event handlers must be unwired before unregister.
|
||||
|
||||
## MXAccess Documentation
|
||||
|
||||
`mxaccess_documentation.md` in the project root contains the full ArchestrA MXAccess Toolkit User's Guide. Key API: `ArchestrA.MxAccess` namespace, `LMXProxyServer` class. The toolkit DLLs are in `Program Files (x86)\ArchestrA\Framework\bin`.
|
||||
|
||||
## 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).
|
||||
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.
|
||||
|
||||
## Build Commands
|
||||
|
||||
@@ -71,11 +68,48 @@ dotnet test tests/ZB.MOM.WW.OtOpcUa.IntegrationTests # integration tests
|
||||
dotnet test --filter "FullyQualifiedName~MyTestClass.MyMethod" # single test
|
||||
```
|
||||
|
||||
## 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).
|
||||
|
||||
Docker workloads run on a shared Linux host at **`10.100.0.35`** — not on this VM. Stacks live at `/opt/otopcua-<driver>/` on the host and carry the `project=lmxopcua` label so they're discoverable via `docker ps --filter label=project=lmxopcua`.
|
||||
|
||||
**`docker -H ssh://...` does NOT work from this VM.** Windows OpenSSH ↔ docker.exe stdio bridging hangs (`docker system dial-stdio` runs server-side but no API data flows). Use the helper below — it SSHes into the docker host and runs `docker compose` server-side.
|
||||
|
||||
**Use `lmxopcua-fix.ps1` (in `~/bin`) to control fixtures from this VM:**
|
||||
|
||||
```powershell
|
||||
lmxopcua-fix ls # list all lmxopcua-tagged containers on the host
|
||||
lmxopcua-fix up modbus standard # bring a profile up
|
||||
lmxopcua-fix up abcip controllogix
|
||||
lmxopcua-fix up s7 s7_1500
|
||||
lmxopcua-fix up opcuaclient # single-service stack, no profile arg
|
||||
lmxopcua-fix down modbus # tear stack down
|
||||
lmxopcua-fix logs modbus
|
||||
lmxopcua-fix sync modbus # rsync this repo's tests/.../Docker/ → /opt/otopcua-modbus/
|
||||
```
|
||||
|
||||
**`sync` is the deployment step.** When you edit a fixture's compose file or Dockerfile under `tests/.../Docker/`, run `lmxopcua-fix sync <driver>` to push the changes to the docker host before bringing the stack up. The repo files are the source of truth; `/opt/otopcua-<driver>/` is a mirrored deployment.
|
||||
|
||||
**Endpoints (defaults already point at the docker host):**
|
||||
- SQL Server (always-on): `10.100.0.35,14330` — used by `appsettings.json` for `ConfigDb`.
|
||||
- Modbus: `10.100.0.35:5020` (`MODBUS_SIM_ENDPOINT`)
|
||||
- AB CIP: `10.100.0.35:44818` (`AB_SERVER_ENDPOINT`)
|
||||
- S7: `10.100.0.35:1102` (`S7_SIM_ENDPOINT`)
|
||||
- OPC UA reference (opc-plc): `opc.tcp://10.100.0.35:50000` (`OPCUA_SIM_ENDPOINT`)
|
||||
|
||||
Override any endpoint via the env var to point at a real PLC. The local OtOpcUa server runs on this VM at `opc.tcp://localhost:4840` — **that's not on the docker host**.
|
||||
|
||||
See `docs/v2/dev-environment.md` for the full inventory and rationale.
|
||||
|
||||
## Build & Runtime Constraints
|
||||
|
||||
- Language: C#, .NET Framework 4.8, **x86 (32-bit)** platform target — required for MXAccess COM interop
|
||||
- MXAccess requires a deployed ArchestrA Platform on the machine running the server
|
||||
- COM apartment: MXAccess objects must live on an STA thread with a message pump
|
||||
- Language: C#, .NET 10, AnyCPU. The MXAccess COM bitness constraint
|
||||
is owned by the mxaccessgw worker (x86 net48), not by anything in
|
||||
this repo.
|
||||
- The gateway's MXAccess worker requires a deployed ArchestrA Platform
|
||||
on the machine running the gateway. The OtOpcUa server itself does
|
||||
not.
|
||||
|
||||
## Transport Security
|
||||
|
||||
@@ -83,7 +117,7 @@ The server supports configurable OPC UA transport security via the `Security` se
|
||||
|
||||
## Redundancy
|
||||
|
||||
The server supports non-transparent warm/hot redundancy via the `Redundancy` section in `appsettings.json`. Two instances share the same Galaxy DB and MXAccess runtime but have unique `ApplicationUri` values. Each exposes `RedundancySupport`, `ServerUriArray`, and a dynamic `ServiceLevel` based on role and runtime health. The primary advertises a higher ServiceLevel than the secondary. See `docs/Redundancy.md` for the full guide.
|
||||
The server supports non-transparent warm/hot redundancy via the `Redundancy` section in `appsettings.json`. Two instances share the same Galaxy DB and the same mxaccessgw (under distinct `MxAccess.ClientName` values) but have unique `ApplicationUri` values. Each exposes `RedundancySupport`, `ServerUriArray`, and a dynamic `ServiceLevel` based on role and runtime health. The primary advertises a higher ServiceLevel than the secondary. See `docs/Redundancy.md` for the full guide.
|
||||
|
||||
## LDAP Authentication
|
||||
|
||||
@@ -94,7 +128,6 @@ The server uses LDAP-based user authentication via the `Authentication.Ldap` sec
|
||||
- **Logging**: Serilog with rolling daily file sink
|
||||
- **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 (Galaxy.Host)**: plain console app wrapped by NSSM (`.NET Framework 4.8 x86` — required by MXAccess COM bitness)
|
||||
- **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
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
OPC UA Clients
|
||||
(CLI, Desktop UI, 3rd-party)
|
||||
|
|
||||
v
|
||||
+-----------------+ +------------------+ +-----------------+
|
||||
| Galaxy Repo DB |---->| OPC UA Server |<--->| MXAccess Client |
|
||||
| (SQL Server) | | (address space) | | (STA + COM) |
|
||||
+-----------------+ +------------------+ +-----------------+
|
||||
| |
|
||||
+-------+--------+ +---------+---------+
|
||||
| Status Dashboard| | Historian Runtime |
|
||||
| (HTTP/JSON) | | (SQL Server) |
|
||||
+----------------+ +-------------------+
|
||||
OPC UA Clients (CLI, Desktop UI, 3rd-party)
|
||||
|
|
||||
v
|
||||
+-------------------------------------+
|
||||
| OtOpcUa.Server (.NET 10 AnyCPU) |
|
||||
| address space + capability fan-out|
|
||||
+-------------------------------------+
|
||||
| | | | | | | |
|
||||
Galaxy Modbus S7 AbCip AbLeg TwinCAT FOCAS OpcUaClient
|
||||
|
|
||||
v
|
||||
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) |
|
||||
|-------------------------------|------------------------------|
|
||||
| `TestMachine_001/DelmiaReceiver/DownloadPath` | `DelmiaReceiver_001.DownloadPath` |
|
||||
| `TestMachine_001/MESReceiver/MoveInBatchID` | `MESReceiver_001.MoveInBatchID` |
|
||||
## Prerequisites
|
||||
|
||||
---
|
||||
- .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
|
||||
|
||||
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
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
dotnet restore ZB.MOM.WW.LmxOpcUa.slnx
|
||||
dotnet build src/ZB.MOM.WW.LmxOpcUa.Host
|
||||
dotnet run --project src/ZB.MOM.WW.LmxOpcUa.Host
|
||||
dotnet restore ZB.MOM.WW.OtOpcUa.slnx
|
||||
dotnet build ZB.MOM.WW.OtOpcUa.slnx
|
||||
dotnet test ZB.MOM.WW.OtOpcUa.slnx
|
||||
|
||||
# Run the server in dev (foreground)
|
||||
dotnet run --project src/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/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
|
||||
cd src/ZB.MOM.WW.LmxOpcUa.Host/bin/Debug/net48
|
||||
ZB.MOM.WW.LmxOpcUa.Host.exe install
|
||||
ZB.MOM.WW.LmxOpcUa.Host.exe start
|
||||
dotnet run --project src/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/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 -- write -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -v 42
|
||||
dotnet run --project src/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 ***`.
|
||||
|
||||
### 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
|
||||
```
|
||||
See [docs/Client.CLI.md](docs/Client.CLI.md) and [docs/Client.UI.md](docs/Client.UI.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
### Server
|
||||
### Architecture deep-dives
|
||||
|
||||
| Component | Description |
|
||||
| Topic | Doc |
|
||||
|---|---|
|
||||
| [OPC UA Server](docs/OpcUaServer.md) | Endpoint, sessions, security policy, server lifecycle |
|
||||
| [Address Space](docs/AddressSpace.md) | Hierarchy nodes, variable nodes, primitive grouping, NodeId scheme |
|
||||
| [Galaxy Repository](docs/GalaxyRepository.md) | SQL queries, deployed package chain, change detection |
|
||||
| [MXAccess Bridge](docs/MxAccessBridge.md) | STA thread, COM interop, subscriptions, reconnection |
|
||||
| [Data Type Mapping](docs/DataTypeMapping.md) | Galaxy to OPC UA types, arrays, security classification |
|
||||
| [Read/Write Operations](docs/ReadWriteOperations.md) | Value reads, writes, access level enforcement, array element writes |
|
||||
| [Subscriptions](docs/Subscriptions.md) | Ref-counted MXAccess subscriptions, data change dispatch |
|
||||
| [Alarm Tracking](docs/AlarmTracking.md) | AlarmConditionState nodes, InAlarm monitoring, event reporting |
|
||||
| [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 |
|
||||
| OPC UA server composition, namespace fan-out, Polly invoker | [docs/OpcUaServer.md](docs/OpcUaServer.md) |
|
||||
| Address space layout | [docs/AddressSpace.md](docs/AddressSpace.md) |
|
||||
| Read / Write dispatch (driver vs virtual vs scripted-alarm) | [docs/ReadWriteOperations.md](docs/ReadWriteOperations.md) |
|
||||
| Incremental sync (driver-backend rediscovery + config publishes) | [docs/IncrementalSync.md](docs/IncrementalSync.md) |
|
||||
| Service hosting (Server + Admin + optional historian sidecar) | [docs/ServiceHosting.md](docs/ServiceHosting.md) |
|
||||
| Security (transport, LDAP, certificates) | [docs/security.md](docs/security.md) |
|
||||
| Redundancy | [docs/Redundancy.md](docs/Redundancy.md) |
|
||||
| Status dashboard | [docs/StatusDashboard.md](docs/StatusDashboard.md) |
|
||||
|
||||
### Client
|
||||
### Drivers
|
||||
|
||||
| Component | Description |
|
||||
| Topic | Doc |
|
||||
|---|---|
|
||||
| [Client CLI](docs/Client.CLI.md) | Connect, browse, read, write, subscribe, historyread, alarms, redundancy commands |
|
||||
| [Client UI](docs/Client.UI.md) | Avalonia desktop client: browse, subscribe, alarms, history, write values |
|
||||
| Driver specs (per-driver capability surface, config, addressing) | [docs/v2/driver-specs.md](docs/v2/driver-specs.md) |
|
||||
| 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
|
||||
- [Data Type Mapping](gr/data_type_mapping.md) — Galaxy to OPC UA type mapping with security classification
|
||||
| Topic | Doc |
|
||||
|---|---|
|
||||
| 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
|
||||
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Admin/ZB.MOM.WW.OtOpcUa.Admin.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.csproj"/>
|
||||
@@ -46,12 +43,6 @@
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Server.Tests/ZB.MOM.WW.OtOpcUa.Server.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ZB.MOM.WW.OtOpcUa.Admin.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.csproj"/>
|
||||
<Project Path="tests/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.Historian.Wonderware.Client.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests.csproj"/>
|
||||
|
||||
@@ -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"}
|
||||
+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
|
||||
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;
|
||||
```
|
||||
| Source | Driver capability | Path |
|
||||
|----------------------------------|--------------------------|------|
|
||||
| **Galaxy MxAccess (driver-native)** | `GalaxyDriver : IAlarmSource` | gateway → worker → MxAccess alarm sink → `MX_EVENT_FAMILY_ON_ALARM_TRANSITION` → `EventPump` → driver `OnAlarmEvent` → `AlarmConditionService` |
|
||||
| **Galaxy sub-attribute fallback** | `IWritable` writes to `$Alarm*` sub-attributes | gateway data subscription → driver `OnDataChange` → `DriverNodeManager` ConditionSink → `AlarmConditionService` |
|
||||
| **Scripted alarms** | `Phase7EngineComposer` | server-side script evaluator → `Phase7EngineComposer.RouteToHistorianAsync` + `AlarmConditionService` |
|
||||
|
||||
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/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.
|
||||
- `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.
|
||||
- `SubscribeAlarmsAsync(sourceNodeIds)` → returns a sentinel handle.
|
||||
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
|
||||
- `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
|
||||
When both paths report the same condition,
|
||||
`AlarmConditionService.AlarmConditionState` keeps the
|
||||
driver-native record and discards the duplicate sub-attribute
|
||||
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 |
|
||||
|---|---|
|
||||
| `Active` | `SetActiveState(true)`, `SetAcknowledgedState(false)`, `Retain = true` |
|
||||
| `Acknowledged` | `SetAcknowledgedState(true)` |
|
||||
| `Inactive` | `SetActiveState(false)`; `Retain = false` once both inactive and acknowledged |
|
||||
The OPC UA Part 9 `AlarmConditionState.OnAcknowledge` delegate
|
||||
already validates the session's `AlarmAck` role before dispatching,
|
||||
so the gateway-side ack RPC only sees authenticated, authorised
|
||||
calls.
|
||||
|
||||
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.
|
||||
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.
|
||||
Galaxy-native alarms with `$Alarm*` extensions reach AVEVA Historian
|
||||
directly via System Platform's `HistorizeToAveva` toggle on the
|
||||
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
|
||||
|
||||
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/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
|
||||
- Plan: [docs/plans/alarms-over-gateway.md](plans/alarms-over-gateway.md)
|
||||
- v1 archive: [docs/v1/AlarmTracking.md](v1/AlarmTracking.md)
|
||||
- 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)
|
||||
- Security + ACL: [docs/Security.md](Security.md)
|
||||
|
||||
+20
-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.
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
@@ -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 |
|
||||
| [AddressSpace.md](AddressSpace.md) | `GenericDriverNodeManager` + `ITagDiscovery` + `IAddressSpaceBuilder` |
|
||||
| [ReadWriteOperations.md](ReadWriteOperations.md) | OPC UA Read/Write → `CapabilityInvoker` → `IReadable`/`IWritable` |
|
||||
| [Subscriptions.md](Subscriptions.md) | Monitored items → `ISubscribable` + per-driver subscription refcount |
|
||||
| [AlarmTracking.md](AlarmTracking.md) | `IAlarmSource` + `AlarmSurfaceInvoker` + OPC UA alarm conditions |
|
||||
| [DataTypeMapping.md](DataTypeMapping.md) | Per-driver `DriverAttributeInfo` → OPC UA variable types |
|
||||
| [Subscriptions.md](v1/Subscriptions.md) | Monitored items → `ISubscribable` + per-driver subscription refcount (v1 archive) |
|
||||
| [AlarmTracking.md](v1/AlarmTracking.md) | `IAlarmSource` + `AlarmSurfaceInvoker` + OPC UA alarm conditions (v1 archive) |
|
||||
| [DataTypeMapping.md](v1/DataTypeMapping.md) | Per-driver `DriverAttributeInfo` → OPC UA variable types (v1 archive — live mapping is in `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`) |
|
||||
| [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 |
|
||||
| [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 |
|
||||
|---------|-----|
|
||||
| `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 |
|
||||
|
||||
### Drivers
|
||||
@@ -44,8 +43,8 @@ Two Core subsystems are shipped without a dedicated top-level doc; see the secti
|
||||
| Doc | Covers |
|
||||
|-----|--------|
|
||||
| [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-Repository.md](drivers/Galaxy-Repository.md) | Galaxy-specific discovery via the ZB SQL database |
|
||||
| [drivers/Galaxy.md](drivers/Galaxy.md) | Galaxy driver — in-process gRPC client to the mxaccessgw sidecar |
|
||||
| [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).
|
||||
|
||||
@@ -53,10 +52,10 @@ For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics
|
||||
|
||||
| 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 |
|
||||
| [Redundancy.md](Redundancy.md) | `RedundancyCoordinator`, `ServiceLevelCalculator`, apply-lease, Prometheus metrics |
|
||||
| [ServiceHosting.md](ServiceHosting.md) | Three-process deploy (Server + Admin + Galaxy.Host) install/uninstall |
|
||||
| [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) |
|
||||
|
||||
### Client tooling
|
||||
@@ -79,10 +78,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/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/GalaxyRepositoryReqs.md](reqs/GalaxyRepositoryReqs.md) | Galaxy-scoped repository reqs |
|
||||
| [reqs/MxAccessClientReqs.md](reqs/MxAccessClientReqs.md) | Galaxy-scoped MXAccess reqs |
|
||||
| [v1/reqs/GalaxyRepositoryReqs.md](v1/reqs/GalaxyRepositoryReqs.md) | Galaxy-scoped repository reqs (v1 archive — owned by mxaccessgw today) |
|
||||
| [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 |
|
||||
|
||||
## Implementation history (`docs/v2/`)
|
||||
@@ -96,4 +95,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/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/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/lmx-followups.md](v2/lmx-followups.md) — historical Galaxy-bridge follow-ups (pre-PR-7.2)
|
||||
- [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.
|
||||
|
||||
+41
-112
@@ -2,132 +2,61 @@
|
||||
|
||||
## Overview
|
||||
|
||||
A production OtOpcUa deployment runs **three processes**, each with a distinct runtime, platform target, and install surface:
|
||||
A production OtOpcUa deployment runs **two or three processes**, each
|
||||
with a distinct runtime and install surface:
|
||||
|
||||
| Process | Project | Runtime | Platform | Responsibility |
|
||||
|---|---|---|---|---|
|
||||
| **OtOpcUa Server** | `src/ZB.MOM.WW.OtOpcUa.Server` | .NET 10 | x64 | Hosts the OPC UA endpoint; loads every non-Galaxy driver in-process; exposes `/healthz`. |
|
||||
| **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 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 Galaxy.Host** | `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host` | .NET Framework 4.8 | x86 (32-bit) | Hosts MXAccess COM on a dedicated STA thread with a Win32 message pump; exposes a named-pipe IPC surface consumed by `Driver.Galaxy.Proxy` inside the Server process. |
|
||||
| **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`. |
|
||||
|
||||
The x86 / .NET Framework 4.8 constraint applies **only** to Galaxy.Host because the MXAccess toolkit DLLs (`Program Files (x86)\ArchestrA\Framework\bin`) are 32-bit-only COM. Every other driver (Modbus, S7, OpcUaClient, AbCip, AbLegacy, TwinCAT, FOCAS) runs in-process in the 64-bit Server.
|
||||
Galaxy access uses a separately-installed **mxaccessgw** running out
|
||||
of a sibling repo (`c:\Users\dohertj2\Desktop\mxaccessgw\`) — see
|
||||
`docs/v2/Galaxy.ParityRig.md` for setup. The mxaccessgw owns the
|
||||
MXAccess COM bitness constraint (its worker is x86 net48); nothing
|
||||
in the OtOpcUa repo carries that constraint anymore. PR 7.2 retired
|
||||
the legacy in-process `Galaxy.Host` / `Galaxy.Proxy` / `Galaxy.Shared`
|
||||
projects + the `OtOpcUaGalaxyHost` Windows service.
|
||||
|
||||
## Server process
|
||||
## OtOpcUa Server
|
||||
|
||||
`src/ZB.MOM.WW.OtOpcUa.Server/Program.cs` uses the generic host:
|
||||
Hosted via `Microsoft.Extensions.Hosting` with `AddWindowsService`
|
||||
(decision #30 — replaced TopShelf in v2). The host's `Build()`
|
||||
returns immediately when launched interactively (e.g. `dotnet run`)
|
||||
but blocks for SCM signals when running as a Windows service.
|
||||
|
||||
```csharp
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Services.AddSerilog();
|
||||
builder.Services.AddWindowsService(o => o.ServiceName = "OtOpcUa");
|
||||
…
|
||||
builder.Services.AddHostedService<OpcUaServerService>();
|
||||
builder.Services.AddHostedService<HostStatusPublisher>();
|
||||
```
|
||||
In-process drivers are registered at startup in `Program.cs`'s
|
||||
`DriverFactoryRegistry` block; the `DriverInstance` rows in the
|
||||
central Config DB select which driver factories materialise into
|
||||
live `IDriver` instances. See `docs/v2/driver-specs.md` for the
|
||||
per-driver `DriverConfig` JSON shapes.
|
||||
|
||||
`OpcUaServerService` is a `BackgroundService` (decision #30 — TopShelf from v1 was replaced by the generic-host `AddWindowsService` wrapper; no TopShelf dependency remains in any csproj). It owns:
|
||||
## OtOpcUa Admin
|
||||
|
||||
1. Config bootstrap — reads `Node:NodeId`, `Node:ClusterId`, `Node:ConfigDbConnectionString`, `Node:LocalCachePath` from `appsettings.json`.
|
||||
2. `NodeBootstrap` — pulls the latest published generation from the Config DB into the LiteDB local cache (`LiteDbConfigCache`) so the node starts even if the central DB is briefly unreachable.
|
||||
3. `DriverHost` — instantiates configured driver instances from the generation, wires each through `CapabilityInvoker` resilience pipelines.
|
||||
4. `OpcUaApplicationHost` — builds the OPC UA endpoint, applies `OpcUaServerOptions` + `LdapOptions`, registers `AuthorizationGate` at dispatch.
|
||||
5. `HostStatusPublisher` — a second hosted service that heartbeats `DriverHostStatus` rows so the Admin UI Fleet view sees the node.
|
||||
Same hosting model; runs the Blazor Server UI + SignalR hubs.
|
||||
Reads from the same Config DB the Server writes to.
|
||||
|
||||
### Installation
|
||||
## OtOpcUa Wonderware Historian (optional)
|
||||
|
||||
Same executable, different modes driven by the .NET generic-host `AddWindowsService` wrapper:
|
||||
When `Historian:Wonderware:Enabled=true`, the Server speaks to a
|
||||
sidecar that wraps the Wonderware Historian SDK (which is .NET
|
||||
Framework only). The pipe IPC contract is in
|
||||
`src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/Contracts/`
|
||||
and the sidecar's pipe handler lives at
|
||||
`src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Pipe/`.
|
||||
|
||||
| Mode | Invocation |
|
||||
|---|---|
|
||||
| Console | `ZB.MOM.WW.OtOpcUa.Server.exe` |
|
||||
| Install as Windows service | `sc create OtOpcUa binPath="C:\Program Files\OtOpcUa\Server\ZB.MOM.WW.OtOpcUa.Server.exe" start=auto` |
|
||||
| Start | `sc start OtOpcUa` |
|
||||
| Stop | `sc stop OtOpcUa` |
|
||||
| Uninstall | `sc delete OtOpcUa` |
|
||||
Install via the `-InstallWonderwareHistorian` switch on
|
||||
`scripts/install/Install-Services.ps1`.
|
||||
|
||||
### Health endpoints
|
||||
## Install / Uninstall
|
||||
|
||||
The Server exposes `/healthz` + `/readyz` used by (a) the Admin `FleetStatusPoller` as input to Fleet status and (b) `PeerReachabilityTracker` in a peer Server process as the HTTP side of the peer-reachability probe.
|
||||
- `scripts/install/Install-Services.ps1` — installs `OtOpcUa` and
|
||||
optionally `OtOpcUaWonderwareHistorian`.
|
||||
- `scripts/install/Uninstall-Services.ps1` — stops + removes both,
|
||||
plus `OtOpcUaGalaxyHost` if a pre-7.2 rig still carries it.
|
||||
|
||||
## Admin process
|
||||
## Logging
|
||||
|
||||
`src/ZB.MOM.WW.OtOpcUa.Admin/Program.cs` is a stock `WebApplication`. Highlights:
|
||||
|
||||
- Cookie auth (`CookieAuthenticationDefaults`, scheme name `OtOpcUa.Admin`) + Blazor Server (`AddInteractiveServerComponents`) + SignalR.
|
||||
- Authorization policies gated by `AdminRoles`: `ConfigViewer`, `ConfigEditor`, `FleetAdmin` (see `Services/AdminRoles.cs`). `CanEdit` policy requires `ConfigEditor` or `FleetAdmin`; `CanPublish` requires `FleetAdmin`.
|
||||
- `OtOpcUaConfigDbContext` registered against `ConnectionStrings:ConfigDb`.
|
||||
- Scoped services: `ClusterService`, `GenerationService`, `EquipmentService`, `UnsService`, `NamespaceService`, `DriverInstanceService`, `NodeAclService`, `PermissionProbeService`, `AclChangeNotifier`, `ReservationService`, `DraftValidationService`, `AuditLogService`, `HostStatusService`, `ClusterNodeService`, `EquipmentImportBatchService`, `ILdapGroupRoleMappingService`.
|
||||
- Singleton `RedundancyMetrics` (meter name `ZB.MOM.WW.OtOpcUa.Redundancy`) + `CertTrustService` (promotes rejected client certs in the Server's PKI store to trusted via the Admin Certificates page).
|
||||
- `LdapAuthService` bound to `Authentication:Ldap` — same LDAP flow as ScadaLink CentralUI for visual parity.
|
||||
- SignalR hubs mapped at `/hubs/fleet` and `/hubs/alerts`; `FleetStatusPoller` runs as a hosted service and pushes `RoleChanged`, host status, and alert events.
|
||||
- OpenTelemetry → Prometheus exporter at `/metrics` when `Metrics:Prometheus:Enabled=true` (default). Pull-based means no Collector required in the common K8s deploy.
|
||||
|
||||
### Installation
|
||||
|
||||
Deployed as an ASP.NET Core service; the generic-host `AddWindowsService` wrapper (or IIS reverse-proxy for multi-node fleets) provides install/uninstall. Listens on whatever `ASPNETCORE_URLS` specifies.
|
||||
|
||||
## Galaxy.Host process
|
||||
|
||||
`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Program.cs` is a .NET Framework 4.8 x86 console executable. Configuration comes from environment variables supplied by the supervisor (`Driver.Galaxy.Proxy.Supervisor`):
|
||||
|
||||
| Env var | Purpose |
|
||||
|---|---|
|
||||
| `OTOPCUA_GALAXY_PIPE` | Pipe name the host listens on (default `OtOpcUaGalaxy`). |
|
||||
| `OTOPCUA_ALLOWED_SID` | SID of the Server process's principal; anyone else is refused during the handshake. |
|
||||
| `OTOPCUA_GALAXY_SECRET` | Per-spawn shared secret the client must present in the Hello frame. |
|
||||
| `OTOPCUA_GALAXY_BACKEND` | `mxaccess` (default), `db` (ZB-only, no COM), `stub` (in-memory; for tests). |
|
||||
| `OTOPCUA_GALAXY_ZB_CONN` | SQL connection string to the ZB Galaxy repository. |
|
||||
| `OTOPCUA_HISTORIAN_*` | Optional Wonderware Historian SDK config if Historian is enabled for this node. |
|
||||
|
||||
The host spins up `StaPump` (the STA thread with message pump), creates the MXAccess `LMXProxyServer` COM object on that thread, and handles all COM calls there; the IPC layer marshals work items via `PostThreadMessage`.
|
||||
|
||||
### Pipe security
|
||||
|
||||
`PipeServer` builds a `PipeAcl` from the provided `SecurityIdentifier` + uses `NamedPipeServerStream` with `maxNumberOfServerInstances: 1`. The handshake requires a matching shared secret in the first Hello frame; callers whose SID doesn't match `OTOPCUA_ALLOWED_SID` are rejected before any frame is processed via `NamedPipeServerStream.RunAsClient` + a SID comparison against the configured allow list. The DACL grants `ReadWrite | Synchronize` only to the allowed SID and denies `LocalSystem`. The installed dev host (`OtOpcUaGalaxyHost`) runs as `dohertj2` with the secret at `.local/galaxy-host-secret.txt`.
|
||||
|
||||
### Installation
|
||||
|
||||
NSSM-wrapped (the Non-Sucking Service Manager) because the executable itself is a plain console app, not a `ServiceBase` Windows service. The supervisor then adopts the child process over the pipe after install. Install/uninstall commands follow the NSSM pattern:
|
||||
|
||||
```bash
|
||||
nssm install OtOpcUaGalaxyHost "C:\Program Files (x86)\OtOpcUa\Galaxy.Host\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.exe"
|
||||
nssm set OtOpcUaGalaxyHost ObjectName .\dohertj2 <password>
|
||||
nssm set OtOpcUaGalaxyHost AppEnvironmentExtra OTOPCUA_GALAXY_BACKEND=mxaccess OTOPCUA_GALAXY_SECRET=… OTOPCUA_ALLOWED_SID=…
|
||||
nssm start OtOpcUaGalaxyHost
|
||||
```
|
||||
|
||||
(Exact values for the environment block are generated by the Admin UI + committed alongside `.local/galaxy-host-secret.txt` on the dev box.)
|
||||
|
||||
## Inter-process communication
|
||||
|
||||
```
|
||||
┌──────────────────────────┐ LDAP bind (Authentication:Ldap) ┌──────────────────────────┐
|
||||
│ OtOpcUa Admin (x64) │ ─────────────────────────────────────────────▶│ LDAP / AD │
|
||||
│ Blazor Server + SignalR │ └──────────────────────────┘
|
||||
│ /metrics (Prometheus) │ FleetStatusPoller → ClusterNode poll
|
||||
│ │ ─────────────────────────────────────────────▶┌──────────────────────────┐
|
||||
│ │ Cluster/Generation/ACL writes │ Config DB (SQL Server) │
|
||||
└──────────────────────────┘ ─────────────────────────────────────────────▶│ OtOpcUaConfigDbContext │
|
||||
▲ └──────────────────────────┘
|
||||
│ SignalR ▲
|
||||
│ (role change, │ sp_GetCurrentGenerationForCluster
|
||||
│ host status, │ sp_PublishGeneration
|
||||
│ alerts) │
|
||||
┌──────────────────────────┐ │
|
||||
│ OtOpcUa Server (x64) │ ──────────────────────────────────────────────────────────┘
|
||||
│ OPC UA endpoint │
|
||||
│ Non-Galaxy drivers │ Named pipe (OtOpcUaGalaxy) ┌──────────────────────────┐
|
||||
│ Driver.Galaxy.Proxy │ ─────────────────────────────────────────────▶│ Galaxy.Host (x86 .NFx) │
|
||||
│ │ SID + shared-secret handshake │ STA + message pump │
|
||||
│ /healthz /readyz │ │ MXAccess COM │
|
||||
└──────────────────────────┘ │ Historian SDK (opt) │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
## appsettings.json boundary
|
||||
|
||||
Each process reads its own `appsettings.json` for **bootstrap only** — connection strings, LDAP bind config, transport security profile, redundancy node id, logging. The authoritative configuration tree (drivers, UNS, tags, ACLs) lives in the Config DB and is edited through the Admin UI. See [`Configuration.md`](Configuration.md) for the split.
|
||||
|
||||
## Development bootstrap
|
||||
|
||||
For the Windows install steps (SQL Server in Docker, .NET 10 SDK, .NET Framework 4.8 SDK, Docker Desktop WSL 2 backend, EF Core CLI, first-run migration), see [`docs/v2/dev-environment.md`](v2/dev-environment.md).
|
||||
Serilog with rolling-daily file sinks. Each service writes to
|
||||
`%ProgramData%\OtOpcUa\<service>-*.log` plus stdout (NSSM-friendly).
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
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),
|
||||
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).
|
||||
|
||||
## What it talks to
|
||||
@@ -210,8 +209,9 @@ latency spike once per cadence.
|
||||
driver surface via `FakeFocasClient`. Includes the alarm-projection raise /
|
||||
clear diffing tests.
|
||||
- **Integration tests** — `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/`
|
||||
hold the Docker simulator scaffold (Stream B / C of the simulator plan —
|
||||
`docs/v2/implementation/focas-simulator-plan.md`).
|
||||
hold the Docker simulator scaffold; see
|
||||
[`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
|
||||
CNC (or the simulator) and exercises connect → read → write → subscribe
|
||||
round-trips. See [`docs/drivers/FOCAS-Test-Fixture.md`](FOCAS-Test-Fixture.md)
|
||||
|
||||
+77
-184
@@ -1,211 +1,104 @@
|
||||
# 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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
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.
|
||||
|
||||
## Why Out-of-Process
|
||||
|
||||
Two reasons drive the split, per `docs/v2/plan.md`:
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## IPC Transport
|
||||
|
||||
`GalaxyProxyDriver` → `GalaxyIpcClient` → named pipe → `Galaxy.Host` pipe server.
|
||||
|
||||
- Pipe name: `otopcua-galaxy-{DriverInstanceId}` (localhost-only, no TCP surface)
|
||||
- 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);
|
||||
```
|
||||
+---------------------------------------+
|
||||
| OtOpcUa.Server (.NET 10 AnyCPU) |
|
||||
| GalaxyDriver (in-process) |
|
||||
| ITagDiscovery / IReadable / |
|
||||
| IWritable / ISubscribable / |
|
||||
| IRediscoverable / |
|
||||
| IHostConnectivityProbe / |
|
||||
| IAlarmSource |
|
||||
+-------------------+-------------------+
|
||||
|
|
||||
gRPC (default http://localhost:5120)
|
||||
|
|
||||
v
|
||||
+---------------------------------------+
|
||||
| mxaccessgw (sibling repo) |
|
||||
| +-------------------------------+ |
|
||||
| | MxGateway.Worker (x86 net48) | |
|
||||
| | STA + WM_APP pump | |
|
||||
| | ArchestrA.MxAccess COM | |
|
||||
| | Galaxy Repository SQL | |
|
||||
| | Wonderware Historian SDK | |
|
||||
| +-------------------------------+ |
|
||||
+---------------------------------------+
|
||||
```
|
||||
|
||||
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)
|
||||
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
|
||||
The driver ships as a single project: `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/` (.NET 10, AnyCPU). Sub-folders:
|
||||
|
||||
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
|
||||
2. **`Unregister(handle)`** — Unwires event handlers, calls `Unregister`, releases the COM object via `Marshal.ReleaseComObject`
|
||||
## Capability Surface
|
||||
|
||||
## 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
|
||||
2. **`AdviseSupervisory(handle, itemHandle)`** — Subscribes the item for supervisory data-change callbacks
|
||||
3. The runtime begins delivering `OnDataChange` events
|
||||
## Configuration
|
||||
|
||||
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`
|
||||
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`)
|
||||
`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.
|
||||
|
||||
### 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/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.
|
||||
|
||||
### Monitor loop
|
||||
|
||||
`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
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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/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/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/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
|
||||
@@ -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/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 |
|
||||
@@ -0,0 +1,161 @@
|
||||
> **✅ 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
|
||||
|
||||
This document tracks the scenario × result matrix that the
|
||||
`Driver.Galaxy.ParityTests` suite drives against both Galaxy backends —
|
||||
the legacy out-of-process **Galaxy.Host** (.NET 4.8 x86 + MXAccess COM,
|
||||
fronted by `GalaxyProxyDriver`) and the new in-process **mxgateway**
|
||||
backend (`GalaxyDriver`, .NET 10 + gRPC against `mxaccessgw`).
|
||||
|
||||
Maintained alongside Phase 5 (PR 5.W). The Phase 7 default flip
|
||||
(PR 7.1) consumes this matrix as its go/no-go gate — every row must be
|
||||
either green or carry an explicit *accepted-delta* justification.
|
||||
|
||||
## Reading the matrix
|
||||
|
||||
- **Status: green** — the scenario asserts strict parity and passes
|
||||
(or skips cleanly when the rig isn't up).
|
||||
- **Status: yellow** — soft pin only (count or shape parity, not value
|
||||
parity) — acceptable when the underlying COM/gRPC stacks have known
|
||||
divergences in raw payloads but the surface presented to the
|
||||
DriverNodeManager is equivalent.
|
||||
- **Status: red** — divergence detected. Row carries a fix or a
|
||||
follow-up task ID.
|
||||
|
||||
## Scenarios
|
||||
|
||||
Last verified end-to-end on the dev parity rig: **2026-04-30**
|
||||
(legacy `OtOpcUaGalaxyHost` mxaccess backend; mxaccessgw v1.x at
|
||||
`http://localhost:5120`; sandbox `OtOpcUaParityTest_001` deployed in
|
||||
the `ZB` galaxy; 13 passed / 1 skipped / 0 failed in 19 minutes).
|
||||
|
||||
| PR | Test class | Scenario | Status | Notes |
|
||||
|----|-----------|----------|--------|-------|
|
||||
| 5.2 | `BrowseAndReadParityTests` | Same variable set | green | symmetric set diff on full-reference set, after `[]` array-suffix workaround in `GalaxyDiscoverer` |
|
||||
| 5.2 | `BrowseAndReadParityTests` | Same DataType / SecurityClass / IsHistorized | green | per-attribute meta triple parity |
|
||||
| 5.2 | `BrowseAndReadParityTests` | Same StatusCode-class on a sampled read | yellow | pins status class (Bad/Uncertain/Good); CLR type intentionally not asserted — see "Accepted deltas" #6 |
|
||||
| 5.3 | `SubscribeAndEventRateParityTests` | Subscribe returns a handle on each backend | green | symmetric Unsubscribe cleanup |
|
||||
| 5.3 | `SubscribeAndEventRateParityTests` | Event rate within ±50% over 3s | yellow | both backends fed by the same upstream MXAccess subscriptions; tolerance absorbs scheduler jitter |
|
||||
| 5.4 | `WriteByClassificationParityTests` | FreeAccess / Operate write status-class parity | yellow | pins status class only; legacy flat-maps every failure to BadInternalError, mxgw distinguishes (BadCommunicationError, BadDeviceFailure, etc.) — see "Accepted deltas" #7 |
|
||||
| 5.4 | `WriteByClassificationParityTests` | Configure / Tune routes via secured-write | yellow | same status-class pin |
|
||||
| 5.5 | `AlarmTransitionParityTests` | Same alarm-condition source-node-id set | green | one-way invariant on sub-attribute refs (legacy populated → mxgw matches; legacy null → mxgw free to populate per AlarmRefBuilder) |
|
||||
| 5.5 | `AlarmTransitionParityTests` | IsAlarm-marked variable count parity | green | soft pin — count must match, doesn't have to be non-zero |
|
||||
| 5.6 | `HistoryReadParityTests` | Same historized attribute set | green | what HistoryRouter consumes when routing to the Wonderware sidecar |
|
||||
| 5.6 | `HistoryReadParityTests` | New mxgw GalaxyDriver does not implement `IHistoryProvider` | green | architectural pin from Phase 1 (PR 1.3) on the *new* path; legacy `GalaxyProxyDriver` keeps the interface for back-compat until PR 7.2 — see "Accepted deltas" #8 |
|
||||
| 5.7 | `ReconnectParityTests` | Reinitialize → both Healthy + reads succeed | green | recovery latency is *not* pinned (legacy: pipe + COM client; mxgw: re-Register gw session) |
|
||||
| 5.7 | `ReconnectParityTests` | Health diverges only when one side recovers | yellow | soft pin until a toxiproxy-style fault injector lands |
|
||||
| 5.8 | `ScanStateProbeParityTests` | Same per-platform host set | n/a — deferred | dev rig is licensed for one `$WinPlatform` only; multi-platform parity deferred to a customer rig (PR 4.7's unit tests pin the state-decoder + member-tracking logic) |
|
||||
| 5.8 | `ScanStateProbeParityTests` | Same `HostState` per overlapping platform | n/a — deferred | same single-platform constraint |
|
||||
|
||||
## Accepted deltas
|
||||
|
||||
These are intentional differences between the two backends — the parity
|
||||
suite skips or tolerates them by design.
|
||||
|
||||
1. **Transport-entry host name.** The legacy backend's
|
||||
`IHostConnectivityProbe` surface includes a host entry named after
|
||||
the Galaxy.Host process identity; the mxgw backend uses the
|
||||
configured `MxAccess.ClientName`. The names differ, but both are
|
||||
correct for their respective sessions — the parity test compares
|
||||
only the platform-host subset.
|
||||
|
||||
2. **Reconnect latency cadence.** Legacy reconnect roundtrips an OS
|
||||
named pipe + an MxAccess COM client + a Galaxy.Host process restart
|
||||
if the host died. The mxgw reconnect re-Registers the gateway session
|
||||
over an existing gRPC channel. Sub-second vs multi-second recoveries
|
||||
are both correct for their own paths; only the eventual `Healthy`
|
||||
convergence is pinned.
|
||||
|
||||
3. **Read-value drift.** A read sampled twice on a live Galaxy can
|
||||
return different values legitimately. We pin `StatusCode`-class
|
||||
parity (Bad/Uncertain/Good); value equality is not pinned.
|
||||
|
||||
4. **Event-rate variance.** Both backends consume the same upstream
|
||||
MXAccess publish events but route them through different deserializers
|
||||
(LMXProxyServer COM events vs gRPC `MxEvent` protos). Scheduler
|
||||
jitter on either side can shift counts within a 3s window; we pin a
|
||||
±50% ratio, not strict equality.
|
||||
|
||||
5. **`IHistoryProvider` on the new path only.** Phase 1 (PR 1.3) lifted
|
||||
history off the per-driver path onto the server-owned
|
||||
`HistoryRouter` for the *new* in-process `GalaxyDriver`. The legacy
|
||||
`GalaxyProxyDriver` still surfaces `IHistoryProvider` for back-compat
|
||||
with the legacy server bootstrap path — it's an accepted delta
|
||||
retired in PR 7.2 alongside the rest of the legacy projects. The
|
||||
pin we want to enforce is "the new path doesn't regress to per-driver
|
||||
history."
|
||||
|
||||
6. **Read value-CLR-type.** Legacy returns the raw VARIANT (e.g.
|
||||
`Byte[]`) for an attribute that hasn't received its first value
|
||||
cycle from MxAccess yet, while mxgw returns the typed value
|
||||
(`Single`, `Int32`, etc.). Once a real value is written or scanned,
|
||||
both converge. Pinning CLR-type equality across the uninitialized
|
||||
window adds noise without a real parity invariant — the
|
||||
`StatusCode`-class assertion already covers the
|
||||
"did the read succeed" question.
|
||||
|
||||
7. **Write-failure StatusCode mapping.** Legacy
|
||||
`MxAccessGalaxyBackend.WriteValuesAsync` flat-maps every failure to
|
||||
`BadInternalError` (`0x80020000`); mxgw
|
||||
`GatewayGalaxyDataWriter.TranslateReply` uses
|
||||
`MxStatusProxy.RawDetectedBy` to distinguish gw-layer faults
|
||||
(`BadCommunicationError`, `0x80050000`) from MxAccess HRESULT
|
||||
faults (`BadDeviceFailure`, `BadNotConnected`, etc.). Both yield
|
||||
Bad-status — the parity invariant is the *status class*, not the
|
||||
exact code. Tighter mapping parity isn't worth investing in: the
|
||||
legacy mapping retires alongside `GalaxyProxyDriver` in PR 7.2.
|
||||
|
||||
8. **Single-platform scope on the dev rig.** Two
|
||||
`ScanStateProbeParityTests` scenarios are deferred to a customer
|
||||
rig with multiple deployed `$WinPlatform` instances; this dev box
|
||||
is licensed for one. PR 4.7's unit tests (`PerPlatformProbeWatcherTests`)
|
||||
pin the state-decoder + member-tracking logic at the seam level,
|
||||
so the runtime parity check becomes a customer-rig acceptance gate
|
||||
before that customer goes live, not a precondition for retiring
|
||||
the legacy projects on this dev box.
|
||||
|
||||
9. **Workaround for the gw `[]` array-suffix bug.**
|
||||
`mxaccessgw/src/MxGateway.Server/Galaxy/GalaxyRepository.cs:173-175`
|
||||
appends `[]` to the `full_tag_reference` of array-typed attributes,
|
||||
which `MxAccess COM IInstance.AddItem` doesn't accept. The lmxopcua
|
||||
discoverer (`GalaxyDiscoverer.StripArraySuffix`) defensively strips
|
||||
the suffix. Tracked in `mxaccessgw/requirements-array-suffix-fix.md`;
|
||||
the workaround is removed when that gw fix lands.
|
||||
|
||||
## Outstanding deltas
|
||||
|
||||
None as of 2026-04-30. Phase 7 (PR 7.1) flipped the default to
|
||||
`mxgw`; PR 7.2 (legacy project deletion) is unblocked — the matrix
|
||||
gate is satisfied and no further soak/pilot precondition applies.
|
||||
|
||||
## Running the matrix
|
||||
|
||||
```bash
|
||||
# Both backends must be reachable for any row to run; rows skip
|
||||
# cleanly when their backend is unavailable.
|
||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/
|
||||
```
|
||||
|
||||
Environment overrides for the mxgw backend:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `OTOPCUA_PARITY_GW_ENDPOINT` | `http://localhost:5120` | mxaccessgw gRPC endpoint |
|
||||
| `OTOPCUA_PARITY_GW_API_KEY` | `parity-suite-key` | API key handed to `MxGatewayClient` |
|
||||
| `OTOPCUA_PARITY_CLIENT_NAME` | `OtOpcUa-Parity` | `MxAccess.ClientName` for the session |
|
||||
|
||||
The legacy backend reads ZB SQL on `localhost:1433` and spawns
|
||||
`OtOpcUa.Driver.Galaxy.Host.exe` from
|
||||
`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/bin/Debug/net48/` — both
|
||||
must exist for the legacy half to resolve.
|
||||
@@ -0,0 +1,381 @@
|
||||
# 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
|
||||
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`
|
||||
can run for real. Closing the parity matrix was the gate for PR 7.2
|
||||
(retire legacy Galaxy projects).
|
||||
|
||||
## Conceptual layout
|
||||
|
||||
```
|
||||
Galaxy ZB SQL ──┬── OtOpcUaGalaxyHost (NSSM service, net48 x86) [DELETED in PR 7.2]
|
||||
│ └── MxAccess COM, ClientName "OtOpcUa-Galaxy.Host"
|
||||
│ └── named pipe "OtOpcUaGalaxy"
|
||||
│ ▲
|
||||
│ │ pipe IPC
|
||||
│ │
|
||||
│ GalaxyProxyDriver ◄── parity test (legacy half)
|
||||
│
|
||||
└── mxaccessgw service
|
||||
└── MxAccess COM, ClientName "OtOpcUa-Parity"
|
||||
└── gRPC on http://localhost:5120
|
||||
▲
|
||||
│ gRPC
|
||||
│
|
||||
GalaxyDriver (in-process) ◄── parity test (mxgw half)
|
||||
```
|
||||
|
||||
Both halves talk to the **same Galaxy** through **two distinct MxAccess
|
||||
sessions** (different ClientNames so they don't evict each other).
|
||||
|
||||
## What was on the dev box at the time
|
||||
|
||||
Per `~/.claude/projects/.../memory/` *as of the rig run*:
|
||||
|
||||
- **AVEVA System Platform + Galaxy + MXAccess runtime** — `project_aveva_platform_installed.md`.
|
||||
- **`OtOpcUaGalaxyHost`** Windows service running as `dohertj2`, NSSM-wrapped,
|
||||
binary at `C:\publish\OtOpcUaGalaxyHost\OtOpcUa.Driver.Galaxy.Host.exe`,
|
||||
shared secret at `.local/galaxy-host-secret.txt`, ZB SQL on `localhost:1433`
|
||||
— `project_galaxy_host_installed.md`. **(Service uninstalled and binary
|
||||
retired as part of PR 7.2; the host source project no longer exists in
|
||||
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)
|
||||
|
||||
### 1. Build + run mxaccessgw
|
||||
|
||||
The gateway source is at `c:\Users\dohertj2\Desktop\mxaccessgw\`.
|
||||
Build both halves — the worker has to be x86 net48 (MxAccess COM
|
||||
bitness), the server is .NET 10:
|
||||
|
||||
```powershell
|
||||
cd C:\Users\dohertj2\Desktop\mxaccessgw
|
||||
dotnet build src\MxGateway.Worker -c Release # produces bin\x86\Release\net48\MxGateway.Worker.exe
|
||||
dotnet build src\MxGateway.Server -c Release # produces bin\Release\net10.0\MxGateway.Server.dll
|
||||
```
|
||||
|
||||
Initialize the auth database and mint an API key. The CLI mode is
|
||||
gated by an `apikey` first-arg prefix:
|
||||
|
||||
```powershell
|
||||
$env:MxGateway__ApiKeyPepper = "parity-rig-dev-pepper" # any stable string for dev
|
||||
$srv = "C:\Users\dohertj2\Desktop\mxaccessgw\src\MxGateway.Server\bin\Release\net10.0\MxGateway.Server.dll"
|
||||
|
||||
dotnet $srv apikey init-db # → "init-db: initialized"
|
||||
|
||||
dotnet $srv apikey create-key `
|
||||
--key-id parity-rig `
|
||||
--display-name "OtOpcUa-Parity" `
|
||||
--scopes "session:open,session:close,invoke:read,invoke:write,invoke:secure,events:read,metadata:read"
|
||||
# → "API key: mxgw_parity-rig_<base64suffix>" ← capture this; you can't list secrets later
|
||||
```
|
||||
|
||||
Save that exact key string for `OTOPCUA_PARITY_GW_API_KEY` in step 2.
|
||||
|
||||
Run the server with three env-var overrides — the defaults don't
|
||||
quite match what gRPC + the parity test need:
|
||||
|
||||
```powershell
|
||||
$env:MxGateway__ApiKeyPepper = "parity-rig-dev-pepper" # MUST match the create-key invocation
|
||||
$env:Kestrel__Endpoints__Http__Url = "http://localhost:5120"
|
||||
$env:Kestrel__Endpoints__Http__Protocols = "Http2" # gRPC needs h2c on plain HTTP
|
||||
$env:MxGateway__Worker__ExecutablePath = `
|
||||
"C:\Users\dohertj2\Desktop\mxaccessgw\src\MxGateway.Worker\bin\x86\Release\net48\MxGateway.Worker.exe"
|
||||
# appsettings.json's relative path is missing the \net48 segment; absolute path sidesteps that
|
||||
|
||||
dotnet $srv
|
||||
# → "Now listening on: http://localhost:5120"
|
||||
```
|
||||
|
||||
The worker spawns lazily on the first OpenSession RPC — there's no
|
||||
worker process visible in Task Manager until the first session. If
|
||||
the worker can't spawn, the server returns `Failed to open session
|
||||
session-…` with a `WorkerProcessLaunchException` in the server log.
|
||||
|
||||
NSSM-wrap it later if the rig becomes long-lived; for first-pass
|
||||
provisioning a console window is easier to inspect.
|
||||
|
||||
### 2. Set the parity env vars
|
||||
|
||||
In the test-runner shell:
|
||||
|
||||
```powershell
|
||||
$env:OTOPCUA_PARITY_GW_ENDPOINT = "http://localhost:5120"
|
||||
$env:OTOPCUA_PARITY_GW_API_KEY = "parity-suite-key" # match the gw config
|
||||
$env:OTOPCUA_PARITY_CLIENT_NAME = "OtOpcUa-Parity"
|
||||
```
|
||||
|
||||
Elevation status doesn't matter — the legacy Galaxy.Host pipe ACL accepts
|
||||
elevated and non-elevated `dohertj2` shells alike (the Administrators deny
|
||||
ACE was removed 2026-04-24; see `project_galaxy_host_installed.md`).
|
||||
|
||||
### 3. Verify both halves resolve
|
||||
|
||||
```powershell
|
||||
cd C:\Users\dohertj2\Desktop\lmxopcua
|
||||
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests\ `
|
||||
--filter "FullyQualifiedName~HarnessShapeTests"
|
||||
```
|
||||
|
||||
`Harness_records_a_skip_reason_for_each_unavailable_backend` is the
|
||||
two-line truth-teller:
|
||||
|
||||
- Both `LegacyDriver` non-null + both `MxGatewayDriver` non-null → rig is up.
|
||||
- One side null → read its `LegacySkipReason` / `MxGatewaySkipReason` and fix.
|
||||
|
||||
## Running the matrix
|
||||
|
||||
Once both halves resolve:
|
||||
|
||||
```powershell
|
||||
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests\ `
|
||||
--filter "Category=ParityE2E"
|
||||
```
|
||||
|
||||
This runs all 17 scenario tests across the seven scenario classes
|
||||
(BrowseAndRead / Subscribe / Write / Alarm / History / Reconnect /
|
||||
ScanState). Each scenario class is independent — failures in one don't
|
||||
block the rest.
|
||||
|
||||
Track the result against `docs/v2/Galaxy.ParityMatrix.md`. Update each
|
||||
row to:
|
||||
|
||||
- **green** if the scenario passes
|
||||
- **yellow** if it skipped because the dev Galaxy doesn't have the right
|
||||
shape (see coverage matrix below)
|
||||
- **red** if it asserted a real delta — those are the deltas that block
|
||||
PR 7.2; chase each before retiring the legacy backend
|
||||
|
||||
## Galaxy shape needed for full coverage
|
||||
|
||||
Skip-on-empty-shape scenarios fail-soft today. To turn a skip into a
|
||||
real result, the dev Galaxy needs the shape in the right column:
|
||||
|
||||
| Scenario | Needs | Local rig |
|
||||
|---|---|---|
|
||||
| `BrowseAndReadParityTests` (3 tests) | Any deployed objects with attributes | ✅ existing seed |
|
||||
| `SubscribeAndEventRateParityTests` event-rate | ≥5 attributes whose values *change* in 3s | ⚙ scriptable via graccess-cli |
|
||||
| `WriteByClassificationParityTests` (FreeAccess/Operate) | A FreeAccess/Operate numeric attribute | ⚙ scriptable via graccess-cli |
|
||||
| `WriteByClassificationParityTests` (Configure/Tune) | A Configure/Tune attribute | ⚙ scriptable via graccess-cli |
|
||||
| `AlarmTransitionParityTests` (2 tests) | Attributes with the `$Alarm*` extension | ⚙ scriptable via graccess-cli |
|
||||
| `HistoryReadParityTests` (historized set) | Attributes with the History extension | ⚙ scriptable via graccess-cli |
|
||||
| `ScanStateProbeParityTests` (2 tests) | Multiple `$WinPlatform` / `$AppEngine` objects | ❌ **deferred to customer rig** — this dev box is provisioned for one platform only |
|
||||
|
||||
### The single-platform constraint
|
||||
|
||||
The dev box at `DESKTOP-6JL3KKO` is licensed / configured for a single
|
||||
deployed `$WinPlatform`. Adding a second platform isn't feasible here,
|
||||
so `ScanStateProbeParityTests` will skip in a "no overlap" branch on
|
||||
this rig. Both of its scenarios already handle that case gracefully
|
||||
(`Assert.Skip("no overlapping platform hosts between backends — likely
|
||||
the transport names differ but no $WinPlatform was discovered")`), so
|
||||
the matrix reports them as **n/a (deferred)** rather than red.
|
||||
|
||||
Plan: defer the two ScanState scenarios to a customer rig with multiple
|
||||
platforms. The PR 7.2 gate accepts "n/a, deferred" on these rows
|
||||
provided the legacy `GalaxyRuntimeProbeManager` and the in-process
|
||||
`PerPlatformProbeWatcher` have matching unit-test coverage of the
|
||||
state-decoder + member-tracking logic — which they do (PR 4.7's tests).
|
||||
Treat the runtime parity check as a customer-rig acceptance gate before
|
||||
that customer goes live, not a precondition for retiring the legacy
|
||||
projects on this dev box.
|
||||
|
||||
### Provisioning the rest via graccess-cli
|
||||
|
||||
`C:\Users\dohertj2\Desktop\graccess\graccess_cli\` is a .NET Framework
|
||||
4.8 console app over the ArchestrA GRAccess COM API. It can configure
|
||||
templates, instances, attributes, UDAs, extensions, and attribute
|
||||
security — i.e. every row above marked ⚙ scriptable. Full surface in
|
||||
`graccess/graccess_cli/docs/usage.md` and per-area workflow guides
|
||||
(`attribute-editing.md`, `template-editing.md`,
|
||||
`template-instance-editing.md`).
|
||||
|
||||
Reserve a sandbox UDO (e.g. `OtOpcUaParityTest`) to avoid mutating
|
||||
attributes on plant-relevant objects. Concrete commands per requirement:
|
||||
|
||||
**A FreeAccess/Operate numeric attribute** (covers WriteByClassification
|
||||
FreeAccess/Operate scenario):
|
||||
|
||||
```powershell
|
||||
graccess object uda add `
|
||||
--galaxy ZB --name OtOpcUaParityTest --type template `
|
||||
--uda OperateValue --data-type MxFloat `
|
||||
--category MxCategoryWriteable_C --security MxSecurityOperate `
|
||||
--confirm --confirm-target OtOpcUaParityTest
|
||||
```
|
||||
|
||||
**A Configure / Tune attribute** (covers WriteByClassification
|
||||
Configure/Tune scenario):
|
||||
|
||||
```powershell
|
||||
# Tune
|
||||
graccess object uda add `
|
||||
--galaxy ZB --name OtOpcUaParityTest --type template `
|
||||
--uda TuneValue --data-type MxFloat `
|
||||
--category MxCategoryWriteable_T --security MxSecurityTune `
|
||||
--confirm --confirm-target OtOpcUaParityTest
|
||||
|
||||
# Configure
|
||||
graccess object uda add `
|
||||
--galaxy ZB --name OtOpcUaParityTest --type template `
|
||||
--uda ConfigValue --data-type MxFloat `
|
||||
--category MxCategoryWriteable_C --security MxSecurityConfigure `
|
||||
--confirm --confirm-target OtOpcUaParityTest
|
||||
```
|
||||
|
||||
**A changing-value attribute** (covers Subscribe event-rate scenario).
|
||||
Two ways:
|
||||
|
||||
1. *On-scan increment* — bind a script extension that bumps a counter
|
||||
each scan. Simplest to author with `object extension add` against
|
||||
`ScriptExtension` plus `object attribute set` for the script body
|
||||
(see `attribute-editing.md` §"Edit Extensions" for the pattern).
|
||||
2. *External writer loop* — leave the attribute as plain Float and run
|
||||
a one-liner that writes incrementing values from the parity-test
|
||||
shell. Uses the legacy backend path so it's available before the
|
||||
mxgw subscriber is up. This keeps the Galaxy template clean.
|
||||
|
||||
For first-pass validation pick #2 — no template surgery needed, and the
|
||||
write loop runs only during `dotnet test`.
|
||||
|
||||
**Attributes with the `$Alarm*` extension** (covers AlarmTransition
|
||||
scenario). Per `attribute-editing.md` §"Edit Alarm Settings" the
|
||||
likely-named attributes vary by extension type
|
||||
(`Limit`, `RateOfChange`, etc.). Add the extension via:
|
||||
|
||||
```powershell
|
||||
graccess object extension add `
|
||||
--galaxy ZB --name OtOpcUaParityTest --type template `
|
||||
--extension-type AnalogLimitAlarm --primitive AlarmInput `
|
||||
--object-extension `
|
||||
--confirm --confirm-target OtOpcUaParityTest
|
||||
```
|
||||
|
||||
Then set HiHi/Hi/Lo/LoLo limit values + priority on the resulting
|
||||
attributes via `object attribute set`. Inspect first via
|
||||
`object attributes` to see the names the extension introduces — they
|
||||
differ across Aveva versions.
|
||||
|
||||
**Attributes with the History extension** (covers HistoryRead routing
|
||||
scenario). History settings are usually attribute or extension
|
||||
attributes; `attribute-editing.md` §"Edit History Settings" covers the
|
||||
discovery flow. Quick start:
|
||||
|
||||
```powershell
|
||||
graccess object extension add `
|
||||
--galaxy ZB --name OtOpcUaParityTest --type template `
|
||||
--extension-type HistoryExtension --primitive HistoryRecord `
|
||||
--object-extension `
|
||||
--confirm --confirm-target OtOpcUaParityTest
|
||||
|
||||
# Then enable history on whichever attribute the extension points at
|
||||
graccess object attribute set `
|
||||
--galaxy ZB --name OtOpcUaParityTest --type template `
|
||||
--attribute HistoryEnabled --value true --data-type bool `
|
||||
--confirm --confirm-target OtOpcUaParityTest
|
||||
```
|
||||
|
||||
**Deploy + restart Galaxy.Host after any of the above** so MxAccess
|
||||
sees the change:
|
||||
|
||||
```powershell
|
||||
graccess object deploy --galaxy ZB --name OtOpcUaParityTest_001 `
|
||||
--confirm --confirm-target OtOpcUaParityTest_001
|
||||
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
|
||||
now find a sandbox attribute matching their selector and assert.
|
||||
|
||||
## Soak run
|
||||
|
||||
The 24h × 50k soak gates the production confidence half of PR 7.2.
|
||||
|
||||
```powershell
|
||||
$env:OTOPCUA_SOAK_RUN = "1"
|
||||
$env:OTOPCUA_SOAK_TAGS = "<actual tag count if Galaxy < 50k>"
|
||||
$env:OTOPCUA_SOAK_MINUTES = "1440" # default 24h; compress for first runs
|
||||
$env:OTOPCUA_SOAK_DROP_PCT = "0.5"
|
||||
|
||||
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests\ `
|
||||
--filter "Category=Soak"
|
||||
```
|
||||
|
||||
The test logs a per-minute CSV-style line to stdout:
|
||||
|
||||
```
|
||||
soak,1.0,received=51234,dispatched=51234,dropped=0,ws_mb=412
|
||||
soak,2.0,received=102468,dispatched=102468,dropped=0,ws_mb=415
|
||||
...
|
||||
```
|
||||
|
||||
Capture stdout to a file for post-run analysis. The three guards
|
||||
(`received` growing, `dropped/received` ratio, working-set delta) all
|
||||
fire mid-run rather than at end-of-test, so a failure surfaces within
|
||||
the first few minutes if the architecture is wrong.
|
||||
|
||||
## Compressed-tag soak (when Galaxy isn't 50k tags)
|
||||
|
||||
A first-pass validation is fine with the override:
|
||||
|
||||
```powershell
|
||||
$env:OTOPCUA_SOAK_RUN = "1"
|
||||
$env:OTOPCUA_SOAK_TAGS = "500" # whatever the dev Galaxy has
|
||||
$env:OTOPCUA_SOAK_MINUTES = "60" # one hour is enough to surface plumbing bugs
|
||||
$env:OTOPCUA_SOAK_DROP_PCT = "1.0"
|
||||
```
|
||||
|
||||
This validates the *plumbing* (bounded channel, pump invariants, leak
|
||||
guard) but doesn't pin the 50k-tag scaling assertion. Defer the full
|
||||
50k validation to a customer rig with that scale, or build a synthetic
|
||||
Galaxy with a script that imports 50k attributes onto a generated UDO
|
||||
(~2 hours of one-off work).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **`MxGatewaySkipReason` says "mxaccessgw not reachable"** — the gw
|
||||
isn't listening, or it's on a different port. `Test-NetConnection
|
||||
localhost -Port 5120` is the quick check.
|
||||
- **`MxGatewaySkipReason` says "mxgateway backend boot failed:
|
||||
RpcException: Unauthenticated"** — API key mismatch. Verify the
|
||||
`OTOPCUA_PARITY_GW_API_KEY` env var matches the gw's configured key.
|
||||
- **`LegacySkipReason` says "Galaxy ZB SQL not reachable on
|
||||
localhost:1433"** — SQL Server isn't running, or its TCP listener is
|
||||
off. Check `services.msc` for the SQL Server (default) instance.
|
||||
- **`LegacySkipReason` says "Galaxy.Host EXE not built"** — at rig time
|
||||
the parity harness looked under
|
||||
`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/bin/Debug/net48/` for the
|
||||
EXE it spawned as a subprocess, separate from the published copy at
|
||||
`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
|
||||
the expected outcome the rig exists to surface. Review each delta
|
||||
against `docs/v2/Galaxy.ParityMatrix.md`'s "Accepted deltas" section
|
||||
to decide whether it's a real bug or a pre-accepted divergence.
|
||||
|
||||
## After the rig is green
|
||||
|
||||
When the matrix is fully green or carries documented accepted-deltas,
|
||||
PR 7.2 (legacy project deletion) is unblocked. The only follow-up is
|
||||
to promote any newly-discovered accepted-delta to the matrix doc with
|
||||
the why so the matrix history stays auditable.
|
||||
@@ -0,0 +1,152 @@
|
||||
# Galaxy backend performance
|
||||
|
||||
This document covers the performance surface of the in-process
|
||||
`GalaxyDriver` (the v2 mxgw backend) — the ActivitySource it emits, the
|
||||
metrics on its EventPump, the soak scenario that validates it, and the
|
||||
tuning knobs you can reach for when the dev parity rig surfaces a hot
|
||||
spot.
|
||||
|
||||
## Tracing surface (PR 6.1)
|
||||
|
||||
The driver emits spans on the `ZB.MOM.WW.OtOpcUa.Driver.Galaxy`
|
||||
ActivitySource. No package dependency on OpenTelemetry — the host
|
||||
process picks the listener (OTLP exporter, dotnet-trace, Application
|
||||
Insights). Wire it via `OpenTelemetry.Trace.AddSource(...)` in the
|
||||
host's tracing pipeline.
|
||||
|
||||
| Span | Source | Tags |
|
||||
|------|--------|------|
|
||||
| `galaxy.subscribe_bulk` | `TracedGalaxySubscriber` | `galaxy.client`, `galaxy.tag_count`, `galaxy.buffered_interval_ms`, `galaxy.success_count` |
|
||||
| `galaxy.unsubscribe_bulk` | `TracedGalaxySubscriber` | `galaxy.client`, `galaxy.tag_count` |
|
||||
| `galaxy.stream_events` | `TracedGalaxySubscriber` | `galaxy.client`, `galaxy.event_count` (set on stream end) |
|
||||
| `galaxy.write` | `TracedGalaxyDataWriter` | `galaxy.client`, `galaxy.tag_count`, `galaxy.secured_write_count`, `galaxy.success_count` |
|
||||
| `galaxy.get_hierarchy` | `TracedGalaxyHierarchySource` | `galaxy.client`, `galaxy.object_count` |
|
||||
|
||||
The stream-events span deliberately covers the *entire* stream lifetime
|
||||
rather than per-event spans — at 50k tags / 1Hz the per-event volume
|
||||
would dominate the trace pipeline. Per-event visibility flows through
|
||||
the metrics surface instead.
|
||||
|
||||
## Metrics surface (PR 6.2)
|
||||
|
||||
`EventPump` publishes three counters on the
|
||||
`ZB.MOM.WW.OtOpcUa.Driver.Galaxy` meter, each tagged with
|
||||
`galaxy.client` so multi-driver hosts can split by source:
|
||||
|
||||
| Counter | Unit | Meaning |
|
||||
|---------|------|---------|
|
||||
| `galaxy.events.received` | `{event}` | MxEvents read from the gateway StreamEvents stream |
|
||||
| `galaxy.events.dispatched` | `{event}` | MxEvents that made it through the bounded channel into `OnDataChange` |
|
||||
| `galaxy.events.dropped` | `{event}` | MxEvents discarded because the bounded channel was full (newest-dropped) |
|
||||
|
||||
The invariant is `received = dispatched + dropped + (in-flight in the
|
||||
channel)`. Watch the dropped counter — it is the leading indicator of
|
||||
listener back-pressure. A non-zero dropped rate means a downstream
|
||||
consumer (DriverNodeManager → UA notification queue → client) is
|
||||
slower than the gw event stream; investigate that consumer before
|
||||
raising `EventPump` channel capacity.
|
||||
|
||||
### Bounded channel design
|
||||
|
||||
The pump runs two background tasks:
|
||||
|
||||
1. **Producer** — reads from `IGalaxySubscriber.StreamEventsAsync`,
|
||||
increments `events.received`, and `TryWrite`s into a bounded
|
||||
`Channel<MxEvent>`. When the channel is full, the producer counts
|
||||
the drop and continues reading the gw stream so back-pressure does
|
||||
not propagate upstream (which would stall the gw worker and cascade
|
||||
to *all* driver instances sharing that worker).
|
||||
2. **Consumer** — reads from the channel, fans out via
|
||||
`SubscriptionRegistry`, increments `events.dispatched`.
|
||||
|
||||
Default channel capacity is 50_000 (one second of headroom at 50k
|
||||
tags / 1Hz). Override via the `EventPump` constructor's
|
||||
`channelCapacity` parameter; the public-facing wiring path in
|
||||
`GalaxyDriver.EnsureEventPumpStarted` does not yet expose this through
|
||||
`GalaxyDriverOptions` because no parity scenario has needed it. Add it
|
||||
when soak data does.
|
||||
|
||||
## Buffered update interval (PR 6.3)
|
||||
|
||||
`MxAccess.PublishingIntervalMs` (default 1000) flows through both
|
||||
subscribe paths:
|
||||
|
||||
- `GalaxyDriver.SubscribeAsync` — the caller's `publishingInterval`
|
||||
wins when non-zero (the server's UA subscription publishingInterval
|
||||
drives this in production). When the caller passes
|
||||
`TimeSpan.Zero`, the configured option is the fallback.
|
||||
- `PerPlatformProbeWatcher` — the watcher passes the configured value
|
||||
through `SubscribeBulkAsync` so probe `ScanState` changes publish at
|
||||
the deployment's chosen cadence.
|
||||
|
||||
A session-level `SetBufferedUpdateInterval` RPC exists in the gw
|
||||
protocol but the .NET client doesn't expose a typed helper yet —
|
||||
adjusting an existing subscription's interval mid-flight is a
|
||||
follow-up. Today's path subscribes once at the right interval, which
|
||||
covers the common case.
|
||||
|
||||
## Soak scenario (PR 6.4)
|
||||
|
||||
`SoakScenarioTests.Soak_HoldsSubscription_AndKeepsEventStreamFlowing`
|
||||
in `Driver.Galaxy.ParityTests` is the long-running validation. It
|
||||
subscribes a configurable tag count (default 50_000), holds the
|
||||
subscription for a configurable duration (default 24h), polls the
|
||||
three counters every minute, and asserts:
|
||||
|
||||
- `events.received` continues to grow (gw stream isn't stuck)
|
||||
- `events.dropped / events.received` stays under the configured
|
||||
ceiling (default 0.5%)
|
||||
- process working-set doesn't grow more than 1 GB above baseline
|
||||
(leak guard)
|
||||
|
||||
Always skipped unless the operator opts in:
|
||||
|
||||
```bash
|
||||
# Full 24h × 50k soak (production validation)
|
||||
OTOPCUA_SOAK_RUN=1 dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/
|
||||
|
||||
# Compressed CI-friendly run (10min × 1k tags, 1% drop ceiling)
|
||||
OTOPCUA_SOAK_RUN=1 OTOPCUA_SOAK_MINUTES=10 OTOPCUA_SOAK_TAGS=1000 \
|
||||
OTOPCUA_SOAK_DROP_PCT=1.0 \
|
||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/
|
||||
```
|
||||
|
||||
The scenario writes a per-minute CSV-style row to stdout
|
||||
(`soak,<minutes>,received=…,dispatched=…,dropped=…,ws_mb=…`) so an
|
||||
operator can grep the test runner output mid-run.
|
||||
|
||||
## Tuned defaults (PR 6.5)
|
||||
|
||||
| Option | Default | Source | Notes |
|
||||
|--------|---------|--------|-------|
|
||||
| `Gateway.ConnectTimeoutSeconds` | 10 | unchanged | Cold-start network paths fit comfortably; soak never observed >2s |
|
||||
| `Gateway.DefaultCallTimeoutSeconds` | 30 | **bumped from 5** in PR 6.5 | A 50k-tag `SubscribeBulk` can exceed 5s under MxAccess COM apartment lock contention; 30s leaves headroom while still failing fast on a wedged worker |
|
||||
| `Gateway.StreamTimeoutSeconds` | 0 (unlimited) | unchanged | The stream must run for the lifetime of the driver |
|
||||
| `MxAccess.PublishingIntervalMs` | 1000 | unchanged | Matches the legacy `LMXProxyServer` cadence; deployments needing tighter health visibility can dial down |
|
||||
| `Reconnect.InitialBackoffMs` | 500 | unchanged | First retry shouldn't dogpile a recovering gw |
|
||||
| `Reconnect.MaxBackoffMs` | 30_000 | unchanged | 30s ceiling so a long-down gw doesn't sit in 5+ min backoff |
|
||||
| `Repository.DiscoverPageSize` | 5000 | unchanged | One Galaxy page round-trip per ~5k objects; soak hadn't surfaced pressure |
|
||||
| `EventPump` channel capacity | 50_000 | unchanged | One second of headroom at 50k tags / 1Hz |
|
||||
|
||||
The unchanged rows are not "definitely correct" — they are "no live
|
||||
data argues for changing them." Re-run the soak scenario after every
|
||||
substantive driver change, and revise this table when the data does.
|
||||
|
||||
## Where to look first when something's slow
|
||||
|
||||
1. **Slow `Discover`?** Inspect `galaxy.get_hierarchy` span duration
|
||||
and `galaxy.object_count`. The gw walks the Galaxy DB serially;
|
||||
slow Discovers usually mean a slow ZB SQL.
|
||||
2. **Subscribe pile-up?** `galaxy.subscribe_bulk` span duration
|
||||
correlates with `galaxy.tag_count`. If duration ÷ tag_count starts
|
||||
climbing, the gw worker is probably under apartment-lock pressure.
|
||||
3. **Events stalled?** Watch `galaxy.events.received`. Flat-lined
|
||||
means the gw stream is wedged — kick the reconnect supervisor by
|
||||
forcing a `ReinitializeAsync`.
|
||||
4. **Dropped events?** Non-zero `galaxy.events.dropped` means a slow
|
||||
downstream consumer. Profile `OnDataChange` handlers in
|
||||
`DriverNodeManager` before bumping the channel capacity.
|
||||
5. **Memory growing?** Confirm with the soak scenario's working-set
|
||||
leak guard. Likely culprits: lingering subscription handles in
|
||||
`SubscriptionRegistry`, or a downstream consumer retaining
|
||||
`DataValueSnapshot` references past their useful life.
|
||||
+88
-42
@@ -4,6 +4,7 @@
|
||||
>
|
||||
> **Branch**: `v2`
|
||||
> **Created**: 2026-04-17
|
||||
> **Updated 2026-04-28**: Docker workloads moved off the Windows dev VM to a shared Linux Docker host at `10.100.0.35` so the dev VM can have its GPU re-attached via ESXi passthrough (Hyper-V/WSL2 was blocking it). The two-tier model below is updated accordingly: per-developer Docker Desktop is gone; SQL Server + driver fixtures all live on the central Linux host, identifiable via `docker ps --filter label=project=lmxopcua`.
|
||||
|
||||
## Scope
|
||||
|
||||
@@ -13,30 +14,31 @@ Every external resource a developer needs on their machine, plus the dedicated i
|
||||
|
||||
## Two Environment Tiers
|
||||
|
||||
Per decision #99:
|
||||
Per decision #99 (updated 2026-04-28):
|
||||
|
||||
| Tier | Purpose | Where it runs | Resources |
|
||||
|------|---------|---------------|-----------|
|
||||
| **PR-CI / inner-loop dev** | Fast, runs on minimal Windows + Linux build agents and developer laptops | Each developer's machine; CI runners | Pure-managed in-process simulators (NModbus, OPC Foundation reference server, FOCAS TCP stub from test project). No Docker, no VMs. |
|
||||
| **Nightly / integration CI** | Full driver-stack validation against real wire protocols | One dedicated Windows host with Docker Desktop + Hyper-V + a TwinCAT XAR VM | All Docker simulators (`oitc/modbus-server`, `ab_server`, Snap7), TwinCAT XAR VM, Galaxy.Host installer + dev Galaxy access, FOCAS TCP stub binary, FOCAS FaultShim assembly |
|
||||
| **PR-CI / inner-loop dev** | Fast, runs on minimal Windows + Linux build agents and developer laptops | Each developer's machine; CI runners | Pure-managed in-process simulators (NModbus, OPC Foundation reference server, FOCAS TCP stub from test project). No Docker, no VMs locally. |
|
||||
| **Integration / nightly CI** | Full driver-stack validation against real wire protocols | **Shared Linux Docker host at `10.100.0.35`** (Debian 13, Docker 29.2.1) — one host for all developers; replaces the former per-developer Docker Desktop + Hyper-V model | All Docker simulators (pymodbus, ab_server, python-snap7, opc-plc) + central SQL Server, all running as `/opt/otopcua-<driver>/` stacks with the `project=lmxopcua` label. TwinCAT XAR + the Galaxy/mxaccessgw stack stay on the Windows dev VM (license + Hyper-V constraints unchanged) |
|
||||
|
||||
The tier split keeps developer onboarding fast (no Docker required for first build) while concentrating the heavy simulator setup on one machine the team maintains.
|
||||
The Linux Docker host is shared because (a) only one team member needs it active at a time, (b) it removes the per-developer Docker Desktop install, and (c) the dev VM no longer needs Hyper-V/WSL2 — freeing it for GPU passthrough.
|
||||
|
||||
## Installed Inventory — This Machine
|
||||
## Installed Inventory — Dev VM (`DESKTOP-6JL3KKO`)
|
||||
|
||||
Running record of every v2 dev service stood up on this developer machine. Updated on every install / config change. Credentials here are **dev-only** per decision #137 — production uses Integrated Security / gMSA per decision #46 and never any value in this table.
|
||||
Running record of v2 dev services on the Windows dev VM. Updated on every install / config change. Credentials here are **dev-only** per decision #137 — production uses Integrated Security / gMSA per decision #46 and never any value in this table.
|
||||
|
||||
**Last updated**: 2026-04-17
|
||||
**Last updated**: 2026-04-28 — Docker Desktop + WSL2 removed; Docker workloads now live on the Linux Docker host (see next section).
|
||||
|
||||
### Host
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Machine name | `DESKTOP-6JL3KKO` |
|
||||
| User | `dohertj2` (member of local Administrators + `docker-users`) |
|
||||
| VM platform | VMware (`VMware20,1`), nested virtualization enabled |
|
||||
| Machine name | `DESKTOP-6JL3KKO` (10.100.0.48) |
|
||||
| User | `dohertj2` (local Administrators) |
|
||||
| VM platform | VMware ESXi |
|
||||
| CPU | Intel Xeon E5-2697 v4 @ 2.30GHz (3 vCPUs) |
|
||||
| OS | Windows (WSL2 + Hyper-V Platform features installed) |
|
||||
| OS | Windows 10 Enterprise (10.0.19045) |
|
||||
| GPU | (Re-attached after WSL2/Hyper-V removal) |
|
||||
|
||||
### Toolchain
|
||||
|
||||
@@ -46,36 +48,40 @@ Running record of every v2 dev service stood up on this developer machine. Updat
|
||||
| .NET AspNetCore runtime | 10.0.5 | `C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\` | Pre-installed |
|
||||
| .NET NETCore runtime | 10.0.5 | `C:\Program Files\dotnet\shared\Microsoft.NETCore.App\` | Pre-installed |
|
||||
| .NET WindowsDesktop runtime | 10.0.5 | `C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\` | Pre-installed |
|
||||
| .NET Framework 4.8 SDK | — | Pending (needed for Phase 2 Galaxy.Host; not yet required) | — |
|
||||
| .NET Framework 4.8 SDK | — | Optional — only needed when building the mxaccessgw worker (sibling repo, x86 net48) | — |
|
||||
| Git | Pre-installed | Standard | — |
|
||||
| PowerShell 7 | Pre-installed | Standard | — |
|
||||
| winget | v1.28.220 | Standard Windows feature | — |
|
||||
| WSL | Default v2, distro `docker-desktop` `STATE Running` | — | `wsl --install --no-launch` (2026-04-17) |
|
||||
| Docker Desktop | 29.3.1 (engine) / Docker Desktop 4.68.0 (app) | Standard | `winget install --id Docker.DockerDesktop` (2026-04-17) |
|
||||
| Docker CLI (standalone, no daemon) | 29.3.1 | `%USERPROFILE%\bin\docker.exe` | Static binary from download.docker.com (2026-04-28) |
|
||||
| Docker Compose CLI plugin | latest | `%USERPROFILE%\.docker\cli-plugins\docker-compose.exe` | Direct download from github.com/docker/compose (2026-04-28) |
|
||||
| `lmxopcua-fix.ps1` helper | n/a | `%USERPROFILE%\bin\lmxopcua-fix.ps1` | See "Docker host" section below |
|
||||
| `dotnet-ef` CLI | 10.0.6 | `%USERPROFILE%\.dotnet\tools\dotnet-ef.exe` | `dotnet tool install --global dotnet-ef --version 10.0.*` (2026-04-17) |
|
||||
| ~~Docker Desktop~~ | — | Removed 2026-04-28 — replaced by remote Linux Docker host | — |
|
||||
| ~~WSL2 (`docker-desktop` distro)~~ | — | Removed 2026-04-28 (frees Hyper-V for GPU passthrough) | — |
|
||||
|
||||
### Services
|
||||
|
||||
| Service | Container / Process | Version | Host:Port | Credentials (dev-only) | Data location | Status |
|
||||
|---------|---------------------|---------|-----------|------------------------|---------------|--------|
|
||||
| **Central config DB** | Docker container `otopcua-mssql` (image `mcr.microsoft.com/mssql/server:2022-latest`) | 16.0.4250.1 (RTM-CU24-GDR, KB5083252) | `localhost:14330` (host) → `1433` (container) — remapped from 1433 to avoid collision with the native MSSQL14 instance that hosts the Galaxy `ZB` DB (both bind 0.0.0.0:1433; whichever wins the race gets connections) | User `sa` / Password `OtOpcUaDev_2026!` | Docker named volume `otopcua-mssql-data` (mounted at `/var/opt/mssql` inside container) | ✅ Running — `InitialSchema` migration applied, 16 entity tables live |
|
||||
| **Central config DB** | Docker container `otopcua-mssql` on the Linux Docker host (image `mcr.microsoft.com/mssql/server:2022-latest`) | 16.0.4250.1 (RTM-CU24-GDR, KB5083252) | `10.100.0.35:14330` → `1433` (container) — port 14330 retained from the previous local-container setup so connection-string ports don't churn | User `sa` / Password `OtOpcUaDev_2026!` | Docker named volume `otopcua-mssql-data` on the Docker host | ✅ Running on Docker host (`/opt/otopcua-mssql/`) since 2026-04-28; carries `project=lmxopcua` label |
|
||||
| Dev Galaxy (AVEVA System Platform) | Local install on this dev box — full ArchestrA + Historian + OI-Server stack | v1 baseline | Local COM via MXAccess (`C:\Program Files (x86)\ArchestrA\Framework\bin\ArchestrA.MXAccess.dll`); Historian via `aaH*` services; SuiteLink via `slssvc` | Windows Auth | Galaxy repository DB `ZB` on local SQL Server (separate instance from `otopcua-mssql` — legacy v1 Galaxy DB, not related to v2 config DB) | ✅ **Fully available — Phase 2 lift unblocked.** 27 ArchestrA / AVEVA / Wonderware services running incl. `aaBootstrap`, `aaGR` (Galaxy Repository), `aaLogger`, `aaUserValidator`, `aaPim`, `ArchestrADataStore`, `AsbServiceManager`, `AutoBuild_Service`; full Historian set (`aahClientAccessPoint`, `aahGateway`, `aahInSight`, `aahSearchIndexer`, `aahSupervisor`, `InSQLStorage`, `InSQLConfiguration`, `InSQLEventSystem`, `InSQLIndexing`, `InSQLIOServer`, `InSQLManualStorage`, `InSQLSystemDriver`, `HistorianSearch-x64`); `slssvc` (Wonderware SuiteLink); `OI-Gateway` install present at `C:\Program Files (x86)\Wonderware\OI-Server\OI-Gateway\` (decision #142 AppServer-via-OI-Gateway smoke test now also unblocked) |
|
||||
| GLAuth (LDAP) | Local install at `C:\publish\glauth\` | v2.4.0 | `localhost:3893` (LDAP) / `3894` (LDAPS, disabled) | Direct-bind `cn={user},dc=lmxopcua,dc=local` per `auth.md`; users `readonly`/`writeop`/`writetune`/`writeconfig`/`alarmack`/`admin`/`serviceaccount` (passwords in `glauth.cfg` as SHA-256) | `C:\publish\glauth\` | ✅ Running (NSSM service `GLAuth`). Phase 1 Admin uses GroupToRole map `ReadOnly→ConfigViewer`, `WriteOperate→ConfigEditor`, `AlarmAck→FleetAdmin`. v2-rebrand to `dc=otopcua,dc=local` is a future cosmetic change |
|
||||
| OPC Foundation reference server | Not yet built | — | `localhost:62541` (target) | `user1` / `password1` (reference-server defaults) | — | Pending (needed for Phase 5 OPC UA Client driver testing) |
|
||||
| FOCAS TCP stub | Not yet built | — | `localhost:8193` (target) | n/a | — | Pending (built in Phase 5) |
|
||||
| Modbus simulator (`oitc/modbus-server`) | — | — | `localhost:502` (target) | n/a | — | Pending (needed for Phase 3 Modbus driver; moves to integration host per two-tier model) |
|
||||
| libplctag `ab_server` | — | — | `localhost:44818` (target) | n/a | — | Pending (Phase 3/4 AB CIP and AB Legacy drivers) |
|
||||
| Snap7 Server | — | — | `localhost:102` (target) | n/a | — | Pending (Phase 4 S7 driver) |
|
||||
| TwinCAT XAR VM | — | — | `localhost:48898` (ADS) (target) | TwinCAT default route creds | — | Pending — runs in Hyper-V VM, not on this dev box (per decision #135) |
|
||||
| OPC Foundation reference server | Not yet built | — | `10.100.0.35:62541` (target) | `user1` / `password1` (reference-server defaults) | — | Pending (needed for Phase 5 OPC UA Client driver testing) |
|
||||
| FOCAS TCP stub | Not yet built | — | `10.100.0.35:8193` (target) | n/a | — | Pending (built in Phase 5; runs on Docker host) |
|
||||
| Modbus simulator (`otopcua-pymodbus:3.13.0`) | Docker compose at `/opt/otopcua-modbus/` on Docker host | pinned 3.13.0 | `10.100.0.35:5020` | n/a | n/a | Stack staged; bring up with `lmxopcua-fix up modbus <profile>` from this VM |
|
||||
| AB CIP fixture (`otopcua-ab-server:libplctag-release`) | Docker compose at `/opt/otopcua-abcip/` on Docker host | source-pinned `release` tag | `10.100.0.35:44818` | n/a | n/a | Stack staged; bring up with `lmxopcua-fix up abcip <profile>` from this VM |
|
||||
| S7 fixture (`otopcua-python-snap7:1.0`) | Docker compose at `/opt/otopcua-s7/` on Docker host | python-snap7 ≥2.0 | `10.100.0.35:1102` | n/a | n/a | Stack staged; bring up with `lmxopcua-fix up s7 s7_1500` from this VM |
|
||||
| OPC UA simulator (`mcr.microsoft.com/iotedge/opc-plc:2.14.10`) | Docker compose at `/opt/otopcua-opcuaclient/` on Docker host | pinned 2.14.10 | `10.100.0.35:50000` | anonymous | n/a | Stack staged; bring up with `lmxopcua-fix up opcuaclient` from this VM |
|
||||
| TwinCAT XAR VM | — | — | TBD via Hyper-V on a separate Windows host (NOT this dev VM) | TwinCAT default route creds | — | Pending — Hyper-V removed from this dev VM; XAR will live on a separate dedicated Windows machine if needed |
|
||||
|
||||
### Connection strings for `appsettings.Development.json`
|
||||
|
||||
Copy-paste-ready. **Never commit these to the repo** — they go in `appsettings.Development.json` (gitignored per the standard .NET convention) or in user-scoped dotnet secrets.
|
||||
Copy-paste-ready. The checked-in `appsettings.json` defaults already point at the Docker host (`10.100.0.35,14330`), so `appsettings.Development.json` is only needed for per-developer overrides.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"ConfigDatabase": {
|
||||
"ConnectionString": "Server=localhost,14330;Database=OtOpcUaConfig_Dev;User Id=sa;Password=OtOpcUaDev_2026!;TrustServerCertificate=true;Encrypt=false;"
|
||||
"ConnectionString": "Server=10.100.0.35,14330;Database=OtOpcUaConfig_Dev;User Id=sa;Password=OtOpcUaDev_2026!;TrustServerCertificate=true;Encrypt=false;"
|
||||
},
|
||||
"Authentication": {
|
||||
"Ldap": {
|
||||
@@ -89,29 +95,26 @@ Copy-paste-ready. **Never commit these to the repo** — they go in `appsettings
|
||||
}
|
||||
```
|
||||
|
||||
LDAP host stays `localhost` because GLAuth still runs as a native NSSM service on this dev VM (not yet migrated to the Docker host).
|
||||
|
||||
For xUnit test fixtures that need a throwaway DB per test run, build connection strings with `Database=OtOpcUaConfig_Test_{timestamp}` to avoid cross-run pollution.
|
||||
|
||||
### Container management quick reference
|
||||
|
||||
All commands SSH into the Docker host. The standalone Windows `docker.exe` on this VM has no daemon — every operation runs server-side via the helper.
|
||||
|
||||
```powershell
|
||||
# Start / stop the SQL Server container (survives reboots via Docker Desktop auto-start)
|
||||
docker stop otopcua-mssql
|
||||
docker start otopcua-mssql
|
||||
# Status / log / lifecycle from this VM
|
||||
lmxopcua-fix ls # list lmxopcua-tagged containers + status
|
||||
lmxopcua-fix logs mssql # SQL Server log tail
|
||||
ssh dohertj2@10.100.0.35 'docker stop otopcua-mssql; docker start otopcua-mssql'
|
||||
ssh dohertj2@10.100.0.35 'docker logs otopcua-mssql --tail 50'
|
||||
|
||||
# Logs (useful for diagnosing startup failures or login issues)
|
||||
docker logs otopcua-mssql --tail 50
|
||||
# sqlcmd inside the container (run on the Docker host)
|
||||
ssh dohertj2@10.100.0.35 'docker exec otopcua-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "OtOpcUaDev_2026!" -C -Q "SELECT @@VERSION"'
|
||||
|
||||
# Shell into the container (rarely needed; sqlcmd is the usual tool)
|
||||
docker exec -it otopcua-mssql bash
|
||||
|
||||
# Query via sqlcmd inside the container (Git Bash needs MSYS_NO_PATHCONV=1 to avoid path mangling)
|
||||
MSYS_NO_PATHCONV=1 docker exec otopcua-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "OtOpcUaDev_2026!" -C -Q "SELECT @@VERSION"
|
||||
|
||||
# Nuclear reset: drop the container + volume (destroys all DB data)
|
||||
docker stop otopcua-mssql
|
||||
docker rm otopcua-mssql
|
||||
docker volume rm otopcua-mssql-data
|
||||
# …then re-run the docker run command from Bootstrap Step 6
|
||||
# Nuclear reset (destroys dev DB data)
|
||||
ssh dohertj2@10.100.0.35 'cd /opt/otopcua-mssql && docker compose down -v && docker compose up -d'
|
||||
```
|
||||
|
||||
### Credential rotation
|
||||
@@ -125,7 +128,7 @@ Dev credentials in this inventory are convenience defaults, not secrets. Change
|
||||
| Resource | Purpose | Type | Default port | Default credentials | Owner |
|
||||
|----------|---------|------|--------------|---------------------|-------|
|
||||
| **.NET 10 SDK** | Build all .NET 10 x64 projects | OS install | n/a | n/a | Developer |
|
||||
| **.NET Framework 4.8 SDK + targeting pack** | Build `Driver.Galaxy.Host` (Phase 2+) | Windows install | n/a | n/a | Developer |
|
||||
| **.NET Framework 4.8 SDK + targeting pack** | Optional — build the mxaccessgw worker (sibling repo, x86 net48) | Windows install | n/a | n/a | Developer |
|
||||
| **Visual Studio 2022 17.8+ or Rider 2024+** | IDE (any C# IDE works; these are the supported configs) | OS install | n/a | n/a | Developer |
|
||||
| **Git** | Source control | OS install | n/a | n/a | Developer |
|
||||
| **PowerShell 7.4+** | Compliance scripts (`phase-N-compliance.ps1`) | OS install | n/a | n/a | Developer |
|
||||
@@ -247,7 +250,7 @@ Order matters because some installs have prerequisites and several need admin el
|
||||
winget install --id Microsoft.DotNet.SDK.10 --accept-package-agreements --accept-source-agreements
|
||||
```
|
||||
|
||||
2. **Install .NET Framework 4.8 SDK + targeting pack** — only needed when starting Phase 2 (Galaxy.Host); skip for Phase 0–1 if not yet there
|
||||
2. **Install .NET Framework 4.8 SDK + targeting pack** — optional, only needed when building the mxaccessgw worker (sibling repo, x86 net48). Not required by anything in this repo.
|
||||
```powershell
|
||||
winget install --id Microsoft.DotNet.Framework.DeveloperPack_4 --accept-package-agreements --accept-source-agreements
|
||||
```
|
||||
@@ -405,6 +408,49 @@ For production:
|
||||
- Per-NodeId credentials in `ClusterNodeCredential` table (per decision #83)
|
||||
- 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
|
||||
|
||||
Each environment needs a baseline data set so cross-developer tests are reproducible. Lives in `tests/ZB.MOM.WW.OtOpcUa.IntegrationTests/SeedData/`:
|
||||
@@ -482,7 +528,7 @@ Seeds are idempotent (re-runnable) and gitignored where they contain credentials
|
||||
| Docker Desktop license terms change for org use | Track Docker pricing; budget approved or fall back to Podman if license becomes blocking |
|
||||
| Integration host single point of failure | Document the setup so a second host can be provisioned in <2 days; test fixtures pin to a hostname so failover changes one DNS entry |
|
||||
| GLAuth dev config drifts between developers | Sync script + template (Step 4) keep configs aligned; periodic review |
|
||||
| Galaxy / MXAccess licensing for non-dev-machine | Galaxy stays on the dev machines that already have Aveva licenses; integration host does NOT run Galaxy (Galaxy.Host integration tests run on the dev box, not the shared host) |
|
||||
| Galaxy / MXAccess licensing for non-dev-machine | Galaxy stays on the dev machines that already have Aveva licenses; integration host does NOT run Galaxy (the mxaccessgw worker requires the AVEVA stack and runs on the dev box, not the shared host) |
|
||||
| Long-lived dev env credentials in dev `appsettings.Development.json` | Gitignored; documented as dev-only; production never uses these |
|
||||
|
||||
## Decisions to Add to plan.md
|
||||
|
||||
+59
-283
@@ -10,289 +10,65 @@
|
||||
|
||||
### 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.
|
||||
|
||||
### Library & Dependencies
|
||||
|
||||
| Component | Package / Source | Version | Target | Notes |
|
||||
|-----------|------------------|---------|--------|-------|
|
||||
| **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` |
|
||||
| **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` |
|
||||
| **MessagePack-CSharp** | `MessagePack` NuGet | 2.x | .NET Standard 2.0 (Shared) | IPC serialization; shared contract between Proxy and Host |
|
||||
| **Named pipes** | `System.IO.Pipes` (BCL) | BCL | both sides | IPC transport, localhost only |
|
||||
|
||||
### Required Components
|
||||
|
||||
- **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
|
||||
- **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)
|
||||
- **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
|
||||
- **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.
|
||||
|
||||
### Connection Settings (per driver instance, from central config DB)
|
||||
|
||||
All settings live under a schemaless `DriverConfig` JSON blob on the `DriverInstance` row. Current v1 equivalents (defaults and source file references in parentheses):
|
||||
|
||||
**MXAccess** (`MxAccessConfiguration.cs`):
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `ClientName` | string | `"LmxOpcUa"` | Registration name passed to `LMXProxyServer.Register()` |
|
||||
| `NodeName` | string? | `null` | Optional ArchestrA node override (null = local) |
|
||||
| `GalaxyName` | string? | `null` | Optional Galaxy name override |
|
||||
| `ReadTimeoutSeconds` | int | `5` | Per-read timeout |
|
||||
| `WriteTimeoutSeconds` | int | `5` | Per-write timeout |
|
||||
| `RequestTimeoutSeconds` | int | `30` | Outer safety timeout around any MXAccess request |
|
||||
| `MaxConcurrentOperations` | int | `10` | Pool bound on in-flight MXAccess work items |
|
||||
| `MonitorIntervalSeconds` | int | `5` | Connectivity heartbeat probe interval |
|
||||
| `AutoReconnect` | bool | `true` | Replay stored subscriptions on COM reconnect |
|
||||
| `ProbeTag` | string? | `null` | Optional heartbeat tag for health monitoring |
|
||||
| `ProbeStaleThresholdSeconds` | int | `60` | Mark connection stale if no probe callback within |
|
||||
| `RuntimeStatusProbesEnabled` | bool | `true` | Auto-subscribe `ScanState` for $WinPlatform / $AppEngine |
|
||||
| `RuntimeStatusUnknownTimeoutSeconds` | int | `15` | Grace period before an un-probed host is assumed Stopped |
|
||||
|
||||
**Galaxy repository** (`GalaxyRepositoryConfiguration.cs`):
|
||||
|
||||
| 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 |
|
||||
| `ExtendedAttributes` | bool | `false` | Include extended attribute metadata in discovery |
|
||||
| `Scope` | enum (`Galaxy` \| `LocalPlatform`) | `Galaxy` | Address-space scope filter (commit bc282b6) |
|
||||
| `PlatformName` | string? | `Environment.MachineName` | Platform to scope to when `Scope=LocalPlatform` |
|
||||
|
||||
**IPC** (new for v2):
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `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.
|
||||
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/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/`.
|
||||
|
||||
### Capability Surface
|
||||
|
||||
`GalaxyDriver` (in `GalaxyDriver.cs`) implements `IDriver`, `IDisposable`, plus six driver capabilities — eight interfaces total.
|
||||
|
||||
| Capability | Source files |
|
||||
|------------|--------------|
|
||||
| `ITagDiscovery` | `Browse/GalaxyDiscoverer.cs`, `Browse/GatewayGalaxyHierarchySource.cs`, `Browse/DataTypeMap.cs`, `Browse/SecurityMap.cs`, `Browse/AlarmRefBuilder.cs` |
|
||||
| `IRediscoverable` | `Browse/DeployWatcher.cs`, `Browse/GatewayGalaxyDeployWatchSource.cs` |
|
||||
| `IReadable` | `Runtime/GalaxyMxSession.cs`, `Runtime/MxValueDecoder.cs`, `Runtime/StatusCodeMap.cs` |
|
||||
| `IWritable` | `Runtime/GatewayGalaxyDataWriter.cs` (+ `TracedGalaxyDataWriter.cs`), `Runtime/MxValueEncoder.cs` |
|
||||
| `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` |
|
||||
|
||||
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.
|
||||
|
||||
### DriverConfig JSON shape
|
||||
|
||||
Per `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Config/GalaxyDriverOptions.cs`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"Gateway": {
|
||||
"Endpoint": "http://localhost:5120",
|
||||
"ApiKeySecretRef": "secret:galaxy-gw-api-key",
|
||||
"UseTls": true,
|
||||
"CaCertificatePath": null,
|
||||
"ConnectTimeoutSeconds": 10,
|
||||
"DefaultCallTimeoutSeconds": 30,
|
||||
"StreamTimeoutSeconds": 0
|
||||
},
|
||||
"MxAccess": {
|
||||
"ClientName": "OtOpcUa",
|
||||
"PublishingIntervalMs": 1000,
|
||||
"WriteUserId": 0,
|
||||
"EventPumpChannelCapacity": 50000
|
||||
},
|
||||
"Repository": {
|
||||
"DiscoverPageSize": 5000,
|
||||
"WatchDeployEvents": true
|
||||
},
|
||||
"Reconnect": {
|
||||
"InitialBackoffMs": 500,
|
||||
"MaxBackoffMs": 30000,
|
||||
"ReplayOnSessionLost": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
### Performance, tracing, soak
|
||||
|
||||
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.
|
||||
|
||||
### Parity rig + gateway setup
|
||||
|
||||
See [Galaxy.ParityRig.md](Galaxy.ParityRig.md) and the `mxaccessgw` repo for the gateway worker layout and the dev-rig recipe.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# FOCAS wire protocol — what's authoritative vs. what's guessed
|
||||
|
||||
Companion to [`focas-simulator-plan.md`](focas-simulator-plan.md). Written during
|
||||
Stream B on 2026-04-23 after a research pass through `strangesast/fwlib` +
|
||||
Written during Stream B on 2026-04-23 after a research pass through `strangesast/fwlib` +
|
||||
public FOCAS documentation. Purpose: separate what we *know* about the FOCAS
|
||||
wire protocol (can quote with confidence) from what we're *guessing* (will need
|
||||
Wireshark traces to validate in Stream C).
|
||||
|
||||
@@ -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)
|
||||
|
||||
> **Status**: DRAFT — implementation plan for Phase 2 of the v2 build (`plan.md` §6, `driver-stability.md` §"Galaxy — Deep Dive").
|
||||
|
||||
@@ -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.
|
||||
@@ -70,6 +70,17 @@ integration tests until reproduced on hardware:
|
||||
- TxId drop under load (forum rumour; not reproduced).
|
||||
- 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
|
||||
|
||||
One section per device class, same shape as DL205. Quirks that apply across
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
-- Table: client_control_class_link
|
||||
CREATE TABLE [client_control_class_link] (
|
||||
[gobject_id] int NOT NULL,
|
||||
[file_id] int NULL,
|
||||
[class_name] nvarchar(1024) NOT NULL,
|
||||
CONSTRAINT [PK_client_control_class_link] PRIMARY KEY ([gobject_id])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
-- Table: client_info
|
||||
CREATE TABLE [client_info] (
|
||||
[id] int NOT NULL,
|
||||
[client_unique_identifier] nvarchar(4000) NOT NULL,
|
||||
[client_name] nvarchar(64) NOT NULL,
|
||||
[deployed_files_count] smallint NOT NULL,
|
||||
[time_of_last_deployed_object_components] datetime NULL DEFAULT (getdate()),
|
||||
[timestamp_of_last_synchronized] bigint NOT NULL DEFAULT ((0))
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
-- Table: control_index
|
||||
CREATE TABLE [control_index] (
|
||||
[entity_id] int NOT NULL,
|
||||
[gobject_id] int NOT NULL,
|
||||
[control_id] nvarchar(329) NULL,
|
||||
[control_name] nvarchar(329) NOT NULL,
|
||||
[control_description] nvarchar(2000) NULL,
|
||||
[properties] nvarchar(-1) NULL,
|
||||
[thumbnail] nvarchar(-1) NULL,
|
||||
CONSTRAINT [PK_control_index] PRIMARY KEY ([gobject_id], [control_name], [gobject_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [control_index] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
|
||||
GO
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
-- Table: data_type
|
||||
CREATE TABLE [data_type] (
|
||||
[mx_data_type] tinyint NOT NULL,
|
||||
[description] varchar(30) NOT NULL,
|
||||
[ow_data_type] varchar(10) NULL,
|
||||
CONSTRAINT [PK_data_type] PRIMARY KEY ([mx_data_type])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Table: deleted_gobject
|
||||
CREATE TABLE [deleted_gobject] (
|
||||
[gobject_id] int NOT NULL DEFAULT ((0)),
|
||||
[timestamp_of_delete] timestamp NOT NULL,
|
||||
CONSTRAINT [PK_deleted_gobject] PRIMARY KEY ([timestamp_of_delete])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
-- Table: deleted_ids
|
||||
CREATE TABLE [deleted_ids] (
|
||||
[table_id] smallint NULL,
|
||||
[deleted_id] int NOT NULL,
|
||||
[deletion_timestamp] timestamp NOT NULL,
|
||||
[deletion_time] datetime NULL
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Table: deleted_visual_element
|
||||
CREATE TABLE [deleted_visual_element] (
|
||||
[visual_element_name] nvarchar(329) NULL,
|
||||
[visual_element_type] nvarchar(32) NULL,
|
||||
[timestamp_of_delete] timestamp NOT NULL
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
-- Table: deleted_visual_element_version
|
||||
CREATE TABLE [deleted_visual_element_version] (
|
||||
[gobject_id] int NOT NULL,
|
||||
[package_id] int NOT NULL,
|
||||
[mx_primitive_id] smallint NOT NULL,
|
||||
[visual_element_name] nvarchar(329) NOT NULL,
|
||||
[visual_element_type] nvarchar(32) NOT NULL,
|
||||
[timestamp_of_delete] timestamp NOT NULL,
|
||||
[visual_element_id] int NOT NULL,
|
||||
CONSTRAINT [PK_deleted_visual_element_version] PRIMARY KEY ([gobject_id], [package_id], [timestamp_of_delete])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
-- Table: deployed_file
|
||||
CREATE TABLE [deployed_file] (
|
||||
[deployed_file_id] int NOT NULL,
|
||||
[file_id] int NOT NULL,
|
||||
[node_name] nvarchar(256) NOT NULL,
|
||||
[need_to_delete] int NOT NULL DEFAULT ((0)),
|
||||
[is_package_deployed] bit NOT NULL,
|
||||
[is_editor_deployed] bit NOT NULL,
|
||||
[is_runtime_deployed] bit NOT NULL,
|
||||
[is_browser_deployed] bit NOT NULL,
|
||||
[file_version] nvarchar(50) NOT NULL DEFAULT (''),
|
||||
[file_modified_time] nvarchar(50) NOT NULL DEFAULT (''),
|
||||
CONSTRAINT [PK_deployed_file] PRIMARY KEY ([deployed_file_id], [file_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [deployed_file] ADD FOREIGN KEY ([file_id]) REFERENCES [file_table] ([file_id]);
|
||||
GO
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Table: deployed_intouch_viewapp
|
||||
CREATE TABLE [deployed_intouch_viewapp] (
|
||||
[timestamp_of_deploy] bigint NOT NULL DEFAULT ((1)),
|
||||
[gobject_id] int NOT NULL,
|
||||
[deploy_file_transfering] bit NULL DEFAULT ((0))
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
-- Table: deployed_intouch_viewapp_visual_element_dependency
|
||||
CREATE TABLE [deployed_intouch_viewapp_visual_element_dependency] (
|
||||
[gobject_id] int NULL,
|
||||
[visual_element_name] nvarchar(2000) NULL
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
-- Table: dynamic_attribute
|
||||
CREATE TABLE [dynamic_attribute] (
|
||||
[gobject_id] int NOT NULL,
|
||||
[package_id] int NOT NULL,
|
||||
[mx_primitive_id] smallint NOT NULL,
|
||||
[mx_attribute_id] smallint NOT NULL,
|
||||
[attribute_name] nvarchar(329) NOT NULL,
|
||||
[mx_data_type] smallint NOT NULL,
|
||||
[is_array] bit NOT NULL,
|
||||
[security_classification] smallint NOT NULL,
|
||||
[mx_attribute_category] int NOT NULL,
|
||||
[lock_type] int NOT NULL,
|
||||
[mx_value] text(2147483647) NOT NULL,
|
||||
[owned_by_gobject_id] int NOT NULL DEFAULT ((0)),
|
||||
[original_lock_type] int NOT NULL DEFAULT ((0)),
|
||||
[dynamic_attribute_type] smallint NOT NULL DEFAULT ((0)),
|
||||
[bitvalues] smallint NOT NULL DEFAULT ((0)),
|
||||
[dynamic_attribute_id] bigint NOT NULL,
|
||||
CONSTRAINT [PK_dynamic_attribute] PRIMARY KEY ([gobject_id], [package_id], [mx_primitive_id], [mx_attribute_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 [dynamic_attribute] ADD FOREIGN KEY ([package_id]) REFERENCES [primitive_instance] ([package_id]);
|
||||
GO
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
-- Table: external_content_media_types
|
||||
CREATE TABLE [external_content_media_types] (
|
||||
[entity_id] int NOT NULL,
|
||||
[media_type] nvarchar(255) NOT NULL,
|
||||
[control_entity_id] int NOT NULL,
|
||||
[uri_property_name] nvarchar(1023) NULL,
|
||||
[media_type_property_name] nvarchar(1023) NULL,
|
||||
[is_default] bit NULL,
|
||||
CONSTRAINT [PK_external_content_media_types] PRIMARY KEY ([entity_id])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
-- Table: feature
|
||||
CREATE TABLE [feature] (
|
||||
[feature_id] int NOT NULL,
|
||||
[feature_name] nvarchar(256) NOT NULL,
|
||||
[feature_type] nvarchar(256) NOT NULL,
|
||||
CONSTRAINT [PK_feature] PRIMARY KEY ([feature_id])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
-- Table: feature_file_link
|
||||
CREATE TABLE [feature_file_link] (
|
||||
[feature_id] int NOT NULL,
|
||||
[file_id] int NOT NULL,
|
||||
CONSTRAINT [PK_feature_file_link] PRIMARY KEY ([feature_id], [file_id], [feature_id], [file_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [feature_file_link] ADD FOREIGN KEY ([file_id]) REFERENCES [file_table] ([file_id]);
|
||||
GO
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
-- Table: file_browserinfo_link
|
||||
CREATE TABLE [file_browserinfo_link] (
|
||||
[primitive_definition_id] int NOT NULL,
|
||||
[file_id] int NOT NULL,
|
||||
[assembly_strong_name] nvarchar(512) NOT NULL,
|
||||
[assembly_type_name] nvarchar(256) NOT NULL,
|
||||
CONSTRAINT [PK_file_browserinfo_link] PRIMARY KEY ([primitive_definition_id], [file_id], [file_id], [primitive_definition_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [file_browserinfo_link] ADD FOREIGN KEY ([primitive_definition_id]) REFERENCES [primitive_definition] ([primitive_definition_id]);
|
||||
GO
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
-- Table: file_pending_update
|
||||
CREATE TABLE [file_pending_update] (
|
||||
[file_id] int NOT NULL,
|
||||
[node_name] nvarchar(256) NOT NULL,
|
||||
CONSTRAINT [PK_file_pending_update] PRIMARY KEY ([file_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [file_pending_update] ADD FOREIGN KEY ([file_id]) REFERENCES [file_table] ([file_id]);
|
||||
GO
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
-- Table: file_primitive_definition_link
|
||||
CREATE TABLE [file_primitive_definition_link] (
|
||||
[primitive_definition_id] int NOT NULL,
|
||||
[file_id] int NOT NULL,
|
||||
[is_needed_for_package] bit NOT NULL DEFAULT ((0)),
|
||||
[is_needed_for_runtime] bit NOT NULL DEFAULT ((0)),
|
||||
[is_needed_for_editor] bit NOT NULL DEFAULT ((0)),
|
||||
[is_needed_for_browser] bit NOT NULL DEFAULT ((0)),
|
||||
CONSTRAINT [PK_file_primitive_definition_link] PRIMARY KEY ([primitive_definition_id], [file_id], [file_id], [primitive_definition_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [file_primitive_definition_link] ADD FOREIGN KEY ([primitive_definition_id]) REFERENCES [primitive_definition] ([primitive_definition_id]);
|
||||
GO
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
-- Table: file_table
|
||||
CREATE TABLE [file_table] (
|
||||
[file_id] int NOT NULL,
|
||||
[file_name] nvarchar(256) NOT NULL,
|
||||
[vendor_name] nvarchar(256) NOT NULL,
|
||||
[registration_type] int NOT NULL,
|
||||
[subfolder] nvarchar(256) NOT NULL DEFAULT (''),
|
||||
[file_version] nvarchar(50) NOT NULL DEFAULT (''),
|
||||
[file_modified_time] nvarchar(50) NOT NULL DEFAULT (''),
|
||||
CONSTRAINT [PK_file_table] PRIMARY KEY ([file_id])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
-- Table: folder
|
||||
CREATE TABLE [folder] (
|
||||
[folder_id] int NOT NULL,
|
||||
[folder_type] smallint NOT NULL,
|
||||
[folder_name] nvarchar(64) NOT NULL,
|
||||
[parent_folder_id] int NOT NULL,
|
||||
[depth] int NOT NULL,
|
||||
[has_objects] bit NOT NULL,
|
||||
[has_folders] bit NOT NULL,
|
||||
[timestamp_of_last_change] timestamp NOT NULL,
|
||||
CONSTRAINT [PK_folder] PRIMARY KEY ([folder_id])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
-- Table: folder_gobject_link
|
||||
CREATE TABLE [folder_gobject_link] (
|
||||
[folder_id] int NOT NULL,
|
||||
[folder_type] smallint NOT NULL,
|
||||
[gobject_id] int NOT NULL,
|
||||
[timestamp_of_last_change] timestamp NOT NULL,
|
||||
CONSTRAINT [PK_folder_gobject_link] PRIMARY KEY ([folder_id], [gobject_id], [gobject_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [folder_gobject_link] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
|
||||
GO
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
-- Table: galaxy
|
||||
CREATE TABLE [galaxy] (
|
||||
[time_of_last_deploy] datetime NULL DEFAULT (getdate()),
|
||||
[time_of_last_config_change] datetime NULL DEFAULT (getdate()),
|
||||
[is_galaxy_installed] bit NOT NULL DEFAULT ((1)),
|
||||
[time_of_last_reference_binding] datetime NULL DEFAULT (getdate()),
|
||||
[timestamp_of_last_cascade] bigint NOT NULL DEFAULT ((1)),
|
||||
[timestamp_of_last_visual_element_reference_bind] bigint NOT NULL DEFAULT ((0)),
|
||||
[max_proxy_timestamp] bigint NOT NULL DEFAULT (CONVERT([bigint],@@dbts)),
|
||||
[max_visual_element_timestamp] bigint NOT NULL DEFAULT (CONVERT([bigint],@@dbts)),
|
||||
[is_migration_in_progress] bit NOT NULL DEFAULT ((0)),
|
||||
[time_of_last_association_change] datetime NULL DEFAULT (getdate()),
|
||||
[subscription_id] uniqueidentifier NULL,
|
||||
[batch_id] uniqueidentifier NULL,
|
||||
[iteration_id] int NOT NULL DEFAULT ((0))
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
-- Table: galaxy_data
|
||||
CREATE TABLE [galaxy_data] (
|
||||
[data_type] nvarchar(256) NOT NULL,
|
||||
[data] image(2147483647) NULL
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Table: galaxy_settings
|
||||
CREATE TABLE [galaxy_settings] (
|
||||
[galaxyid] int NULL,
|
||||
[default_qs_data] ntext(1073741823) NOT NULL,
|
||||
[current_qs_data] ntext(1073741823) NOT NULL
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
-- Table: gobject
|
||||
CREATE TABLE [gobject] (
|
||||
[gobject_id] int NOT NULL,
|
||||
[template_definition_id] int NOT NULL,
|
||||
[derived_from_gobject_id] int NOT NULL DEFAULT ((0)),
|
||||
[contained_by_gobject_id] int NOT NULL DEFAULT ((0)),
|
||||
[area_gobject_id] int NOT NULL DEFAULT ((0)),
|
||||
[hosted_by_gobject_id] int NOT NULL DEFAULT ((0)),
|
||||
[checked_out_by_user_guid] uniqueidentifier NULL,
|
||||
[default_symbol_gobject_id] int NOT NULL DEFAULT ((0)),
|
||||
[default_display_gobject_id] int NOT NULL DEFAULT ((0)),
|
||||
[checked_in_package_id] int NOT NULL DEFAULT ((0)),
|
||||
[checked_out_package_id] int NOT NULL DEFAULT ((0)),
|
||||
[deployed_package_id] int NOT NULL DEFAULT ((0)),
|
||||
[last_deployed_package_id] int NOT NULL DEFAULT ((0)),
|
||||
[tag_name] nvarchar(329) NOT NULL,
|
||||
[contained_name] nvarchar(32) NOT NULL DEFAULT (''),
|
||||
[identity_guid] uniqueidentifier NOT NULL DEFAULT (newid()),
|
||||
[configuration_guid] uniqueidentifier NOT NULL,
|
||||
[configuration_version] int NOT NULL,
|
||||
[deployed_version] int NOT NULL DEFAULT ((0)),
|
||||
[is_template] bit NOT NULL DEFAULT ((0)),
|
||||
[is_hidden] bit NOT NULL DEFAULT ((0)),
|
||||
[software_upgrade_needed] bit NOT NULL DEFAULT ((0)),
|
||||
[hosting_tree_level] smallint NOT NULL DEFAULT ((0)),
|
||||
[hierarchical_name] nvarchar(329) NOT NULL DEFAULT (''),
|
||||
[namespace_id] smallint NOT NULL DEFAULT ((1)),
|
||||
[deployment_pending_status] bit NOT NULL DEFAULT ((0)),
|
||||
CONSTRAINT [PK_gobject] PRIMARY KEY ([gobject_id], [namespace_id], [template_definition_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [gobject] ADD FOREIGN KEY ([template_definition_id]) REFERENCES [template_definition] ([template_definition_id]);
|
||||
GO
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
-- Table: gobject_asset_order
|
||||
CREATE TABLE [gobject_asset_order] (
|
||||
[gobject_id] int NOT NULL,
|
||||
[relative_index] float(53,) NOT NULL,
|
||||
CONSTRAINT [PK_gobject_asset_order] PRIMARY KEY ([gobject_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [gobject_asset_order] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
|
||||
GO
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
-- Table: gobject_change_log
|
||||
CREATE TABLE [gobject_change_log] (
|
||||
[gobject_change_log_id] int NOT NULL,
|
||||
[gobject_id] int NOT NULL,
|
||||
[change_date] datetime NULL,
|
||||
[operation_id] smallint NOT NULL,
|
||||
[user_comment] nvarchar(1024) NOT NULL DEFAULT (''),
|
||||
[configuration_version] int NOT NULL DEFAULT ((0)),
|
||||
[user_profile_name] nvarchar(256) NOT NULL,
|
||||
CONSTRAINT [PK_gobject_change_log] PRIMARY KEY ([gobject_id], [operation_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [gobject_change_log] ADD FOREIGN KEY ([operation_id]) REFERENCES [lookup_operation] ([operation_id]);
|
||||
GO
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
-- Table: gobject_filter_info_timestamp
|
||||
CREATE TABLE [gobject_filter_info_timestamp] (
|
||||
[gobject_id] int NULL,
|
||||
[timestamp_of_last_change] timestamp NOT NULL,
|
||||
CONSTRAINT [PK_gobject_filter_info_timestamp] PRIMARY KEY ([timestamp_of_last_change], [gobject_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [gobject_filter_info_timestamp] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
|
||||
GO
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
-- Table: gobject_friendly_name
|
||||
CREATE TABLE [gobject_friendly_name] (
|
||||
[gobject_id] int NOT NULL,
|
||||
[friendly_name] nvarchar(1024) NOT NULL DEFAULT (''),
|
||||
CONSTRAINT [PK_gobject_friendly_name] PRIMARY KEY ([gobject_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [gobject_friendly_name] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
|
||||
GO
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
-- Table: gobject_log_details
|
||||
CREATE TABLE [gobject_log_details] (
|
||||
[gobject_id] int NOT NULL,
|
||||
[tag_name] nvarchar(329) NOT NULL
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
-- Table: gobject_protected
|
||||
CREATE TABLE [gobject_protected] (
|
||||
[gobject_id] int NOT NULL
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
-- Table: instance
|
||||
CREATE TABLE [instance] (
|
||||
[gobject_id] int NOT NULL,
|
||||
[mx_platform_id] smallint NOT NULL DEFAULT ((0)),
|
||||
[mx_engine_id] smallint NOT NULL DEFAULT ((0)),
|
||||
[mx_object_id] smallint NOT NULL DEFAULT ((0)),
|
||||
CONSTRAINT [PK_instance] PRIMARY KEY ([gobject_id], [gobject_id])
|
||||
);
|
||||
GO
|
||||
|
||||
ALTER TABLE [instance] ADD FOREIGN KEY ([gobject_id]) REFERENCES [gobject] ([gobject_id]);
|
||||
GO
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
-- Table: intouchviewapptemplate_allsymbols
|
||||
CREATE TABLE [intouchviewapptemplate_allsymbols] (
|
||||
[gobject_id] int NOT NULL
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Table: lookup_category
|
||||
CREATE TABLE [lookup_category] (
|
||||
[category_id] smallint NOT NULL,
|
||||
[category_name] nvarchar(50) NOT NULL,
|
||||
CONSTRAINT [PK_lookup_category] PRIMARY KEY ([category_id])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
-- Table: lookup_folder
|
||||
CREATE TABLE [lookup_folder] (
|
||||
[folder_type] smallint NOT NULL,
|
||||
[folder_type_name] nvarchar(32) NULL
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
-- Table: lookup_operation
|
||||
CREATE TABLE [lookup_operation] (
|
||||
[operation_id] smallint NOT NULL,
|
||||
[operation_code] nvarchar(50) NOT NULL,
|
||||
[operation_name] nvarchar(256) NOT NULL,
|
||||
CONSTRAINT [PK_lookup_operation] PRIMARY KEY ([operation_id])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Table: lookup_package_op_status
|
||||
CREATE TABLE [lookup_package_op_status] (
|
||||
[status_id] int NOT NULL,
|
||||
[status_name] nvarchar(50) NOT NULL,
|
||||
CONSTRAINT [PK_lookup_package_op_status] PRIMARY KEY ([status_id])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Table: lookup_status
|
||||
CREATE TABLE [lookup_status] (
|
||||
[status_id] int NOT NULL,
|
||||
[status_name] nvarchar(50) NOT NULL,
|
||||
CONSTRAINT [PK_lookup_status] PRIMARY KEY ([status_id])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
-- Table: lookup_table_name
|
||||
CREATE TABLE [lookup_table_name] (
|
||||
[table_id] smallint NOT NULL,
|
||||
[table_name] nvarchar(250) NULL
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Table: namespace
|
||||
CREATE TABLE [namespace] (
|
||||
[namespace_id] smallint NOT NULL,
|
||||
[namespace_name] nvarchar(32) NULL,
|
||||
CONSTRAINT [PK_namespace] PRIMARY KEY ([namespace_id])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
-- Table: object_device_linkage
|
||||
CREATE TABLE [object_device_linkage] (
|
||||
[gobject_id] int NOT NULL,
|
||||
[dio_id] int NOT NULL,
|
||||
[sg_mx_primitive_id] smallint NOT NULL,
|
||||
CONSTRAINT [PK_object_device_linkage] PRIMARY KEY ([gobject_id])
|
||||
);
|
||||
GO
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
-- Table: object_wizard_overview_symbols
|
||||
CREATE TABLE [object_wizard_overview_symbols] (
|
||||
[gobject_id] int NOT NULL,
|
||||
[visual_element_id] int NOT NULL,
|
||||
[change_type] int NOT NULL,
|
||||
[mx_primitive_id] int NULL
|
||||
);
|
||||
GO
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user