docs: update path references for module-folder reorganization

Rewrite src/ and tests/ project paths in docs, CLAUDE.md, README.md, and
test-fixture READMEs to the new module-folder layout (Core/Server/Drivers/
Client/Tooling). References to retired v1 projects (Galaxy.Host/Proxy/Shared,
the legacy monolithic test projects) are left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-17 02:10:29 -04:00
parent a25593a9c6
commit 969b0847a1
64 changed files with 338 additions and 338 deletions

View File

@@ -9,7 +9,7 @@ Build an OPC UA server (.NET 10) that exposes AVEVA System Platform
hierarchy as an OPC UA address space, translating between hierarchy as an OPC UA address space, translating between
contained-name browse paths and tag-name runtime references. Galaxy contained-name browse paths and tag-name runtime references. Galaxy
access flows through the in-process `GalaxyDriver` access flows through the in-process `GalaxyDriver`
(`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/`) talking gRPC to a separately (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/`) talking gRPC to a separately
installed **mxaccessgw** gateway process. The gateway owns the installed **mxaccessgw** gateway process. The gateway owns the
MXAccess COM bitness constraint (its worker is x86 net48); everything MXAccess COM bitness constraint (its worker is x86 net48); everything
in this repo is .NET 10. PR 7.2 retired the legacy in-process in this repo is .NET 10. PR 7.2 retired the legacy in-process
@@ -47,11 +47,11 @@ Example: browsing `TestMachine_001/DelmiaReceiver/DownloadPath` translates to MX
### Data Type Mapping ### Data Type Mapping
Galaxy `mx_data_type` values map to OPC UA types (Boolean, Int32, Float, Double, String, DateTime, etc.). Array attributes use ValueRank=1 with ArrayDimensions from the Galaxy attribute definition. The driver-side mapping lives in `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`. Galaxy `mx_data_type` values map to OPC UA types (Boolean, Int32, Float, Double, String, DateTime, etc.). Array attributes use ValueRank=1 with ArrayDimensions from the Galaxy attribute definition. The driver-side mapping lives in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`.
### Change Detection ### Change Detection
`DeployWatcher` (`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DeployWatcher.cs`) polls the gateway's deploy-event signal and raises `IRediscoverable.OnRediscoveryNeeded` when the Galaxy redeploys. The server's `DriverHost` consumes the signal and rebuilds the address space. `DeployWatcher` (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DeployWatcher.cs`) polls the gateway's deploy-event signal and raises `IRediscoverable.OnRediscoveryNeeded` when the Galaxy redeploys. The server's `DriverHost` consumes the signal and rebuilds the address space.
## mxaccessgw ## mxaccessgw
@@ -121,13 +121,13 @@ The server supports non-transparent warm/hot redundancy via the `Redundancy` sec
## LDAP Authentication ## LDAP Authentication
The server uses LDAP-based user authentication via the `Authentication.Ldap` section in `appsettings.json`. When enabled, credentials are validated by LDAP bind against a GLAuth server (installed at `C:\publish\glauth\`), and LDAP group membership maps to OPC UA permissions: `ReadOnly` (browse/read), `WriteOperate` (write FreeAccess/Operate attributes), `WriteTune` (write Tune attributes), `WriteConfigure` (write Configure attributes), `AlarmAck` (alarm acknowledgment). `LdapUserAuthenticator` (`src/ZB.MOM.WW.OtOpcUa.Server/Security/LdapUserAuthenticator.cs`) implements `IUserAuthenticator`. See `docs/Security.md` for the full guide and `C:\publish\glauth\auth.md` for LDAP user/group reference. The server uses LDAP-based user authentication via the `Authentication.Ldap` section in `appsettings.json`. When enabled, credentials are validated by LDAP bind against a GLAuth server (installed at `C:\publish\glauth\`), and LDAP group membership maps to OPC UA permissions: `ReadOnly` (browse/read), `WriteOperate` (write FreeAccess/Operate attributes), `WriteTune` (write Tune attributes), `WriteConfigure` (write Configure attributes), `AlarmAck` (alarm acknowledgment). `LdapUserAuthenticator` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/LdapUserAuthenticator.cs`) implements `IUserAuthenticator`. See `docs/Security.md` for the full guide and `C:\publish\glauth\auth.md` for LDAP user/group reference.
## Library Preferences ## Library Preferences
- **Logging**: Serilog with rolling daily file sink - **Logging**: Serilog with rolling daily file sink
- **Unit tests**: xUnit + Shouldly for assertions - **Unit tests**: xUnit + Shouldly for assertions
- **Service hosting (Server, Admin)**: .NET generic host with `AddWindowsService` (decision #30 — replaced TopShelf in v2; see `src/ZB.MOM.WW.OtOpcUa.Server/OpcUaServerService.cs`) - **Service hosting (Server, Admin)**: .NET generic host with `AddWindowsService` (decision #30 — replaced TopShelf in v2; see `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUaServerService.cs`)
- **OPC UA**: OPC Foundation UA .NET Standard stack (https://github.com/opcfoundation/ua-.netstandard) — NuGet: `OPCFoundation.NetStandard.Opc.Ua.Server` - **OPC UA**: OPC Foundation UA .NET Standard stack (https://github.com/opcfoundation/ua-.netstandard) — NuGet: `OPCFoundation.NetStandard.Opc.Ua.Server`
## OPC UA .NET Standard Documentation ## OPC UA .NET Standard Documentation
@@ -136,11 +136,11 @@ Use the DeepWiki MCP (`mcp__deepwiki`) to query documentation for the OPC UA .NE
## Testing ## Testing
Use the Client CLI at `src/ZB.MOM.WW.OtOpcUa.Client.CLI/` for manual testing against the running OPC UA server. Supports connect, read, write, browse, subscribe, historyread, alarms, and redundancy commands. See `docs/Client.CLI.md` for full documentation. Use the Client CLI at `src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/` for manual testing against the running OPC UA server. Supports connect, read, write, browse, subscribe, historyread, alarms, and redundancy commands. See `docs/Client.CLI.md` for full documentation.
```bash ```bash
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840 dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 3 dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 3
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode"
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500 dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500
``` ```

View File

@@ -41,10 +41,10 @@ dotnet build ZB.MOM.WW.OtOpcUa.slnx
dotnet test ZB.MOM.WW.OtOpcUa.slnx dotnet test ZB.MOM.WW.OtOpcUa.slnx
# Run the server in dev (foreground) # Run the server in dev (foreground)
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Server dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server
``` ```
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). The server starts on `opc.tcp://localhost:4840` with the `None` security profile. Configure `Security.Profiles` in `src/Server/ZB.MOM.WW.OtOpcUa.Server/appsettings.json` to enable `Basic256Sha256-Sign` or `Basic256Sha256-SignAndEncrypt`. See [docs/security.md](docs/security.md).
## Install as Windows Services ## Install as Windows Services
@@ -61,11 +61,11 @@ Add `-InstallWonderwareHistorian` for the historian sidecar. See the script head
## Client CLI ## Client CLI
```bash ```bash
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840 dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 3 dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 3
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode"
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- write -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -v 42 dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- write -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -v 42
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500 dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500
``` ```
See [docs/Client.CLI.md](docs/Client.CLI.md) and [docs/Client.UI.md](docs/Client.UI.md). See [docs/Client.CLI.md](docs/Client.CLI.md) and [docs/Client.UI.md](docs/Client.UI.md).

View File

@@ -1,6 +1,6 @@
# Address Space # Address Space
Each driver's browsable subtree is built by streaming nodes from the driver's `ITagDiscovery.DiscoverAsync` implementation into an `IAddressSpaceBuilder`. `GenericDriverNodeManager` (`src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs`) owns the shared orchestration; `DriverNodeManager` (`src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) implements `IAddressSpaceBuilder` against the OPC Foundation stack's `CustomNodeManager2`. The same code path serves Galaxy object hierarchies, Modbus PLC registers, AB CIP tags, TwinCAT symbols, FOCAS CNC parameters, and OPC UA Client aggregations — Galaxy is one driver of seven, not the driver. Each driver's browsable subtree is built by streaming nodes from the driver's `ITagDiscovery.DiscoverAsync` implementation into an `IAddressSpaceBuilder`. `GenericDriverNodeManager` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs`) owns the shared orchestration; `DriverNodeManager` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) implements `IAddressSpaceBuilder` against the OPC Foundation stack's `CustomNodeManager2`. The same code path serves Galaxy object hierarchies, Modbus PLC registers, AB CIP tags, TwinCAT symbols, FOCAS CNC parameters, and OPC UA Client aggregations — Galaxy is one driver of seven, not the driver.
## Driver root folder ## Driver root folder
@@ -8,7 +8,7 @@ Every driver's subtree starts with a root `FolderState` under the standard OPC U
## IAddressSpaceBuilder surface ## IAddressSpaceBuilder surface
`IAddressSpaceBuilder` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs`) offers three calls: `IAddressSpaceBuilder` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs`) offers three calls:
- `Folder(browseName, displayName)` — creates a child `FolderState` and returns a child builder scoped to it. - `Folder(browseName, displayName)` — creates a child `FolderState` and returns a child builder scoped to it.
- `Variable(browseName, displayName, DriverAttributeInfo attributeInfo)` — creates a `BaseDataVariableState` and returns an `IVariableHandle` the driver keeps for alarm wiring. - `Variable(browseName, displayName, DriverAttributeInfo attributeInfo)` — creates a `BaseDataVariableState` and returns an `IVariableHandle` the driver keeps for alarm wiring.
@@ -18,7 +18,7 @@ Drivers drive ordering. Typical pattern: root → folder per equipment → varia
## DriverAttributeInfo → OPC UA variable ## DriverAttributeInfo → OPC UA variable
Each variable carries a `DriverAttributeInfo` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs`): Each variable carries a `DriverAttributeInfo` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs`):
| Field | OPC UA target | | Field | OPC UA target |
|---|---| |---|---|
@@ -65,8 +65,8 @@ Drivers that implement `IRediscoverable` fire `OnRediscoveryNeeded` when their b
## Key source files ## Key source files
- `src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — orchestration + `CapturingBuilder` - `src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — orchestration + `CapturingBuilder`
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — OPC UA materialization (`IAddressSpaceBuilder` impl + `NestedBuilder`) - `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — OPC UA materialization (`IAddressSpaceBuilder` impl + `NestedBuilder`)
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs` — builder contract - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs` — builder contract
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs` — driver discovery capability - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs` — driver discovery capability
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs` — per-attribute descriptor - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs` — per-attribute descriptor

View File

@@ -15,7 +15,7 @@ historical reference.
| **Galaxy sub-attribute fallback** | `IWritable` writes to `$Alarm*` sub-attributes | gateway data subscription → driver `OnDataChange``DriverNodeManager` ConditionSink → `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` | | **Scripted alarms** | `Phase7EngineComposer` | server-side script evaluator → `Phase7EngineComposer.RouteToHistorianAsync` + `AlarmConditionService` |
All three converge on `AlarmConditionService` (`src/ZB.MOM.WW.OtOpcUa.Server/Alarms/AlarmConditionService.cs`), All three converge on `AlarmConditionService` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Alarms/AlarmConditionService.cs`),
which owns the OPC UA Part 9 state machine and dispatches transitions which owns the OPC UA Part 9 state machine and dispatches transitions
to the OPC UA condition node managers. Driver-native transitions take to the OPC UA condition node managers. Driver-native transitions take
precedence over sub-attribute synthesis when both arrive for the same precedence over sub-attribute synthesis when both arrive for the same

View File

@@ -9,12 +9,12 @@ The CLI is the primary tool for operators and developers to test and interact wi
## Build and Run ## Build and Run
```bash ```bash
cd src/ZB.MOM.WW.OtOpcUa.Client.CLI cd src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI
dotnet build dotnet build
dotnet run -- <command> [options] dotnet run -- <command> [options]
``` ```
The executable name is `otopcua-cli`. Dev boxes carrying a pre-task-#208 install may still have the legacy `{LocalAppData}/LmxOpcUaClient/` folder on disk; on first launch of any post-#208 CLI or UI build, `ClientStoragePaths` (`src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs`) migrates it to `{LocalAppData}/OtOpcUaClient/` automatically so trusted certificates + saved settings survive the rename. The executable name is `otopcua-cli`. Dev boxes carrying a pre-task-#208 install may still have the legacy `{LocalAppData}/LmxOpcUaClient/` folder on disk; on first launch of any post-#208 CLI or UI build, `ClientStoragePaths` (`src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs`) migrates it to `{LocalAppData}/OtOpcUaClient/` automatically so trusted certificates + saved settings survive the rename.
## Architecture ## Architecture
@@ -240,5 +240,5 @@ Application URI: urn:localhost:OtOpcUa:instance1
The Client CLI has 52 unit tests covering option parsing, service invocation, output formatting, and cleanup behavior: The Client CLI has 52 unit tests covering option parsing, service invocation, output formatting, and cleanup behavior:
```bash ```bash
dotnet test tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests
``` ```

View File

@@ -9,7 +9,7 @@ The UI provides a single-window interface for browsing the address space, readin
## Build and Run ## Build and Run
```bash ```bash
cd src/ZB.MOM.WW.OtOpcUa.Client.UI cd src/Client/ZB.MOM.WW.OtOpcUa.Client.UI
dotnet build dotnet build
dotnet run dotnet run
``` ```
@@ -254,7 +254,7 @@ All service event handlers (data changes, alarm events, connection state changes
The UI has 102 unit tests covering ViewModel logic and headless rendering: The UI has 102 unit tests covering ViewModel logic and headless rendering:
```bash ```bash
dotnet test tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests
``` ```
Tests use: Tests use:

View File

@@ -10,7 +10,7 @@ TwinCAT). Shares `Driver.Cli.Common` with the others.
## Build + run ## Build + run
```powershell ```powershell
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli -- --help dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli -- --help
``` ```
## Common flags ## Common flags

View File

@@ -10,7 +10,7 @@ others.
## Build + run ## Build + run
```powershell ```powershell
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli -- --help dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli -- --help
``` ```
## Common flags ## Common flags
@@ -99,7 +99,7 @@ otopcua-ablegacy-cli subscribe -g ab://192.168.1.20/1,0 -a N7:10 -t Int -i 500
The integration-fixture `ab_server` Docker container accepts TCP but its PCCC The integration-fixture `ab_server` Docker container accepts TCP but its PCCC
dispatcher doesn't actually respond — see dispatcher doesn't actually respond — see
[`tests/...AbLegacy.IntegrationTests/Docker/README.md`](../tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md). [`tests/...AbLegacy.IntegrationTests/Docker/README.md`](../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md).
Point `--gateway` at real hardware or an RSEmulate 500 box for end-to-end Point `--gateway` at real hardware or an RSEmulate 500 box for end-to-end
wire-level validation. The CLI itself is correct regardless of which endpoint wire-level validation. The CLI itself is correct regardless of which endpoint
you target. you target.

View File

@@ -17,7 +17,7 @@ process Host arrangement required. The CLI loads `FocasDriver` with
components. components.
A dev-friendly mock is available — start A dev-friendly mock is available — start
`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml` `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml`
and point `--cnc-host` at `localhost` for end-to-end CLI exercises and point `--cnc-host` at `localhost` for end-to-end CLI exercises
without a real CNC. See without a real CNC. See
[drivers/FOCAS-Test-Fixture.md](drivers/FOCAS-Test-Fixture.md). [drivers/FOCAS-Test-Fixture.md](drivers/FOCAS-Test-Fixture.md).
@@ -25,14 +25,14 @@ without a real CNC. See
## Build + run ## Build + run
```powershell ```powershell
dotnet build src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli dotnet build src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli -- --help dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli -- --help
``` ```
Or publish a self-contained binary: Or publish a self-contained binary:
```powershell ```powershell
dotnet publish src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli -c Release -o publish/focas-cli dotnet publish src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli -c Release -o publish/focas-cli
publish/focas-cli/otopcua-focas-cli.exe --help publish/focas-cli/otopcua-focas-cli.exe --help
``` ```

View File

@@ -13,14 +13,14 @@ without copy-paste.
## Build + run ## Build + run
```powershell ```powershell
dotnet build src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli dotnet build src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -- --help dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -- --help
``` ```
Or publish a self-contained binary: Or publish a self-contained binary:
```powershell ```powershell
dotnet publish src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -c Release -o publish/modbus-cli dotnet publish src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -c Release -o publish/modbus-cli
publish/modbus-cli/otopcua-modbus-cli.exe --help publish/modbus-cli/otopcua-modbus-cli.exe --help
``` ```

View File

@@ -9,7 +9,7 @@ Fourth of four driver test-client CLIs.
## Build + run ## Build + run
```powershell ```powershell
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli -- --help dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli -- --help
``` ```
## Common flags ## Common flags

View File

@@ -10,7 +10,7 @@ Fifth (final) of the driver test-client CLIs.
## Build + run ## Build + run
```powershell ```powershell
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli -- --help dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli -- --help
``` ```
## Prerequisite: AMS router ## Prerequisite: AMS router

View File

@@ -37,7 +37,7 @@ Every driver CLI exposes the same four verbs:
## Shared infrastructure ## Shared infrastructure
All six CLIs depend on `src/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/`: All six CLIs depend on `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/`:
- `DriverCommandBase``--verbose` + Serilog configuration + the abstract - `DriverCommandBase``--verbose` + Serilog configuration + the abstract
`Timeout` surface every protocol-specific base overrides with its own `Timeout` surface every protocol-specific base overrides with its own
@@ -91,5 +91,5 @@ Tasks #249 / #250 / #251 shipped the original five. The FOCAS CLI followed
alongside the Tier-C isolation work on task #220 — no CLI-level test alongside the Tier-C isolation work on task #220 — no CLI-level test
project (hardware-gated). 122 unit tests cumulative across the first five project (hardware-gated). 122 unit tests cumulative across the first five
(16 shared-lib + 106 CLI-specific) — run (16 shared-lib + 106 CLI-specific) — run
`dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests` + `dotnet test tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests` +
`tests/ZB.MOM.WW.OtOpcUa.Driver.*.Cli.Tests` to re-verify. `tests/ZB.MOM.WW.OtOpcUa.Driver.*.Cli.Tests` to re-verify.

View File

@@ -4,7 +4,7 @@ Two distinct change-detection paths feed the running server: driver-backend redi
## Driver-backend rediscovery — IRediscoverable ## Driver-backend rediscovery — IRediscoverable
Drivers whose backend has a native change signal implement `IRediscoverable` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs`): Drivers whose backend has a native change signal implement `IRediscoverable` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs`):
```csharp ```csharp
public interface IRediscoverable public interface IRediscoverable
@@ -28,7 +28,7 @@ Static drivers (Modbus, S7, AB CIP, AB Legacy, FOCAS) do not implement `IRedisco
Tag-set changes authored in the Admin UI (UNS edits, CSV imports, driver-config edits) accumulate in a draft generation and commit via `sp_PublishGeneration`. The delta between the currently-published generation and the proposed next one is computed by `sp_ComputeGenerationDiff`, which drives: Tag-set changes authored in the Admin UI (UNS edits, CSV imports, driver-config edits) accumulate in a draft generation and commit via `sp_PublishGeneration`. The delta between the currently-published generation and the proposed next one is computed by `sp_ComputeGenerationDiff`, which drives:
- The **DiffViewer** in Admin (`src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffViewer.razor`) so operators can preview what will change before clicking Publish. - The **DiffViewer** in Admin (`src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffViewer.razor`) so operators can preview what will change before clicking Publish.
- The 409-on-stale-draft flow (decision #161) — a UNS drag-reorder preview carries a `DraftRevisionToken` so Confirm returns `409 Conflict / refresh-required` if the draft advanced between preview and commit. - The 409-on-stale-draft flow (decision #161) — a UNS drag-reorder preview carries a `DraftRevisionToken` so Confirm returns `409 Conflict / refresh-required` if the draft advanced between preview and commit.
After publish, the server's generation applier invokes `IDriver.ReinitializeAsync(driverConfigJson, ct)` on every driver whose `DriverInstance.DriverConfig` row changed in the new generation. Reinitialize is the in-process recovery path for Tier A/B drivers; if it fails the driver is marked `DriverState.Faulted` and its nodes go Bad quality — but the server process stays running. See `docs/v2/driver-stability.md`. After publish, the server's generation applier invokes `IDriver.ReinitializeAsync(driverConfigJson, ct)` on every driver whose `DriverInstance.DriverConfig` row changed in the new generation. Reinitialize is the in-process recovery path for Tier A/B drivers; if it fails the driver is marked `DriverState.Faulted` and its nodes go Bad quality — but the server process stays running. See `docs/v2/driver-stability.md`.
@@ -53,7 +53,7 @@ When `RediscoveryEventArgs.ScopeHint` is non-null (e.g. a folder path), Core res
## Virtual tags in the rebuild ## Virtual tags in the rebuild
Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), virtual (scripted) tags live in the same address space as driver tags and flow through the same rebuild. `EquipmentNodeWalker` (`src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs`) emits virtual-tag children alongside driver-tag children with `DriverAttributeInfo.Source = NodeSourceKind.Virtual`, and `DriverNodeManager` registers each variable's source in `_sourceByFullRef` so the dispatch branches correctly after rebuild. Virtual-tag script changes published from the Admin UI land through the same generation-publish path — the `VirtualTagEngine` recompiles its script bundle when its config row changes and `DriverNodeManager` re-registers any added/removed virtual variables through the standard diff path. Subscription restoration after rebuild runs through each source's `ISubscribable` — either the driver's or `VirtualTagSource` — without special-casing. Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), virtual (scripted) tags live in the same address space as driver tags and flow through the same rebuild. `EquipmentNodeWalker` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs`) emits virtual-tag children alongside driver-tag children with `DriverAttributeInfo.Source = NodeSourceKind.Virtual`, and `DriverNodeManager` registers each variable's source in `_sourceByFullRef` so the dispatch branches correctly after rebuild. Virtual-tag script changes published from the Admin UI land through the same generation-publish path — the `VirtualTagEngine` recompiles its script bundle when its config row changes and `DriverNodeManager` re-registers any added/removed virtual variables through the standard diff path. Subscription restoration after rebuild runs through each source's `ISubscribable` — either the driver's or `VirtualTagSource` — without special-casing.
## Active subscriptions survive rebuild ## Active subscriptions survive rebuild
@@ -61,9 +61,9 @@ Subscriptions for unchanged references stay live across rebuilds — their ref-c
## Key source files ## Key source files
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs` — backend-change capability - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs` — backend-change capability
- `src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — discovery orchestration - `src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — discovery orchestration
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs``ReinitializeAsync` contract - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs``ReinitializeAsync` contract
- `src/ZB.MOM.WW.OtOpcUa.Admin/Services/GenerationService.cs` — publish-flow driver - `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/GenerationService.cs` — publish-flow driver
- `docs/v2/config-db-schema.md``sp_PublishGeneration` + `sp_ComputeGenerationDiff` - `docs/v2/config-db-schema.md``sp_PublishGeneration` + `sp_ComputeGenerationDiff`
- `docs/v2/admin-ui.md` — DiffViewer + draft-revision-token flow - `docs/v2/admin-ui.md` — DiffViewer + draft-revision-token flow

View File

@@ -1,14 +1,14 @@
# OPC UA Server # OPC UA Server
The OPC UA server component (`src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs`) hosts the OPC UA stack and exposes one browsable subtree per registered driver. The server itself is driver-agnostic — Galaxy/MXAccess, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client are all plugged in as `IDriver` implementations via the capability interfaces in `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/`. The OPC UA server component (`src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs`) hosts the OPC UA stack and exposes one browsable subtree per registered driver. The server itself is driver-agnostic — Galaxy/MXAccess, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client are all plugged in as `IDriver` implementations via the capability interfaces in `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/`.
## Composition ## Composition
`OtOpcUaServer` subclasses the OPC Foundation `StandardServer` and wires: `OtOpcUaServer` subclasses the OPC Foundation `StandardServer` and wires:
- A `DriverHost` (`src/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs`) which registers drivers and holds the per-instance `IDriver` references. - A `DriverHost` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs`) which registers drivers and holds the per-instance `IDriver` references.
- One `DriverNodeManager` per registered driver (`src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`), constructed in `CreateMasterNodeManager`. Each manager owns its own namespace URI (`urn:OtOpcUa:{DriverInstanceId}`) and exposes the driver as a subtree under the standard `Objects` folder. - One `DriverNodeManager` per registered driver (`src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`), constructed in `CreateMasterNodeManager`. Each manager owns its own namespace URI (`urn:OtOpcUa:{DriverInstanceId}`) and exposes the driver as a subtree under the standard `Objects` folder.
- A `CapabilityInvoker` (`src/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs`) per driver instance, keyed on `(DriverInstanceId, HostName, DriverCapability)` against the shared `DriverResiliencePipelineBuilder`. Every Read/Write/Discovery/Subscribe/HistoryRead/AlarmSubscribe call on the driver flows through this invoker so the Polly pipeline (retry / timeout / breaker / bulkhead) applies. The OTOPCUA0001 Roslyn analyzer enforces the wrapping at compile time. - A `CapabilityInvoker` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs`) per driver instance, keyed on `(DriverInstanceId, HostName, DriverCapability)` against the shared `DriverResiliencePipelineBuilder`. Every Read/Write/Discovery/Subscribe/HistoryRead/AlarmSubscribe call on the driver flows through this invoker so the Polly pipeline (retry / timeout / breaker / bulkhead) applies. The OTOPCUA0001 Roslyn analyzer enforces the wrapping at compile time.
- An `IUserAuthenticator` (LDAP in production, injected stub in tests) for `UserName` token validation in the `ImpersonateUser` hook. - An `IUserAuthenticator` (LDAP in production, injected stub in tests) for `UserName` token validation in the `ImpersonateUser` hook.
- Optional `AuthorizationGate` + `NodeScopeResolver` (Phase 6.2) that sit in front of every dispatch call. In lax mode the gate passes through when the identity lacks LDAP groups so existing integration tests keep working; strict mode (`Authorization:StrictMode = true`) denies those cases. - Optional `AuthorizationGate` + `NodeScopeResolver` (Phase 6.2) that sit in front of every dispatch call. In lax mode the gate passes through when the identity lacks LDAP groups so existing integration tests keep working; strict mode (`Authorization:StrictMode = true`) denies those cases.
@@ -50,7 +50,7 @@ The host name fed to the invoker comes from `IPerCallHostResolver.ResolveHost(fu
## Redundancy ## Redundancy
`Redundancy.Enabled = true` on the `ServerInstance` activates the `RedundancyCoordinator` + `ServiceLevelCalculator` (`src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`). Standard OPC UA redundancy nodes (`Server/ServerRedundancy/RedundancySupport`, `ServerUriArray`, `Server/ServiceLevel`) are populated on startup; `ServiceLevel` recomputes whenever any driver's `DriverHealth` changes. The apply-lease mechanism prevents two instances from concurrently applying a generation. See `docs/Redundancy.md`. `Redundancy.Enabled = true` on the `ServerInstance` activates the `RedundancyCoordinator` + `ServiceLevelCalculator` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`). Standard OPC UA redundancy nodes (`Server/ServerRedundancy/RedundancySupport`, `ServerUriArray`, `Server/ServiceLevel`) are populated on startup; `ServiceLevel` recomputes whenever any driver's `DriverHealth` changes. The apply-lease mechanism prevents two instances from concurrently applying a generation. See `docs/Redundancy.md`.
## Server class hierarchy ## Server class hierarchy
@@ -79,10 +79,10 @@ Certificate stores default to `%LOCALAPPDATA%\OPC Foundation\pki\` (directory-ba
## Key source files ## Key source files
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs``StandardServer` subclass + `ImpersonateUser` hook - `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs``StandardServer` subclass + `ImpersonateUser` hook
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — per-driver `CustomNodeManager2` + dispatch surface - `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — per-driver `CustomNodeManager2` + dispatch surface
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs` — programmatic `ApplicationConfiguration` + lifecycle - `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs` — programmatic `ApplicationConfiguration` + lifecycle
- `src/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs` — driver registration - `src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs` — driver registration
- `src/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — Polly pipeline entry point - `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — Polly pipeline entry point
- `src/ZB.MOM.WW.OtOpcUa.Core/Authorization/` — Phase 6.2 permission trie + evaluator - `src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/` — Phase 6.2 permission trie + evaluator
- `src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` — stack-to-evaluator bridge - `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` — stack-to-evaluator bridge

View File

@@ -25,7 +25,7 @@ The project was originally called **LmxOpcUa** (a single-driver Galaxy/MXAccess
| [ReadWriteOperations.md](ReadWriteOperations.md) | OPC UA Read/Write → `CapabilityInvoker``IReadable`/`IWritable` | | [ReadWriteOperations.md](ReadWriteOperations.md) | OPC UA Read/Write → `CapabilityInvoker``IReadable`/`IWritable` |
| [Subscriptions.md](v1/Subscriptions.md) | Monitored items → `ISubscribable` + per-driver subscription refcount (v1 archive) | | [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) | | [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`) | | [DataTypeMapping.md](v1/DataTypeMapping.md) | Per-driver `DriverAttributeInfo` → OPC UA variable types (v1 archive — live mapping is in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`) |
| [IncrementalSync.md](IncrementalSync.md) | Address-space rebuild on redeploy + `sp_ComputeGenerationDiff` | | [IncrementalSync.md](IncrementalSync.md) | Address-space rebuild on redeploy + `sp_ComputeGenerationDiff` |
| [HistoricalDataAccess.md](v1/HistoricalDataAccess.md) | `IHistoryProvider` as a per-driver optional capability (v1 archive) | | [HistoricalDataAccess.md](v1/HistoricalDataAccess.md) | `IHistoryProvider` as a per-driver optional capability (v1 archive) |
| [VirtualTags.md](VirtualTags.md) | `Core.Scripting` + `Core.VirtualTags` — Roslyn script sandbox, engine, dispatch alongside driver tags | | [VirtualTags.md](VirtualTags.md) | `Core.Scripting` + `Core.VirtualTags` — Roslyn script sandbox, engine, dispatch alongside driver tags |

View File

@@ -1,13 +1,13 @@
# Read/Write Operations # Read/Write Operations
`DriverNodeManager` (`src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) wires the OPC UA stack's per-variable `OnReadValue` and `OnWriteValue` hooks to each driver's `IReadable` and `IWritable` capabilities. Every dispatch flows through `CapabilityInvoker` so the Polly pipeline (retry / timeout / breaker / bulkhead) applies uniformly across Galaxy, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client drivers. `DriverNodeManager` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) wires the OPC UA stack's per-variable `OnReadValue` and `OnWriteValue` hooks to each driver's `IReadable` and `IWritable` capabilities. Every dispatch flows through `CapabilityInvoker` so the Polly pipeline (retry / timeout / breaker / bulkhead) applies uniformly across Galaxy, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client drivers.
## Driver vs virtual dispatch ## Driver vs virtual dispatch
Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), a single `DriverNodeManager` routes reads and writes across both driver-sourced and virtual (scripted) tags. At discovery time each variable registers a `NodeSourceKind` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs`) in the manager's `_sourceByFullRef` lookup; the read/write hooks pattern-match on that value to pick the backend: Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), a single `DriverNodeManager` routes reads and writes across both driver-sourced and virtual (scripted) tags. At discovery time each variable registers a `NodeSourceKind` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs`) in the manager's `_sourceByFullRef` lookup; the read/write hooks pattern-match on that value to pick the backend:
- `NodeSourceKind.Driver` — dispatches to the driver's `IReadable` / `IWritable` through `CapabilityInvoker` (the rest of this doc). - `NodeSourceKind.Driver` — dispatches to the driver's `IReadable` / `IWritable` through `CapabilityInvoker` (the rest of this doc).
- `NodeSourceKind.Virtual` — dispatches to `VirtualTagSource` (`src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs`), which wraps `VirtualTagEngine`. Writes are rejected with `BadUserAccessDenied` before the branch per Phase 7 decision #6 — scripts are the only write path into virtual tags. - `NodeSourceKind.Virtual` — dispatches to `VirtualTagSource` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs`), which wraps `VirtualTagEngine`. Writes are rejected with `BadUserAccessDenied` before the branch per Phase 7 decision #6 — scripts are the only write path into virtual tags.
- `NodeSourceKind.ScriptedAlarm` — dispatches to the Phase 7 `ScriptedAlarmReadable` shim. - `NodeSourceKind.ScriptedAlarm` — dispatches to the Phase 7 `ScriptedAlarmReadable` shim.
ACL enforcement (`WriteAuthzPolicy` + `AuthorizationGate`) runs before the source branch, so the gates below apply uniformly to all three source kinds. ACL enforcement (`WriteAuthzPolicy` + `AuthorizationGate`) runs before the source branch, so the gates below apply uniformly to all three source kinds.
@@ -60,8 +60,8 @@ Per decision #12, exceptions in the driver's capability call are logged and conv
## Key source files ## Key source files
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs``OnReadValue` / `OnWriteValue` hooks - `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs``OnReadValue` / `OnWriteValue` hooks
- `src/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs` — classification-to-role policy - `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs` — classification-to-role policy
- `src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` — Phase 6.2 trie gate - `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` — Phase 6.2 trie gate
- `src/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs``ExecuteAsync` / `ExecuteWriteAsync` - `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs``ExecuteAsync` / `ExecuteWriteAsync`
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs`, `IWritable.cs`, `WriteIdempotentAttribute.cs` - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs`, `IWritable.cs`, `WriteIdempotentAttribute.cs`

View File

@@ -4,7 +4,7 @@
OtOpcUa supports OPC UA **non-transparent** warm/hot redundancy. Two (or more) OtOpcUa Server processes run side-by-side, share the same Config DB, the same driver backends (Galaxy ZB, MXAccess runtime, remote PLCs), and advertise the same OPC UA node tree. Each process owns a distinct `ApplicationUri`; OPC UA clients see both endpoints via the standard `ServerUriArray` and pick one based on the `ServiceLevel` that each server publishes. OtOpcUa supports OPC UA **non-transparent** warm/hot redundancy. Two (or more) OtOpcUa Server processes run side-by-side, share the same Config DB, the same driver backends (Galaxy ZB, MXAccess runtime, remote PLCs), and advertise the same OPC UA node tree. Each process owns a distinct `ApplicationUri`; OPC UA clients see both endpoints via the standard `ServerUriArray` and pick one based on the `ServiceLevel` that each server publishes.
The redundancy surface lives in `src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`: The redundancy surface lives in `src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`:
| Class | Role | | Class | Role |
|---|---| |---|---|
@@ -18,7 +18,7 @@ The redundancy surface lives in `src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`:
## Data model ## Data model
Per-node redundancy state lives in the Config DB `ClusterNode` table (`src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs`): Per-node redundancy state lives in the Config DB `ClusterNode` table (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs`):
| Column | Role | | Column | Role |
|---|---| |---|---|
@@ -64,7 +64,7 @@ Because role transitions are **operator-driven** (write `RedundancyRole` in the
## Metrics ## Metrics
`RedundancyMetrics` in `src/ZB.MOM.WW.OtOpcUa.Admin/Services/RedundancyMetrics.cs` registers the `ZB.MOM.WW.OtOpcUa.Redundancy` meter on the Admin process. Instruments: `RedundancyMetrics` in `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/RedundancyMetrics.cs` registers the `ZB.MOM.WW.OtOpcUa.Redundancy` meter on the Admin process. Instruments:
| Name | Kind | Tags | Description | | Name | Kind | Tags | Description |
|---|---|---|---| |---|---|---|---|
@@ -77,7 +77,7 @@ Admin `Program.cs` wires OpenTelemetry to the Prometheus exporter when `Metrics:
## Real-time notifications (Admin UI) ## Real-time notifications (Admin UI)
`FleetStatusPoller` in `src/ZB.MOM.WW.OtOpcUa.Admin/Hubs/` polls the `ClusterNode` table, records role transitions, updates `RedundancyMetrics.SetClusterCounts`, and pushes a `RoleChanged` SignalR event onto `FleetStatusHub` when a transition is observed. `RedundancyTab.razor` subscribes with `_hub.On<RoleChangedMessage>("RoleChanged", …)` so connected Admin sessions see role swaps the moment they happen. `FleetStatusPoller` in `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Hubs/` polls the `ClusterNode` table, records role transitions, updates `RedundancyMetrics.SetClusterCounts`, and pushes a `RoleChanged` SignalR event onto `FleetStatusHub` when a transition is observed. `RedundancyTab.razor` subscribes with `_hub.On<RoleChangedMessage>("RoleChanged", …)` so connected Admin sessions see role swaps the moment they happen.
## Configuring a redundant pair ## Configuring a redundant pair
@@ -96,7 +96,7 @@ Role swaps, stand-alone promotions, and base-level adjustments all happen throug
## Client-side failover ## Client-side failover
The OtOpcUa Client CLI at `src/ZB.MOM.WW.OtOpcUa.Client.CLI` supports `-F` / `--failover-urls` for automatic client-side failover; for long-running subscriptions the CLI monitors session KeepAlive and reconnects to the next available server, recreating the subscription on the new endpoint. See [`Client.CLI.md`](Client.CLI.md) for the command reference. The OtOpcUa Client CLI at `src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI` supports `-F` / `--failover-urls` for automatic client-side failover; for long-running subscriptions the CLI monitors session KeepAlive and reconnects to the next available server, recreating the subscription on the new endpoint. See [`Client.CLI.md`](Client.CLI.md) for the command reference.
## Depth reference ## Depth reference

View File

@@ -6,7 +6,7 @@ This file covers the engine internals — predicate evaluation, state machine, p
## Definition shape ## Definition shape
`ScriptedAlarmDefinition` (`src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs`) is the runtime contract the engine consumes. The generation-publish path materialises these from the `ScriptedAlarm` + `Script` config tables via `Phase7EngineComposer.ProjectScriptedAlarms`. `ScriptedAlarmDefinition` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs`) is the runtime contract the engine consumes. The generation-publish path materialises these from the `ScriptedAlarm` + `Script` config tables via `Phase7EngineComposer.ProjectScriptedAlarms`.
| Field | Notes | | Field | Notes |
|---|---| |---|---|
@@ -100,26 +100,26 @@ Emissions map into `AlarmEventArgs` as `AlarmType = Kind.ToString()`, `SourceNod
## Composition ## Composition
`Phase7EngineComposer.Compose` (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs`) is the single call site that instantiates the engine. It takes the generation's `Script` / `VirtualTag` / `ScriptedAlarm` rows, the shared `CachedTagUpstreamSource`, an `IAlarmStateStore`, and an `IAlarmHistorianSink`, and returns a `Phase7ComposedSources` the caller owns. When `scriptedAlarms.Count > 0`: `Phase7EngineComposer.Compose` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs`) is the single call site that instantiates the engine. It takes the generation's `Script` / `VirtualTag` / `ScriptedAlarm` rows, the shared `CachedTagUpstreamSource`, an `IAlarmStateStore`, and an `IAlarmHistorianSink`, and returns a `Phase7ComposedSources` the caller owns. When `scriptedAlarms.Count > 0`:
1. `ProjectScriptedAlarms` resolves each row's `PredicateScriptId` against the script dictionary and produces a `ScriptedAlarmDefinition` list. Unknown or disabled scripts throw immediately — the DB publish guarantees referential integrity but this is a belt-and-braces check. 1. `ProjectScriptedAlarms` resolves each row's `PredicateScriptId` against the script dictionary and produces a `ScriptedAlarmDefinition` list. Unknown or disabled scripts throw immediately — the DB publish guarantees referential integrity but this is a belt-and-braces check.
2. A `ScriptedAlarmEngine` is constructed with the upstream source, the store, a shared `ScriptLoggerFactory` keyed to `scripts-*.log`, and the root Serilog logger. 2. A `ScriptedAlarmEngine` is constructed with the upstream source, the store, a shared `ScriptLoggerFactory` keyed to `scripts-*.log`, and the root Serilog logger.
3. `alarmEngine.OnEvent` is wired to `RouteToHistorianAsync`, which projects each emission into an `AlarmHistorianEvent` and enqueues it on the sink. Fire-and-forget — the SQLite store-and-forward sink is already non-blocking. 3. `alarmEngine.OnEvent` is wired to `RouteToHistorianAsync`, which projects each emission into an `AlarmHistorianEvent` and enqueues it on the sink. Fire-and-forget — the SQLite store-and-forward sink is already non-blocking.
4. `LoadAsync(alarmDefs)` runs synchronously on the startup thread: it compiles every predicate, subscribes to the union of predicate inputs and message-template tokens, seeds the value cache, loads persisted state, re-derives `ActiveState` from a fresh predicate evaluation, and starts the 5s shelving timer. Compile failures are aggregated into one `InvalidOperationException` so operators see every bad predicate in one startup log line rather than one at a time. 4. `LoadAsync(alarmDefs)` runs synchronously on the startup thread: it compiles every predicate, subscribes to the union of predicate inputs and message-template tokens, seeds the value cache, loads persisted state, re-derives `ActiveState` from a fresh predicate evaluation, and starts the 5s shelving timer. Compile failures are aggregated into one `InvalidOperationException` so operators see every bad predicate in one startup log line rather than one at a time.
5. A `ScriptedAlarmSource` is created for the event stream, and a `ScriptedAlarmReadable` (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs`) is created for OPC UA variable reads on the alarm's active-state node (task #245) — unknown alarm ids return `BadNodeIdUnknown` rather than silently reading `false`. 5. A `ScriptedAlarmSource` is created for the event stream, and a `ScriptedAlarmReadable` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs`) is created for OPC UA variable reads on the alarm's active-state node (task #245) — unknown alarm ids return `BadNodeIdUnknown` rather than silently reading `false`.
Both engine and source are added to `Phase7ComposedSources.Disposables`, which `Phase7Composer` disposes on server shutdown. Both engine and source are added to `Phase7ComposedSources.Disposables`, which `Phase7Composer` disposes on server shutdown.
## Key source files ## Key source files
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs` — orchestrator, cascade wiring, shelving timer, `OnEvent` emission - `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs` — orchestrator, cascade wiring, shelving timer, `OnEvent` emission
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs``IAlarmSource` adapter over the engine - `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs``IAlarmSource` adapter over the engine
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs` — runtime definition record - `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs` — runtime definition record
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs` — pure-function state machine + `TransitionResult` / `EmissionKind` - `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs` — pure-function state machine + `TransitionResult` / `EmissionKind`
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs` — persisted state record + `AlarmComment` audit entry + `ShelvingState` - `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs` — persisted state record + `AlarmComment` audit entry + `ShelvingState`
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmPredicateContext.cs` — script-side `ScriptContext` (read-only, write rejected) - `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmPredicateContext.cs` — script-side `ScriptContext` (read-only, write rejected)
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmTypes.cs``AlarmKind` + the four Part 9 enums - `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmTypes.cs``AlarmKind` + the four Part 9 enums
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs``{path}` placeholder resolver - `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs``{path}` placeholder resolver
- `src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs` — persistence contract + `InMemoryAlarmStateStore` default - `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs` — persistence contract + `InMemoryAlarmStateStore` default
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — composition, config-row projection, historian routing - `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — composition, config-row projection, historian routing
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs``IReadable` adapter exposing `ActiveState` to OPC UA variable reads - `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs``IReadable` adapter exposing `ActiveState` to OPC UA variable reads

View File

@@ -7,9 +7,9 @@ with a distinct runtime and install surface:
| Process | Project | Runtime | Platform | Responsibility | | Process | Project | Runtime | Platform | Responsibility |
|---|---|---|---|---| |---|---|---|---|---|
| **OtOpcUa Server** | `src/ZB.MOM.WW.OtOpcUa.Server` | .NET 10 | x64 | Hosts the OPC UA endpoint; loads every driver in-process (Modbus, S7, AbCip, AbLegacy, TwinCAT, FOCAS, OPC UA Client, Galaxy via mxaccessgw); exposes `/healthz`. | | **OtOpcUa Server** | `src/Server/ZB.MOM.WW.OtOpcUa.Server` | .NET 10 | x64 | Hosts the OPC UA endpoint; loads every driver in-process (Modbus, S7, AbCip, AbLegacy, TwinCAT, FOCAS, OPC UA Client, Galaxy via mxaccessgw); exposes `/healthz`. |
| **OtOpcUa Admin** | `src/ZB.MOM.WW.OtOpcUa.Admin` | .NET 10 (ASP.NET Core / Blazor Server) | x64 | Operator UI for Config DB editing + fleet status, SignalR hubs (`FleetStatusHub`, `AlertHub`), Prometheus `/metrics`. | | **OtOpcUa Admin** | `src/Server/ZB.MOM.WW.OtOpcUa.Admin` | .NET 10 (ASP.NET Core / Blazor Server) | x64 | Operator UI for Config DB editing + fleet status, SignalR hubs (`FleetStatusHub`, `AlertHub`), Prometheus `/metrics`. |
| **OtOpcUa Wonderware Historian** *(optional)* | `src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware` | .NET Framework 4.8 | x86 (32-bit) | Out-of-process sidecar exposing the Wonderware Historian SDK over a named pipe. Required only when `Historian:Wonderware:Enabled=true` in `appsettings.json`. | | **OtOpcUa Wonderware Historian** *(optional)* | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware` | .NET Framework 4.8 | x86 (32-bit) | Out-of-process sidecar exposing the Wonderware Historian SDK over a named pipe. Required only when `Historian:Wonderware:Enabled=true` in `appsettings.json`. |
Galaxy access uses a separately-installed **mxaccessgw** running out Galaxy access uses a separately-installed **mxaccessgw** running out
of a sibling repo (`c:\Users\dohertj2\Desktop\mxaccessgw\`) — see of a sibling repo (`c:\Users\dohertj2\Desktop\mxaccessgw\`) — see
@@ -42,9 +42,9 @@ Reads from the same Config DB the Server writes to.
When `Historian:Wonderware:Enabled=true`, the Server speaks to a When `Historian:Wonderware:Enabled=true`, the Server speaks to a
sidecar that wraps the Wonderware Historian SDK (which is .NET sidecar that wraps the Wonderware Historian SDK (which is .NET
Framework only). The pipe IPC contract is in Framework only). The pipe IPC contract is in
`src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/Contracts/` `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/Contracts/`
and the sidecar's pipe handler lives at and the sidecar's pipe handler lives at
`src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Pipe/`. `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Pipe/`.
Install via the `-InstallWonderwareHistorian` switch on Install via the `-InstallWonderwareHistorian` switch on
`scripts/install/Install-Services.ps1`. `scripts/install/Install-Services.ps1`.

View File

@@ -97,13 +97,13 @@ Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md) Option B,
`ITagUpstreamSource` and `IHistoryWriter` are the two ports the engine requires from its host. Both live in `Core.VirtualTags`. In the Server process: `ITagUpstreamSource` and `IHistoryWriter` are the two ports the engine requires from its host. Both live in `Core.VirtualTags`. In the Server process:
- **`CachedTagUpstreamSource`** (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/CachedTagUpstreamSource.cs`) implements the interface (and the parallel `Core.ScriptedAlarms.ITagUpstreamSource` — identical shape, distinct namespace). A `ConcurrentDictionary<path, DataValueSnapshot>` cache. `Push(path, snapshot)` updates the cache and fans out synchronously to every observer. Reads of never-pushed paths return `BadNodeIdUnknown` quality (`UpstreamNotConfigured = 0x80340000`). - **`CachedTagUpstreamSource`** (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/CachedTagUpstreamSource.cs`) implements the interface (and the parallel `Core.ScriptedAlarms.ITagUpstreamSource` — identical shape, distinct namespace). A `ConcurrentDictionary<path, DataValueSnapshot>` cache. `Push(path, snapshot)` updates the cache and fans out synchronously to every observer. Reads of never-pushed paths return `BadNodeIdUnknown` quality (`UpstreamNotConfigured = 0x80340000`).
- **`DriverSubscriptionBridge`** (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/DriverSubscriptionBridge.cs`) feeds the cache. For each registered `ISubscribable` driver it batches a single `SubscribeAsync` for every fullRef the script graph references, installs an `OnDataChange` handler that translates driver-opaque fullRefs back to UNS paths via a reverse map, and pushes each delta into `CachedTagUpstreamSource`. Unsubscribes on dispose. The bridge suppresses `OTOPCUA0001` (the Roslyn analyzer that requires `ISubscribable` callers to go through `CapabilityInvoker`) on the documented basis that this is a lifecycle wiring, not per-evaluation hot path. - **`DriverSubscriptionBridge`** (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/DriverSubscriptionBridge.cs`) feeds the cache. For each registered `ISubscribable` driver it batches a single `SubscribeAsync` for every fullRef the script graph references, installs an `OnDataChange` handler that translates driver-opaque fullRefs back to UNS paths via a reverse map, and pushes each delta into `CachedTagUpstreamSource`. Unsubscribes on dispose. The bridge suppresses `OTOPCUA0001` (the Roslyn analyzer that requires `ISubscribable` callers to go through `CapabilityInvoker`) on the documented basis that this is a lifecycle wiring, not per-evaluation hot path.
- **`IHistoryWriter`** — no production implementation is currently wired for virtual tags; `VirtualTagEngine` gets `NullHistoryWriter` by default from `Phase7EngineComposer`. - **`IHistoryWriter`** — no production implementation is currently wired for virtual tags; `VirtualTagEngine` gets `NullHistoryWriter` by default from `Phase7EngineComposer`.
## Composition ## Composition
`Phase7Composer` (`src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs`) is an `IAsyncDisposable` injected into `OpcUaServerService`: `Phase7Composer` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs`) is an `IAsyncDisposable` injected into `OpcUaServerService`:
1. `PrepareAsync(generationId, ct)` — called after the bootstrap generation loads and before `OpcUaApplicationHost.StartAsync`. Reads the `Script` / `VirtualTag` / `ScriptedAlarm` rows for that generation from the config DB (`OtOpcUaConfigDbContext`). Empty-config fast path returns `Phase7ComposedSources.Empty`. 1. `PrepareAsync(generationId, ct)` — called after the bootstrap generation loads and before `OpcUaApplicationHost.StartAsync`. Reads the `Script` / `VirtualTag` / `ScriptedAlarm` rows for that generation from the config DB (`OtOpcUaConfigDbContext`). Empty-config fast path returns `Phase7ComposedSources.Empty`.
2. Constructs a `CachedTagUpstreamSource` + hands it to `Phase7EngineComposer.Compose`. 2. Constructs a `CachedTagUpstreamSource` + hands it to `Phase7EngineComposer.Compose`.
@@ -117,26 +117,26 @@ Definition reload on config publish: `VirtualTagEngine.Load` is re-entrant — a
## Key source files ## Key source files
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptContext.cs` — abstract `ctx` API scripts see - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptContext.cs` — abstract `ctx` API scripts see
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptGlobals.cs` — generic globals wrapper naming the field `ctx` - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptGlobals.cs` — generic globals wrapper naming the field `ctx`
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs` — assembly allow-list + imports - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs` — assembly allow-list + imports
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs` — post-compile semantic deny-list - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs` — post-compile semantic deny-list
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptEvaluator.cs` — three-step compile pipeline - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptEvaluator.cs` — three-step compile pipeline
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/TimedScriptEvaluator.cs` — 250ms default timeout wrapper - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/TimedScriptEvaluator.cs` — 250ms default timeout wrapper
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/CompiledScriptCache.cs` — SHA-256-keyed compile cache - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/CompiledScriptCache.cs` — SHA-256-keyed compile cache
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs` — static `ctx.GetTag` / `ctx.SetVirtualTag` inference - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs` — static `ctx.GetTag` / `ctx.SetVirtualTag` inference
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLoggerFactory.cs` — per-script Serilog logger - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLoggerFactory.cs` — per-script Serilog logger
- `src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogCompanionSink.cs` — error mirror to main log - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogCompanionSink.cs` — error mirror to main log
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagDefinition.cs` — per-tag config record - `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagDefinition.cs` — per-tag config record
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagContext.cs` — evaluation-scoped `ctx` - `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagContext.cs` — evaluation-scoped `ctx`
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs` — Kahn topo-sort + iterative Tarjan SCC - `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs` — Kahn topo-sort + iterative Tarjan SCC
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs` — load / evaluate / cascade pipeline - `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs` — load / evaluate / cascade pipeline
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/TimerTriggerScheduler.cs` — periodic re-evaluation - `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/TimerTriggerScheduler.cs` — periodic re-evaluation
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ITagUpstreamSource.cs` — driver-tag read + subscribe port - `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ITagUpstreamSource.cs` — driver-tag read + subscribe port
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/IHistoryWriter.cs` — historize sink port + `NullHistoryWriter` - `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/IHistoryWriter.cs` — historize sink port + `NullHistoryWriter`
- `src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs``IReadable` + `ISubscribable` adapter - `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs``IReadable` + `ISubscribable` adapter
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/CachedTagUpstreamSource.cs` — production `ITagUpstreamSource` - `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/CachedTagUpstreamSource.cs` — production `ITagUpstreamSource`
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/DriverSubscriptionBridge.cs` — driver `ISubscribable` → cache feed - `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/DriverSubscriptionBridge.cs` — driver `ISubscribable` → cache feed
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — row projection + engine instantiation - `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — row projection + engine instantiation
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` — lifecycle host: load rows, compose, wire bridge - `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` — lifecycle host: load rows, compose, wire bridge
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs``SelectReadable` + `IsWriteAllowedBySource` dispatch kernel - `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs``SelectReadable` + `IsWriteAllowedBySource` dispatch kernel

View File

@@ -4,7 +4,7 @@ Coverage map + gap inventory for the AB Legacy (PCCC) driver — SLC 500 /
MicroLogix / PLC-5 / LogixPccc-mode. MicroLogix / PLC-5 / LogixPccc-mode.
**TL;DR:** Docker integration-test scaffolding lives at **TL;DR:** Docker integration-test scaffolding lives at
`tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/` (task #224), `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/` (task #224),
reusing the AB CIP `ab_server` image in PCCC mode with per-family reusing the AB CIP `ab_server` image in PCCC mode with per-family
compose profiles (`slc500` / `micrologix` / `plc5`). Scaffold passes compose profiles (`slc500` / `micrologix` / `plc5`). Scaffold passes
the skip-when-absent contract cleanly. **Wire-level round-trip against the skip-when-absent contract cleanly. **Wire-level round-trip against
@@ -19,7 +19,7 @@ via `FakeAbLegacyTag` still carry the contract coverage.
**Integration layer** (task #224, scaffolded with a known ab_server **Integration layer** (task #224, scaffolded with a known ab_server
gap): gap):
`tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/` with `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/` with
`AbLegacyServerFixture` (TCP-probes `localhost:44818`) + three smoke `AbLegacyServerFixture` (TCP-probes `localhost:44818`) + three smoke
tests (parametric read across families, SLC500 write-then-read). Reuses tests (parametric read across families, SLC500 write-then-read). Reuses
the AB CIP `otopcua-ab-server:libplctag-release` image via a relative the AB CIP `otopcua-ab-server:libplctag-release` image via a relative
@@ -27,7 +27,7 @@ the AB CIP `otopcua-ab-server:libplctag-release` image via a relative
`--plc` flags. See `Docker/README.md` §Known limitations for the `--plc` flags. See `Docker/README.md` §Known limitations for the
ab_server PCCC round-trip gap + resolution paths. ab_server PCCC round-trip gap + resolution paths.
**Unit layer**: `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/` is **Unit layer**: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/` is
still the primary coverage. All tests tagged `[Trait("Category", "Unit")]`. still the primary coverage. All tests tagged `[Trait("Category", "Unit")]`.
The driver accepts `IAbLegacyTagFactory` via ctor DI; every test The driver accepts `IAbLegacyTagFactory` via ctor DI; every test
supplies a `FakeAbLegacyTag`. supplies a `FakeAbLegacyTag`.
@@ -113,16 +113,16 @@ cover the common ones but uncommon ones (`R` counters, `S` status files,
## Key fixture / config files ## Key fixture / config files
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyServerFixture.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyServerFixture.cs`
— TCP probe + skip attributes + env-var parsing — TCP probe + skip attributes + env-var parsing
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyReadSmokeTests.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyReadSmokeTests.cs`
— wire-level smoke tests; pass against the ab_server Docker fixture — wire-level smoke tests; pass against the ab_server Docker fixture
with `AB_LEGACY_COMPOSE_PROFILE` set to the running container with `AB_LEGACY_COMPOSE_PROFILE` set to the running container
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml`
— compose profiles reusing AB CIP Dockerfile — compose profiles reusing AB CIP Dockerfile
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md`
— known-limitations write-up + resolution paths — known-limitations write-up + resolution paths
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs`
in-process fake + factory in-process fake + factory
- `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — scope remarks - `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — scope remarks
at the top of the file at the top of the file

View File

@@ -126,7 +126,7 @@ behaviours from unit-only to end-to-end wire-level coverage:
```powershell ```powershell
$env:AB_SERVER_PROFILE = 'emulate' $env:AB_SERVER_PROFILE = 'emulate'
$env:AB_SERVER_ENDPOINT = '<emulate-pc-ip>:44818' $env:AB_SERVER_ENDPOINT = '<emulate-pc-ip>:44818'
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
``` ```
With `AB_SERVER_PROFILE` unset or `abserver`, the Emulate-tier classes With `AB_SERVER_PROFILE` unset or `abserver`, the Emulate-tier classes
@@ -154,7 +154,7 @@ via `AbServerProfileGate.SkipUnless`):
#177 ALMD projection, verified against the real ALMD instruction #177 ALMD projection, verified against the real ALMD instruction
**Required Studio 5000 project state** is documented in **Required Studio 5000 project state** is documented in
[`tests/…/AbCip.IntegrationTests/LogixProject/README.md`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md); [`tests/…/AbCip.IntegrationTests/LogixProject/README.md`](../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md);
the `.L5X` export lands there once the Emulate PC is on-site + the the `.L5X` export lands there once the Emulate PC is on-site + the
project is authored. project is authored.
@@ -201,16 +201,16 @@ options are roughly:
See also: See also:
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerFixture.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerFixture.cs`
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfile.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfile.cs`
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs`
`AB_SERVER_PROFILE` tier gate `AB_SERVER_PROFILE` tier gate
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cs`
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/` — ab_server - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/` — ab_server
image + compose image + compose
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/` — Logix - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/` — Logix
Emulate tier tests Emulate tier tests
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md`
— L5X project state the Emulate tier expects — L5X project state the Emulate tier expects
- `docs/v2/test-data-sources.md` §2 — the broader test-data-source picking - `docs/v2/test-data-sources.md` §2 — the broader test-data-source picking
rationale this fixture slots into rationale this fixture slots into

View File

@@ -6,7 +6,7 @@ Coverage map + gap inventory for the FANUC FOCAS2 CNC driver.
via the pure-managed [`Focas.Wire`](https://github.com/Ladder99/focas-mock/tree/main/dotnet/Focas.Wire) via the pure-managed [`Focas.Wire`](https://github.com/Ladder99/focas-mock/tree/main/dotnet/Focas.Wire)
client. Integration tests run the managed driver end-to-end against the client. Integration tests run the managed driver end-to-end against the
vendored `focas-mock` Python server (at vendored `focas-mock` Python server (at
[`tests/.../Docker/focas-mock/`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/focas-mock/VENDORED.md)) [`tests/.../Docker/focas-mock/`](../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/focas-mock/VENDORED.md))
whose native FOCAS Ethernet responder is verified PDU-by-PDU against the whose native FOCAS Ethernet responder is verified PDU-by-PDU against the
real `fwlibe64.dll`. real `fwlibe64.dll`.
@@ -21,7 +21,7 @@ but the mock's wire responder covers every FOCAS call OtOpcUa issues.
### Unit layer (no container required) ### Unit layer (no container required)
`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/` uses `FakeFocasClient` `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/` uses `FakeFocasClient`
injected via `IFocasClientFactory`: injected via `IFocasClientFactory`:
- `FocasCapabilityTests` — data-type mapping (PMC bit / byte / word / - `FocasCapabilityTests` — data-type mapping (PMC bit / byte / word /
@@ -48,7 +48,7 @@ message naming the CNC series + documented limit.
### Integration layer (mock only, no CNC, no shim) ### Integration layer (mock only, no CNC, no shim)
`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/` drives the `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/` drives the
managed `FocasDriver` end-to-end. A single gate: managed `FocasDriver` end-to-end. A single gate:
**Docker compose up** — tests skip when the TCP probe to **Docker compose up** — tests skip when the TCP probe to
@@ -120,10 +120,10 @@ stays as the CI quality gate.
```powershell ```powershell
# 1) Start the mock on a chosen profile. # 1) Start the mock on a chosen profile.
docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml up -d docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml up -d
# 2) Run the tests. No shim build, no DLL copy — the driver dials the mock directly. # 2) Run the tests. No shim build, no DLL copy — the driver dials the mock directly.
dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/ dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/
``` ```
Or use `scripts/integration/run-focas.ps1` which wraps compose up / test Or use `scripts/integration/run-focas.ps1` which wraps compose up / test
@@ -131,20 +131,20 @@ Or use `scripts/integration/run-focas.ps1` which wraps compose up / test
## Key fixture / config files ## Key fixture / config files
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/focas-mock/` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/focas-mock/`
— vendored `focas-mock` Python source + Dockerfile — vendored `focas-mock` Python source + Dockerfile
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml`
— per-series compose profiles — per-series compose profiles
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/FocasSimFixture.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/FocasSimFixture.cs`
— collection fixture + mock admin API client — collection fixture + mock admin API client
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/FixedTreePopulatesTests.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/FixedTreePopulatesTests.cs`
— fixed-tree end-to-end tests — fixed-tree end-to-end tests
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/WireBackendTests.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Series/WireBackendTests.cs`
— pure-wire-backend end-to-end tests — pure-wire-backend end-to-end tests
- `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FakeFocasClient.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FakeFocasClient.cs`
in-process unit fake in-process unit fake
- `src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs` — the - `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs` — the
managed wire client backing production deployments managed wire client backing production deployments
- `src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs` - `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs`
per-series range validator per-series range validator
- `docs/v2/focas-version-matrix.md` — authoritative range reference - `docs/v2/focas-version-matrix.md` — authoritative range reference

View File

@@ -19,7 +19,7 @@ protocol using the documented command IDs. Writes return
| Project | Target | Role | | Project | Target | Role |
|---------|--------|------| |---------|--------|------|
| `src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/` | net10.0 | In-process driver — hosts `WireFocasClient` which speaks FOCAS2 over TCP directly | | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/` | net10.0 | In-process driver — hosts `WireFocasClient` which speaks FOCAS2 over TCP directly |
Previous `Driver.FOCAS.Host` / `Driver.FOCAS.Shared` Tier-C split has been Previous `Driver.FOCAS.Host` / `Driver.FOCAS.Shared` Tier-C split has been
retired — the managed wire client removes the native-crash blast radius retired — the managed wire client removes the native-crash blast radius
@@ -205,10 +205,10 @@ latency spike once per cadence.
## Testing ## Testing
- **Unit tests** — `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/` cover the - **Unit tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/` cover the
driver surface via `FakeFocasClient`. Includes the alarm-projection raise / driver surface via `FakeFocasClient`. Includes the alarm-projection raise /
clear diffing tests. clear diffing tests.
- **Integration tests** — `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/` - **Integration tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/`
hold the Docker simulator scaffold; see hold the Docker simulator scaffold; see
[`docs/v2/implementation/focas-wire-protocol.md`](../v2/implementation/focas-wire-protocol.md) [`docs/v2/implementation/focas-wire-protocol.md`](../v2/implementation/focas-wire-protocol.md)
for what the simulator emits vs. real CNC behaviour. for what the simulator emits vs. real CNC behaviour.

View File

@@ -49,7 +49,7 @@ for the v2-final architecture.
## Project Layout ## Project Layout
The driver ships as a single project: `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/` (.NET 10, AnyCPU). Sub-folders: The driver ships as a single project: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/` (.NET 10, AnyCPU). Sub-folders:
| Folder | Role | | Folder | Role |
|--------|------| |--------|------|
@@ -93,7 +93,7 @@ Full per-field descriptions live in `Config/GalaxyDriverOptions.cs`. The full JS
## Testing ## Testing
- **Unit tests**: `tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/` — fakes the gateway gRPC surface; covers Browse, Runtime, Health, and Config in isolation. - **Unit tests**: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/` — fakes the gateway gRPC surface; covers Browse, Runtime, Health, and Config in isolation.
- **Parity rig + dev-rig walkthrough**: see [docs/v2/Galaxy.ParityRig.md](../v2/Galaxy.ParityRig.md). The rig stands up a real `mxaccessgw` against a live Galaxy and exercises the full read / write / subscribe / rediscover path. - **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). - **Performance + soak**: see [docs/v2/Galaxy.Performance.md](../v2/Galaxy.Performance.md).

View File

@@ -13,7 +13,7 @@ shaped (neither is a Modbus-side concept).
- **Simulator**: `pymodbus` (Python, BSD) launched as a pinned Docker - **Simulator**: `pymodbus` (Python, BSD) launched as a pinned Docker
container at container at
`tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/`. `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/`.
Docker is the only supported launch path. Docker is the only supported launch path.
- **Lifecycle**: `ModbusSimulatorFixture` (collection-scoped) TCP-probes - **Lifecycle**: `ModbusSimulatorFixture` (collection-scoped) TCP-probes
`localhost:5020` on first use. `MODBUS_SIM_ENDPOINT` env var overrides the `localhost:5020` on first use. `MODBUS_SIM_ENDPOINT` env var overrides the
@@ -115,9 +115,9 @@ Not a Modbus concept. Driver doesn't implement `IAlarmSource` or
## Key fixture / config files ## Key fixture / config files
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs`
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs`
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Mitsubishi/MitsubishiProfile.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Mitsubishi/MitsubishiProfile.cs`
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500Profile.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500Profile.cs`
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/`
Dockerfile + compose + per-family JSON profiles Dockerfile + compose + per-family JSON profiles

View File

@@ -18,7 +18,7 @@ image (follow-up).
## What the fixture is ## What the fixture is
**Integration layer** (task #215): **Integration layer** (task #215):
`tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/` stands up `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/` stands up
`mcr.microsoft.com/iotedge/opc-plc:2.14.10` via `Docker/docker-compose.yml` `mcr.microsoft.com/iotedge/opc-plc:2.14.10` via `Docker/docker-compose.yml`
on `opc.tcp://localhost:50000`. `OpcPlcFixture` probes the port at on `opc.tcp://localhost:50000`. `OpcPlcFixture` probes the port at
collection init + skips tests with a clear message when the container's collection init + skips tests with a clear message when the container's
@@ -30,7 +30,7 @@ resets on each spin-up), `--alm` (alarm simulation for IAlarmSource
follow-up coverage), `--pn=50000` (port). follow-up coverage), `--pn=50000` (port).
**Unit layer**: **Unit layer**:
`tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` is still the primary `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` is still the primary
coverage. Tests inject fakes through the driver's construction path; the coverage. Tests inject fakes through the driver's construction path; the
OPCFoundation.NetStandard `Session` surface is wrapped behind an interface OPCFoundation.NetStandard `Session` surface is wrapped behind an interface
the tests mock. the tests mock.
@@ -137,7 +137,7 @@ ConditionType events (non-base `BaseEventType`) is not verified.
The easiest win here is to **wire the client driver tests against this The easiest win here is to **wire the client driver tests against this
repo's own server**. The integration test project repo's own server**. The integration test project
`tests/ZB.MOM.WW.OtOpcUa.Server.Tests/OpcUaServerIntegrationTests.cs` `tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/OpcUaServerIntegrationTests.cs`
already stands up a real OPC UA server on a non-default port with a seeded already stands up a real OPC UA server on a non-default port with a seeded
FakeDriver. An `OpcUaClientLiveLoopbackTests` that connects the client FakeDriver. An `OpcUaClientLiveLoopbackTests` that connects the client
driver to that server would give: driver to that server would give:
@@ -161,10 +161,10 @@ Beyond that:
## Key fixture / config files ## Key fixture / config files
- `tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` — unit tests with - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` — unit tests with
mocked `Session` mocked `Session`
- `src/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs` — ctor + - `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs` — ctor +
session-factory seam tests mock through session-factory seam tests mock through
- `tests/ZB.MOM.WW.OtOpcUa.Server.Tests/OpcUaServerIntegrationTests.cs` - `tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/OpcUaServerIntegrationTests.cs`
the server-side integration harness a future loopback client test could the server-side integration harness a future loopback client test could
piggyback on piggyback on

View File

@@ -1,6 +1,6 @@
# Drivers # Drivers
OtOpcUa is a multi-driver OPC UA server. The Core (`ZB.MOM.WW.OtOpcUa.Core` + `Core.Abstractions` + `Server`) owns the OPC UA stack, address space, session/security/subscription machinery, resilience pipeline, and namespace kinds (Equipment + SystemPlatform). Drivers plug in through **capability interfaces** defined in `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/`: OtOpcUa is a multi-driver OPC UA server. The Core (`ZB.MOM.WW.OtOpcUa.Core` + `Core.Abstractions` + `Server`) owns the OPC UA stack, address space, session/security/subscription machinery, resilience pipeline, and namespace kinds (Equipment + SystemPlatform). Drivers plug in through **capability interfaces** defined in `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/`:
- `IDriver` — lifecycle (`InitializeAsync`, `ReinitializeAsync`, `ShutdownAsync`, `GetHealth`) - `IDriver` — lifecycle (`InitializeAsync`, `ReinitializeAsync`, `ShutdownAsync`, `GetHealth`)
- `IReadable` / `IWritable` — one-shot reads and writes - `IReadable` / `IWritable` — one-shot reads and writes
@@ -14,7 +14,7 @@ OtOpcUa is a multi-driver OPC UA server. The Core (`ZB.MOM.WW.OtOpcUa.Core` + `C
Each driver opts into only the capabilities it supports. Every async capability call at the Server dispatch layer goes through `CapabilityInvoker` (`Core/Resilience/CapabilityInvoker.cs`), which wraps it in a Polly pipeline keyed on `(DriverInstanceId, HostName, DriverCapability)`. The `OTOPCUA0001` analyzer enforces the wrap at build time. Drivers themselves never depend on Polly; they just implement the capability interface and let the Core wrap it. Each driver opts into only the capabilities it supports. Every async capability call at the Server dispatch layer goes through `CapabilityInvoker` (`Core/Resilience/CapabilityInvoker.cs`), which wraps it in a Polly pipeline keyed on `(DriverInstanceId, HostName, DriverCapability)`. The `OTOPCUA0001` analyzer enforces the wrap at build time. Drivers themselves never depend on Polly; they just implement the capability interface and let the Core wrap it.
Driver type metadata is registered at startup in `DriverTypeRegistry` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs`). The registry records each type's allowed namespace kinds (`Equipment` / `SystemPlatform` / `Simulated`), its JSON Schema for `DriverConfig` / `DeviceConfig` / `TagConfig` columns, and its stability tier per [docs/v2/driver-stability.md](../v2/driver-stability.md). Driver type metadata is registered at startup in `DriverTypeRegistry` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs`). The registry records each type's allowed namespace kinds (`Equipment` / `SystemPlatform` / `Simulated`), its JSON Schema for `DriverConfig` / `DeviceConfig` / `TagConfig` columns, and its stability tier per [docs/v2/driver-stability.md](../v2/driver-stability.md).
## Ground-truth driver list ## Ground-truth driver list

View File

@@ -14,7 +14,7 @@ session types, PUT/GET-disabled enforcement — all need real hardware.
## What the fixture is ## What the fixture is
**Integration layer** (task #216): **Integration layer** (task #216):
`tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/` stands up a `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/` stands up a
python-snap7 `Server` via `Docker/docker-compose.yml --profile s7_1500` python-snap7 `Server` via `Docker/docker-compose.yml --profile s7_1500`
on `localhost:1102` (pinned `python:3.12-slim-bookworm` base + on `localhost:1102` (pinned `python:3.12-slim-bookworm` base +
`python-snap7>=2.0`). Docker is the only supported launch path. `python-snap7>=2.0`). Docker is the only supported launch path.
@@ -24,7 +24,7 @@ clear message when unreachable (matches the pymodbus pattern).
+ seeds DB/MB bytes at declared offsets; seeds are typed (`u16` / `i16` + seeds DB/MB bytes at declared offsets; seeds are typed (`u16` / `i16`
/ `i32` / `f32` / `bool` / `ascii` for S7 STRING). / `i32` / `f32` / `bool` / `ascii` for S7 STRING).
**Unit layer**: `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` covers **Unit layer**: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` covers
everything the wire-level suite doesn't — address parsing, error everything the wire-level suite doesn't — address parsing, error
branches, probe-loop contract. All tests tagged branches, probe-loop contract. All tests tagged
`[Trait("Category", "Unit")]`. `[Trait("Category", "Unit")]`.
@@ -115,7 +115,7 @@ from field deployments, not from the test suite.
## Key fixture / config files ## Key fixture / config files
- `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` — unit tests only, no harness - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` — unit tests only, no harness
- `src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs` — ctor takes - `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs` — ctor takes
`IS7ClientFactory` which tests fake; docstring lines 8-20 note the deferred `IS7ClientFactory` which tests fake; docstring lines 8-20 note the deferred
integration fixture integration fixture

View File

@@ -3,7 +3,7 @@
Coverage map + gap inventory for the Beckhoff TwinCAT ADS driver. Coverage map + gap inventory for the Beckhoff TwinCAT ADS driver.
**TL;DR:** Integration-test suite lives at **TL;DR:** Integration-test suite lives at
`tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/`. `TwinCATXarFixture` `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/`. `TwinCATXarFixture`
probes TCP 48898 on an operator-supplied runtime; the suite runs **14 probes TCP 48898 on an operator-supplied runtime; the suite runs **14
`[TwinCATFact]` methods + one 16-case `[TwinCATTheory]` = 30 test cases** end-to-end `[TwinCATFact]` methods + one 16-case `[TwinCATTheory]` = 30 test cases** end-to-end
through the real ADS stack when the runtime is reachable, skips cleanly through the real ADS stack when the runtime is reachable, skips cleanly
@@ -18,7 +18,7 @@ also contract-tested rigorously at the unit layer.
## What the fixture is ## What the fixture is
**Integration layer**: `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/` **Integration layer**: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/`
`TwinCATXarFixture` TCP-probes ADS port 48898 on the host supplied by `TwinCATXarFixture` TCP-probes ADS port 48898 on the host supplied by
`TWINCAT_TARGET_HOST` (defaults to `localhost`) + requires `TWINCAT_TARGET_HOST` (defaults to `localhost`) + requires
`TWINCAT_TARGET_NETID` (AmsNetId of the runtime). Optionally takes `TWINCAT_TARGET_NETID` (AmsNetId of the runtime). Optionally takes
@@ -29,7 +29,7 @@ kernel scheduler, so the runtime stays operator-managed.
gate on `[TwinCATFact]` / `[TwinCATTheory]` and skip cleanly when gate on `[TwinCATFact]` / `[TwinCATTheory]` and skip cleanly when
`TWINCAT_TARGET_NETID` is unset or the probe fails. `TWINCAT_TARGET_NETID` is unset or the probe fails.
**Unit layer**: `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/` remains the **Unit layer**: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/` remains the
primary contract coverage. `FakeTwinCATClient` fakes the primary contract coverage. `FakeTwinCATClient` fakes the
`AddDeviceNotification` flow so tests can trigger callbacks without a running `AddDeviceNotification` flow so tests can trigger callbacks without a running
runtime. runtime.
@@ -174,13 +174,13 @@ license-rotation automation, and a dedicated lab IPC.
## Key fixture / config files ## Key fixture / config files
- `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATXarFixture.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATXarFixture.cs`
— TCP probe + skip-attributes + env-var parsing — TCP probe + skip-attributes + env-var parsing
- `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCAT3SmokeTests.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCAT3SmokeTests.cs`
— wire-level test suite (14 `[TwinCATFact]` + 16-case `[TwinCATTheory]`) — wire-level test suite (14 `[TwinCATFact]` + 16-case `[TwinCATTheory]`)
- `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCatProject/README.md` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCatProject/README.md`
— project spec + VM setup + license-rotation notes — project spec + VM setup + license-rotation notes
- `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/FakeTwinCATClient.cs` - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/FakeTwinCATClient.cs`
in-process fake with the notification-fire harness in-process fake with the notification-fire harness
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs` — ctor is - `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs` — ctor is
`(TwinCATDriverOptions, string driverInstanceId, ITwinCATClientFactory? = null)` `(TwinCATDriverOptions, string driverInstanceId, ITwinCATClientFactory? = null)`

View File

@@ -332,7 +332,7 @@ depends on a specific A-PR — see the sequencing matrix below.
**Files:** **Files:**
- `src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\EventPump.cs:160` - `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\EventPump.cs:160`
current `Dispatch(MxEvent ev)` returns early for any non-`OnDataChange` current `Dispatch(MxEvent ev)` returns early for any non-`OnDataChange`
family. Add a branch: family. Add a branch:
```csharp ```csharp
@@ -350,7 +350,7 @@ depends on a specific A-PR — see the sequencing matrix below.
numeric severity (250 / 500 / 700 / 900 ladder per v1's numeric severity (250 / 500 / 700 / 900 ladder per v1's
`AlarmTracking.md`). `AlarmTracking.md`).
**Tests** (`tests\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests\Runtime\`): **Tests** (`tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests\Runtime\`):
- `EventPumpAlarmTests` — feed three synthetic MxEvents (raise / ack / - `EventPumpAlarmTests` — feed three synthetic MxEvents (raise / ack /
clear); assert each fires `OnAlarmEvent` on the driver with correct clear); assert each fires `OnAlarmEvent` on the driver with correct
@@ -365,7 +365,7 @@ dispatch).
**Files:** **Files:**
- `src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\GalaxyDriver.cs:28` — extend the - `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\GalaxyDriver.cs:28` — extend the
class declaration: class declaration:
```csharp ```csharp
public sealed class GalaxyDriver public sealed class GalaxyDriver
@@ -402,7 +402,7 @@ dispatch).
**Files:** **Files:**
- `src\ZB.MOM.WW.OtOpcUa.Server\OpcUa\DriverNodeManager.cs` — when - `src\Server\ZB.MOM.WW.OtOpcUa.Server\OpcUa\DriverNodeManager.cs` — when
registering an `AlarmConditionState` for a Galaxy variable, check registering an `AlarmConditionState` for a Galaxy variable, check
whether the driver is `IAlarmSource`. If yes, prefer the whether the driver is `IAlarmSource`. If yes, prefer the
`OnAlarmEvent`-driven path; the value-driven sub-attribute path `OnAlarmEvent`-driven path; the value-driven sub-attribute path
@@ -435,7 +435,7 @@ for the sidecar-side work; B.4 is the lmxopcua-side consumer.
**Files:** **Files:**
- New `src\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client\SidecarAlarmHistorianWriter.cs` - New `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client\SidecarAlarmHistorianWriter.cs`
implementing `IAlarmHistorianWriter`. Sends batches over the existing implementing `IAlarmHistorianWriter`. Sends batches over the existing
named-pipe IPC using the **already-defined** named-pipe IPC using the **already-defined**
`WriteAlarmEventsRequest` / `WriteAlarmEventsReply` contracts at `WriteAlarmEventsRequest` / `WriteAlarmEventsReply` contracts at
@@ -498,7 +498,7 @@ storage) plug into the same path.
## Track C — historian sidecar wires the dormant write path ## Track C — historian sidecar wires the dormant write path
The Wonderware historian sidecar at The Wonderware historian sidecar at
`src\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\` is a separately `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\` is a separately
deployable Windows service (NSSM-wrapped) that already loads deployable Windows service (NSSM-wrapped) that already loads
`aahClientManaged` x64 and serves a named-pipe IPC for read operations. `aahClientManaged` x64 and serves a named-pipe IPC for read operations.
The `WriteAlarmEvents` IPC slot is defined but unwired (`Program.cs:57` The `WriteAlarmEvents` IPC slot is defined but unwired (`Program.cs:57`
@@ -508,7 +508,7 @@ completes that slot. Two PRs in the sidecar + one consumer-side PR
### PR C.1 — sidecar: AahClientManagedAlarmEventWriter ### PR C.1 — sidecar: AahClientManagedAlarmEventWriter
**Files** (`src\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\Backend\`): **Files** (`src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\Backend\`):
1. New `AahClientManagedAlarmEventWriter.cs` implementing the existing 1. New `AahClientManagedAlarmEventWriter.cs` implementing the existing
`IAlarmEventWriter` interface (defined in `Ipc\HistorianFrameHandler.cs:242`). `IAlarmEventWriter` interface (defined in `Ipc\HistorianFrameHandler.cs:242`).
@@ -523,7 +523,7 @@ completes that slot. Two PRs in the sidecar + one consumer-side PR
gating — no new TCP work needed; the same session that serves gating — no new TCP work needed; the same session that serves
reads can issue writes too. reads can issue writes too.
**Tests** (`tests\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests\`): **Tests** (`tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests\`):
- Outcome-mapping table: every documented MxStatus on alarm-write → - Outcome-mapping table: every documented MxStatus on alarm-write →
expected `HistorianWriteOutcome`. expected `HistorianWriteOutcome`.
@@ -534,7 +534,7 @@ completes that slot. Two PRs in the sidecar + one consumer-side PR
### PR C.2 — sidecar: wire IAlarmEventWriter into Program.cs ### PR C.2 — sidecar: wire IAlarmEventWriter into Program.cs
**Files** (`src\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\Program.cs`): **Files** (`src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\Program.cs`):
1. Build an `AahClientManagedAlarmEventWriter` next to the existing 1. Build an `AahClientManagedAlarmEventWriter` next to the existing
`BuildHistorian()` call. `BuildHistorian()` call.
@@ -858,9 +858,9 @@ during the original install (see commit `80104ca`).
output): output):
```powershell ```powershell
$repo = "C:\Users\dohertj2\Desktop\lmxopcua" $repo = "C:\Users\dohertj2\Desktop\lmxopcua"
dotnet publish "$repo\src\ZB.MOM.WW.OtOpcUa.Server" ` dotnet publish "$repo\src\Server\ZB.MOM.WW.OtOpcUa.Server" `
-c Release -o "C:\publish\lmxopcua" -c Release -o "C:\publish\lmxopcua"
dotnet publish "$repo\src\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware" ` dotnet publish "$repo\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware" `
-c Release -o "C:\publish\lmxopcua\WonderwareHistorian" -c Release -o "C:\publish\lmxopcua\WonderwareHistorian"
``` ```
@@ -1086,19 +1086,19 @@ needed); land B.4 last and only after end-of-epic gate is green.
**lmxopcua — Galaxy driver + server (Track B):** **lmxopcua — Galaxy driver + server (Track B):**
- `src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\EventPump.cs` (B.1) - `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\EventPump.cs` (B.1)
- `src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\MxAccessSeverityMapper.cs` *(new — B.1)* - `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\MxAccessSeverityMapper.cs` *(new — B.1)*
- `src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\IGalaxyAlarmAcknowledger.cs` *(new — B.2)* - `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\IGalaxyAlarmAcknowledger.cs` *(new — B.2)*
- `src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\GatewayGalaxyAlarmAcknowledger.cs` *(new — B.2)* - `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\GatewayGalaxyAlarmAcknowledger.cs` *(new — B.2)*
- `src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\GalaxyDriver.cs` (B.2) - `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\GalaxyDriver.cs` (B.2)
- `src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\GalaxyDriverFactory.cs` (B.2) - `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\GalaxyDriverFactory.cs` (B.2)
- `src\ZB.MOM.WW.OtOpcUa.Server\OpcUa\DriverNodeManager.cs` (B.3) - `src\Server\ZB.MOM.WW.OtOpcUa.Server\OpcUa\DriverNodeManager.cs` (B.3)
- `src\ZB.MOM.WW.OtOpcUa.Server\Alarms\AlarmConditionService.cs` (B.3) - `src\Server\ZB.MOM.WW.OtOpcUa.Server\Alarms\AlarmConditionService.cs` (B.3)
- `src\ZB.MOM.WW.OtOpcUa.Server\Phase7\Phase7Composer.cs` (B.4) - `src\Server\ZB.MOM.WW.OtOpcUa.Server\Phase7\Phase7Composer.cs` (B.4)
- `src\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client\SidecarAlarmHistorianWriter.cs` *(new — B.4)* - `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client\SidecarAlarmHistorianWriter.cs` *(new — B.4)*
- `tests\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests\Runtime\` (B.1, B.2) - `tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests\Runtime\` (B.1, B.2)
- `tests\ZB.MOM.WW.OtOpcUa.Server.Tests\Alarms\` (B.3) - `tests\Server\ZB.MOM.WW.OtOpcUa.Server.Tests\Alarms\` (B.3)
- `tests\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests\` (B.4 — new tests) - `tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests\` (B.4 — new tests)
- `docs\drivers\Galaxy.md` (B.5) - `docs\drivers\Galaxy.md` (B.5)
- `docs\AlarmTracking.md` *(new — B.5)* - `docs\AlarmTracking.md` *(new — B.5)*
- `docs\v1\AlarmTracking.md` (B.5 — banner update) - `docs\v1\AlarmTracking.md` (B.5 — banner update)
@@ -1106,10 +1106,10 @@ needed); land B.4 last and only after end-of-epic gate is green.
**lmxopcua — Wonderware historian sidecar (Track C):** **lmxopcua — Wonderware historian sidecar (Track C):**
- `src\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\Backend\AahClientManagedAlarmEventWriter.cs` *(new — C.1)* - `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\Backend\AahClientManagedAlarmEventWriter.cs` *(new — C.1)*
- `src\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\Program.cs` (C.2 — wire writer) - `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\Program.cs` (C.2 — wire writer)
- `scripts\install\Install-Services.ps1` (C.2 — env-var toggle for write-enable) - `scripts\install\Install-Services.ps1` (C.2 — env-var toggle for write-enable)
- `tests\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests\` (C.1 — outcome mapping + batch + cluster failover) - `tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests\` (C.1 — outcome mapping + batch + cluster failover)
**lmxopcua — deployment refresh (Track D):** **lmxopcua — deployment refresh (Track D):**
@@ -1144,21 +1144,21 @@ needed); land B.4 last and only after end-of-epic gate is green.
**lmxopcua — OPC UA client refresh (Track E.7):** **lmxopcua — OPC UA client refresh (Track E.7):**
- `src\ZB.MOM.WW.OtOpcUa.Core.Abstractions\AlarmEventArgs.cs` (extend) - `src\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\AlarmEventArgs.cs` (extend)
- `src\ZB.MOM.WW.OtOpcUa.Server\OpcUa\DriverNodeManager.cs` (Part 9 field population) - `src\Server\ZB.MOM.WW.OtOpcUa.Server\OpcUa\DriverNodeManager.cs` (Part 9 field population)
- `src\ZB.MOM.WW.OtOpcUa.Client.Shared\Models\AlarmEventArgs.cs` (DTO mirror) - `src\Client\ZB.MOM.WW.OtOpcUa.Client.Shared\Models\AlarmEventArgs.cs` (DTO mirror)
- `src\ZB.MOM.WW.OtOpcUa.Client.CLI\Commands\AlarmsCommand.cs` (verbose / json flags) - `src\Client\ZB.MOM.WW.OtOpcUa.Client.CLI\Commands\AlarmsCommand.cs` (verbose / json flags)
- `src\ZB.MOM.WW.OtOpcUa.Client.UI\ViewModels\AlarmEventViewModel.cs` - `src\Client\ZB.MOM.WW.OtOpcUa.Client.UI\ViewModels\AlarmEventViewModel.cs`
- `src\ZB.MOM.WW.OtOpcUa.Client.UI\ViewModels\AlarmsViewModel.cs` - `src\Client\ZB.MOM.WW.OtOpcUa.Client.UI\ViewModels\AlarmsViewModel.cs`
- `src\ZB.MOM.WW.OtOpcUa.Client.UI\Views\AlarmsView.axaml` (+ `.cs`) - `src\Client\ZB.MOM.WW.OtOpcUa.Client.UI\Views\AlarmsView.axaml` (+ `.cs`)
- `src\ZB.MOM.WW.OtOpcUa.Client.UI\Views\AckAlarmWindow.axaml` (+ `.cs`) - `src\Client\ZB.MOM.WW.OtOpcUa.Client.UI\Views\AckAlarmWindow.axaml` (+ `.cs`)
- `docs\Client.CLI.md` (alarms section examples) - `docs\Client.CLI.md` (alarms section examples)
- `docs\Client.UI.md` (Show-details toggle description) - `docs\Client.UI.md` (Show-details toggle description)
- `docs\reqs\ClientRequirements.md` (extend AlarmEventArgs contract) - `docs\reqs\ClientRequirements.md` (extend AlarmEventArgs contract)
- `docs\AlarmTracking.md` (B.5 — cross-link client examples) - `docs\AlarmTracking.md` (B.5 — cross-link client examples)
- `tests\ZB.MOM.WW.OtOpcUa.Client.Shared.Tests\` (DTO round-trip) - `tests\Client\ZB.MOM.WW.OtOpcUa.Client.Shared.Tests\` (DTO round-trip)
- `tests\ZB.MOM.WW.OtOpcUa.Client.CLI.Tests\` (flag behaviour) - `tests\Client\ZB.MOM.WW.OtOpcUa.Client.CLI.Tests\` (flag behaviour)
- `tests\ZB.MOM.WW.OtOpcUa.Client.UI.Tests\` (view-model bindings) - `tests\Client\ZB.MOM.WW.OtOpcUa.Client.UI.Tests\` (view-model bindings)
Total: ~10 source files added/modified in mxaccessgw server/worker Total: ~10 source files added/modified in mxaccessgw server/worker
side; ~14 in lmxopcua server/driver side; ~3 in the historian sidecar; side; ~14 in lmxopcua server/driver side; ~3 in the historian sidecar;

View File

@@ -95,7 +95,7 @@ The Server accepts three OPC UA identity-token types:
| Token | Handler | Notes | | Token | Handler | Notes |
|---|---|---| |---|---|---|
| Anonymous | `IUserAuthenticator.AuthenticateAsync(username: "", password: "")` | Refused in strict mode unless explicit anonymous grants exist; allowed in lax mode for backward compatibility. | | Anonymous | `IUserAuthenticator.AuthenticateAsync(username: "", password: "")` | Refused in strict mode unless explicit anonymous grants exist; allowed in lax mode for backward compatibility. |
| UserName/Password | `LdapUserAuthenticator` (`src/ZB.MOM.WW.OtOpcUa.Server/Security/LdapUserAuthenticator.cs`) | LDAP bind + group lookup; resolved `LdapGroups` flow into the session's identity bearer (`ILdapGroupsBearer`). | | UserName/Password | `LdapUserAuthenticator` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/LdapUserAuthenticator.cs`) | LDAP bind + group lookup; resolved `LdapGroups` flow into the session's identity bearer (`ILdapGroupsBearer`). |
| X.509 Certificate | Stack-level acceptance + role mapping via CN | X.509 identity carries `AuthenticatedUser` + read roles; finer-grain authorization happens through the data-plane ACLs. | | X.509 Certificate | Stack-level acceptance + role mapping via CN | X.509 identity carries `AuthenticatedUser` + read roles; finer-grain authorization happens through the data-plane ACLs. |
### LDAP bind flow (`LdapUserAuthenticator`) ### LDAP bind flow (`LdapUserAuthenticator`)
@@ -164,7 +164,7 @@ ACLs are evaluated against the UNS path:
ClusterId → Namespace → UnsArea → UnsLine → Equipment → Tag ClusterId → Namespace → UnsArea → UnsLine → Equipment → Tag
``` ```
Each level can carry `NodeAcl` rows (`src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs`) that grant a permission bundle to a set of `LdapGroups`. Each level can carry `NodeAcl` rows (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs`) that grant a permission bundle to a set of `LdapGroups`.
### Permission flags ### Permission flags
@@ -196,7 +196,7 @@ The three Write tiers map to Galaxy's v1 `SecurityClassification` — `FreeAcces
### Evaluator — `PermissionTrie` ### Evaluator — `PermissionTrie`
`src/ZB.MOM.WW.OtOpcUa.Core/Authorization/`: `src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/`:
| Class | Role | | Class | Role |
|---|---| |---|---|
@@ -209,7 +209,7 @@ The three Write tiers map to Galaxy's v1 `SecurityClassification` — `FreeAcces
### Dispatch gate — `AuthorizationGate` ### Dispatch gate — `AuthorizationGate`
`src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` bridges the OPC UA stack's `ISystemContext.UserIdentity` to the evaluator. `DriverNodeManager` holds exactly one reference to it and calls `IsAllowed(identity, OpcUaOperation.*, NodeScope)` on every Read, Write, HistoryRead, Browse, Subscribe, AckAlarm, Call path. A false return short-circuits the dispatch with `BadUserAccessDenied`. `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` bridges the OPC UA stack's `ISystemContext.UserIdentity` to the evaluator. `DriverNodeManager` holds exactly one reference to it and calls `IsAllowed(identity, OpcUaOperation.*, NodeScope)` on every Read, Write, HistoryRead, Browse, Subscribe, AckAlarm, Call path. A false return short-circuits the dispatch with `BadUserAccessDenied`.
Key properties: Key properties:
@@ -219,7 +219,7 @@ Key properties:
### Probe-this-permission (Admin UI) ### Probe-this-permission (Admin UI)
`PermissionProbeService` (`src/ZB.MOM.WW.OtOpcUa.Admin/Services/PermissionProbeService.cs`) lets an operator ask "if a user with groups X, Y, Z asked to do operation O on node N, would it succeed?" The answer is rendered in the AclsTab "Probe" dialog — same evaluator, same trie, so the Admin UI answer and the live Server answer cannot disagree. `PermissionProbeService` (`src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/PermissionProbeService.cs`) lets an operator ask "if a user with groups X, Y, Z asked to do operation O on node N, would it succeed?" The answer is rendered in the AclsTab "Probe" dialog — same evaluator, same trie, so the Admin UI answer and the live Server answer cannot disagree.
### Full model ### Full model
@@ -235,7 +235,7 @@ Per decision #150 control-plane roles are **deliberately independent of data-pla
### Roles ### Roles
`src/ZB.MOM.WW.OtOpcUa.Admin/Services/AdminRoles.cs`: `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/AdminRoles.cs`:
| Role | Capabilities | | Role | Capabilities |
|---|---| |---|---|
@@ -255,17 +255,17 @@ Razor pages and API endpoints gate with `[Authorize(Policy = "CanEdit")]` / `"Ca
### Role grant source ### Role grant source
Admin reads `LdapGroupRoleMapping` rows from the Config DB (`src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs`) — the same pattern as the data-plane `NodeAcl` but scoped to Admin roles + (optionally) cluster scope for multi-site fleets. The `RoleGrants.razor` page lets FleetAdmins edit these mappings without leaving the UI. Admin reads `LdapGroupRoleMapping` rows from the Config DB (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs`) — the same pattern as the data-plane `NodeAcl` but scoped to Admin roles + (optionally) cluster scope for multi-site fleets. The `RoleGrants.razor` page lets FleetAdmins edit these mappings without leaving the UI.
--- ---
## OTOPCUA0001 Analyzer — Compile-Time Guard ## OTOPCUA0001 Analyzer — Compile-Time Guard
Per-capability resilience (retry, timeout, circuit-breaker, bulkhead) is applied by `CapabilityInvoker` in `src/ZB.MOM.WW.OtOpcUa.Core/Resilience/`. A driver-capability call made **outside** the invoker bypasses resilience entirely — which in production looks like inconsistent timeouts, un-wrapped retries, and unbounded blocking. Per-capability resilience (retry, timeout, circuit-breaker, bulkhead) is applied by `CapabilityInvoker` in `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/`. A driver-capability call made **outside** the invoker bypasses resilience entirely — which in production looks like inconsistent timeouts, un-wrapped retries, and unbounded blocking.
`OTOPCUA0001` (Roslyn analyzer at `src/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs`) fires as a compile-time **warning** when an `async`/`Task`-returning method on one of the seven guarded capability interfaces (`IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider`) is invoked **outside** a lambda passed to `CapabilityInvoker.ExecuteAsync` / `ExecuteWriteAsync` / `AlarmSurfaceInvoker.*`. The analyzer walks up the syntax tree from the call site, finds any enclosing invoker invocation, and verifies the call lives transitively inside that invocation's anonymous-function argument — a sibling pattern (do the call, then invoke `ExecuteAsync` on something unrelated nearby) does not satisfy the rule. `OTOPCUA0001` (Roslyn analyzer at `src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs`) fires as a compile-time **warning** when an `async`/`Task`-returning method on one of the seven guarded capability interfaces (`IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider`) is invoked **outside** a lambda passed to `CapabilityInvoker.ExecuteAsync` / `ExecuteWriteAsync` / `AlarmSurfaceInvoker.*`. The analyzer walks up the syntax tree from the call site, finds any enclosing invoker invocation, and verifies the call lives transitively inside that invocation's anonymous-function argument — a sibling pattern (do the call, then invoke `ExecuteAsync` on something unrelated nearby) does not satisfy the rule.
Five xUnit-v3 + Shouldly tests at `tests/ZB.MOM.WW.OtOpcUa.Analyzers.Tests` cover the common fail/pass shapes + the sibling-pattern regression guard. Five xUnit-v3 + Shouldly tests at `tests/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers.Tests` cover the common fail/pass shapes + the sibling-pattern regression guard.
The rule is intentionally scoped to async surfaces — pure in-memory accessors like `IHostConnectivityProbe.GetHostStatuses()` return synchronously and do not require the invoker wrap. The rule is intentionally scoped to async surfaces — pure in-memory accessors like `IHostConnectivityProbe.GetHostStatuses()` return synchronously and do not require the invoker wrap.

View File

@@ -8,7 +8,7 @@
> See [docs/AlarmTracking.md](../AlarmTracking.md) for the v2 final > See [docs/AlarmTracking.md](../AlarmTracking.md) for the v2 final
> architecture — that is the document to read for current behaviour. > 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. Alarm surfacing is an optional driver capability exposed via `IAlarmSource` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs`). Drivers whose backends have an alarm concept implement it — today: Galaxy (MXAccess alarms), FOCAS (CNC alarms), OPC UA Client (A&C events from the upstream server). Modbus / S7 / AB CIP / AB Legacy / TwinCAT do not implement the interface and the feature is simply absent from their subtrees.
## IAlarmSource surface ## IAlarmSource surface
@@ -25,7 +25,7 @@ The driver fires `OnAlarmEvent` for every transition (`Active`, `Acknowledged`,
## AlarmSurfaceInvoker ## AlarmSurfaceInvoker
`AlarmSurfaceInvoker` (`src/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs`) wraps the three mutating surfaces through `CapabilityInvoker`: `AlarmSurfaceInvoker` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs`) wraps the three mutating surfaces through `CapabilityInvoker`:
- `SubscribeAlarmsAsync` / `UnsubscribeAlarmsAsync` run through the `DriverCapability.AlarmSubscribe` pipeline — retries apply under the tier configuration. - `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. - `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.
@@ -81,7 +81,7 @@ Distinct from the live `IAlarmSource` stream and the Part 9 `AlarmConditionState
### `IAlarmHistorianSink` ### `IAlarmHistorianSink`
`src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs` defines the intake contract: `src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs` defines the intake contract:
```csharp ```csharp
Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken); Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken);
@@ -90,11 +90,11 @@ 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`. `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. The sink scope is defined to span every alarm source (plan decision #15: scripted, Galaxy-native, AB CIP ALMD, any future `IAlarmSource`), gated per-alarm by a `HistorizeToAveva` toggle on the producer. Today only `Phase7EngineComposer.RouteToHistorianAsync` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs`) is wired — it subscribes to `ScriptedAlarmEngine.OnEvent` and marshals each emission into `AlarmHistorianEvent`. Galaxy-native alarms continue to reach AVEVA Historian via the driver's direct `aahClientManaged` path and do not flow through the sink; the AB CIP ALMD path remains unwired pending a producer-side integration.
### `SqliteStoreAndForwardSink` ### `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. Default production implementation (`src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs`). A local SQLite queue absorbs every `EnqueueAsync` synchronously; a background `Timer` drains batches asynchronously to an `IAlarmHistorianWriter` so operator actions are never blocked on historian reachability.
Queue schema (single table `Queue`): `RowId PK autoincrement`, `AlarmId`, `EnqueuedUtc`, `PayloadJson` (serialized `AlarmHistorianEvent`), `AttemptCount`, `LastAttemptUtc`, `LastError`, `DeadLettered` (bool), plus `IX_Queue_Drain (DeadLettered, RowId)`. Default capacity `1_000_000` non-dead-lettered rows; oldest rows evict with a WARN log past the cap. 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.
@@ -114,23 +114,23 @@ Dead-letter retention defaults to 30 days (plan decision #21). `PurgeAgedDeadLet
### Composition and writer resolution ### 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`. `Phase7Composer.ResolveHistorianSink` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs`) scans the registered drivers for one that implements `IAlarmHistorianWriter`. Today that is `GalaxyProxyDriver` via `GalaxyHistorianWriter` (`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/Ipc/GalaxyHistorianWriter.cs`), which forwards batches over the Galaxy.Host pipe to the `aahClientManaged` alarm schema. When a writer is found, a `SqliteStoreAndForwardSink` is instantiated against `%ProgramData%/OtOpcUa/alarm-historian-queue.db` with a 2 s drain tick and the writer attached. When no driver provides a writer the fallback is the DI-registered `NullAlarmHistorianSink` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Program.cs`), which silently discards and reports `HistorianDrainState.Disabled`.
### Status and observability ### 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`. `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. The Admin UI `/alarms/historian` page surfaces this through `HistorianDiagnosticsService` (`src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/HistorianDiagnosticsService.cs`), which also exposes `TryRetryDeadLettered` — it calls through to `SqliteStoreAndForwardSink.RetryDeadLettered` when the live sink is the SQLite implementation and returns 0 otherwise.
## Key source files ## Key source files
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs` — capability contract + `AlarmEventArgs` - `src/Core/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/Core/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/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs``CapturingBuilder` + alarm forwarder
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs``VariableHandle.MarkAsAlarmCondition` + `ConditionSink` - `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs``VariableHandle.MarkAsAlarmCondition` + `ConditionSink`
- `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/Alarms/GalaxyAlarmTracker.cs` — Galaxy-specific alarm-event production - `src/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/Core/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/Core/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/Server/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/Server/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 - `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/HistorianDiagnosticsService.cs` — Admin UI `/alarms/historian` status + retry-dead-lettered operator action

View File

@@ -17,7 +17,7 @@ The rule: if the setting describes *how the process connects to the rest of the
Each of the three processes (Server, Admin, Galaxy.Host) reads its own `appsettings.json` plus environment overrides. Each of the three processes (Server, Admin, Galaxy.Host) reads its own `appsettings.json` plus environment overrides.
### OtOpcUa Server — `src/ZB.MOM.WW.OtOpcUa.Server/appsettings.json` ### OtOpcUa Server — `src/Server/ZB.MOM.WW.OtOpcUa.Server/appsettings.json`
Bootstrap-only. `Program.cs` reads four top-level sections: Bootstrap-only. `Program.cs` reads four top-level sections:
@@ -51,7 +51,7 @@ Minimal example:
} }
``` ```
### OtOpcUa Admin — `src/ZB.MOM.WW.OtOpcUa.Admin/appsettings.json` ### OtOpcUa Admin — `src/Server/ZB.MOM.WW.OtOpcUa.Admin/appsettings.json`
| Section | Purpose | | Section | Purpose |
|---|---| |---|---|
@@ -73,7 +73,7 @@ Standard .NET config layering applies: `appsettings.{Environment}.json`, then en
## Authoritative configuration (Config DB) ## Authoritative configuration (Config DB)
The Config DB is the single source of truth for every setting that a v1 deployment used to carry in `appsettings.json` as driver-specific state. `OtOpcUaConfigDbContext` (`src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs`) is the EF Core context used by both the Admin writer and every Server reader. The Config DB is the single source of truth for every setting that a v1 deployment used to carry in `appsettings.json` as driver-specific state. `OtOpcUaConfigDbContext` (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs`) is the EF Core context used by both the Admin writer and every Server reader.
### Top-level sections operators touch ### Top-level sections operators touch
@@ -103,7 +103,7 @@ Old generations are retained; rollback is "publish older generation as new". `Co
### Offline cache ### Offline cache
Each Server process caches the last-seen published generation in `Node:LocalCachePath` via LiteDB (`LiteDbConfigCache` in `src/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/`). The cache lets a node start without the central DB reachable; once the DB comes back, `NodeBootstrap` syncs to the current generation. Each Server process caches the last-seen published generation in `Node:LocalCachePath` via LiteDB (`LiteDbConfigCache` in `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/`). The cache lets a node start without the central DB reachable; once the DB comes back, `NodeBootstrap` syncs to the current generation.
### Full schema reference ### Full schema reference

View File

@@ -1,10 +1,10 @@
# Data Type Mapping # Data Type Mapping
Data-type mapping is driver-defined. Each driver translates its native attribute metadata into two driver-agnostic enums from `Core.Abstractions``DriverDataType` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs`) and `SecurityClassification` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/SecurityClassification.cs`) — and populates the `DriverAttributeInfo` record it hands to `IAddressSpaceBuilder.Variable(...)`. Core doesn't interpret the native types; it trusts the driver's translation. Data-type mapping is driver-defined. Each driver translates its native attribute metadata into two driver-agnostic enums from `Core.Abstractions``DriverDataType` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs`) and `SecurityClassification` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/SecurityClassification.cs`) — and populates the `DriverAttributeInfo` record it hands to `IAddressSpaceBuilder.Variable(...)`. Core doesn't interpret the native types; it trusts the driver's translation.
## DriverDataType → OPC UA built-in type ## DriverDataType → OPC UA built-in type
`DriverNodeManager.MapDataType` (`src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) is the single translation table for every driver: `DriverNodeManager.MapDataType` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs`) is the single translation table for every driver:
| DriverDataType | OPC UA NodeId | | DriverDataType | OPC UA NodeId |
|---|---| |---|---|
@@ -23,8 +23,8 @@ The enum also carries `Int16 / Int64 / UInt16 / UInt32 / UInt64 / Reference` mem
Each driver owns its native → `DriverDataType` translation: Each driver owns its native → `DriverDataType` translation:
- **Galaxy Proxy** — `GalaxyProxyDriver.MapDataType(int mxDataType)` and `MapSecurity(int mxSec)` (inline in `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/GalaxyProxyDriver.cs`). The Galaxy `mx_data_type` integer is sent across the Host↔Proxy pipe and mapped on the Proxy side. Galaxy's full classic 16-entry table (Boolean / Integer / Float / Double / String / Time / ElapsedTime / Reference / Enumeration / Custom / InternationalizedString) is preserved but compressed into the seven-entry `DriverDataType` enum — `ElapsedTime``Float64`, `InternationalizedString``String`, `Reference``Reference`, enumerations → `Int32`. - **Galaxy Proxy** — `GalaxyProxyDriver.MapDataType(int mxDataType)` and `MapSecurity(int mxSec)` (inline in `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/GalaxyProxyDriver.cs`). The Galaxy `mx_data_type` integer is sent across the Host↔Proxy pipe and mapped on the Proxy side. Galaxy's full classic 16-entry table (Boolean / Integer / Float / Double / String / Time / ElapsedTime / Reference / Enumeration / Custom / InternationalizedString) is preserved but compressed into the seven-entry `DriverDataType` enum — `ElapsedTime``Float64`, `InternationalizedString``String`, `Reference``Reference`, enumerations → `Int32`.
- **AB CIP** — `src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDataType.cs` maps CIP tag type codes. - **AB CIP** — `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDataType.cs` maps CIP tag type codes.
- **Modbus** — `src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs` maps register shapes (16-bit signed, 16-bit unsigned, 32-bit float, etc.) including the DirectLogic quirk table in `DirectLogicAddress.cs`. - **Modbus** — `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs` maps register shapes (16-bit signed, 16-bit unsigned, 32-bit float, etc.) including the DirectLogic quirk table in `DirectLogicAddress.cs`.
- **S7 / AB Legacy / TwinCAT / FOCAS / OPC UA Client** — each has its own inline mapper or `*DataType.cs` file per the same pattern. - **S7 / AB Legacy / TwinCAT / FOCAS / OPC UA Client** — each has its own inline mapper or `*DataType.cs` file per the same pattern.
The driver's mapping is authoritative — when a field type is ambiguous (a `LREAL` that could be bit-reinterpreted, a BCD counter, a string of a particular encoding), the driver decides the exposed OPC UA shape. The driver's mapping is authoritative — when a field type is ambiguous (a `LREAL` that could be bit-reinterpreted, a BCD counter, a string of a particular encoding), the driver decides the exposed OPC UA shape.
@@ -35,7 +35,7 @@ The driver's mapping is authoritative — when a field type is ambiguous (a `LRE
## SecurityClassification — metadata, not ACL ## SecurityClassification — metadata, not ACL
`SecurityClassification` is driver-reported metadata only. Drivers never enforce write permissions themselves — the classification flows into the Server project where `WriteAuthzPolicy.IsAllowed(classification, userRoles)` (`src/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs`) gates the write against the session's LDAP-derived roles, and (Phase 6.2) the `AuthorizationGate` + permission trie apply on top. This is the "ACL at server layer" invariant documented in `docs/security.md`. `SecurityClassification` is driver-reported metadata only. Drivers never enforce write permissions themselves — the classification flows into the Server project where `WriteAuthzPolicy.IsAllowed(classification, userRoles)` (`src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs`) gates the write against the session's LDAP-derived roles, and (Phase 6.2) the `AuthorizationGate` + permission trie apply on top. This is the "ACL at server layer" invariant documented in `docs/security.md`.
The classification values mirror the v1 Galaxy model so existing Galaxy galaxies keep their published semantics: The classification values mirror the v1 Galaxy model so existing Galaxy galaxies keep their published semantics:
@@ -57,9 +57,9 @@ Drivers whose backend has no notion of classification (Modbus, most PLCs) defaul
## Key source files ## Key source files
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs` — driver-agnostic type enum - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs` — driver-agnostic type enum
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/SecurityClassification.cs` — write-authz tier metadata - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/SecurityClassification.cs` — write-authz tier metadata
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs` — per-attribute descriptor - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs` — per-attribute descriptor
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs``MapDataType` translation - `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs``MapDataType` translation
- `src/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs` — classification-to-role policy - `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs` — classification-to-role policy
- Per-driver mappers in each `Driver.*` project - Per-driver mappers in each `Driver.*` project

View File

@@ -1,6 +1,6 @@
# Historical Data Access # Historical Data Access
OPC UA HistoryRead is a **per-driver optional capability** in OtOpcUa. The Core dispatches HistoryRead service calls to the owning driver through the `IHistoryProvider` capability interface (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs`). Drivers that don't implement the interface return `BadHistoryOperationUnsupported` for every history call on their nodes; that is the expected behavior for protocol drivers (Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS) whose wire protocols carry no time-series data. OPC UA HistoryRead is a **per-driver optional capability** in OtOpcUa. The Core dispatches HistoryRead service calls to the owning driver through the `IHistoryProvider` capability interface (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs`). Drivers that don't implement the interface return `BadHistoryOperationUnsupported` for every history call on their nodes; that is the expected behavior for protocol drivers (Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS) whose wire protocols carry no time-series data.
Historian integration is no longer a separate bolt-on assembly, as it was in v1 (`ZB.MOM.WW.LmxOpcUa.Historian.Aveva` plugin). It is now one optional capability any driver can implement. The first implementation is the Galaxy driver's Wonderware Historian integration; OPC UA Client forwards HistoryRead to the upstream server. Every other driver leaves the capability unimplemented and the Core short-circuits history calls on nodes that belong to those drivers. Historian integration is no longer a separate bolt-on assembly, as it was in v1 (`ZB.MOM.WW.LmxOpcUa.Historian.Aveva` plugin). It is now one optional capability any driver can implement. The first implementation is the Galaxy driver's Wonderware Historian integration; OPC UA Client forwards HistoryRead to the upstream server. Every other driver leaves the capability unimplemented and the Core short-circuits history calls on nodes that belong to those drivers.
@@ -26,7 +26,7 @@ Supporting DTOs live alongside the interface in `Core.Abstractions`:
`IHistoryProvider.ReadEventsAsync` is the **pull** path: an OPC UA client calls `HistoryReadEvents` against a notifier node and the driver walks its own backend event store to satisfy the request. The Galaxy driver's implementation reads from AVEVA Historian's event schema via `aahClientManaged`; every other driver leaves the default `NotSupportedException` in place. `IHistoryProvider.ReadEventsAsync` is the **pull** path: an OPC UA client calls `HistoryReadEvents` against a notifier node and the driver walks its own backend event store to satisfy the request. The Galaxy driver's implementation reads from AVEVA Historian's event schema via `aahClientManaged`; every other driver leaves the default `NotSupportedException` in place.
There is also a separate **push** path for persisting alarm transitions from any `IAlarmSource` (and the Phase 7 scripted-alarm engine) into a durable event log, independent of any client HistoryRead call. That path is covered by `IAlarmHistorianSink` + `SqliteStoreAndForwardSink` in `src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/` and is documented in [AlarmTracking.md#alarm-historian-sink](AlarmTracking.md#alarm-historian-sink). The two paths are complementary — the sink populates an external historian's alarm schema; `ReadEventsAsync` reads from whatever event store the driver owns — and share neither interface nor dispatch. There is also a separate **push** path for persisting alarm transitions from any `IAlarmSource` (and the Phase 7 scripted-alarm engine) into a durable event log, independent of any client HistoryRead call. That path is covered by `IAlarmHistorianSink` + `SqliteStoreAndForwardSink` in `src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/` and is documented in [AlarmTracking.md#alarm-historian-sink](AlarmTracking.md#alarm-historian-sink). The two paths are complementary — the sink populates an external historian's alarm schema; `ReadEventsAsync` reads from whatever event store the driver owns — and share neither interface nor dispatch.
## Dispatch through `CapabilityInvoker` ## Dispatch through `CapabilityInvoker`

View File

@@ -20,7 +20,7 @@ For current architecture see:
|---|---| |---|---|
| `AlarmTracking.md` | v1 alarm-tracking flow through the in-process MXAccess client | | `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) | | `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`) | | `DataTypeMapping.md` | Galaxy `mx_data_type` → OPC UA type mapping (still accurate as a reference; the live mapping logic is in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`) |
| `HistoricalDataAccess.md` | v1 IHistoryProvider on the Host side; current path is the server-level HistoryRouter + Wonderware sidecar | | `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 | | `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-Repository.md` | v1 Host-side ZB SQL repository client; the gateway owns this path now |

View File

@@ -1,13 +1,13 @@
# Subscriptions # Subscriptions
Driver-side data-change subscriptions live behind `ISubscribable` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs`). The interface is deliberately mechanism-agnostic: it covers native subscriptions (Galaxy MXAccess advisory, OPC UA monitored items on an upstream server, TwinCAT ADS notifications) and driver-internal polled subscriptions (Modbus, AB CIP, S7, FOCAS). Core sees the same event shape regardless — drivers fire `OnDataChange` and Core dispatches to the matching OPC UA monitored items. Driver-side data-change subscriptions live behind `ISubscribable` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs`). The interface is deliberately mechanism-agnostic: it covers native subscriptions (Galaxy MXAccess advisory, OPC UA monitored items on an upstream server, TwinCAT ADS notifications) and driver-internal polled subscriptions (Modbus, AB CIP, S7, FOCAS). Core sees the same event shape regardless — drivers fire `OnDataChange` and Core dispatches to the matching OPC UA monitored items.
## Driver vs virtual dispatch ## Driver vs virtual dispatch
Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), `DriverNodeManager` routes subscriptions across both driver tags and virtual (scripted) tags through the same `ISubscribable` contract. The per-variable `NodeSourceKind` (registered from `DriverAttributeInfo` at discovery) selects the backend: Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md), `DriverNodeManager` routes subscriptions across both driver tags and virtual (scripted) tags through the same `ISubscribable` contract. The per-variable `NodeSourceKind` (registered from `DriverAttributeInfo` at discovery) selects the backend:
- `NodeSourceKind.Driver` — subscribes via the driver's `ISubscribable`, wrapped by `CapabilityInvoker` (the rest of this doc). - `NodeSourceKind.Driver` — subscribes via the driver's `ISubscribable`, wrapped by `CapabilityInvoker` (the rest of this doc).
- `NodeSourceKind.Virtual` — subscribes via `VirtualTagSource` (`src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs`), which forwards change events emitted by `VirtualTagEngine` as `OnDataChange`. The ref-counting, initial-value, and transfer-restoration behaviour below applies identically. - `NodeSourceKind.Virtual` — subscribes via `VirtualTagSource` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs`), which forwards change events emitted by `VirtualTagEngine` as `OnDataChange`. The ref-counting, initial-value, and transfer-restoration behaviour below applies identically.
Because both kinds expose `ISubscribable`, Core's dispatch, ref-count map, and monitored-item fan-out are unchanged across the source branch. Because both kinds expose `ISubscribable`, Core's dispatch, ref-count map, and monitored-item fan-out are unchanged across the source branch.
@@ -63,7 +63,7 @@ When an OPC UA session is resumed (client reconnect with `TransferSubscriptions`
## Key source files ## Key source files
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs` — capability contract - `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs` — capability contract
- `src/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — pipeline wrapping - `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — pipeline wrapping
- `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Sta/StaPump.cs` — Galaxy STA thread + message pump - `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Sta/StaPump.cs` — Galaxy STA thread + message pump
- Per-driver subscribe implementations in each `Driver.*` project - Per-driver subscribe implementations in each `Driver.*` project

View File

@@ -147,10 +147,10 @@ Dev credentials in this inventory are convenience defaults, not secrets. Change
| Resource | Purpose | Type | Default port | Default credentials | Owner | | Resource | Purpose | Type | Default port | Default credentials | Owner |
|----------|---------|------|--------------|---------------------|-------| |----------|---------|------|--------------|---------------------|-------|
| **Docker Desktop for Windows** | Host for every driver test-fixture simulator (Modbus / AB CIP / S7 / OpcUaClient) + SQL Server | Install | (Hyper-V required; not compatible with TwinCAT runtime — see TwinCAT row below for the workaround) | n/a | Integration host admin | | **Docker Desktop for Windows** | Host for every driver test-fixture simulator (Modbus / AB CIP / S7 / OpcUaClient) + SQL Server | Install | (Hyper-V required; not compatible with TwinCAT runtime — see TwinCAT row below for the workaround) | n/a | Integration host admin |
| **Modbus fixture — `otopcua-pymodbus:3.13.0`** | Modbus driver integration tests | Docker image (local build, see `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/`); 4 compose profiles: `standard` / `dl205` / `mitsubishi` / `s7_1500` | 5020 (non-privileged) | n/a (no auth in protocol) | Developer (per machine) | | **Modbus fixture — `otopcua-pymodbus:3.13.0`** | Modbus driver integration tests | Docker image (local build, see `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/`); 4 compose profiles: `standard` / `dl205` / `mitsubishi` / `s7_1500` | 5020 (non-privileged) | n/a (no auth in protocol) | Developer (per machine) |
| **AB CIP fixture — `otopcua-ab-server:libplctag-release`** | AB CIP driver integration tests | Docker image (multi-stage build of libplctag's `ab_server` from source, pinned to the `release` tag; see `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/`); 4 compose profiles: `controllogix` / `compactlogix` / `micro800` / `guardlogix` | 44818 (CIP / EtherNet/IP) | n/a | Developer (per machine) | | **AB CIP fixture — `otopcua-ab-server:libplctag-release`** | AB CIP driver integration tests | Docker image (multi-stage build of libplctag's `ab_server` from source, pinned to the `release` tag; see `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/`); 4 compose profiles: `controllogix` / `compactlogix` / `micro800` / `guardlogix` | 44818 (CIP / EtherNet/IP) | n/a | Developer (per machine) |
| **S7 fixture — `otopcua-python-snap7:1.0`** | S7 driver integration tests | Docker image (local build, `python-snap7>=2.0`; see `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/`); 1 compose profile: `s7_1500` | 1102 (non-privileged; driver honours `S7DriverOptions.Port`) | n/a | Developer (per machine) | | **S7 fixture — `otopcua-python-snap7:1.0`** | S7 driver integration tests | Docker image (local build, `python-snap7>=2.0`; see `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/`); 1 compose profile: `s7_1500` | 1102 (non-privileged; driver honours `S7DriverOptions.Port`) | n/a | Developer (per machine) |
| **OPC UA Client fixture — `mcr.microsoft.com/iotedge/opc-plc:2.14.10`** | OpcUaClient driver integration tests | Docker image (Microsoft-maintained, pinned; see `tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/`) | 50000 (OPC UA) | Anonymous (`--daa` off); auto-accept certs (`--aa`) | Developer (per machine) | | **OPC UA Client fixture — `mcr.microsoft.com/iotedge/opc-plc:2.14.10`** | OpcUaClient driver integration tests | Docker image (Microsoft-maintained, pinned; see `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/`) | 50000 (OPC UA) | Anonymous (`--daa` off); auto-accept certs (`--aa`) | Developer (per machine) |
| **TwinCAT XAR runtime VM** | TwinCAT ADS testing (per `test-data-sources.md` §5; Beckhoff XAR cannot coexist with Hyper-V on the same OS) | Hyper-V VM with Windows + TwinCAT XAR installed under 7-day renewable trial | 48898 (ADS over TCP) | TwinCAT default route credentials configured per Beckhoff docs | Integration host admin | | **TwinCAT XAR runtime VM** | TwinCAT ADS testing (per `test-data-sources.md` §5; Beckhoff XAR cannot coexist with Hyper-V on the same OS) | Hyper-V VM with Windows + TwinCAT XAR installed under 7-day renewable trial | 48898 (ADS over TCP) | TwinCAT default route credentials configured per Beckhoff docs | Integration host admin |
| **Rockwell Studio 5000 Logix Emulate** | AB CIP golden-box tier — closes UDT / ALMD / AOI / GuardLogix-safety / CompactLogix-ConnectionSize gaps the ab_server simulator can't cover. Loads the L5X project documented at `tests/.../AbCip.IntegrationTests/LogixProject/README.md`. Tests gated on `AB_SERVER_PROFILE=emulate` + `AB_SERVER_ENDPOINT=<ip>:44818`; see `docs/drivers/AbServer-Test-Fixture.md` §Logix Emulate golden-box tier | Windows-only install; **Hyper-V conflict** — can't coexist with Docker Desktop's WSL 2 backend on the same OS, same story as TwinCAT XAR. Runs on a dedicated Windows PC reachable on the LAN | 44818 (CIP / EtherNet/IP) | None required at the CIP layer; Studio 5000 project credentials per Rockwell install | Integration host admin (license + install); Developer (per session — open Emulate, load L5X, click Run) | | **Rockwell Studio 5000 Logix Emulate** | AB CIP golden-box tier — closes UDT / ALMD / AOI / GuardLogix-safety / CompactLogix-ConnectionSize gaps the ab_server simulator can't cover. Loads the L5X project documented at `tests/.../AbCip.IntegrationTests/LogixProject/README.md`. Tests gated on `AB_SERVER_PROFILE=emulate` + `AB_SERVER_ENDPOINT=<ip>:44818`; see `docs/drivers/AbServer-Test-Fixture.md` §Logix Emulate golden-box tier | Windows-only install; **Hyper-V conflict** — can't coexist with Docker Desktop's WSL 2 backend on the same OS, same story as TwinCAT XAR. Runs on a dedicated Windows PC reachable on the LAN | 44818 (CIP / EtherNet/IP) | None required at the CIP layer; Studio 5000 project credentials per Rockwell install | Integration host admin (license + install); Developer (per session — open Emulate, load L5X, click Run) |
| **FOCAS TCP stub** (`Driver.Focas.TestStub`) | FOCAS functional testing (per `test-data-sources.md` §6) | Local .NET 10 console app from this repo | 8193 (FOCAS) | n/a | Developer / integration host (run on demand) | | **FOCAS TCP stub** (`Driver.Focas.TestStub`) | FOCAS functional testing (per `test-data-sources.md` §6) | Local .NET 10 console app from this repo | 8193 (FOCAS) | n/a | Developer / integration host (run on demand) |
@@ -165,10 +165,10 @@ init + skip cleanly when nothing's running.
| Driver | Fixture image | Compose file | Bring up | | Driver | Fixture image | Compose file | Bring up |
|---|---|---|---| |---|---|---|---|
| Modbus | local-build `otopcua-pymodbus:3.13.0` | `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile <standard\|dl205\|mitsubishi\|s7_1500> up -d` | | Modbus | local-build `otopcua-pymodbus:3.13.0` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile <standard\|dl205\|mitsubishi\|s7_1500> up -d` |
| AB CIP | local-build `otopcua-ab-server:libplctag-release` | `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile <controllogix\|compactlogix\|micro800\|guardlogix> up -d` | | AB CIP | local-build `otopcua-ab-server:libplctag-release` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile <controllogix\|compactlogix\|micro800\|guardlogix> up -d` |
| S7 | local-build `otopcua-python-snap7:1.0` | `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile s7_1500 up -d` | | S7 | local-build `otopcua-python-snap7:1.0` | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> --profile s7_1500 up -d` |
| OpcUaClient | `mcr.microsoft.com/iotedge/opc-plc:2.14.10` (pinned) | `tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> up -d` | | OpcUaClient | `mcr.microsoft.com/iotedge/opc-plc:2.14.10` (pinned) | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> up -d` |
First build of a local-build image takes 15 minutes; subsequent runs use First build of a local-build image takes 15 minutes; subsequent runs use
layer cache. `ab_server` is the slowest (multi-stage build clones layer cache. `ab_server` is the slowest (multi-stage build clones

View File

@@ -10,7 +10,7 @@
### Summary ### Summary
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/`. Galaxy (MXAccess) is a **Tier-A in-process driver** that runs in the OtOpcUa server's .NET 10 AnyCPU process and speaks gRPC to a separately installed `mxaccessgw` (sibling repo at `c:\Users\dohertj2\Desktop\mxaccessgw\`). The gateway owns the MXAccess COM apartment, the STA pump, and the Galaxy Repository / Historian SDK on its own host; the driver itself is platform-agnostic and carries no COM or x86 bitness constraint. Project lives at `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/`.
### Capability Surface ### Capability Surface
@@ -29,7 +29,7 @@ History reads + alarm condition tracking now live in the server-layer `IHistoryR
### DriverConfig JSON shape ### DriverConfig JSON shape
Per `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Config/GalaxyDriverOptions.cs`: Per `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Config/GalaxyDriverOptions.cs`:
```jsonc ```jsonc
{ {

View File

@@ -1,7 +1,7 @@
# FOCAS version / capability matrix # FOCAS version / capability matrix
Authoritative source for the per-CNC-series ranges that Authoritative source for the per-CNC-series ranges that
[`FocasCapabilityMatrix`](../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs) [`FocasCapabilityMatrix`](../../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs)
enforces at driver init time. Every row cites the Fanuc FOCAS Developer enforces at driver init time. Every row cites the Fanuc FOCAS Developer
Kit function whose documented input range determines the ceiling. Kit function whose documented input range determines the ceiling.
@@ -122,7 +122,7 @@ matrix: Macro variable #50000 is outside the documented range
## How this matrix stays honest ## How this matrix stays honest
- Every row is covered by a parameterized test in - Every row is covered by a parameterized test in
[`FocasCapabilityMatrixTests.cs`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasCapabilityMatrixTests.cs) [`FocasCapabilityMatrixTests.cs`](../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasCapabilityMatrixTests.cs)
— 46 cases across macro / parameter / PMC-letter / PMC-number — 46 cases across macro / parameter / PMC-letter / PMC-number
boundaries + unknown-series permissiveness + rejection-message boundaries + unknown-series permissiveness + rejection-message
content + case-insensitivity. content + case-insensitivity.

View File

@@ -72,7 +72,7 @@ takes the form of the per-driver test suites + e2e scripts:
- [x] **Integration tests**`Driver.*.IntegrationTests` stands up Docker-hosted simulators (pymodbus, ab_server, python-snap7, opc-plc) at collection init and exercises real wire-level read/write/subscribe/probe per driver. - [x] **Integration tests**`Driver.*.IntegrationTests` stands up Docker-hosted simulators (pymodbus, ab_server, python-snap7, opc-plc) at collection init and exercises real wire-level read/write/subscribe/probe per driver.
- [x] **CLI tests**`Driver.*.Cli.Tests` covers the per-driver test-client CLIs (#249#251). - [x] **CLI tests**`Driver.*.Cli.Tests` covers the per-driver test-client CLIs (#249#251).
- [x] **E2E scripts**`scripts/e2e/test-<driver>.ps1` covers the driver-CLI → PLC → OtOpcUa server → OPC UA client round-trip for all seven drivers + Galaxy; `test-all.ps1` aggregates; README status section (rewritten this session) summarises live-boot evidence. - [x] **E2E scripts**`scripts/e2e/test-<driver>.ps1` covers the driver-CLI → PLC → OtOpcUa server → OPC UA client round-trip for all seven drivers + Galaxy; `test-all.ps1` aggregates; README status section (rewritten this session) summarises live-boot evidence.
- [x] **Factory registration** — all seven factories plus Galaxy register in `src/ZB.MOM.WW.OtOpcUa.Server/Program.cs` inside the `DriverFactoryRegistry` composition; the `DriverInstanceBootstrapper` can materialise any configured row. - [x] **Factory registration** — all seven factories plus Galaxy register in `src/Server/ZB.MOM.WW.OtOpcUa.Server/Program.cs` inside the `DriverFactoryRegistry` composition; the `DriverInstanceBootstrapper` can materialise any configured row.
- [x] **Seed SQL**#210#213 provide per-driver Config DB seed scripts so a fresh Config DB is populatable without Admin UI interaction. - [x] **Seed SQL**#210#213 provide per-driver Config DB seed scripts so a fresh Config DB is populatable without Admin UI interaction.
### Live-boot verification ### Live-boot verification

View File

@@ -49,7 +49,7 @@ Covered by `scripts/compliance/phase-7-compliance.ps1`:
Originally kept out of the capstone so the gate could close cleanly. Each landed as a targeted follow-up PR; audit this session verified them against the repo: Originally kept out of the capstone so the gate could close cleanly. Each landed as a targeted follow-up PR; audit this session verified them against the repo:
- [x] **SealedBootstrap composition root** (task #239) — **CLOSED**. `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` instantiates `VirtualTagEngine` + `ScriptedAlarmEngine` via `Phase7EngineComposer.Compose`, and `SqliteStoreAndForwardSink` in `ResolveHistorianSink` when a registered driver provides `IAlarmHistorianWriter` (today: `GalaxyProxyDriver`). `OpcUaServerService.ExecuteAsync` calls `Phase7Composer.PrepareAsync` then `OpcUaApplicationHost.SetPhase7Sources` **before** `applicationHost.StartAsync` so `OtOpcUaServer` + `DriverNodeManager` capture the `VirtualReadable` / `ScriptedAlarmReadable` at construction. 38 tests green under `tests/ZB.MOM.WW.OtOpcUa.Server.Tests/Phase7/` + `SealedBootstrapIntegrationTests`. The work landed under the label "Phase 7 follow-up #246" and was never re-labelled against #239. - [x] **SealedBootstrap composition root** (task #239) — **CLOSED**. `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` instantiates `VirtualTagEngine` + `ScriptedAlarmEngine` via `Phase7EngineComposer.Compose`, and `SqliteStoreAndForwardSink` in `ResolveHistorianSink` when a registered driver provides `IAlarmHistorianWriter` (today: `GalaxyProxyDriver`). `OpcUaServerService.ExecuteAsync` calls `Phase7Composer.PrepareAsync` then `OpcUaApplicationHost.SetPhase7Sources` **before** `applicationHost.StartAsync` so `OtOpcUaServer` + `DriverNodeManager` capture the `VirtualReadable` / `ScriptedAlarmReadable` at construction. 38 tests green under `tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/Phase7/` + `SealedBootstrapIntegrationTests`. The work landed under the label "Phase 7 follow-up #246" and was never re-labelled against #239.
- [x] **Live OPC UA end-to-end smoke** (task #240) — **CLOSED**. `scripts/e2e/test-phase7-virtualtags.ps1` drives a full Client.CLI read of a driver-sourced input, reads the VirtualTag computed off it, triggers a scripted alarm by writing the trigger value, and subscribes to the alarm condition — all through a running OtOpcUa server. Covered in `scripts/e2e/test-all.ps1` + `scripts/e2e/README.md` matrix. - [x] **Live OPC UA end-to-end smoke** (task #240) — **CLOSED**. `scripts/e2e/test-phase7-virtualtags.ps1` drives a full Client.CLI read of a driver-sourced input, reads the VirtualTag computed off it, triggers a scripted alarm by writing the trigger value, and subscribes to the alarm condition — all through a running OtOpcUa server. Covered in `scripts/e2e/test-all.ps1` + `scripts/e2e/README.md` matrix.
- [x] **sp_ComputeGenerationDiff extension** (task #241) — **CLOSED**. Migration `20260420232000_ExtendComputeGenerationDiffWithPhase7.cs` extends the stored proc to emit Script / VirtualTag / ScriptedAlarm sections alongside the existing NodeAcl / Tag / Equipment / DriverInstance / Namespace output. Admin DiffViewer picks them up through its existing section-plugin architecture (Phase 6.4 Stream C). - [x] **sp_ComputeGenerationDiff extension** (task #241) — **CLOSED**. Migration `20260420232000_ExtendComputeGenerationDiffWithPhase7.cs` extends the stored proc to emit Script / VirtualTag / ScriptedAlarm sections alongside the existing NodeAcl / Tag / Equipment / DriverInstance / Namespace output. Admin DiffViewer picks them up through its existing section-plugin architecture (Phase 6.4 Stream C).

View File

@@ -142,7 +142,7 @@ itself is verifiable without Fwlib32 actually being called:
assert rejection. assert rejection.
- **Fwlib32 integration itself**: still untestable without hardware. - **Fwlib32 integration itself**: still untestable without hardware.
When a real CNC becomes available, the smoke tests already When a real CNC becomes available, the smoke tests already
scaffolded in `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/` scaffolded in `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/`
run against it via `FOCAS_ENDPOINT`. run against it via `FOCAS_ENDPOINT`.
## Decisions to confirm before starting ## Decisions to confirm before starting

View File

@@ -5,7 +5,7 @@ public FOCAS documentation. Purpose: separate what we *know* about the FOCAS
wire protocol (can quote with confidence) from what we're *guessing* (will need wire protocol (can quote with confidence) from what we're *guessing* (will need
Wireshark traces to validate in Stream C). Wireshark traces to validate in Stream C).
This document directly informs `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/server/`. This document directly informs `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/server/`.
## Authoritative — from Fanuc's public `fwlib32.h` ## Authoritative — from Fanuc's public `fwlib32.h`
@@ -269,7 +269,7 @@ mock is already correct. Only the framing layer needs iteration.
This is the iterative Wireshark loop — no point starting until the Windows rig This is the iterative Wireshark loop — no point starting until the Windows rig
+ licensed Fwlib64.dll + real CNC are all available. See the implementer's + licensed Fwlib64.dll + real CNC are all available. See the implementer's
checklist in checklist in
[`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/README.md`](../../../tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/README.md). [`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/README.md`](../../../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/README.md).
### Phase 3 — flip the C# test gate ### Phase 3 — flip the C# test gate
@@ -283,8 +283,8 @@ Once Phase 2 proves Fwlib64 can talk to the mock:
## References ## References
- [`src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibNative.cs`](../../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibNative.cs) — P/Invoke surface, authoritative struct layouts - [`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibNative.cs`](../../../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibNative.cs) — P/Invoke surface, authoritative struct layouts
- [`src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibFocasClient.cs`](../../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibFocasClient.cs) — reference C# implementation of each FWLIB call - [`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibFocasClient.cs`](../../../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibFocasClient.cs) — reference C# implementation of each FWLIB call
- [`src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs`](../../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs) — EW_* → OPC UA status mapping - [`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs`](../../../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs) — EW_* → OPC UA status mapping
- Fanuc FOCAS Developer Kit (licensed, not in repo) — ultimate source of truth - Fanuc FOCAS Developer Kit (licensed, not in repo) — ultimate source of truth
- `strangesast/fwlib` on GitHub — redistributes `fwlib32.h` + runtime binaries; no wire protocol docs - `strangesast/fwlib` on GitHub — redistributes `fwlib32.h` + runtime binaries; no wire protocol docs

View File

@@ -74,9 +74,9 @@ Save the result to `docs/v2/implementation/phase-0-rename-inventory.md` (gitigno
Per project (11 projects total — 5 src + 6 tests): Per project (11 projects total — 5 src + 6 tests):
```bash ```bash
git mv src/ZB.MOM.WW.LmxOpcUa.Client.CLI src/ZB.MOM.WW.OtOpcUa.Client.CLI git mv src/ZB.MOM.WW.LmxOpcUa.Client.CLI src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI
git mv src/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.LmxOpcUa.Client.CLI.csproj \ git mv src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.LmxOpcUa.Client.CLI.csproj \
src/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj
``` ```
Repeat for: `Client.Shared`, `Client.UI`, `Historian.Aveva`, `Host`, and all 6 test projects. Repeat for: `Client.Shared`, `Client.UI`, `Historian.Aveva`, `Host`, and all 6 test projects.
@@ -156,8 +156,8 @@ dotnet test ZB.MOM.WW.OtOpcUa.slnx
Plus manual smoke test of Client.CLI against a running v1 OPC UA server: Plus manual smoke test of Client.CLI against a running v1 OPC UA server:
```bash ```bash
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840 dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 2 dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 2
``` ```
**Acceptance**: **Acceptance**:

View File

@@ -63,7 +63,7 @@ Phase 1 is large — broken into 5 work streams (AE) that can partly overlap.
#### Task A.1 — Define driver capability interfaces #### Task A.1 — Define driver capability interfaces
Create `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/` (.NET 10, no dependencies). Define: Create `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/` (.NET 10, no dependencies). Define:
```csharp ```csharp
public interface IDriver { /* lifecycle, metadata, health */ } public interface IDriver { /* lifecycle, metadata, health */ }
@@ -131,7 +131,7 @@ In v2.0 v1 only registers the `Galaxy` type (`AllowedNamespaceKinds = SystemPlat
#### Task B.1 — EF Core schema + initial migration #### Task B.1 — EF Core schema + initial migration
Create `src/ZB.MOM.WW.OtOpcUa.Configuration/` (.NET 10, EF Core 10). Create `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/` (.NET 10, EF Core 10).
Implement DbContext with entities matching `config-db-schema.md` exactly: Implement DbContext with entities matching `config-db-schema.md` exactly:
- `ServerCluster`, `ClusterNode`, `ClusterNodeCredential` - `ServerCluster`, `ClusterNode`, `ClusterNodeCredential`
@@ -146,7 +146,7 @@ Implement DbContext with entities matching `config-db-schema.md` exactly:
Generate the initial migration: Generate the initial migration:
```bash ```bash
dotnet ef migrations add InitialSchema --project src/ZB.MOM.WW.OtOpcUa.Configuration dotnet ef migrations add InitialSchema --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration
``` ```
**Acceptance**: **Acceptance**:
@@ -338,7 +338,7 @@ If the central DB is unreachable at startup, load the most recent cached generat
#### Task E.1 — Project scaffold mirroring ScadaLink CentralUI (decision #102) #### Task E.1 — Project scaffold mirroring ScadaLink CentralUI (decision #102)
Copy the project layout from `scadalink-design/src/ScadaLink.CentralUI/` (decision #104): Copy the project layout from `scadalink-design/src/ScadaLink.CentralUI/` (decision #104):
- `src/ZB.MOM.WW.OtOpcUa.Admin/`: Razor Components project, .NET 10, `AddInteractiveServerComponents` - `src/Server/ZB.MOM.WW.OtOpcUa.Admin/`: Razor Components project, .NET 10, `AddInteractiveServerComponents`
- `Auth/AuthEndpoints.cs`, `Auth/CookieAuthenticationStateProvider.cs` - `Auth/AuthEndpoints.cs`, `Auth/CookieAuthenticationStateProvider.cs`
- `Components/Layout/MainLayout.razor`, `Components/Layout/NavMenu.razor` - `Components/Layout/MainLayout.razor`, `Components/Layout/NavMenu.razor`
- `Components/Pages/Login.razor`, `Components/Pages/Dashboard.razor` - `Components/Pages/Login.razor`, `Components/Pages/Dashboard.razor`
@@ -496,10 +496,10 @@ A `phase-1-compliance.ps1` script that exits non-zero on any failure:
```powershell ```powershell
# Run all migrations against a clean SQL Server instance # Run all migrations against a clean SQL Server instance
dotnet ef database update --project src/ZB.MOM.WW.OtOpcUa.Configuration --connection "Server=...;Database=OtOpcUaConfig_Test_$(date +%s);..." dotnet ef database update --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration --connection "Server=...;Database=OtOpcUaConfig_Test_$(date +%s);..."
# Run schema-introspection tests # Run schema-introspection tests
dotnet test tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests --filter "Category=SchemaCompliance" dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests --filter "Category=SchemaCompliance"
``` ```
Expected: every table, column, index, FK, CHECK, and stored procedure in `config-db-schema.md` is present and matches. Expected: every table, column, index, FK, CHECK, and stored procedure in `config-db-schema.md` is present and matches.

View File

@@ -3,7 +3,7 @@
> **Status**: **SHIPPED (core + Stream C)** — original body merged 2026-04-19; audit 2026-04-23 promoted **Stream C (task #147)** into shipped state. > **Status**: **SHIPPED (core + Stream C)** — original body merged 2026-04-19; audit 2026-04-23 promoted **Stream C (task #147)** into shipped state.
> >
> **In** (verified in repo): > **In** (verified in repo):
> - Stream A — `ClusterTopologyLoader`, `RedundancyCoordinator`, `RedundancyTopology`, `PeerReachability` all present under `src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`. Coordinator is now also hosted by `Program.cs` via the new `RedundancyPublisherHostedService`, which calls `RefreshAsync` on startup. > - Stream A — `ClusterTopologyLoader`, `RedundancyCoordinator`, `RedundancyTopology`, `PeerReachability` all present under `src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`. Coordinator is now also hosted by `Program.cs` via the new `RedundancyPublisherHostedService`, which calls `RefreshAsync` on startup.
> - Stream B — `ServiceLevelCalculator` + `RecoveryStateManager`. > - Stream B — `ServiceLevelCalculator` + `RecoveryStateManager`.
> - **Stream C (task #147) — OPC UA node wiring**. `ServerRedundancyNodeWriter` maintains `Server.ServiceLevel` (i=2267), `Server.ServerRedundancy.RedundancySupport` (i=2994), and `Server.ServerRedundancy.ServerUriArray` (non-transparent subtype) by writing the `PropertyState.Value` + calling `ClearChangeMasks`. `RedundancyPublisherHostedService` drives the publisher on a 1 s tick and fans `OnStateChanged` / `OnServerUriArrayChanged` into the writer. Mapping of `Configuration.RedundancyMode` → Part 4 `RedundancySupport` is Warm/Hot/None (v2 clusters don't enumerate Cold / HotAndMirrored per decision #85). Idempotent per-value dedupe prevents spurious OPC UA notifications. Unit coverage: `ServerRedundancyNodeWriterTests` (4 tests, green). > - **Stream C (task #147) — OPC UA node wiring**. `ServerRedundancyNodeWriter` maintains `Server.ServiceLevel` (i=2267), `Server.ServerRedundancy.RedundancySupport` (i=2994), and `Server.ServerRedundancy.ServerUriArray` (non-transparent subtype) by writing the `PropertyState.Value` + calling `ClearChangeMasks`. `RedundancyPublisherHostedService` drives the publisher on a 1 s tick and fans `OnStateChanged` / `OnServerUriArrayChanged` into the writer. Mapping of `Configuration.RedundancyMode` → Part 4 `RedundancySupport` is Warm/Hot/None (v2 clusters don't enumerate Cold / HotAndMirrored per decision #85). Idempotent per-value dedupe prevents spurious OPC UA notifications. Unit coverage: `ServerRedundancyNodeWriterTests` (4 tests, green).
> - Stream D — `ApplyLeaseRegistry`. > - Stream D — `ApplyLeaseRegistry`.

View File

@@ -3,13 +3,13 @@
> **Status**: **SHIPPED (mostly)** 2026-04-19; audit 2026-04-23 confirms what landed separately after the data-layer PR #91: > **Status**: **SHIPPED (mostly)** 2026-04-19; audit 2026-04-23 confirms what landed separately after the data-layer PR #91:
> >
> **In** (verified in repo): > **In** (verified in repo):
> - **Task #153 Stream A UI** — `UnsTab.razor` with drag/drop handlers + concurrent-edit via `DraftRevisionToken` + `UnsImpactAnalyzer`; Playwright smoke test in `tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/UnsTabDragDropE2ETests.cs`. > - **Task #153 Stream A UI** — `UnsTab.razor` with drag/drop handlers + concurrent-edit via `DraftRevisionToken` + `UnsImpactAnalyzer`; Playwright smoke test in `tests/Server/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/UnsTabDragDropE2ETests.cs`.
> - **Task #155 Stream B** — `EquipmentImportBatch` entity + migration, `EquipmentImportBatchService.CreateBatchAsync` / `FinaliseBatchAsync` / `DropBatchAsync` / `ListByUserAsync`, `ImportEquipment.razor` UI. > - **Task #155 Stream B** — `EquipmentImportBatch` entity + migration, `EquipmentImportBatchService.CreateBatchAsync` / `FinaliseBatchAsync` / `DropBatchAsync` / `ListByUserAsync`, `ImportEquipment.razor` UI.
> - **Task #156 Stream C** — `DiffViewer.razor` + `DiffSection.razor` refactor in place. > - **Task #156 Stream C** — `DiffViewer.razor` + `DiffSection.razor` refactor in place.
> - Admin UI `IdentificationFields.razor` surface shipped (part of #157). > - Admin UI `IdentificationFields.razor` surface shipped (part of #157).
> >
> **Closed this session (2026-04-23)**: > **Closed this session (2026-04-23)**:
> - **Task #157 Stream D server-side half** was a stale audit claim. `src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/IdentificationFolderBuilder.cs` ships the OPC 40010 Identification sub-folder materializer (Manufacturer / Model / SerialNumber / HardwareRevision / SoftwareRevision / YearOfConstruction / AssetLocation / ManufacturerUri / DeviceManualUri); `EquipmentNodeWalker.Walk` calls it per equipment; `IdentificationFolderBuilderTests` (158 lines) + two walker-level tests (`Walk_Materializes_Identification_Subfolder_When_AnyFieldPresent`, `Walk_Omits_Identification_Subfolder_When_AllFieldsNull`) cover the null-handling branches. The initial audit grepped only `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/`; the builder lives in `Core/OpcUa/`. > - **Task #157 Stream D server-side half** was a stale audit claim. `src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/IdentificationFolderBuilder.cs` ships the OPC 40010 Identification sub-folder materializer (Manufacturer / Model / SerialNumber / HardwareRevision / SoftwareRevision / YearOfConstruction / AssetLocation / ManufacturerUri / DeviceManualUri); `EquipmentNodeWalker.Walk` calls it per equipment; `IdentificationFolderBuilderTests` (158 lines) + two walker-level tests (`Walk_Materializes_Identification_Subfolder_When_AnyFieldPresent`, `Walk_Omits_Identification_Subfolder_When_AllFieldsNull`) cover the null-handling branches. The initial audit grepped only `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/`; the builder lives in `Core/OpcUa/`.
> >
> **Phase 6.4 is now FULLY SHIPPED — no deferred surfaces remain.** > **Phase 6.4 is now FULLY SHIPPED — no deferred surfaces remain.**
> >

View File

@@ -12,7 +12,7 @@ End-to-end validation that the Phase 7 production wiring chain (#243 / #244 / #2
| `OtOpcUaGalaxyHost` Windows service running | `sc query OtOpcUaGalaxyHost``STATE: 4 RUNNING` | | `OtOpcUaGalaxyHost` Windows service running | `sc query OtOpcUaGalaxyHost``STATE: 4 RUNNING` |
| Galaxy.Host shared secret matches `.local/galaxy-host-secret.txt` | Set during NSSM install — see `docs/ServiceHosting.md` | | Galaxy.Host shared secret matches `.local/galaxy-host-secret.txt` | Set during NSSM install — see `docs/ServiceHosting.md` |
| SQL Server reachable, `OtOpcUaConfig` DB exists with all migrations applied | `sqlcmd -S "localhost,14330" -d OtOpcUaConfig -U sa -P "..." -Q "SELECT COUNT(*) FROM dbo.__EFMigrationsHistory"` returns ≥ 11 | | SQL Server reachable, `OtOpcUaConfig` DB exists with all migrations applied | `sqlcmd -S "localhost,14330" -d OtOpcUaConfig -U sa -P "..." -Q "SELECT COUNT(*) FROM dbo.__EFMigrationsHistory"` returns ≥ 11 |
| Server's `appsettings.json` `Node:ConfigDbConnectionString` matches your SQL Server | `cat src/ZB.MOM.WW.OtOpcUa.Server/appsettings.json` | | Server's `appsettings.json` `Node:ConfigDbConnectionString` matches your SQL Server | `cat src/Server/ZB.MOM.WW.OtOpcUa.Server/appsettings.json` |
> **Galaxy.Host pipe ACL.** The pipe allows the configured `OTOPCUA_ALLOWED_SID` (typically the user that runs `OtOpcUaGalaxyHost` — `dohertj2` on the dev box). Run the Server under the same user; elevation doesn't matter — `PipeAcl.cs` no longer denies `BUILTIN\Administrators` since UAC's deny-only Admins SID would have blocked non-elevated dev-box admins too. > **Galaxy.Host pipe ACL.** The pipe allows the configured `OTOPCUA_ALLOWED_SID` (typically the user that runs `OtOpcUaGalaxyHost` — `dohertj2` on the dev box). Run the Server under the same user; elevation doesn't matter — `PipeAcl.cs` no longer denies `BUILTIN\Administrators` since UAC's deny-only Admins SID would have blocked non-elevated dev-box admins too.
@@ -21,7 +21,7 @@ End-to-end validation that the Phase 7 production wiring chain (#243 / #244 / #2
### 1. Migrate the Config DB ### 1. Migrate the Config DB
```powershell ```powershell
cd src/ZB.MOM.WW.OtOpcUa.Configuration cd src/Core/ZB.MOM.WW.OtOpcUa.Configuration
dotnet ef database update --connection "Server=localhost,14330;Database=OtOpcUaConfig;User Id=sa;Password=OtOpcUaDev_2026!;TrustServerCertificate=True;Encrypt=False;" dotnet ef database update --connection "Server=localhost,14330;Database=OtOpcUaConfig;User Id=sa;Password=OtOpcUaDev_2026!;TrustServerCertificate=True;Encrypt=False;"
``` ```
@@ -126,7 +126,7 @@ Dev-box GLAuth ships `writeop` / `writeop123` in the `WriteOperate` group, `admi
### 5. Start the Server ### 5. Start the Server
```powershell ```powershell
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Server dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server
``` ```
Expected log markers (in order): Expected log markers (in order):
@@ -146,7 +146,7 @@ Any line missing = follow up the failure surface (each step has its own log sign
### 6. Validate via Client.CLI ### 6. Validate via Client.CLI
```powershell ```powershell
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840/OtOpcUa -r -d 5 dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840/OtOpcUa -r -d 5
``` ```
Expect to see under the namespace root: `lab-floor → galaxy-line → reactor-1` with three child variables: `Source` (driver-sourced Int32), `MachineStatus` (virtual tag Boolean, `Source > 0`), and `OverTemp` (scripted alarm Boolean, `Source > 50`). NodeIds are path-based per OPC UA Part 3 §5.2.2 — the walker mints them from `{driverId}/{folder-path}/{browseName}` and stores the driver-side FullReference in an internal NodeId→FullRef map, so client subscriptions survive backend address renames. Expect to see under the namespace root: `lab-floor → galaxy-line → reactor-1` with three child variables: `Source` (driver-sourced Int32), `MachineStatus` (virtual tag Boolean, `Source > 0`), and `OverTemp` (scripted alarm Boolean, `Source > 50`). NodeIds are path-based per OPC UA Part 3 §5.2.2 — the walker mints them from `{driverId}/{folder-path}/{browseName}` and stores the driver-side FullReference in an internal NodeId→FullRef map, so client subscriptions survive backend address renames.
@@ -154,7 +154,7 @@ Expect to see under the namespace root: `lab-floor → galaxy-line → reactor-1
#### Read the virtual tag #### Read the virtual tag
```powershell ```powershell
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- read ` dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
-u opc.tcp://localhost:4840/OtOpcUa ` -u opc.tcp://localhost:4840/OtOpcUa `
-n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/MachineStatus" -n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/MachineStatus"
``` ```
@@ -164,7 +164,7 @@ Expected: `Boolean`. Push a value change into the Source Galaxy attribute and re
#### Read the scripted alarm #### Read the scripted alarm
```powershell ```powershell
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- read ` dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
-u opc.tcp://localhost:4840/OtOpcUa ` -u opc.tcp://localhost:4840/OtOpcUa `
-n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/OverTemp" -n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/OverTemp"
``` ```
@@ -177,7 +177,7 @@ Push a Source value above 50 — either from Galaxy itself, or via the Server's
```powershell ```powershell
# OPC UA write path — requires LDAP from step 4a + a writeop-class user. # OPC UA write path — requires LDAP from step 4a + a writeop-class user.
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- write ` dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- write `
-u opc.tcp://localhost:4840/OtOpcUa -S sign ` -u opc.tcp://localhost:4840/OtOpcUa -S sign `
-n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/Source" ` -n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/Source" `
-v 75 -U writeop -P writeop123 -v 75 -U writeop -P writeop123

View File

@@ -7,7 +7,7 @@ populations disagree with the spec in small, device-specific ways, and a driver
passes textbook tests can still misbehave against actual equipment. passes textbook tests can still misbehave against actual equipment.
This doc is the harness-and-quirks playbook. The project it describes lives at This doc is the harness-and-quirks playbook. The project it describes lives at
`tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/` — scaffolded in PR 30 with `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/` — scaffolded in PR 30 with
the simulator fixture, DL205 profile stub, and one write/read smoke test. Each the simulator fixture, DL205 profile stub, and one write/read smoke test. Each
confirmed DL205 quirk lands in a follow-up PR as a named test in that project. confirmed DL205 quirk lands in a follow-up PR as a named test in that project.
@@ -33,7 +33,7 @@ under `tests/.../Modbus.IntegrationTests/Docker/`. See that folder's
**Setup pattern**: **Setup pattern**:
1. `docker compose -f tests\...\Modbus.IntegrationTests\Docker\docker-compose.yml --profile <standard|dl205|mitsubishi|s7_1500> up -d`. 1. `docker compose -f tests\...\Modbus.IntegrationTests\Docker\docker-compose.yml --profile <standard|dl205|mitsubishi|s7_1500> up -d`.
2. `dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` 2. `dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests`
tests auto-skip when the endpoint is unreachable. Default endpoint is tests auto-skip when the endpoint is unreachable. Default endpoint is
`localhost:5020`; override via `MODBUS_SIM_ENDPOINT` for a real PLC on its `localhost:5020`; override via `MODBUS_SIM_ENDPOINT` for a real PLC on its
native port 502. native port 502.
@@ -116,7 +116,7 @@ vendors get promoted into driver defaults or opt-in options:
## Next concrete PRs ## Next concrete PRs
- **PR 30 — Integration test project + DL205 profile scaffold** — **DONE**. - **PR 30 — Integration test project + DL205 profile scaffold** — **DONE**.
Shipped `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` with Shipped `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` with
`ModbusSimulatorFixture` (TCP-probe, skips with a clear `SkipReason` when the `ModbusSimulatorFixture` (TCP-probe, skips with a clear `SkipReason` when the
endpoint is unreachable), `DL205/DL205Profile.cs` (tag map stub), and endpoint is unreachable), `DL205/DL205Profile.cs` (tag map stub), and
`DL205/DL205SmokeTests.cs` (write-then-read round-trip). `DL205/DL205SmokeTests.cs` (write-then-read round-trip).

View File

@@ -121,7 +121,7 @@ flips A4 from "deferred" to "expected pass").
redundancy implementations we don't control. redundancy implementations we don't control.
- For the sub-set of scenarios that *can* be automated — the self-loopback - For the sub-set of scenarios that *can* be automated — the self-loopback
case where our own `otopcua-cli` drives Primary + Backup — the existing case where our own `otopcua-cli` drives Primary + Backup — the existing
`tests/ZB.MOM.WW.OtOpcUa.Server.Tests/RedundancyStatePublisherTests` + `tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/RedundancyStatePublisherTests` +
`ServiceLevelCalculatorTests` (unit) + `ClusterTopologyLoaderTests` `ServiceLevelCalculatorTests` (unit) + `ClusterTopologyLoaderTests`
(integration) already cover the math + data path. The wire-level assertion (integration) already cover the math + data path. The wire-level assertion
that the values actually land on the right OPC UA nodes is covered by that the values actually land on the right OPC UA nodes is covered by

View File

@@ -191,7 +191,7 @@ Modbus has no native String, DateTime, or Int64 — those rows are skipped on th
### CI fixture (task #180) ### CI fixture (task #180)
The integration harness at `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/` is Docker-only — `ab_server` is a source-only tool under libplctag's `src/tools/ab_server/`, and the fixture's multi-stage `Docker/Dockerfile` is the only supported reproducible build path. The integration harness at `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/` is Docker-only — `ab_server` is a source-only tool under libplctag's `src/tools/ab_server/`, and the fixture's multi-stage `Docker/Dockerfile` is the only supported reproducible build path.
- **`AbServerFixture(AbServerProfile)`** — thin TCP probe against `127.0.0.1:44818` (or `AB_SERVER_ENDPOINT` override). Does not spawn the simulator; the operator brings up the compose service for whichever family the test class targets (`controllogix` / `compactlogix` / `micro800` / `guardlogix`). - **`AbServerFixture(AbServerProfile)`** — thin TCP probe against `127.0.0.1:44818` (or `AB_SERVER_ENDPOINT` override). Does not spawn the simulator; the operator brings up the compose service for whichever family the test class targets (`controllogix` / `compactlogix` / `micro800` / `guardlogix`).
- **`KnownProfiles.{ControlLogix, CompactLogix, Micro800, GuardLogix}`** — thin `(Family, ComposeProfile, Notes)` records. The compose file (`Docker/docker-compose.yml`) is the canonical source of truth for which tags each family seeds + which `--plc` mode the simulator boots in. `Micro800` uses the dedicated `--plc=Micro800` mode; `GuardLogix` uses `ControlLogix` emulation because ab_server has no safety subsystem (the `_S`-suffixed seed tag triggers driver-side ViewOnly classification only). - **`KnownProfiles.{ControlLogix, CompactLogix, Micro800, GuardLogix}`** — thin `(Family, ComposeProfile, Notes)` records. The compose file (`Docker/docker-compose.yml`) is the canonical source of truth for which tags each family seeds + which `--plc` mode the simulator boots in. `Micro800` uses the dedicated `--plc=Micro800` mode; `GuardLogix` uses `ControlLogix` emulation because ab_server has no safety subsystem (the `_S`-suffixed seed tag triggers driver-side ViewOnly classification only).
@@ -205,7 +205,7 @@ The integration harness at `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTest
- name: Start ab_server Docker container - name: Start ab_server Docker container
shell: pwsh shell: pwsh
run: | run: |
docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml ` docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml `
--profile controllogix up -d --build --profile controllogix up -d --build
# Wait for :44818 to accept connections (compose healthcheck-equivalent) # Wait for :44818 to accept connections (compose healthcheck-equivalent)
for ($i = 0; $i -lt 30; $i++) { for ($i = 0; $i -lt 30; $i++) {

View File

@@ -54,7 +54,7 @@ read-only tag.
## Status ## Status
All seven driver factories are registered in All seven driver factories are registered in
`src/ZB.MOM.WW.OtOpcUa.Server/Program.cs` — Galaxy, FOCAS, Modbus, `src/Server/ZB.MOM.WW.OtOpcUa.Server/Program.cs` — Galaxy, FOCAS, Modbus,
AB CIP, AB Legacy, S7, TwinCAT. `DriverInstanceBootstrapper` can AB CIP, AB Legacy, S7, TwinCAT. `DriverInstanceBootstrapper` can
materialise any `DriverType` row from the central Config DB into a materialise any `DriverType` row from the central Config DB into a
live driver. The factory-wiring block that originally gated stages live driver. The factory-wiring block that originally gated stages
@@ -108,7 +108,7 @@ tracks under their hardware-fixture tasks (#221 / #222).
opc-plc cover Modbus / AB / S7 / OPC UA Client. FOCAS and TwinCAT opc-plc cover Modbus / AB / S7 / OPC UA Client. FOCAS and TwinCAT
have no public simulator; they are gated with env-var skip flags have no public simulator; they are gated with env-var skip flags
below. For OpcUaClient, `docker compose -f below. For OpcUaClient, `docker compose -f
tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/ tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/
docker-compose.yml up -d` brings up `opc-plc` on port 50000. docker-compose.yml up -d` brings up `opc-plc` on port 50000.
3. **PowerShell 7+**. The runner uses null-coalescing + `Set-StrictMode`; 3. **PowerShell 7+**. The runner uses null-coalescing + `Set-StrictMode`;
the Windows-PowerShell-5.1 shell will not parse `test-all.ps1`. the Windows-PowerShell-5.1 shell will not parse `test-all.ps1`.

View File

@@ -60,7 +60,7 @@ pwsh .\scripts\integration\run-focas.ps1 -Profile powermotion
``` ```
Full profile list is in Full profile list is in
`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml`. `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/docker-compose.yml`.
### Exit codes ### Exit codes

View File

@@ -19,7 +19,7 @@ From the repo root:
```powershell ```powershell
# ControlLogix — widest-coverage profile # ControlLogix — widest-coverage profile
docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests\Docker\docker-compose.yml --profile controllogix up docker compose -f tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests\Docker\docker-compose.yml --profile controllogix up
# Per-family # Per-family
docker compose -f tests\...\Docker\docker-compose.yml --profile compactlogix up docker compose -f tests\...\Docker\docker-compose.yml --profile compactlogix up
@@ -49,7 +49,7 @@ In a separate shell with a container up:
```powershell ```powershell
cd C:\Users\dohertj2\Desktop\lmxopcua cd C:\Users\dohertj2\Desktop\lmxopcua
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
``` ```
`AbServerFixture` TCP-probes `localhost:44818` at collection init + `AbServerFixture` TCP-probes `localhost:44818` at collection init +

View File

@@ -86,7 +86,7 @@ On the dev box:
```powershell ```powershell
$env:AB_SERVER_PROFILE = 'emulate' $env:AB_SERVER_PROFILE = 'emulate'
$env:AB_SERVER_ENDPOINT = '10.0.0.42:44818' # replace with the Emulate PC IP $env:AB_SERVER_ENDPOINT = '10.0.0.42:44818' # replace with the Emulate PC IP
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
``` ```
With `AB_SERVER_PROFILE` unset or `abserver`, the `Emulate/*Tests.cs` With `AB_SERVER_PROFILE` unset or `abserver`, the `Emulate/*Tests.cs`

View File

@@ -21,7 +21,7 @@ From the repo root:
```powershell ```powershell
# SLC 500 family — widest PCCC coverage # SLC 500 family — widest PCCC coverage
docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests\Docker\docker-compose.yml --profile slc500 up docker compose -f tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests\Docker\docker-compose.yml --profile slc500 up
# Per-family # Per-family
docker compose -f tests\...\Docker\docker-compose.yml --profile micrologix up docker compose -f tests\...\Docker\docker-compose.yml --profile micrologix up
@@ -63,7 +63,7 @@ running so only the matching theory-parameterization executes:
```powershell ```powershell
cd C:\Users\dohertj2\Desktop\lmxopcua cd C:\Users\dohertj2\Desktop\lmxopcua
$env:AB_LEGACY_COMPOSE_PROFILE = "slc500" # or "micrologix" / "plc5" $env:AB_LEGACY_COMPOSE_PROFILE = "slc500" # or "micrologix" / "plc5"
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests
``` ```
Against real SLC / MicroLogix / PLC-5 hardware, set the endpoint + an Against real SLC / MicroLogix / PLC-5 hardware, set the endpoint + an
@@ -73,7 +73,7 @@ run (real PLCs answer any valid family):
```powershell ```powershell
$env:AB_LEGACY_ENDPOINT = "10.0.1.50:44818" $env:AB_LEGACY_ENDPOINT = "10.0.1.50:44818"
$env:AB_LEGACY_CIP_PATH = "" # empty — real hardware has no backplane $env:AB_LEGACY_CIP_PATH = "" # empty — real hardware has no backplane
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests
``` ```
`AbLegacyServerFixture` TCP-probes the endpoint at collection init and `AbLegacyServerFixture` TCP-probes the endpoint at collection init and

View File

@@ -42,7 +42,7 @@ Pick one compose profile (they all publish 8193 — only one at a time):
```powershell ```powershell
docker compose -f Docker/docker-compose.yml --profile thirtyone up -d docker compose -f Docker/docker-compose.yml --profile thirtyone up -d
dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests
docker compose -f Docker/docker-compose.yml --profile thirtyone down docker compose -f Docker/docker-compose.yml --profile thirtyone down
``` ```

View File

@@ -20,20 +20,20 @@ From the repo root:
```powershell ```powershell
# Build + start the standard profile # Build + start the standard profile
docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile standard up docker compose -f tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile standard up
# DL205 quirks # DL205 quirks
docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile dl205 up docker compose -f tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile dl205 up
# Mitsubishi MELSEC quirks # Mitsubishi MELSEC quirks
docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile mitsubishi up docker compose -f tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile mitsubishi up
# Siemens S7-1500 MB_SERVER quirks # Siemens S7-1500 MB_SERVER quirks
docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile s7_1500 up docker compose -f tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile s7_1500 up
# Exception-injection — end-to-end coverage of every Modbus exception code # Exception-injection — end-to-end coverage of every Modbus exception code
# (01/02/03/04/05/06/0A/0B), not just the 02 + 03 pymodbus emits naturally # (01/02/03/04/05/06/0A/0B), not just the 02 + 03 pymodbus emits naturally
docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile exception_injection up docker compose -f tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile exception_injection up
``` ```
Detached + stop: Detached + stop:
@@ -59,7 +59,7 @@ In a separate shell with one profile live:
```powershell ```powershell
cd C:\Users\dohertj2\Desktop\lmxopcua cd C:\Users\dohertj2\Desktop\lmxopcua
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests
``` ```
`ModbusSimulatorFixture` probes `localhost:5020` at collection init + `ModbusSimulatorFixture` probes `localhost:5020` at collection init +

View File

@@ -23,7 +23,7 @@ configured with the WSL 2 backend — nothing new to install.
From the repo root: From the repo root:
```powershell ```powershell
docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests\Docker\docker-compose.yml up docker compose -f tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests\Docker\docker-compose.yml up
``` ```
Or from this folder: Or from this folder:
@@ -79,7 +79,7 @@ In a separate shell, with the simulator running:
```powershell ```powershell
cd C:\Users\dohertj2\Desktop\lmxopcua cd C:\Users\dohertj2\Desktop\lmxopcua
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests
``` ```
Tests auto-skip with a clear `SkipReason` when `localhost:50000` isn't Tests auto-skip with a clear `SkipReason` when `localhost:50000` isn't

View File

@@ -17,7 +17,7 @@ nothing else.
From the repo root: From the repo root:
```powershell ```powershell
docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests\Docker\docker-compose.yml --profile s7_1500 up docker compose -f tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests\Docker\docker-compose.yml --profile s7_1500 up
``` ```
Detached + stop: Detached + stop:
@@ -42,7 +42,7 @@ In a separate shell with the container up:
```powershell ```powershell
cd C:\Users\dohertj2\Desktop\lmxopcua cd C:\Users\dohertj2\Desktop\lmxopcua
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests
``` ```
`Snap7ServerFixture` probes `localhost:1102` at collection init + records `Snap7ServerFixture` probes `localhost:1102` at collection init + records

View File

@@ -204,7 +204,7 @@ On the dev box:
$env:TWINCAT_TARGET_NETID = '5.23.91.23.1.1' # replace with the VM AmsNetId — REQUIRED $env:TWINCAT_TARGET_NETID = '5.23.91.23.1.1' # replace with the VM AmsNetId — REQUIRED
$env:TWINCAT_TARGET_HOST = '10.0.0.42' # replace with the VM IP (defaults to localhost) $env:TWINCAT_TARGET_HOST = '10.0.0.42' # replace with the VM IP (defaults to localhost)
# $env:TWINCAT_TARGET_PORT = '852' # only if not using PLC runtime 1 (default 851) # $env:TWINCAT_TARGET_PORT = '852' # only if not using PLC runtime 1 (default 851)
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests
``` ```
Only `TWINCAT_TARGET_NETID` is required — fixture gates on it specifically. Only `TWINCAT_TARGET_NETID` is required — fixture gates on it specifically.