Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56bb1ceaf5 | |||
| 8a51842e89 | |||
| fea2b34e9a | |||
| 392b219233 | |||
| 41f133a337 | |||
| bc8ff7a5fe | |||
| ca149ce907 | |||
| 1913bda6b8 | |||
| fa965ede3d | |||
| 7b3b6580b3 | |||
| 41da84293a | |||
| 16a87b08f3 | |||
| da8a3e46f7 | |||
| 09af8d2830 | |||
| 6c78027b5a | |||
| bb1854b2f8 | |||
| 70d7166a39 | |||
| 6968872e5d | |||
| 020c30f9a6 | |||
| a8dabc47f9 | |||
| 43291d7fdd | |||
| 75b91ebb97 | |||
| 412cdec9b1 | |||
| b90718013e | |||
| 4a2e993a95 | |||
| adbbb5e7d0 | |||
| a8ef73dcb5 | |||
| 22fd314694 | |||
| 8adb83afee | |||
| 1e04796953 | |||
| 5f5bfe1ea5 | |||
| 482d5f5637 | |||
| 31b9468102 | |||
| cf024c8150 | |||
| 0aee14686b | |||
| 4e1751e1a4 | |||
| 969b0847a1 | |||
| a25593a9c6 |
@@ -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
|
||||
contained-name browse paths and tag-name runtime references. Galaxy
|
||||
access flows through the in-process `GalaxyDriver`
|
||||
(`src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/`) talking gRPC to a separately
|
||||
(`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/`) talking gRPC to a separately
|
||||
installed **mxaccessgw** gateway process. The gateway owns the
|
||||
MXAccess COM bitness constraint (its worker is x86 net48); everything
|
||||
in this repo is .NET 10. PR 7.2 retired the legacy in-process
|
||||
@@ -47,11 +47,11 @@ Example: browsing `TestMachine_001/DelmiaReceiver/DownloadPath` translates to MX
|
||||
|
||||
### 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
|
||||
|
||||
`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
|
||||
|
||||
@@ -62,12 +62,18 @@ The gateway lives in a sibling repo at `c:\Users\dohertj2\Desktop\mxaccessgw\`.
|
||||
```bash
|
||||
dotnet restore ZB.MOM.WW.OtOpcUa.slnx
|
||||
dotnet build ZB.MOM.WW.OtOpcUa.slnx
|
||||
dotnet test ZB.MOM.WW.OtOpcUa.slnx # all tests
|
||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.Tests # unit tests only
|
||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.IntegrationTests # integration tests only
|
||||
dotnet test --filter "FullyQualifiedName~MyTestClass.MyMethod" # single test
|
||||
dotnet test ZB.MOM.WW.OtOpcUa.slnx # all tests
|
||||
dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests # a single test project
|
||||
dotnet test --filter "FullyQualifiedName~MyTestClass.MyMethod" # a single test
|
||||
```
|
||||
|
||||
Test projects live under `tests/<module>/` (Core, Server, Drivers,
|
||||
Drivers/Cli, Client, Tooling) — there is no single unit-test project.
|
||||
Unit suites are named `*.Tests`; integration suites are `*.IntegrationTests`
|
||||
and need their Docker fixture up (see Docker Workflow). DB-backed tests in
|
||||
`*.Configuration.Tests`, `*.Admin.Tests`, and `*.Server.Tests` require the
|
||||
central SQL Server.
|
||||
|
||||
## Docker Workflow (driver fixtures + central SQL Server)
|
||||
|
||||
> **Migrated 2026-04-28**: Docker config + host moved off this dev VM (DESKTOP-6JL3KKO) onto the shared Linux Docker host (`DOCKER`, 10.100.0.35) so the dev VM could shed WSL2/Hyper-V and have its GPU re-attached via ESXi passthrough. Docker Desktop is no longer installed here. All checked-in `appsettings.json` defaults, fixture-class default endpoints, and `e2e-config.sample.json` were rewritten to target `10.100.0.35`. The driver fixture compose files under `tests/.../Docker/docker-compose.yml` now carry a `project: lmxopcua` label on every service. See `docs/v2/dev-environment.md` for the full rewrite (header dated 2026-04-28).
|
||||
@@ -121,13 +127,13 @@ The server supports non-transparent warm/hot redundancy via the `Redundancy` sec
|
||||
|
||||
## 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
|
||||
|
||||
- **Logging**: Serilog with rolling daily file sink
|
||||
- **Unit tests**: xUnit + Shouldly for assertions
|
||||
- **Service hosting (Server, Admin)**: .NET generic host with `AddWindowsService` (decision #30 — replaced TopShelf in v2; see `src/ZB.MOM.WW.OtOpcUa.Server/OpcUaServerService.cs`)
|
||||
- **Service hosting (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 .NET Standard Documentation
|
||||
@@ -136,11 +142,11 @@ Use the DeepWiki MCP (`mcp__deepwiki`) to query documentation for the OPC UA .NE
|
||||
|
||||
## 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
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 3
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode"
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
|
||||
dotnet run --project src/Client/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 -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode"
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500
|
||||
```
|
||||
|
||||
@@ -41,10 +41,10 @@ dotnet build ZB.MOM.WW.OtOpcUa.slnx
|
||||
dotnet test ZB.MOM.WW.OtOpcUa.slnx
|
||||
|
||||
# Run the server in dev (foreground)
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Server
|
||||
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
|
||||
|
||||
@@ -61,11 +61,11 @@ Add `-InstallWonderwareHistorian` for the historian sidecar. See the script head
|
||||
## Client CLI
|
||||
|
||||
```bash
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 3
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode"
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- write -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -v 42
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
|
||||
dotnet run --project src/Client/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 -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode"
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- write -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -v 42
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500
|
||||
```
|
||||
|
||||
See [docs/Client.CLI.md](docs/Client.CLI.md) and [docs/Client.UI.md](docs/Client.UI.md).
|
||||
|
||||
+95
-73
@@ -1,75 +1,97 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core/ZB.MOM.WW.OtOpcUa.Core.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ZB.MOM.WW.OtOpcUa.Core.Scripting.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Admin/ZB.MOM.WW.OtOpcUa.Admin.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ZB.MOM.WW.OtOpcUa.Driver.Modbus.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.S7/ZB.MOM.WW.OtOpcUa.Driver.S7.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/ZB.MOM.WW.OtOpcUa.Driver.AbCip.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.Shared/ZB.MOM.WW.OtOpcUa.Client.Shared.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.UI/ZB.MOM.WW.OtOpcUa.Client.UI.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Analyzers/ZB.MOM.WW.OtOpcUa.Analyzers.csproj"/>
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.Tests/ZB.MOM.WW.OtOpcUa.Core.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Server.Tests/ZB.MOM.WW.OtOpcUa.Server.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ZB.MOM.WW.OtOpcUa.Admin.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Analyzers.Tests/ZB.MOM.WW.OtOpcUa.Analyzers.Tests.csproj"/>
|
||||
</Folder>
|
||||
<Folder Name="/src/" />
|
||||
<Folder Name="/src/Core/">
|
||||
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj" />
|
||||
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ZB.MOM.WW.OtOpcUa.Configuration.csproj" />
|
||||
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core/ZB.MOM.WW.OtOpcUa.Core.csproj" />
|
||||
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ZB.MOM.WW.OtOpcUa.Core.Scripting.csproj" />
|
||||
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.csproj" />
|
||||
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.csproj" />
|
||||
<Project Path="src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/src/Server/">
|
||||
<Project Path="src/Server/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj" />
|
||||
<Project Path="src/Server/ZB.MOM.WW.OtOpcUa.Admin/ZB.MOM.WW.OtOpcUa.Admin.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/src/Drivers/">
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ZB.MOM.WW.OtOpcUa.Driver.Modbus.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/ZB.MOM.WW.OtOpcUa.Driver.S7.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/ZB.MOM.WW.OtOpcUa.Driver.AbCip.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/src/Drivers/Driver CLIs/">
|
||||
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.csproj" />
|
||||
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.csproj" />
|
||||
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.csproj" />
|
||||
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.csproj" />
|
||||
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.csproj" />
|
||||
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.csproj" />
|
||||
<Project Path="src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/src/Client/">
|
||||
<Project Path="src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/ZB.MOM.WW.OtOpcUa.Client.Shared.csproj" />
|
||||
<Project Path="src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj" />
|
||||
<Project Path="src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ZB.MOM.WW.OtOpcUa.Client.UI.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/src/Tooling/">
|
||||
<Project Path="src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/ZB.MOM.WW.OtOpcUa.Analyzers.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/" />
|
||||
<Folder Name="/tests/Core/">
|
||||
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.csproj" />
|
||||
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/ZB.MOM.WW.OtOpcUa.Configuration.Tests.csproj" />
|
||||
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/ZB.MOM.WW.OtOpcUa.Core.Tests.csproj" />
|
||||
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests.csproj" />
|
||||
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests.csproj" />
|
||||
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests.csproj" />
|
||||
<Project Path="tests/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.Tests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/Server/">
|
||||
<Project Path="tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/ZB.MOM.WW.OtOpcUa.Server.Tests.csproj" />
|
||||
<Project Path="tests/Server/ZB.MOM.WW.OtOpcUa.Admin.Tests/ZB.MOM.WW.OtOpcUa.Admin.Tests.csproj" />
|
||||
<Project Path="tests/Server/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/Drivers/">
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/Drivers/Driver CLIs/">
|
||||
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/Client/">
|
||||
<Project Path="tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.csproj" />
|
||||
<Project Path="tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.csproj" />
|
||||
<Project Path="tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/Tooling/">
|
||||
<Project Path="tests/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers.Tests/ZB.MOM.WW.OtOpcUa.Analyzers.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -8,7 +8,7 @@ Every driver's subtree starts with a root `FolderState` under the standard OPC U
|
||||
|
||||
## 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.
|
||||
- `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
|
||||
|
||||
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 |
|
||||
|---|---|
|
||||
@@ -65,8 +65,8 @@ Drivers that implement `IRediscoverable` fire `OnRediscoveryNeeded` when their b
|
||||
|
||||
## Key source files
|
||||
|
||||
- `src/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/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs` — builder contract
|
||||
- `src/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/OpcUa/GenericDriverNodeManager.cs` — orchestration + `CapturingBuilder`
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — OPC UA materialization (`IAddressSpaceBuilder` impl + `NestedBuilder`)
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs` — builder contract
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs` — driver discovery capability
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs` — per-attribute descriptor
|
||||
|
||||
@@ -15,7 +15,7 @@ historical reference.
|
||||
| **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` |
|
||||
|
||||
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
|
||||
to the OPC UA condition node managers. Driver-native transitions take
|
||||
precedence over sub-attribute synthesis when both arrive for the same
|
||||
|
||||
+3
-3
@@ -9,12 +9,12 @@ The CLI is the primary tool for operators and developers to test and interact wi
|
||||
## Build and Run
|
||||
|
||||
```bash
|
||||
cd src/ZB.MOM.WW.OtOpcUa.Client.CLI
|
||||
cd src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI
|
||||
dotnet build
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
```bash
|
||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests
|
||||
dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests
|
||||
```
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ The UI provides a single-window interface for browsing the address space, readin
|
||||
## Build and Run
|
||||
|
||||
```bash
|
||||
cd src/ZB.MOM.WW.OtOpcUa.Client.UI
|
||||
cd src/Client/ZB.MOM.WW.OtOpcUa.Client.UI
|
||||
dotnet build
|
||||
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:
|
||||
|
||||
```bash
|
||||
dotnet test tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests
|
||||
dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests
|
||||
```
|
||||
|
||||
Tests use:
|
||||
|
||||
@@ -10,7 +10,7 @@ TwinCAT). Shares `Driver.Cli.Common` with the others.
|
||||
## Build + run
|
||||
|
||||
```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
|
||||
|
||||
@@ -10,7 +10,7 @@ others.
|
||||
## Build + run
|
||||
|
||||
```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
|
||||
@@ -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
|
||||
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
|
||||
wire-level validation. The CLI itself is correct regardless of which endpoint
|
||||
you target.
|
||||
|
||||
@@ -17,7 +17,7 @@ process Host arrangement required. The CLI loads `FocasDriver` with
|
||||
components.
|
||||
|
||||
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
|
||||
without a real CNC. See
|
||||
[drivers/FOCAS-Test-Fixture.md](drivers/FOCAS-Test-Fixture.md).
|
||||
@@ -25,14 +25,14 @@ without a real CNC. See
|
||||
## Build + run
|
||||
|
||||
```powershell
|
||||
dotnet build src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli -- --help
|
||||
dotnet build src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli
|
||||
dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli -- --help
|
||||
```
|
||||
|
||||
Or publish a self-contained binary:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ without copy-paste.
|
||||
## Build + run
|
||||
|
||||
```powershell
|
||||
dotnet build src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -- --help
|
||||
dotnet build src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli
|
||||
dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli -- --help
|
||||
```
|
||||
|
||||
Or publish a self-contained binary:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Fourth of four driver test-client CLIs.
|
||||
## Build + run
|
||||
|
||||
```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
|
||||
|
||||
@@ -10,7 +10,7 @@ Fifth (final) of the driver test-client CLIs.
|
||||
## Build + run
|
||||
|
||||
```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
|
||||
|
||||
+2
-2
@@ -37,7 +37,7 @@ Every driver CLI exposes the same four verbs:
|
||||
|
||||
## 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
|
||||
`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
|
||||
project (hardware-gated). 122 unit tests cumulative across the first five
|
||||
(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.
|
||||
|
||||
@@ -4,7 +4,7 @@ Two distinct change-detection paths feed the running server: driver-backend redi
|
||||
|
||||
## 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
|
||||
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:
|
||||
|
||||
- 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.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -61,9 +61,9 @@ Subscriptions for unchanged references stay live across rebuilds — their ref-c
|
||||
|
||||
## Key source files
|
||||
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs` — backend-change capability
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — discovery orchestration
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs` — `ReinitializeAsync` contract
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Admin/Services/GenerationService.cs` — publish-flow driver
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs` — backend-change capability
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — discovery orchestration
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs` — `ReinitializeAsync` contract
|
||||
- `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/admin-ui.md` — DiffViewer + draft-revision-token flow
|
||||
|
||||
+12
-12
@@ -1,14 +1,14 @@
|
||||
# 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
|
||||
|
||||
`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.
|
||||
- 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.
|
||||
- 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 `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/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/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.
|
||||
- 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.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
|
||||
|
||||
@@ -79,10 +79,10 @@ Certificate stores default to `%LOCALAPPDATA%\OPC Foundation\pki\` (directory-ba
|
||||
|
||||
## Key source files
|
||||
|
||||
- `src/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/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs` — programmatic `ApplicationConfiguration` + lifecycle
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs` — driver registration
|
||||
- `src/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/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` — stack-to-evaluator bridge
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs` — `StandardServer` subclass + `ImpersonateUser` hook
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — per-driver `CustomNodeManager2` + dispatch surface
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs` — programmatic `ApplicationConfiguration` + lifecycle
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs` — driver registration
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — Polly pipeline entry point
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/` — Phase 6.2 permission trie + evaluator
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` — stack-to-evaluator bridge
|
||||
|
||||
+3
-2
@@ -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` |
|
||||
| [Subscriptions.md](v1/Subscriptions.md) | Monitored items → `ISubscribable` + per-driver subscription refcount (v1 archive) |
|
||||
| [AlarmTracking.md](v1/AlarmTracking.md) | `IAlarmSource` + `AlarmSurfaceInvoker` + OPC UA alarm conditions (v1 archive) |
|
||||
| [DataTypeMapping.md](v1/DataTypeMapping.md) | Per-driver `DriverAttributeInfo` → OPC UA variable types (v1 archive — live mapping is in `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`) |
|
||||
| [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` |
|
||||
| [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 |
|
||||
@@ -55,6 +55,7 @@ For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics
|
||||
| [Configuration.md](v1/Configuration.md) | appsettings bootstrap + Config DB + Admin UI draft/publish (v1 archive — `OTOPCUA_GALAXY_*` env vars now live in mxaccessgw config) |
|
||||
| [security.md](security.md) | Transport security profiles, LDAP auth, ACL trie, role grants, OTOPCUA0001 analyzer |
|
||||
| [Redundancy.md](Redundancy.md) | `RedundancyCoordinator`, `ServiceLevelCalculator`, apply-lease, Prometheus metrics |
|
||||
| [Reservations.md](Reservations.md) | Fleet-wide ZTag / SAPID external-ID reservations — publish-time claim, release flow |
|
||||
| [ServiceHosting.md](ServiceHosting.md) | Two-process deploy (Server + Admin) install/uninstall, plus the optional `OtOpcUaWonderwareHistorian` sidecar |
|
||||
| [StatusDashboard.md](StatusDashboard.md) | Pointer — superseded by [v2/admin-ui.md](v2/admin-ui.md) |
|
||||
|
||||
@@ -97,7 +98,7 @@ Design decisions + phase plans + execution notes. Load-bearing cross-references
|
||||
- [v2/test-data-sources.md](v2/test-data-sources.md) — integration-test simulator matrix (includes the pinned libplctag `ab_server` version for AB CIP tests)
|
||||
- [v2/multi-host-dispatch.md](v2/multi-host-dispatch.md) — per-PLC circuit breakers (Phase 6.1 decision #144)
|
||||
- [v2/v2-release-readiness.md](v2/v2-release-readiness.md) — release-readiness tracker
|
||||
- [v2/lmx-followups.md](v2/lmx-followups.md) — historical Galaxy-bridge follow-ups (pre-PR-7.2)
|
||||
- [v2/phase-7-status.md](v2/phase-7-status.md) — Phase 7 reconciliation: what shipped vs. the plan, and the five remaining gaps
|
||||
- [v2/implementation/phase-*-*.md](v2/implementation/) — per-phase execution plans with exit-gate evidence
|
||||
|
||||
## v1 archive
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 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
|
||||
|
||||
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.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.
|
||||
|
||||
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
|
||||
|
||||
- `src/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/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/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs`, `IWritable.cs`, `WriteIdempotentAttribute.cs`
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `OnReadValue` / `OnWriteValue` hooks
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs` — classification-to-role policy
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs` — Phase 6.2 trie gate
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — `ExecuteAsync` / `ExecuteWriteAsync`
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs`, `IWritable.cs`, `WriteIdempotentAttribute.cs`
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@
|
||||
|
||||
OtOpcUa supports OPC UA **non-transparent** warm/hot redundancy. Two (or more) OtOpcUa Server processes run side-by-side, share the same Config DB, the same driver backends (Galaxy ZB, MXAccess runtime, remote PLCs), and advertise the same OPC UA node tree. Each process owns a distinct `ApplicationUri`; OPC UA clients see both endpoints via the standard `ServerUriArray` and pick one based on the `ServiceLevel` that each server publishes.
|
||||
|
||||
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 |
|
||||
|---|---|
|
||||
@@ -18,7 +18,7 @@ The redundancy surface lives in `src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/`:
|
||||
|
||||
## 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 |
|
||||
|---|---|
|
||||
@@ -64,7 +64,7 @@ Because role transitions are **operator-driven** (write `RedundancyRole` in the
|
||||
|
||||
## 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 |
|
||||
|---|---|---|---|
|
||||
@@ -77,7 +77,7 @@ Admin `Program.cs` wires OpenTelemetry to the Prometheus exporter when `Metrics:
|
||||
|
||||
## 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
|
||||
|
||||
@@ -96,7 +96,7 @@ Role swaps, stand-alone promotions, and base-level adjustments all happen throug
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# External-ID Reservations
|
||||
|
||||
The reservation subsystem guarantees that an asset's **external identifiers**
|
||||
— its `ZTag` and `SAPID` — belong to exactly one piece of equipment across the
|
||||
entire fleet, for all time. It is the mechanism that stops two pieces of
|
||||
equipment (in the same cluster or different clusters, in the current generation
|
||||
or an old one) from silently claiming the same plant tag.
|
||||
|
||||
This is **decision #124** in `docs/v2/plan.md`.
|
||||
|
||||
## What a reservation is
|
||||
|
||||
An `ExternalIdReservation` row is a permanent, fleet-wide claim on one
|
||||
identifier value by one `EquipmentUuid`. There are two kinds
|
||||
(`ReservationKind`):
|
||||
|
||||
| Kind | What it is |
|
||||
|---------|------------|
|
||||
| `ZTag` | The plant's tag identity for a physical asset. |
|
||||
| `SAPID` | The asset's SAP record ID. |
|
||||
|
||||
An `Equipment` row may carry a `ZTag`, a `SAPID`, both, or neither. Whenever it
|
||||
carries one and the generation is published, a reservation is created for that
|
||||
value.
|
||||
|
||||
## Why it sits outside the generation flow
|
||||
|
||||
Every other part of the configuration is **generation-versioned** — authored in
|
||||
a draft, promoted by publish, superseded by the next publish, and reversible by
|
||||
rollback. Reservations deliberately are **not**.
|
||||
|
||||
The reason: a single ZTag can legitimately appear in many places at once — the
|
||||
current published generation, several superseded generations, and a piece of
|
||||
equipment that has since been disabled. A per-generation uniqueness index would
|
||||
fail the instant you roll back to an older generation or re-enable disabled
|
||||
equipment, because the "old" copy of the identifier is still on disk.
|
||||
|
||||
So the reservation table is a flat, fleet-wide ledger that lives *beside* the
|
||||
generation flow. It is append-mostly: rows are created, their `LastPublishedAt`
|
||||
is refreshed, and they are eventually *released* — but never silently deleted.
|
||||
|
||||
## The table
|
||||
|
||||
`ExternalIdReservation` (Config DB):
|
||||
|
||||
| Column | Notes |
|
||||
|--------------------|-------|
|
||||
| `ReservationId` | Surrogate PK (`NEWSEQUENTIALID()`). |
|
||||
| `Kind` | `ZTag` or `SAPID`. |
|
||||
| `Value` | The reserved identifier string. |
|
||||
| `EquipmentUuid` | The equipment that owns the claim. Stays bound even when that equipment is disabled. |
|
||||
| `ClusterId` | The first cluster to publish the reservation. |
|
||||
| `FirstPublishedAt` / `FirstPublishedBy` | When and by whom the claim was first made. |
|
||||
| `LastPublishedAt` | Refreshed on every subsequent publish that re-asserts the same `(Kind, Value, EquipmentUuid)`. |
|
||||
| `ReleasedAt` / `ReleasedBy` / `ReleaseReason` | Non-null once a FleetAdmin explicitly releases the claim. A row with `ReleasedAt IS NULL` is *active*. |
|
||||
|
||||
There is no foreign key from `EquipmentUuid` / `ClusterId` to their tables — by
|
||||
design, so a reservation survives the deletion or disabling of the equipment
|
||||
that owns it.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
### 1. Authoring
|
||||
|
||||
You give an `Equipment` row a `ZTag` and/or `SAPID` in a **draft** generation —
|
||||
either by hand in the draft editor or via equipment CSV import. Nothing is
|
||||
reserved yet; the draft is just a proposal.
|
||||
|
||||
> Equipment CSV import does **not** pre-check reservation conflicts (tracked as
|
||||
> task #197). A conflict introduced by import surfaces at publish time, below.
|
||||
|
||||
### 2. Publish precheck
|
||||
|
||||
`sp_PublishGeneration` runs the draft validation first. If a `ZTag` or `SAPID`
|
||||
in the draft is already reserved — `ReleasedAt IS NULL` — by a **different**
|
||||
`EquipmentUuid`, the publish is rejected:
|
||||
|
||||
```
|
||||
BadDuplicateExternalIdentifier: a ZTag in the draft is reserved by a
|
||||
different EquipmentUuid
|
||||
```
|
||||
|
||||
The same value owned by the *same* `EquipmentUuid` is fine — that is just the
|
||||
asset keeping its identifier across generations.
|
||||
|
||||
### 3. Publish (the reservation is created)
|
||||
|
||||
When the publish succeeds, `sp_PublishGeneration` runs a `MERGE` into
|
||||
`ExternalIdReservation` for every `ZTag`/`SAPID` in the published generation:
|
||||
|
||||
- **New** `(Kind, Value, EquipmentUuid)` → a reservation row is **inserted**.
|
||||
`FirstPublishedBy` is the publishing user; `ClusterId` is the publishing
|
||||
cluster.
|
||||
- **Already present** → only `LastPublishedAt` is bumped.
|
||||
|
||||
So the *first* publish of an equipment carrying a ZTag is what claims that ZTag
|
||||
for the fleet. After that the claim is permanent — it survives the equipment
|
||||
being disabled, the generation being superseded, or a rollback.
|
||||
|
||||
### 4. Release
|
||||
|
||||
Reusing an identifier for a **different** piece of equipment requires a
|
||||
FleetAdmin to explicitly release the existing claim. Release runs
|
||||
`sp_ReleaseExternalIdReservation`, which:
|
||||
|
||||
- Requires a non-empty **reason** — a hard audit invariant; the procedure
|
||||
raises an error without one.
|
||||
- Stamps `ReleasedAt`, `ReleasedBy` (`SUSER_SNAME()`), and `ReleaseReason`
|
||||
rather than deleting the row, so the history is preserved.
|
||||
- Once released, the `(Kind, Value)` pair is free — a different
|
||||
`EquipmentUuid` can claim it on a future publish.
|
||||
|
||||
Release the claim **only** when the physical asset is permanently retired and
|
||||
its identifier genuinely needs to be reused. A reservation is meant to be
|
||||
permanent for the life of the asset.
|
||||
|
||||
## The Admin page
|
||||
|
||||
`/reservations` (Admin UI) is the operator surface. It is **FleetAdmin-only**
|
||||
(the `CanPublish` policy).
|
||||
|
||||
- **Active** table — every reservation with `ReleasedAt IS NULL`: kind, value,
|
||||
owning `EquipmentUuid`, cluster, and the first/last publish stamps. Each row
|
||||
has a **Release…** action.
|
||||
- **Released** table — the 100 most recently released reservations, with the
|
||||
releasing user and reason.
|
||||
- **Release dialog** — opened from an active row; it requires a reason before
|
||||
the Release button will submit, mirroring the procedure's audit invariant.
|
||||
|
||||
You cannot *create* a reservation from this page — reservations only ever come
|
||||
into existence as a side-effect of publishing a generation. The page is for
|
||||
inspection and for the release flow.
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/v2/plan.md` — decision #124 (reservations outside the generation flow).
|
||||
- `docs/v2/admin-ui.md` — § "Release an external-ID reservation".
|
||||
- `docs/v2/config-db-schema.md` — full Config DB schema.
|
||||
- `OpcUaServer.md` — generations, draft/publish flow.
|
||||
+14
-14
@@ -6,7 +6,7 @@ This file covers the engine internals — predicate evaluation, state machine, p
|
||||
|
||||
## Definition shape
|
||||
|
||||
`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 |
|
||||
|---|---|
|
||||
@@ -100,26 +100,26 @@ Emissions map into `AlarmEventArgs` as `AlarmType = Kind.ToString()`, `SourceNod
|
||||
|
||||
## 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.
|
||||
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.
|
||||
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.
|
||||
|
||||
## Key source files
|
||||
|
||||
- `src/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/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/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/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/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/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs` — `IReadable` adapter exposing `ActiveState` to OPC UA variable reads
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs` — orchestrator, cascade wiring, shelving timer, `OnEvent` emission
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs` — `IAlarmSource` adapter over the engine
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs` — runtime definition record
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs` — pure-function state machine + `TransitionResult` / `EmissionKind`
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs` — persisted state record + `AlarmComment` audit entry + `ShelvingState`
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmPredicateContext.cs` — script-side `ScriptContext` (read-only, write rejected)
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmTypes.cs` — `AlarmKind` + the four Part 9 enums
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs` — `{path}` placeholder resolver
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs` — persistence contract + `InMemoryAlarmStateStore` default
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — composition, config-row projection, historian routing
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/ScriptedAlarmReadable.cs` — `IReadable` adapter exposing `ActiveState` to OPC UA variable reads
|
||||
|
||||
@@ -7,9 +7,9 @@ with a distinct runtime and install surface:
|
||||
|
||||
| Process | Project | Runtime | Platform | Responsibility |
|
||||
|---|---|---|---|---|
|
||||
| **OtOpcUa Server** | `src/ZB.MOM.WW.OtOpcUa.Server` | .NET 10 | x64 | Hosts the OPC UA endpoint; loads every driver in-process (Modbus, S7, AbCip, AbLegacy, TwinCAT, FOCAS, OPC UA Client, Galaxy via mxaccessgw); exposes `/healthz`. |
|
||||
| **OtOpcUa Admin** | `src/ZB.MOM.WW.OtOpcUa.Admin` | .NET 10 (ASP.NET Core / Blazor Server) | x64 | Operator UI for Config DB editing + fleet status, SignalR hubs (`FleetStatusHub`, `AlertHub`), Prometheus `/metrics`. |
|
||||
| **OtOpcUa 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 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/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/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
|
||||
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
|
||||
sidecar that wraps the Wonderware Historian SDK (which is .NET
|
||||
Framework only). The pipe IPC contract is in
|
||||
`src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/Contracts/`
|
||||
`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/Contracts/`
|
||||
and the sidecar's pipe handler lives at
|
||||
`src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Pipe/`.
|
||||
`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Pipe/`.
|
||||
|
||||
Install via the `-InstallWonderwareHistorian` switch on
|
||||
`scripts/install/Install-Services.ps1`.
|
||||
|
||||
+26
-26
@@ -97,13 +97,13 @@ Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md) Option B,
|
||||
|
||||
`ITagUpstreamSource` and `IHistoryWriter` are the two ports the engine requires from its host. Both live in `Core.VirtualTags`. In the Server process:
|
||||
|
||||
- **`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`).
|
||||
- **`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.
|
||||
- **`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/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`.
|
||||
|
||||
## 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`.
|
||||
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
|
||||
|
||||
- `src/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/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/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/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/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/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/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/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/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/ZB.MOM.WW.OtOpcUa.Server/Phase7/CachedTagUpstreamSource.cs` — production `ITagUpstreamSource`
|
||||
- `src/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/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/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptContext.cs` — abstract `ctx` API scripts see
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptGlobals.cs` — generic globals wrapper naming the field `ctx`
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs` — assembly allow-list + imports
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs` — post-compile semantic deny-list
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptEvaluator.cs` — three-step compile pipeline
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/TimedScriptEvaluator.cs` — 250ms default timeout wrapper
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/CompiledScriptCache.cs` — SHA-256-keyed compile cache
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs` — static `ctx.GetTag` / `ctx.SetVirtualTag` inference
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLoggerFactory.cs` — per-script Serilog logger
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogCompanionSink.cs` — error mirror to main log
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagDefinition.cs` — per-tag config record
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagContext.cs` — evaluation-scoped `ctx`
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs` — Kahn topo-sort + iterative Tarjan SCC
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs` — load / evaluate / cascade pipeline
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/TimerTriggerScheduler.cs` — periodic re-evaluation
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ITagUpstreamSource.cs` — driver-tag read + subscribe port
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/IHistoryWriter.cs` — historize sink port + `NullHistoryWriter`
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs` — `IReadable` + `ISubscribable` adapter
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/CachedTagUpstreamSource.cs` — production `ITagUpstreamSource`
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/DriverSubscriptionBridge.cs` — driver `ISubscribable` → cache feed
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — row projection + engine instantiation
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` — lifecycle host: load rows, compose, wire bridge
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `SelectReadable` + `IsWriteAllowedBySource` dispatch kernel
|
||||
|
||||
@@ -4,7 +4,7 @@ Coverage map + gap inventory for the AB Legacy (PCCC) driver — SLC 500 /
|
||||
MicroLogix / PLC-5 / LogixPccc-mode.
|
||||
|
||||
**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
|
||||
compose profiles (`slc500` / `micrologix` / `plc5`). Scaffold passes
|
||||
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
|
||||
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
|
||||
tests (parametric read across families, SLC500 write-then-read). Reuses
|
||||
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
|
||||
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")]`.
|
||||
The driver accepts `IAbLegacyTagFactory` via ctor DI; every test
|
||||
supplies a `FakeAbLegacyTag`.
|
||||
@@ -113,16 +113,16 @@ cover the common ones but uncommon ones (`R` counters, `S` status 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
|
||||
- `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
|
||||
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
|
||||
- `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
|
||||
- `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
|
||||
- `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
|
||||
|
||||
@@ -126,7 +126,7 @@ behaviours from unit-only to end-to-end wire-level coverage:
|
||||
```powershell
|
||||
$env:AB_SERVER_PROFILE = 'emulate'
|
||||
$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
|
||||
@@ -154,7 +154,7 @@ via `AbServerProfileGate.SkipUnless`):
|
||||
— #177 ALMD projection, verified against the real ALMD instruction
|
||||
|
||||
**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
|
||||
project is authored.
|
||||
|
||||
@@ -201,16 +201,16 @@ options are roughly:
|
||||
|
||||
See also:
|
||||
|
||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerFixture.cs`
|
||||
- `tests/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/AbServerFixture.cs`
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfile.cs`
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs`
|
||||
— `AB_SERVER_PROFILE` tier gate
|
||||
- `tests/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/AbCipReadSmokeTests.cs`
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/` — ab_server
|
||||
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
|
||||
- `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
|
||||
- `docs/v2/test-data-sources.md` §2 — the broader test-data-source picking
|
||||
rationale this fixture slots into
|
||||
|
||||
@@ -6,7 +6,7 @@ Coverage map + gap inventory for the FANUC FOCAS2 CNC driver.
|
||||
via the pure-managed [`Focas.Wire`](https://github.com/Ladder99/focas-mock/tree/main/dotnet/Focas.Wire)
|
||||
client. Integration tests run the managed driver end-to-end against the
|
||||
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
|
||||
real `fwlibe64.dll`.
|
||||
|
||||
@@ -21,7 +21,7 @@ but the mock's wire responder covers every FOCAS call OtOpcUa issues.
|
||||
|
||||
### 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`:
|
||||
|
||||
- `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)
|
||||
|
||||
`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:
|
||||
|
||||
**Docker compose up** — tests skip when the TCP probe to
|
||||
@@ -120,10 +120,10 @@ stays as the CI quality gate.
|
||||
|
||||
```powershell
|
||||
# 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.
|
||||
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
|
||||
@@ -131,20 +131,20 @@ Or use `scripts/integration/run-focas.ps1` which wraps compose up / test
|
||||
|
||||
## 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
|
||||
- `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
|
||||
- `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
|
||||
- `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
|
||||
- `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
|
||||
- `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
|
||||
- `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
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs` —
|
||||
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs` —
|
||||
per-series range validator
|
||||
- `docs/v2/focas-version-matrix.md` — authoritative range reference
|
||||
|
||||
@@ -19,7 +19,7 @@ protocol using the documented command IDs. Writes return
|
||||
|
||||
| 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
|
||||
retired — the managed wire client removes the native-crash blast radius
|
||||
@@ -205,10 +205,10 @@ latency spike once per cadence.
|
||||
|
||||
## 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 /
|
||||
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
|
||||
[`docs/v2/implementation/focas-wire-protocol.md`](../v2/implementation/focas-wire-protocol.md)
|
||||
for what the simulator emits vs. real CNC behaviour.
|
||||
|
||||
@@ -49,7 +49,7 @@ for the v2-final architecture.
|
||||
|
||||
## 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 |
|
||||
|--------|------|
|
||||
@@ -93,7 +93,7 @@ Full per-field descriptions live in `Config/GalaxyDriverOptions.cs`. The full JS
|
||||
|
||||
## 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.
|
||||
- **Performance + soak**: see [docs/v2/Galaxy.Performance.md](../v2/Galaxy.Performance.md).
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ shaped (neither is a Modbus-side concept).
|
||||
|
||||
- **Simulator**: `pymodbus` (Python, BSD) launched as a pinned Docker
|
||||
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.
|
||||
- **Lifecycle**: `ModbusSimulatorFixture` (collection-scoped) TCP-probes
|
||||
`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
|
||||
|
||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs`
|
||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs`
|
||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Mitsubishi/MitsubishiProfile.cs`
|
||||
- `tests/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/ModbusSimulatorFixture.cs`
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs`
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Mitsubishi/MitsubishiProfile.cs`
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500Profile.cs`
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/` —
|
||||
Dockerfile + compose + per-family JSON profiles
|
||||
|
||||
@@ -18,7 +18,7 @@ image (follow-up).
|
||||
## What the fixture is
|
||||
|
||||
**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`
|
||||
on `opc.tcp://localhost:50000`. `OpcPlcFixture` probes the port at
|
||||
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).
|
||||
|
||||
**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
|
||||
OPCFoundation.NetStandard `Session` surface is wrapped behind an interface
|
||||
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
|
||||
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
|
||||
FakeDriver. An `OpcUaClientLiveLoopbackTests` that connects the client
|
||||
driver to that server would give:
|
||||
@@ -161,10 +161,10 @@ Beyond that:
|
||||
|
||||
## 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`
|
||||
- `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
|
||||
- `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
|
||||
piggyback on
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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`)
|
||||
- `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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ session types, PUT/GET-disabled enforcement — all need real hardware.
|
||||
## What the fixture is
|
||||
|
||||
**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`
|
||||
on `localhost:1102` (pinned `python:3.12-slim-bookworm` base +
|
||||
`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`
|
||||
/ `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
|
||||
branches, probe-loop contract. All tests tagged
|
||||
`[Trait("Category", "Unit")]`.
|
||||
@@ -115,7 +115,7 @@ from field deployments, not from the test suite.
|
||||
|
||||
## Key fixture / config files
|
||||
|
||||
- `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` — unit tests only, no harness
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs` — ctor takes
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/` — unit tests only, no harness
|
||||
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs` — ctor takes
|
||||
`IS7ClientFactory` which tests fake; docstring lines 8-20 note the deferred
|
||||
integration fixture
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Coverage map + gap inventory for the Beckhoff TwinCAT ADS driver.
|
||||
|
||||
**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
|
||||
`[TwinCATFact]` methods + one 16-case `[TwinCATTheory]` = 30 test cases** end-to-end
|
||||
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
|
||||
|
||||
**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
|
||||
`TWINCAT_TARGET_HOST` (defaults to `localhost`) + requires
|
||||
`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
|
||||
`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
|
||||
`AddDeviceNotification` flow so tests can trigger callbacks without a running
|
||||
runtime.
|
||||
@@ -174,13 +174,13 @@ license-rotation automation, and a dedicated lab IPC.
|
||||
|
||||
## 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
|
||||
- `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]`)
|
||||
- `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
|
||||
- `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
|
||||
- `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)`
|
||||
|
||||
@@ -332,7 +332,7 @@ depends on a specific A-PR — see the sequencing matrix below.
|
||||
|
||||
**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`
|
||||
family. Add a branch:
|
||||
```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
|
||||
`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 /
|
||||
clear); assert each fires `OnAlarmEvent` on the driver with correct
|
||||
@@ -365,7 +365,7 @@ dispatch).
|
||||
|
||||
**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:
|
||||
```csharp
|
||||
public sealed class GalaxyDriver
|
||||
@@ -402,7 +402,7 @@ dispatch).
|
||||
|
||||
**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
|
||||
whether the driver is `IAlarmSource`. If yes, prefer the
|
||||
`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:**
|
||||
|
||||
- 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
|
||||
named-pipe IPC using the **already-defined**
|
||||
`WriteAlarmEventsRequest` / `WriteAlarmEventsReply` contracts at
|
||||
@@ -498,7 +498,7 @@ storage) plug into the same path.
|
||||
## Track C — historian sidecar wires the dormant write path
|
||||
|
||||
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
|
||||
`aahClientManaged` x64 and serves a named-pipe IPC for read operations.
|
||||
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
|
||||
|
||||
**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
|
||||
`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
|
||||
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 →
|
||||
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
|
||||
|
||||
**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
|
||||
`BuildHistorian()` call.
|
||||
@@ -858,9 +858,9 @@ during the original install (see commit `80104ca`).
|
||||
output):
|
||||
```powershell
|
||||
$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"
|
||||
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"
|
||||
```
|
||||
|
||||
@@ -1086,19 +1086,19 @@ needed); land B.4 last and only after end-of-epic gate is green.
|
||||
|
||||
**lmxopcua — Galaxy driver + server (Track B):**
|
||||
|
||||
- `src\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\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\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\GalaxyDriver.cs` (B.2)
|
||||
- `src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\GalaxyDriverFactory.cs` (B.2)
|
||||
- `src\ZB.MOM.WW.OtOpcUa.Server\OpcUa\DriverNodeManager.cs` (B.3)
|
||||
- `src\ZB.MOM.WW.OtOpcUa.Server\Alarms\AlarmConditionService.cs` (B.3)
|
||||
- `src\ZB.MOM.WW.OtOpcUa.Server\Phase7\Phase7Composer.cs` (B.4)
|
||||
- `src\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\ZB.MOM.WW.OtOpcUa.Server.Tests\Alarms\` (B.3)
|
||||
- `tests\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests\` (B.4 — new tests)
|
||||
- `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\EventPump.cs` (B.1)
|
||||
- `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\MxAccessSeverityMapper.cs` *(new — B.1)*
|
||||
- `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\IGalaxyAlarmAcknowledger.cs` *(new — B.2)*
|
||||
- `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\Runtime\GatewayGalaxyAlarmAcknowledger.cs` *(new — B.2)*
|
||||
- `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\GalaxyDriver.cs` (B.2)
|
||||
- `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\GalaxyDriverFactory.cs` (B.2)
|
||||
- `src\Server\ZB.MOM.WW.OtOpcUa.Server\OpcUa\DriverNodeManager.cs` (B.3)
|
||||
- `src\Server\ZB.MOM.WW.OtOpcUa.Server\Alarms\AlarmConditionService.cs` (B.3)
|
||||
- `src\Server\ZB.MOM.WW.OtOpcUa.Server\Phase7\Phase7Composer.cs` (B.4)
|
||||
- `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client\SidecarAlarmHistorianWriter.cs` *(new — B.4)*
|
||||
- `tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests\Runtime\` (B.1, B.2)
|
||||
- `tests\Server\ZB.MOM.WW.OtOpcUa.Server.Tests\Alarms\` (B.3)
|
||||
- `tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests\` (B.4 — new tests)
|
||||
- `docs\drivers\Galaxy.md` (B.5)
|
||||
- `docs\AlarmTracking.md` *(new — B.5)*
|
||||
- `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):**
|
||||
|
||||
- `src\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\Backend\AahClientManagedAlarmEventWriter.cs` *(new — C.1)*
|
||||
- `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)
|
||||
- `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):**
|
||||
|
||||
@@ -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):**
|
||||
|
||||
- `src\ZB.MOM.WW.OtOpcUa.Core.Abstractions\AlarmEventArgs.cs` (extend)
|
||||
- `src\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\ZB.MOM.WW.OtOpcUa.Client.CLI\Commands\AlarmsCommand.cs` (verbose / json flags)
|
||||
- `src\ZB.MOM.WW.OtOpcUa.Client.UI\ViewModels\AlarmEventViewModel.cs`
|
||||
- `src\ZB.MOM.WW.OtOpcUa.Client.UI\ViewModels\AlarmsViewModel.cs`
|
||||
- `src\ZB.MOM.WW.OtOpcUa.Client.UI\Views\AlarmsView.axaml` (+ `.cs`)
|
||||
- `src\ZB.MOM.WW.OtOpcUa.Client.UI\Views\AckAlarmWindow.axaml` (+ `.cs`)
|
||||
- `src\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\AlarmEventArgs.cs` (extend)
|
||||
- `src\Server\ZB.MOM.WW.OtOpcUa.Server\OpcUa\DriverNodeManager.cs` (Part 9 field population)
|
||||
- `src\Client\ZB.MOM.WW.OtOpcUa.Client.Shared\Models\AlarmEventArgs.cs` (DTO mirror)
|
||||
- `src\Client\ZB.MOM.WW.OtOpcUa.Client.CLI\Commands\AlarmsCommand.cs` (verbose / json flags)
|
||||
- `src\Client\ZB.MOM.WW.OtOpcUa.Client.UI\ViewModels\AlarmEventViewModel.cs`
|
||||
- `src\Client\ZB.MOM.WW.OtOpcUa.Client.UI\ViewModels\AlarmsViewModel.cs`
|
||||
- `src\Client\ZB.MOM.WW.OtOpcUa.Client.UI\Views\AlarmsView.axaml` (+ `.cs`)
|
||||
- `src\Client\ZB.MOM.WW.OtOpcUa.Client.UI\Views\AckAlarmWindow.axaml` (+ `.cs`)
|
||||
- `docs\Client.CLI.md` (alarms section examples)
|
||||
- `docs\Client.UI.md` (Show-details toggle description)
|
||||
- `docs\reqs\ClientRequirements.md` (extend AlarmEventArgs contract)
|
||||
- `docs\AlarmTracking.md` (B.5 — cross-link client examples)
|
||||
- `tests\ZB.MOM.WW.OtOpcUa.Client.Shared.Tests\` (DTO round-trip)
|
||||
- `tests\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.Shared.Tests\` (DTO round-trip)
|
||||
- `tests\Client\ZB.MOM.WW.OtOpcUa.Client.CLI.Tests\` (flag behaviour)
|
||||
- `tests\Client\ZB.MOM.WW.OtOpcUa.Client.UI.Tests\` (view-model bindings)
|
||||
|
||||
Total: ~10 source files added/modified in mxaccessgw server/worker
|
||||
side; ~14 in lmxopcua server/driver side; ~3 in the historian sidecar;
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
# Alarms Worker Wiring Plan
|
||||
|
||||
> **Context**: The alarms-over-gateway epic shipped 19 PRs across the
|
||||
> `lmxopcua` and `mxaccessgw` repos (merged 2026-04-30). Contracts are live;
|
||||
> the sub-attribute fallback path keeps Galaxy alarms functional today. Four
|
||||
> items remain as inert scaffolds gated on a dev-rig finding. This document is
|
||||
> the focused implementation plan for those four items only.
|
||||
>
|
||||
> **Do not duplicate `docs/plans/alarms-over-gateway.md`** — that document is
|
||||
> the full historical record of all 19 PRs. This document covers only what is
|
||||
> still to be done and exactly what blocks each item.
|
||||
>
|
||||
> **This work lives in the mxaccessgw sibling repo** at
|
||||
> `C:\Users\dohertj2\Desktop\mxaccessgw\` — not in this (lmxopcua) repo,
|
||||
> except where lmxopcua changes are noted explicitly.
|
||||
|
||||
---
|
||||
|
||||
## Dev-rig finding that blocks everything (2026-04-30)
|
||||
|
||||
During PR A.2 work the following was discovered on the dev box:
|
||||
|
||||
> The MXAccess COM Toolkit at
|
||||
> `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`
|
||||
> exposes **no alarm-event family** — only `OnDataChange`, `OnWriteComplete`,
|
||||
> `OperationComplete`, `OnBufferedDataChange`.
|
||||
>
|
||||
> AVEVA's `aaAlarmManagedClient` / `ArchestrAAlarmsAndEvents.SDK` assemblies
|
||||
> are **x64-only** and incompatible with the worker's x86 net48 bitness.
|
||||
|
||||
The architectural decision required before any of A.2, A.3/A.4, C.1 can ship:
|
||||
|
||||
> **Either** accept the value-driven sub-attribute path as the production
|
||||
> architecture (operator-comment fidelity is the only v1 regression), **or**
|
||||
> add an x64 alarm-helper sub-process alongside the x86 worker.
|
||||
|
||||
Resolution drives the implementation shape of every item below. The plan
|
||||
presented here assumes the x64 alarm-helper sub-process route (the higher
|
||||
parity option), but notes the sub-attribute-only exit at each step.
|
||||
|
||||
---
|
||||
|
||||
## Discovered AVEVA API surface
|
||||
|
||||
Before implementing, verify the following against the AVEVA SDK actually
|
||||
installed on the dev box and in the mxaccessgw worker's deployment folder:
|
||||
|
||||
| Assembly | Bitness | Likely location | Key types |
|
||||
|----------|---------|-----------------|-----------|
|
||||
| `ArchestrA.MXAccess.dll` | x86 | `C:\Program Files (x86)\ArchestrA\Framework\Bin\` | `IMxAlarmEventSink`, `MxAlarmEventArgs` — **confirm exists at actual version** |
|
||||
| `aaAlarmManagedClient.dll` | x64 | `C:\Program Files\ArchestrA\Framework\Bin\` | `AlarmClient`, `IAlarmConsumer`, `AlarmEventArgs` |
|
||||
| `ArchestrAAlarmsAndEvents.SDK.dll` | x64 | Same or Historian SDK folder | `AlarmHistorianWriter`, `GetAlarmExtendedRec` |
|
||||
|
||||
The AVEVA MXAccess Toolkit reference in the mxaccessgw repo (`gateway.md`) is
|
||||
the canonical API doc for the gateway worker's side. The alarm-client API is
|
||||
documented separately; verify the following call shapes during PR A.2:
|
||||
|
||||
| Operation | Likely API | Notes |
|
||||
|-----------|-----------|-------|
|
||||
| Subscribe to alarm events | `AlarmClient.RegisterConsumer(IAlarmConsumer)` + `AlarmClient.Subscribe(filterSpec)` | Confirm exact method signatures against the SDK version on the dev box |
|
||||
| Receive alarm event | `IAlarmConsumer.OnAlarmEvent(AlarmEventArgs)` callback | Field set: alarm name, source, type, transition kind, severity, timestamps, operator fields |
|
||||
| Acknowledge alarm | `AlarmClient.AcknowledgeAlarm(alarmRef, comment, userPrincipal)` or equivalent | Confirm whether this is synchronous or returns a status |
|
||||
| Query active alarms | `AlarmClient.GetAlarmExtendedRec(filter)` or `GetActiveAlarms()` | Returns current active set for ConditionRefresh |
|
||||
| Get statistics | `AlarmClient.GetStatistics()` | Optional — useful for worker health checks |
|
||||
|
||||
Record the exact method signatures against the installed SDK before starting
|
||||
A.2 — the proto field set in `OnAlarmTransitionEvent` must match the SDK's
|
||||
actual payload.
|
||||
|
||||
---
|
||||
|
||||
## Dependency order
|
||||
|
||||
```
|
||||
A.2 (worker: AlarmClient subscription)
|
||||
└─► A.3 (gateway: dispatch OnAlarmTransition + AcknowledgeAlarm RPC handler)
|
||||
└─► A.4 (gateway: QueryActiveAlarms RPC handler)
|
||||
└─► lmxopcua B.2 (GalaxyDriver IAlarmSource live)
|
||||
└─► C.1 (sidecar: AahClientManagedAlarmEventWriter live)
|
||||
└─► D.1 (smoke artifact captured)
|
||||
```
|
||||
|
||||
A.2 is the single blocking item. All subsequent items unblock serially once
|
||||
A.2 delivers alarm events through the channel.
|
||||
|
||||
---
|
||||
|
||||
## Item A.2 — Worker: subscribe to MxAccess alarm event source
|
||||
|
||||
**Repo**: `mxaccessgw` — `src\MxGateway.Worker\` (net48, x86)
|
||||
|
||||
**What it needs**:
|
||||
|
||||
The worker must subscribe to AVEVA's alarm events and fan them into the same
|
||||
bounded channel the data-change pump uses, translating each MxAccess alarm
|
||||
event into a `WorkerEvent` proto with family `MX_EVENT_FAMILY_ON_ALARM_TRANSITION`
|
||||
(defined in PR A.1, already merged).
|
||||
|
||||
**Architectural choice determines the implementation path**:
|
||||
|
||||
**Option X1 — aaAlarmManagedClient in a new x64 alarm-helper process**
|
||||
|
||||
Add a second worker-mode sub-process (`MxGateway.AlarmWorker`, net8.0 x64)
|
||||
alongside the existing x86 worker. The AlarmWorker:
|
||||
|
||||
1. Loads `aaAlarmManagedClient.dll` (x64) on startup.
|
||||
2. Calls `AlarmClient.RegisterConsumer` with a `WorkerAlarmConsumer` sink.
|
||||
3. Calls `AlarmClient.Subscribe` with a session-level filter (all alarms for
|
||||
the session's Galaxy scope).
|
||||
4. Translates each `IAlarmConsumer.OnAlarmEvent` callback into a protobuf
|
||||
`WorkerEvent` (family `ON_ALARM_TRANSITION`) and writes it to an IPC
|
||||
channel readable by the gateway server-side multiplexer.
|
||||
5. Handles session lifecycle: re-subscribes after reconnect; unsubscribes on
|
||||
session close.
|
||||
|
||||
IPC from AlarmWorker to gateway: simplest option is a named pipe or an
|
||||
in-process queue if the AlarmWorker is hosted in the same gateway process
|
||||
space as a separate `IHostedService`.
|
||||
|
||||
**Option X2 — Accept sub-attribute fallback as production (no A.2 work)**
|
||||
|
||||
If the architectural decision is to accept the sub-attribute path as permanent:
|
||||
|
||||
- `MxAccessAlarmEventSink.Attach()` in the worker remains a no-op (as
|
||||
currently coded with the architectural comment).
|
||||
- The `MX_EVENT_FAMILY_ON_ALARM_TRANSITION` proto family stays defined but
|
||||
the gateway never emits events on it.
|
||||
- lmxopcua's `GalaxyDriver` does not implement `IAlarmSource` for the
|
||||
native path; the value-driven sub-attribute path remains the production
|
||||
path.
|
||||
- The only regression vs. v1 is operator-comment fidelity on Galaxy alarms.
|
||||
- C.1 is still needed if scripted-alarm historian write-back is required.
|
||||
|
||||
**What blocks it**: the architectural decision above. Once made, A.2 becomes
|
||||
a 2–3 day implementation task (sub-process plumbing + proto translation +
|
||||
unit tests for the consumer sink cancellation behaviour).
|
||||
|
||||
**Tests to write (when A.2 proceeds)**:
|
||||
|
||||
- `WorkerAlarmConsumerTests` — fake `IAlarmConsumer` source emits canned
|
||||
transitions; assert each produces the correct `WorkerEvent` body shape.
|
||||
- Cancellation/session-close test — closing the session unsubscribes from
|
||||
the AlarmClient cleanly (no leaked `IAlarmConsumer` reference if the
|
||||
worker is recycled mid-session).
|
||||
- Re-subscribe-after-reconnect test — `ReconnectSupervisor` triggers a
|
||||
reconnect; assert the alarm consumer re-attaches to the new session.
|
||||
|
||||
---
|
||||
|
||||
## Item A.3 / A.4 — Gateway: dispatch and RPC handlers
|
||||
|
||||
**Repo**: `mxaccessgw` — `src\MxGateway.Server\`
|
||||
|
||||
**Depends on**: A.2 delivering `WorkerEvent` bodies with family
|
||||
`MX_EVENT_FAMILY_ON_ALARM_TRANSITION`.
|
||||
|
||||
**What it needs**:
|
||||
|
||||
### A.3 — Dispatch + AcknowledgeAlarm
|
||||
|
||||
1. The session-level event multiplexer (`Sessions\SessionEventStream.cs` or
|
||||
equivalent — verify name in the mxaccessgw repo) must recognise the new
|
||||
`WorkerEvent` body and forward it as an `MxEvent` with family
|
||||
`MX_EVENT_FAMILY_ON_ALARM_TRANSITION` to every `StreamEvents` subscriber
|
||||
for that session.
|
||||
|
||||
2. New RPC handler `AcknowledgeAlarm` builds an `AlarmAcknowledgeCommand`
|
||||
worker command and forwards it to the alarm-helper process (Option X1) or
|
||||
the worker's MxAccess session (Option X2 if MxAccess exposes ack). Maps
|
||||
the reply status to `AcknowledgeAlarmReply.MxStatusProxy`.
|
||||
|
||||
3. Authorization: new API scope `invoke:alarm-ack` on the API key. Keys
|
||||
without it receive `PERMISSION_DENIED`. Follow the existing scope-check
|
||||
pattern used by `invoke:write`.
|
||||
|
||||
### A.4 — QueryActiveAlarms
|
||||
|
||||
1. New RPC handler `QueryActiveAlarms` calls `AlarmClient.GetAlarmExtendedRec`
|
||||
(or `GetActiveAlarms` — confirm the method name during implementation)
|
||||
on the alarm-helper process, batches results into `ActiveAlarmSnapshot`
|
||||
proto messages, and streams them back to the caller.
|
||||
|
||||
2. New API scope `invoke:alarm-query` (separate from ack so read-only clients
|
||||
can refresh without ack rights).
|
||||
|
||||
**What blocks A.3/A.4**: A.2 must deliver `WorkerEvent` bodies on the channel.
|
||||
A.3/A.4 are pure dispatch wiring once the events arrive.
|
||||
|
||||
**Tests to write**:
|
||||
|
||||
- A.3 dispatch test — fake worker emits an `AlarmTransition` event; assert
|
||||
the gateway forwards it on the `StreamEvents` channel of every subscribed
|
||||
session (mirrors existing `OnDataChange` dispatch tests).
|
||||
- A.3 AcknowledgeAlarm auth test — existing key without `invoke:alarm-ack`
|
||||
scope returns `PERMISSION_DENIED`.
|
||||
- A.4 pagination test — synthetic active-alarm set of 0 / 1 / 100 entries;
|
||||
assert each streams back as separate `ActiveAlarmSnapshot` messages.
|
||||
- Integration (parity rig — requires dev box with AVEVA platform):
|
||||
trigger a real Galaxy alarm, call `QueryActiveAlarms`, assert the alarm
|
||||
appears in the stream; call `AcknowledgeAlarm`, assert the alarm transitions
|
||||
to `ActiveAcked` and a `Acknowledge` transition event appears on
|
||||
`StreamEvents`.
|
||||
|
||||
---
|
||||
|
||||
## Item C.1 — Historian sidecar: AahClientManagedAlarmEventWriter
|
||||
|
||||
**Repo**: `lmxopcua` — `src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware\`
|
||||
|
||||
**Depends on**: Architectural decision (the sidecar uses `aahClientManaged`
|
||||
x64, which is not bitness-constrained like the worker). C.1 is independently
|
||||
unblockable from A.2 if the goal is to wire up the scripted-alarm historian
|
||||
path.
|
||||
|
||||
**Current state**:
|
||||
|
||||
`SdkAlarmHistorianWriteBackend` in `src\MxGateway.Worker\MxAccess\` is a
|
||||
placeholder returning `RetryPlease`. The lmxopcua sidecar's `WriteAlarmEvents`
|
||||
IPC slot is defined in `Ipc\Contracts.cs` but `Program.cs` constructs
|
||||
`HistorianFrameHandler` without an `alarmWriter` (line 57 per the alarms plan).
|
||||
The `IAlarmEventWriter` interface exists; only the production implementation
|
||||
and the consumer wiring are missing.
|
||||
|
||||
**What it needs**:
|
||||
|
||||
1. New `AahClientManagedAlarmEventWriter.cs` implementing `IAlarmEventWriter`
|
||||
(defined in `Ipc\HistorianFrameHandler.cs`). Calls `aahClientManaged`'s
|
||||
alarm-event write API — same path v1's `GalaxyHistorianWriter` used.
|
||||
Uses `HistorianClusterEndpointPicker` for multi-node routing.
|
||||
Maps `MxStatus` write outcomes to `HistorianWriteOutcome` enum
|
||||
(Ack / PermanentFail / RetryPlease).
|
||||
|
||||
2. `Program.cs` — build `AahClientManagedAlarmEventWriter` next to the
|
||||
existing `BuildHistorian()` call; pass it to `HistorianFrameHandler`.
|
||||
Gate behind `OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED` env var (default `true`
|
||||
when `OTOPCUA_HISTORIAN_ENABLED=true`).
|
||||
|
||||
3. `Install-Services.ps1` — add the new env var to the install-time block.
|
||||
|
||||
**What blocks C.1**: access to the `aahClientManaged` SDK on the dev box
|
||||
(confirmed available per `project_aveva_platform_installed.md` — AVEVA
|
||||
Historian SDK is present). C.1 can proceed without A.2 since the sidecar's
|
||||
`aahClientManaged` is x64 and does not share the worker's x86 bitness
|
||||
constraint.
|
||||
|
||||
**Tests to write**:
|
||||
|
||||
- Outcome-mapping table: every `MxStatus` on alarm-write → expected
|
||||
`HistorianWriteOutcome`.
|
||||
- Batch test: 1 / 100 / 1000 events through a fake `aahClientManaged`
|
||||
writer; assert per-row outcome list parallel to input order.
|
||||
- Cluster failover: primary Historian node returns `BadCommunicationError`;
|
||||
picker rotates to secondary; eventual success.
|
||||
- `Program.cs` seam: assert handler constructed with alarm writer when env
|
||||
var enabled; without it when disabled.
|
||||
- Live integration (parity rig): write a synthetic alarm event through the
|
||||
IPC; query it back via `ReadEvents`; assert round-trip fidelity.
|
||||
|
||||
---
|
||||
|
||||
## Item D.1 — Smoke artifact
|
||||
|
||||
**Repo**: `lmxopcua` (deployment refresh) + `mxaccessgw` (rig verification)
|
||||
|
||||
**Depends on**: A.2, A.3, A.4, and C.1 all passing on the dev rig with a live
|
||||
Galaxy and live Historian.
|
||||
|
||||
**Current state**: The deployment script `Refresh-Services.ps1` (task D.1) has
|
||||
shipped as PR #417 (merged 2026-04-30). What was NOT captured at that time was
|
||||
a smoke artifact — a log snippet or test output confirming that:
|
||||
|
||||
1. An alarm transition event from a live Galaxy alarm reaches lmxopcua's
|
||||
`AlarmConditionService` via the new `IAlarmSource` path (not the fallback).
|
||||
2. A scripted-alarm historian write-back reaches AVEVA Historian via the
|
||||
sidecar `IAlarmEventWriter`.
|
||||
|
||||
**What it needs**:
|
||||
|
||||
Once A.2, A.3, C.1 are wired on the parity rig:
|
||||
|
||||
1. Deploy the updated mxaccessgw (with A.2 / A.3 / A.4 changes).
|
||||
2. Deploy the updated sidecar (with C.1 changes).
|
||||
3. Run `Refresh-Services.ps1` to confirm clean service restarts.
|
||||
4. Trigger a Galaxy alarm (e.g. set an AnalogLimitAlarm attribute out of
|
||||
range in Galaxy IDE).
|
||||
5. Observe the lmxopcua OPC UA alarm surface via the Client CLI:
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
alarms -u opc.tcp://localhost:4840 --subscribe
|
||||
```
|
||||
|
||||
Pass: the alarm condition appears on the OPC UA A&E surface within
|
||||
2 × publishing interval.
|
||||
|
||||
6. Trigger a scripted alarm via the lmxopcua `ScriptedAlarmEngine`
|
||||
(or an OPC UA method call if one is wired).
|
||||
7. Confirm in the AVEVA Historian that the scripted alarm event is stored
|
||||
(query via the Historian client or HistorianWatch tool).
|
||||
|
||||
8. Capture log snippets:
|
||||
- mxaccessgw log: `[INF] AlarmTransition dispatched sessionId=<> alarmRef=<>`
|
||||
- lmxopcua log: `[INF] AlarmConditionService: IAlarmSource event alarmRef=<> origin=Driver`
|
||||
- Sidecar log: `[INF] AahClientManagedAlarmEventWriter: Wrote <n> alarm events`
|
||||
|
||||
9. Commit the log snippets as `docs/plans/alarms-d1-smoke-artifact.md`
|
||||
(a new doc, not this one).
|
||||
|
||||
**What blocks D.1**: all of A.2, A.3, C.1, plus the operator decision on the
|
||||
x64 alarm-helper architecture (or explicit acceptance of the sub-attribute
|
||||
fallback as production).
|
||||
|
||||
---
|
||||
|
||||
## Summary of blocks
|
||||
|
||||
| Item | Blocked by | Estimated effort once unblocked |
|
||||
|------|-----------|--------------------------------|
|
||||
| A.2 | Architectural decision (x64 alarm-helper vs. sub-attribute fallback as production) | 2–3 days implementation; 1 day tests |
|
||||
| A.3 | A.2 delivering WorkerEvent bodies | 1–2 days |
|
||||
| A.4 | A.2 (active-alarm query needs AlarmClient session) | 1 day |
|
||||
| C.1 | aahClientManaged SDK access (available on dev box); NOT blocked by A.2 | 1–2 days |
|
||||
| D.1 | A.2 + A.3 + C.1 all passing on parity rig | 0.5 day (smoke + artifact capture) |
|
||||
|
||||
C.1 can proceed in parallel with A.2 / A.3 since the sidecar's `aahClientManaged`
|
||||
is x64 and does not share the worker bitness constraint.
|
||||
|
||||
---
|
||||
|
||||
## What this plan does NOT cover
|
||||
|
||||
- The value-driven sub-attribute fallback path — already shipped and
|
||||
functional (not being changed).
|
||||
- Track B (lmxopcua EventPump, GalaxyDriver IAlarmSource re-implementation)
|
||||
and Track E (client SDK surface refresh) from the alarms-over-gateway plan —
|
||||
those are in `lmxopcua` and depend on A.3 being live; they follow naturally
|
||||
once A.3 ships.
|
||||
- Galaxy-native alarm historian path — System Platform's own `HistorizeToAveva`
|
||||
toggle on the Galaxy template; not in scope.
|
||||
- Alarm ACL / role-grant surface — already shipped in Phase 6.2.
|
||||
@@ -0,0 +1,497 @@
|
||||
# Live-Hardware Driver Validation Runbooks
|
||||
|
||||
> **Scope**: These runbooks cover the three driver validation tasks that
|
||||
> require physical hardware or a hardware-equivalent live environment and
|
||||
> cannot be satisfied by the Docker-based simulator fixtures or unit tests
|
||||
> alone.
|
||||
>
|
||||
> Driver implementation is complete. The runbooks document the preconditions,
|
||||
> step-by-step procedure, expected results, and how to record the outcome for
|
||||
> each driver that has an open live-hardware gap.
|
||||
|
||||
---
|
||||
|
||||
## 1. FANUC FOCAS — Live CNC Smoke (task #54)
|
||||
|
||||
### Background
|
||||
|
||||
The FOCAS driver (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/`) uses the
|
||||
pure-managed `WireFocasClient` that speaks FOCAS2 over TCP directly (no
|
||||
`Fwlib64.dll`, no P/Invoke). The integration test suite at
|
||||
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/` runs against
|
||||
the `focas-mock` Python server (PDU-verified against `fwlibe64.dll` upstream)
|
||||
and covers all call-shapes the driver issues. What the mock cannot cover:
|
||||
|
||||
- Series-specific firmware quirks (e.g. 0i-F vs 30i-B parameter range limits)
|
||||
- Real CNC Ethernet stack behaviour (TCP keep-alive, session-close edge cases)
|
||||
- Series gating: some driver nodes are conditionally emitted based on
|
||||
`CncSeries` — only a physical CNC can confirm the suppression works
|
||||
|
||||
### Preconditions
|
||||
|
||||
| Item | Requirement |
|
||||
|------|-------------|
|
||||
| CNC hardware | FANUC CNC with Ethernet option enabled; TCP port 8193 reachable from the dev box or from the host running OtOpcUa |
|
||||
| CNC series | Any of: 0i-D, 0i-F, 0i-MF, 0i-TF, 16i, 30i-B, 31i, 32i, Power Motion i |
|
||||
| CNC state | Running state (not E-stop, not alarm) for live axis-data reads |
|
||||
| Network | TCP reachability from OtOpcUa server host to CNC port 8193 |
|
||||
| OtOpcUa | Server built and deployed (`dotnet publish` or running via `dotnet run`) |
|
||||
| Config | DriverInstance row for FOCAS in Config DB (`Type="FOCAS"`, `Backend="wire"`, `Devices[0].HostAddress="focas://<cnc-ip>:8193"`, `Devices[0].Series="<series>"`) |
|
||||
|
||||
### Procedure
|
||||
|
||||
**Step 1 — Verify TCP reachability**
|
||||
|
||||
```powershell
|
||||
Test-NetConnection -ComputerName <cnc-ip> -Port 8193
|
||||
```
|
||||
|
||||
Pass: `TcpTestSucceeded: True`.
|
||||
|
||||
**Step 2 — Start OtOpcUa with FOCAS driver configured**
|
||||
|
||||
Ensure the Config DB has the DriverInstance row. Start the server:
|
||||
|
||||
```powershell
|
||||
sc start OtOpcUa
|
||||
# or for a dev run:
|
||||
dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server
|
||||
```
|
||||
|
||||
Watch the Serilog log for:
|
||||
|
||||
```
|
||||
[INF] FocasDriver initializing device focas://<cnc-ip>:8193 series=<series>
|
||||
[INF] FocasDriver device <cnc-ip>:8193 Connected
|
||||
```
|
||||
|
||||
If `EW_SOCKET (-1)` appears, the TCP endpoint is unreachable or the CNC
|
||||
Ethernet option is not active.
|
||||
|
||||
**Step 3 — Browse the address space**
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
browse -u opc.tcp://localhost:4840 -r -d 3
|
||||
```
|
||||
|
||||
Expected: a node tree containing at minimum:
|
||||
|
||||
```
|
||||
FOCAS/
|
||||
<device>/
|
||||
Identity/
|
||||
SeriesNumber
|
||||
Version
|
||||
MaxAxes
|
||||
Status/
|
||||
RunState
|
||||
Mode
|
||||
EmergencyStop
|
||||
Axes/
|
||||
<X|Y|Z>/
|
||||
AbsolutePosition
|
||||
MachinePosition
|
||||
```
|
||||
|
||||
Nodes suppressed by the `Series` capability gate will be absent — this is
|
||||
correct behaviour.
|
||||
|
||||
**Step 4 — Read identity nodes**
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
read -u opc.tcp://localhost:4840 -n "ns=2;s=FOCAS/<device>/Identity/SeriesNumber"
|
||||
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
read -u opc.tcp://localhost:4840 -n "ns=2;s=FOCAS/<device>/Identity/MaxAxes"
|
||||
```
|
||||
|
||||
Pass: `Good` quality; `SeriesNumber` matches the string printed on the CNC
|
||||
control panel (e.g. `"0i-F"`); `MaxAxes` is a non-zero integer.
|
||||
|
||||
**Step 5 — Read live status and axis data**
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
read -u opc.tcp://localhost:4840 -n "ns=2;s=FOCAS/<device>/Status/RunState"
|
||||
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
read -u opc.tcp://localhost:4840 -n "ns=2;s=FOCAS/<device>/Axes/X/AbsolutePosition"
|
||||
```
|
||||
|
||||
Pass: both return `Good` quality. `AbsolutePosition` is a `Double` (e.g.
|
||||
`-12.3456` mm). Manually compare against the machine's position display.
|
||||
|
||||
**Step 6 — Subscribe and observe polling**
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
subscribe -u opc.tcp://localhost:4840 `
|
||||
-n "ns=2;s=FOCAS/<device>/Status/RunState" -i 500
|
||||
```
|
||||
|
||||
Let run for 30 s while jogging an axis or changing mode on the CNC operator
|
||||
panel. Pass: at least one data-change event received within 5 s; events
|
||||
continue arriving every ~500 ms.
|
||||
|
||||
**Step 7 — 2-minute soak**
|
||||
|
||||
Let the server run for 2 minutes with the subscription active. Pass: no
|
||||
`EW_SOCKET`, `EW_HANDLE`, `EW_BUSY` errors in the Serilog output; subscribed
|
||||
node continues delivering updates.
|
||||
|
||||
**Step 8 — Run the FOCAS e2e script**
|
||||
|
||||
```powershell
|
||||
pwsh scripts/e2e/test-focas.ps1 -ServerUrl opc.tcp://localhost:4840 `
|
||||
-DriverInstance "<device>" -Series "<series>"
|
||||
```
|
||||
|
||||
Pass: script exits 0.
|
||||
|
||||
### Expected results
|
||||
|
||||
| Check | Expected |
|
||||
|-------|----------|
|
||||
| TCP connect to CNC port 8193 | Success |
|
||||
| FOCAS session open (`cnc_allclibhndl3`) | EW_OK (0) in driver log |
|
||||
| `Identity/SeriesNumber` | Matches CNC panel, `Good` quality |
|
||||
| `Identity/MaxAxes` | Non-zero integer, `Good` quality |
|
||||
| `Status/RunState` | Integer 0–3, `Good` quality |
|
||||
| `Axes/X/AbsolutePosition` | Double, `Good` quality, matches display |
|
||||
| Subscribe: events delivered | >= 3 events in 5 s soak |
|
||||
| 2-minute soak: no FOCAS errors | Clean Serilog log |
|
||||
|
||||
### Recording the outcome
|
||||
|
||||
```
|
||||
FOCAS live-CNC smoke — task #54
|
||||
Date: YYYY-MM-DD
|
||||
CNC: <manufacturer> <model> series=<series> firmware=<version>
|
||||
IP: <cnc-ip>:8193
|
||||
OtOpcUa SHA: <git sha>
|
||||
|
||||
TCP connect: PASS
|
||||
Session open: PASS
|
||||
Identity reads: PASS SeriesNumber="<>" MaxAxes=<n>
|
||||
Status read: PASS RunState=<n>
|
||||
Axis read: PASS X/AbsolutePosition=<value>
|
||||
Subscribe: PASS <n> events in 30s
|
||||
2-min soak: PASS no errors
|
||||
e2e script: PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Allen-Bradley CIP — Live Boot (ControlLogix)
|
||||
|
||||
### Background
|
||||
|
||||
The AB CIP driver (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/`) uses
|
||||
`libplctag` 1.6.x. The Docker `ab_server` simulator covers connectivity and
|
||||
atomic type reads (7 integration tests). Live-boot validation is needed to
|
||||
confirm UDT shape-reading, array tag access, and the CIP packing behaviour on
|
||||
a real ControlLogix backplane — all gaps acknowledged in
|
||||
`docs/drivers/AbServer-Test-Fixture.md`.
|
||||
|
||||
AB CIP live-boot was first verified against a ControlLogix rig at PR #222.
|
||||
Continue running before each release.
|
||||
|
||||
### Preconditions
|
||||
|
||||
| Item | Requirement |
|
||||
|------|-------------|
|
||||
| PLC hardware | ControlLogix (preferred) or CompactLogix; firmware 20+ for request packing |
|
||||
| Network | TCP port 44818 reachable from OtOpcUa server host |
|
||||
| PLC state | Running; at least one DINT / REAL / BOOL / STRING controller-scoped tag defined |
|
||||
| OtOpcUa | Server built and deployed |
|
||||
| Config | DriverInstance row: `Type="AbCip"`, `Host="<plc-ip>"`, `Path="1,0"`, `PlcType="ControlLogix"` |
|
||||
|
||||
### Procedure
|
||||
|
||||
**Step 1 — Verify TCP reachability**
|
||||
|
||||
```powershell
|
||||
Test-NetConnection -ComputerName <plc-ip> -Port 44818
|
||||
```
|
||||
|
||||
Pass: `TcpTestSucceeded: True`.
|
||||
|
||||
**Step 2 — Start OtOpcUa and watch driver log**
|
||||
|
||||
```powershell
|
||||
sc start OtOpcUa
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
```
|
||||
[INF] AbCipDriver device <plc-ip> Connected path=1,0 plcType=ControlLogix
|
||||
```
|
||||
|
||||
**Step 3 — Browse the address space**
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
browse -u opc.tcp://localhost:4840 -r -d 3
|
||||
```
|
||||
|
||||
Pass: node tree shows the tags defined in the ControlLogix project (controller-
|
||||
and program-scoped). UDT members appear as child nodes.
|
||||
|
||||
**Step 4 — Read atomic tags**
|
||||
|
||||
```powershell
|
||||
# Read a DINT tag
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
read -u opc.tcp://localhost:4840 -n "ns=2;s=AbCip/<device>/<TagName>"
|
||||
```
|
||||
|
||||
Pass: `Good` quality; value type matches the PLC tag type.
|
||||
|
||||
**Step 5 — Read a UDT member**
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
read -u opc.tcp://localhost:4840 -n "ns=2;s=AbCip/<device>/<UDT>/<MemberName>"
|
||||
```
|
||||
|
||||
Pass: `Good` quality; value matches the live PLC value.
|
||||
|
||||
**Step 6 — Write a DINT tag (if in ReadWrite mode)**
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
write -u opc.tcp://localhost:4840 `
|
||||
-n "ns=2;s=AbCip/<device>/<TagName>" -v 42 -t Int32
|
||||
```
|
||||
|
||||
Verify the new value via a subsequent read or on the PLC HMI.
|
||||
|
||||
Pass: read back returns 42 with `Good` quality.
|
||||
|
||||
**Step 7 — Subscribe to a tag that changes**
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
subscribe -u opc.tcp://localhost:4840 `
|
||||
-n "ns=2;s=AbCip/<device>/<ChangingTag>" -i 500
|
||||
```
|
||||
|
||||
Jog or trigger a value change on the PLC. Pass: events received within 2 s.
|
||||
|
||||
**Step 8 — Override endpoint to docker sim and confirm parity**
|
||||
|
||||
```powershell
|
||||
$env:AB_SERVER_ENDPOINT = "<plc-ip>:44818"
|
||||
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests `
|
||||
--filter "AbServerFact"
|
||||
```
|
||||
|
||||
Pass: all 7 integration tests pass against the live PLC.
|
||||
|
||||
### Expected results
|
||||
|
||||
| Check | Expected |
|
||||
|-------|----------|
|
||||
| TCP connect | Success |
|
||||
| Driver log `Connected` | Present, no error |
|
||||
| Browse | Node tree mirrors PLC tag list |
|
||||
| Atomic read | `Good` quality, correct type |
|
||||
| UDT member read | `Good` quality, correct value |
|
||||
| Write round-trip | Written value reads back |
|
||||
| Subscribe | Events delivered on value change |
|
||||
| Integration tests with live PLC | 7/7 pass |
|
||||
|
||||
### Recording the outcome
|
||||
|
||||
```
|
||||
AB CIP live-boot
|
||||
Date: YYYY-MM-DD
|
||||
PLC: Allen-Bradley <model> firmware=<version>
|
||||
IP: <plc-ip>:44818 path=1,0
|
||||
OtOpcUa SHA: <git sha>
|
||||
|
||||
TCP connect: PASS
|
||||
Driver connected: PASS
|
||||
Browse: PASS <n> tags visible
|
||||
Atomic read: PASS
|
||||
UDT read: PASS
|
||||
Write round-trip: PASS
|
||||
Subscribe: PASS
|
||||
Integration tests: 7/7 PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Beckhoff TwinCAT — Wire-Live Validation
|
||||
|
||||
### Background
|
||||
|
||||
The TwinCAT driver (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/`) uses the
|
||||
Beckhoff `TwinCAT.Ads` .NET SDK v6. The integration test suite at
|
||||
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/`
|
||||
(`TwinCAT3SmokeTests.cs`) covers 14 `[TwinCATFact]` methods + one 16-case
|
||||
`[TwinCATTheory]` (30 cases total) against a live ADS runtime. The TCBSD ESXi
|
||||
VM at `10.100.0.128` (AmsNetId `41.169.163.43.1.1`) is the primary fixture
|
||||
runtime (project memory `project_tcbsd_fixture.md`) and bypasses the
|
||||
TwinCAT/Hyper-V conflict on the dev box.
|
||||
|
||||
Live-hardware validation extends beyond the TCBSD VM to confirm the driver
|
||||
works against a production PLC (not just the ESXi test VM) and that the three
|
||||
defects found during original integration testing do not regress on newer
|
||||
firmware:
|
||||
|
||||
1. Notification cycle time unit (250 ms was being set to ~41 min — fixed).
|
||||
2. `STRING(N)` / `WSTRING(N)` type mapper (fixed).
|
||||
3. Bit-indexed BOOL path (fixed).
|
||||
|
||||
### Preconditions
|
||||
|
||||
**TCBSD ESXi fixture (primary — no physical hardware needed)**
|
||||
|
||||
| Item | Requirement |
|
||||
|------|-------------|
|
||||
| TCBSD VM | Running on ESXi at `10.100.0.128` |
|
||||
| AMS Net ID | `41.169.163.43.1.1` |
|
||||
| ADS port | `851` (TwinCAT 3 PLC runtime 1) |
|
||||
| PLC project | TwinCAT project from `tests/.../TwinCatProject/` loaded and in Run state |
|
||||
| Network | TCP port 48898 reachable from dev box to `10.100.0.128` |
|
||||
|
||||
**Production PLC (for true wire-live validation)**
|
||||
|
||||
| Item | Requirement |
|
||||
|------|-------------|
|
||||
| TwinCAT hardware | Beckhoff IPC or CX series, TwinCAT 3 (TC3); TC2 is a known gap per fixture doc |
|
||||
| AMS route | Route configured on TwinCAT device back to the OtOpcUa host |
|
||||
| PLC state | Run state |
|
||||
| GVL | At least a `GVL_Fixture.nCounter` DINT and `GVL_Fixture.rSetpoint` REAL present |
|
||||
|
||||
### Procedure — TCBSD ESXi fixture
|
||||
|
||||
**Step 1 — Verify TCBSD VM is reachable**
|
||||
|
||||
```powershell
|
||||
Test-NetConnection -ComputerName 10.100.0.128 -Port 48898
|
||||
```
|
||||
|
||||
Pass: `TcpTestSucceeded: True`.
|
||||
|
||||
**Step 2 — Run the integration test suite**
|
||||
|
||||
```powershell
|
||||
$env:TWINCAT_TARGET_HOST = "10.100.0.128"
|
||||
$env:TWINCAT_TARGET_NETID = "41.169.163.43.1.1"
|
||||
|
||||
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests `
|
||||
--logger "console;verbosity=normal"
|
||||
```
|
||||
|
||||
Pass: all 30 test cases pass (14 `[TwinCATFact]` + 16-case `[TwinCATTheory]`).
|
||||
No `[TwinCATFact]` / `[TwinCATTheory]` skips — the env var is set, so the
|
||||
runtime probe is expected to succeed.
|
||||
|
||||
Key tests to watch:
|
||||
|
||||
| Test | Validates |
|
||||
|------|-----------|
|
||||
| `Driver_subscribe_receives_native_ADS_notifications_on_counter_changes` | Native ADS notification path (the cycle-time-unit bug regression) |
|
||||
| `Driver_reads_every_primitive_type_with_correct_mapping` | 16-type theory incl. `STRING(N)` |
|
||||
| `Driver_reads_bit_indexed_BOOL_from_word` | Bit-indexed BOOL fix regression |
|
||||
| `Driver_auto_reconnects_after_underlying_client_is_disposed` | Reconnect on ADS client dispose |
|
||||
| `Driver_routes_reads_per_device_and_isolates_unreachable_peers` | Multi-device isolation |
|
||||
|
||||
**Step 3 — OtOpcUa server browse/read via Client CLI**
|
||||
|
||||
Start OtOpcUa with a TwinCAT DriverInstance pointing at the TCBSD VM:
|
||||
|
||||
```powershell
|
||||
# appsettings.json DriverInstance: Type=TwinCAT, AmsNetId=41.169.163.43.1.1, AmsPort=851
|
||||
sc start OtOpcUa
|
||||
# or dev run
|
||||
dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server
|
||||
```
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
browse -u opc.tcp://localhost:4840 -r -d 4
|
||||
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
read -u opc.tcp://localhost:4840 -n "ns=2;s=TwinCAT/<device>/GVL_Fixture/nCounter"
|
||||
```
|
||||
|
||||
Pass: browse shows the PLC symbol tree; read returns `Good` quality with an
|
||||
integer value.
|
||||
|
||||
### Procedure — Production PLC (optional, for full wire-live signoff)
|
||||
|
||||
If a Beckhoff production IPC is available in the lab:
|
||||
|
||||
**Step 1** — Configure the AMS route on the TwinCAT device (TwinCAT System
|
||||
Manager → Routes → Add static route from the TwinCAT device back to the
|
||||
OtOpcUa server machine).
|
||||
|
||||
**Step 2** — Set env vars and run the integration suite against the production
|
||||
target:
|
||||
|
||||
```powershell
|
||||
$env:TWINCAT_TARGET_HOST = "<production-plc-ip>"
|
||||
$env:TWINCAT_TARGET_NETID = "<production-ams-net-id>"
|
||||
$env:TWINCAT_TARGET_PORT = "851"
|
||||
|
||||
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests
|
||||
```
|
||||
|
||||
**Step 3** — Subscribe to a counter tag for 30 s to confirm native
|
||||
notifications arrive:
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
subscribe -u opc.tcp://localhost:4840 `
|
||||
-n "ns=2;s=TwinCAT/<device>/GVL_Fixture/nCounter" -i 100
|
||||
```
|
||||
|
||||
Pass: events arrive every ~100 ms driven by the PLC's ADS notification, not
|
||||
by polling.
|
||||
|
||||
### Expected results
|
||||
|
||||
| Check | TCBSD VM | Production PLC |
|
||||
|-------|----------|----------------|
|
||||
| ADS port 48898 reachable | Required | Required |
|
||||
| Integration tests: all 30 pass | Required | Optional (same 30) |
|
||||
| Notification cycle-time test passes | Required | Required |
|
||||
| Server browse shows symbol tree | Required | Optional |
|
||||
| Read `Good` quality | Required | Optional |
|
||||
| Native ADS notifications deliver in subscribe | Required | Recommended |
|
||||
|
||||
### Known gaps (documented — not blockers for v2 GA)
|
||||
|
||||
Per `docs/drivers/TwinCAT-Test-Fixture.md` §"What it does NOT cover":
|
||||
|
||||
- Multi-hop AMS routing — single-hop only.
|
||||
- TC2 (ADS v1) compatibility — TC3 only.
|
||||
- Notification coalescing under sustained CPU load.
|
||||
- `Symbol version changed (0x0702)` storm handling under rapid PLC re-downloads.
|
||||
|
||||
These are deferred to v3 per `docs/v3/twincat-backlog.md`.
|
||||
|
||||
### Recording the outcome
|
||||
|
||||
```
|
||||
TwinCAT wire-live validation
|
||||
Date: YYYY-MM-DD
|
||||
Target: TCBSD VM 10.100.0.128 AmsNetId=41.169.163.43.1.1 (and/or production PLC details)
|
||||
TwinCAT version: <version>
|
||||
OtOpcUa SHA: <git sha>
|
||||
|
||||
ADS port reachable: PASS
|
||||
Integration tests: 30/30 PASS
|
||||
notification-cycle-time test: PASS (regression check)
|
||||
STRING(N) type test: PASS (regression check)
|
||||
bit-indexed BOOL test: PASS (regression check)
|
||||
Server browse: PASS
|
||||
Read Good quality: PASS
|
||||
Native subscription delivery: PASS <n> events in 30s
|
||||
```
|
||||
@@ -0,0 +1,278 @@
|
||||
# Phase 6.3 Redundancy — Client Interop Matrix and Cutover Validation Plan
|
||||
|
||||
> **Scope**: Phase 6.3 redundancy runtime core shipped (PRs #89-90, #98-99,
|
||||
> #24-peerprobe, Stream C node wiring, Stream D lease wrap). What remains is
|
||||
> Stream F (task #150): validating that third-party OPC UA clients honour
|
||||
> our `ServiceLevel` / `ServerUriArray` / `RedundancySupport` signals and
|
||||
> fail over correctly when the Primary drops. This document defines what is
|
||||
> automatable as integration tests, what requires two live instances plus a
|
||||
> real client, and a step-by-step cutover-validation runbook.
|
||||
>
|
||||
> **Source of truth**: `docs/Redundancy.md`, `docs/v2/redundancy-interop-playbook.md`,
|
||||
> `docs/v2/implementation/phase-6-3-redundancy-runtime.md`,
|
||||
> `scripts/compliance/phase-6-3-compliance.ps1`.
|
||||
|
||||
## What is already tested (no live cluster needed)
|
||||
|
||||
The following are covered by existing automated tests that run in ordinary
|
||||
`dotnet test`:
|
||||
|
||||
| Area | Test class(es) | What it asserts |
|
||||
|---|---|---|
|
||||
| `ServiceLevelCalculator` — 8-state matrix | `ServiceLevelCalculatorTests` | All 10 band values; role × self-health × peer-http × peer-ua × apply × recovery × topology combinations |
|
||||
| `RecoveryStateManager` — dwell + witness | `RecoveryStateManagerTests` | 60 s dwell default; premature-exit rejection; witness-required gate |
|
||||
| `ApplyLeaseRegistry` — lease lifecycle | `ApplyLeaseRegistryTests` | Disposal on success / exception / cancellation; watchdog force-close at 10 min |
|
||||
| `ServerRedundancyNodeWriter` — OPC UA variable binding | `ServerRedundancyNodeWriterTests` | `ServiceLevel` byte push; `RedundancySupport` enum; `ServerUriArray` skip-log when node absent |
|
||||
| `RedundancyStatePublisher` — orchestration | `RedundancyStatePublisherTests` | Edge-triggered `OnStateChanged`; idempotent dedup |
|
||||
| `ClusterTopologyLoader` | `ClusterTopologyLoaderTests` | Two-node seed; one-node degenerate; duplicate-URI rejection |
|
||||
| `DraftValidator.ValidateClusterTopology` | `DraftValidatorTests` (8 cases) | NodeCount/mode pairs; Enabled-count vs NodeCount; multiple-Primary rejection |
|
||||
|
||||
Run with:
|
||||
|
||||
```powershell
|
||||
dotnet test ZB.MOM.WW.OtOpcUa.slnx --filter "FullyQualifiedName~Redundancy"
|
||||
```
|
||||
|
||||
Compliance gate (every Phase 6.3 static check):
|
||||
|
||||
```powershell
|
||||
pwsh ./scripts/compliance/phase-6-3-compliance.ps1
|
||||
```
|
||||
|
||||
Pass criteria: exit 0; all `[PASS]` lines green; `[DEFERRED]` lines are
|
||||
known-deferred surfaces, not failures.
|
||||
|
||||
## What cannot be automated — requires two live instances
|
||||
|
||||
The scenarios below require two running `OtOpcUa.Server` processes in the
|
||||
same `ServerCluster`, a real SQL Server Config DB, and at least one driver
|
||||
instance with a reachable endpoint (simulator or real PLC).
|
||||
|
||||
### Why it cannot be unit/integration-tested in-process
|
||||
|
||||
- UaExpert, Kepware KEPServerEX, and AVEVA OI Gateway are closed-source
|
||||
Windows GUI binaries with no headless CLI interface for the
|
||||
subscribe/browse flows.
|
||||
- The AVEVA MXAccess failover leg (`IAlarmSource` reconnect, `$MxAccessClient`
|
||||
quality transition) involves the Galaxy runtime's own client-redundancy
|
||||
policy and the COM-layer session model — both live outside this repo.
|
||||
- Even the automatable sub-set (our own `otopcua-cli` as the client) needs
|
||||
two distinct listening TCP endpoints; that requires two live processes,
|
||||
which is out of scope for `dotnet test` integration fixtures.
|
||||
|
||||
## Test matrix
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Two `OtOpcUa.Server` processes on separate Windows hosts (or separate
|
||||
ports on the same host for dev) sharing one Config DB (`ServerCluster`
|
||||
with `NodeCount=2`, `RedundancyMode=Warm` or `Hot`).
|
||||
2. Each node registered in `ClusterNode`:
|
||||
- Node A: `RedundancyRole=Primary`, `ServiceLevelBase=255`,
|
||||
`ApplicationUri=urn:node-a:OtOpcUa`
|
||||
- Node B: `RedundancyRole=Secondary`, `ServiceLevelBase=100`,
|
||||
`ApplicationUri=urn:node-b:OtOpcUa`
|
||||
3. `PeerHttpProbeLoop` and `PeerUaProbeLoop` HostedServices running on both
|
||||
nodes (registered via `AddHostedService<PeerHttpProbeLoop>` +
|
||||
`AddHostedService<PeerUaProbeLoop>` in `Program.cs`).
|
||||
4. At least one `DriverInstance` in the cluster with a reachable PLC or
|
||||
simulator (e.g. Modbus sim at `10.100.0.35:5020`).
|
||||
5. Client machine with UaExpert >= 1.7 installed.
|
||||
6. Optional second client: Kepware KEPServerEX 6.x QuickClient or AVEVA
|
||||
OI Gateway 2020R2+.
|
||||
|
||||
### Block A — OPC UA protocol signals (UaExpert, no failover yet)
|
||||
|
||||
| ID | Scenario | Procedure | Pass criterion | Automatable? |
|
||||
|----|----------|-----------|----------------|--------------|
|
||||
| A1 | ServiceLevel published on Primary | Connect UaExpert to Node A. Browse `Server/ServerStatus/ServiceLevel`. | Value = 255 (`AuthoritativePrimary`) | No — requires UaExpert GUI |
|
||||
| A2 | ServiceLevel published on Backup | Connect UaExpert to Node B. Read same node. | Value = 100 (`AuthoritativeBackup`) | No |
|
||||
| A3 | ServiceLevel updates when peer drops | Node A connected. Stop Node B (`sc stop OtOpcUa`). Watch `ServiceLevel` on Node A. | Transitions 255 → 230 (`IsolatedPrimary`) within ~6 s (3 × 2 s HTTP probe interval) | No |
|
||||
| A4 | RedundancySupport | Browse `Server/ServerRedundancy/RedundancySupport` on either node. | Value = `Warm` or `Hot` matching the cluster `RedundancyMode` | No |
|
||||
| A5 | ServerUriArray | Browse `Server/ServerRedundancy/ServerUriArray` on either node. | Array contains both `ApplicationUri` values; self listed first. Note: requires non-transparent redundancy-type upgrade (currently logs-and-skips — see known limitation A5 below). | No |
|
||||
| A6 | Mid-apply ServiceLevel dip | Trigger a `sp_PublishGeneration` apply (via Admin UI draft → publish) while watching Node A `ServiceLevel`. | Drops to 200 (`PrimaryMidApply`) for the apply duration; returns to 255 after `RefreshAsync`. | No |
|
||||
| A7 | Client.CLI reads correct ServiceLevel | `dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://<node-a>:4840 -n "i=2267"` | Prints current byte value matching expected band. | **Yes** — scriptable with the Client CLI |
|
||||
| A8 | otopcua-cli failover reconnect | `dotnet run ... -- connect -u opc.tcp://<node-a>:4840 -F opc.tcp://<node-b>:4840` — then kill Node A. | CLI session reconnects to Node B within the session keep-alive timeout. | **Yes** — scriptable with the Client CLI |
|
||||
|
||||
### Block B — Third-party client failover
|
||||
|
||||
| ID | Scenario | Procedure | Pass criterion |
|
||||
|----|----------|-----------|----------------|
|
||||
| B1 | UaExpert picks Primary by ServiceLevel | Configure a Redundancy Group in UaExpert with both endpoint URLs. | Client connects to Node A (higher ServiceLevel) |
|
||||
| B2 | UaExpert cuts over on Primary kill | Kill Node A `OtOpcUa` service. | Client session reconnects to Node B within UaExpert's reconnect timeout (default 5 s). Data-change monitored items resume. |
|
||||
| B3 | UaExpert returns when Primary restores | Start Node A. Wait >= 60 s recovery dwell. | `ServiceLevel` on Node A progresses: 180 (`RecoveringPrimary`) → 255 (`AuthoritativePrimary`). UaExpert may or may not switch back (client-policy-dependent; both outcomes accepted). |
|
||||
| B4 | Kepware QuickClient failover | Repeat B1–B3 with Kepware configured for the same two endpoints. | Same pass criteria; establishes no UaExpert-specific behaviour. |
|
||||
| B5 | AVEVA OI Gateway | Configure OI Gateway OPC DA/UA client object against the cluster. Kill Primary. | OI Gateway data quality recovers within `ReconnectInterval` (default 20 s); no permanent data-loss alert. |
|
||||
|
||||
### Block C — Galaxy MXAccess failover
|
||||
|
||||
This block requires a running Galaxy and `$MxAccessClient` object (AVEVA
|
||||
System Platform installed, Galaxy deployed on dev box — see project memory
|
||||
`project_aveva_platform_installed.md`).
|
||||
|
||||
| ID | Scenario | Procedure | Pass criterion |
|
||||
|----|----------|-----------|----------------|
|
||||
| C1 | Galaxy binds to Primary on first connect | Bring cluster up. Start a Galaxy `$MxAccessClient` with both node URLs configured. | Galaxy reports `QUALITY = Good`; initial values stream from Node A. |
|
||||
| C2 | Galaxy redirects on Primary drop | Stop Node A. | Galaxy `QUALITY` briefly goes `Uncertain`, then returns to `Good`; values continue streaming from Node B within MXAccess's `ReconnectInterval` (default 20 s). |
|
||||
| C3 | Galaxy tolerates mid-apply dip | Trigger generation apply on Node A. | Galaxy remains bound — mid-apply dip (200) is advisory, not a session drop. No quality interruption. |
|
||||
|
||||
Note: A negative result on C1–C3 does not necessarily indicate an OtOpcUa
|
||||
defect. Cross-check with Block A / B first to confirm our `ServiceLevel`
|
||||
signal is correct before debugging the MXAccess client layer.
|
||||
|
||||
## Step-by-step cutover-validation runbook
|
||||
|
||||
This is the minimum procedure to satisfy the v2 GA exit criterion:
|
||||
"Non-transparent redundancy cutover validated with at least one production
|
||||
client (Ignition 8.3 recommended — see decision #85)."
|
||||
|
||||
### Step 1 — Provision the cluster
|
||||
|
||||
```powershell
|
||||
# On the Config DB host, seed or verify cluster rows:
|
||||
# ServerCluster: Id=<id>, Name="test-cluster", NodeCount=2, RedundancyMode=Warm
|
||||
# ClusterNode A: NodeId="node-a", ClusterId=<id>, RedundancyRole=Primary,
|
||||
# ServiceLevelBase=255, ApplicationUri="urn:node-a:OtOpcUa"
|
||||
# ClusterNode B: NodeId="node-b", ClusterId=<id>, RedundancyRole=Secondary,
|
||||
# ServiceLevelBase=100, ApplicationUri="urn:node-b:OtOpcUa"
|
||||
```
|
||||
|
||||
Verify uniqueness constraint: no two `ClusterNode` rows share the same
|
||||
`ApplicationUri` (unique index on `ApplicationUri`).
|
||||
|
||||
### Step 2 — Start both server instances
|
||||
|
||||
On Node A host:
|
||||
|
||||
```powershell
|
||||
# appsettings.json: Node:NodeId = "node-a"
|
||||
sc start OtOpcUa
|
||||
```
|
||||
|
||||
On Node B host:
|
||||
|
||||
```powershell
|
||||
# appsettings.json: Node:NodeId = "node-b"
|
||||
sc start OtOpcUa
|
||||
```
|
||||
|
||||
Wait 10 s for HostedServices to complete first probe cycle.
|
||||
|
||||
### Step 3 — Verify baseline ServiceLevel via Client CLI
|
||||
|
||||
```powershell
|
||||
# Node A should report 255
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
|
||||
-u opc.tcp://<node-a-host>:4840 -n "i=2267"
|
||||
|
||||
# Node B should report 100
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
|
||||
-u opc.tcp://<node-b-host>:4840 -n "i=2267"
|
||||
```
|
||||
|
||||
Pass: Node A = 255, Node B = 100.
|
||||
|
||||
### Step 4 — Verify ServerUriArray
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read `
|
||||
-u opc.tcp://<node-a-host>:4840 -n "i=2271"
|
||||
```
|
||||
|
||||
Pass: array returned contains both `ApplicationUri` strings. If
|
||||
`ServerUriArray` node returns empty or an error, the non-transparent
|
||||
redundancy-type upgrade follow-up is still pending (known limitation —
|
||||
`ServerRedundancyNodeWriter.ApplyServerUriArray` logs-and-skips on the
|
||||
base `ServerRedundancyState` object type).
|
||||
|
||||
### Step 5 — Execute Primary kill + failover (B2 scenario)
|
||||
|
||||
1. Connect UaExpert (or Kepware) Redundancy Group to both endpoints.
|
||||
2. Confirm client is subscribed to at least one variable node.
|
||||
3. Kill Node A: `sc stop OtOpcUa` on Node A host.
|
||||
4. Observe:
|
||||
- Node B `ServiceLevel` should transition: 100 (`AuthoritativeBackup`)
|
||||
→ 80 (`IsolatedBackup`) within ~6 s.
|
||||
- Client should reconnect to Node B and resume data-change events.
|
||||
5. Record: time from kill to client reconnect; whether data gaps occurred.
|
||||
|
||||
### Step 6 — Verify Primary recovery (B3 scenario)
|
||||
|
||||
1. Restart Node A: `sc start OtOpcUa` on Node A host.
|
||||
2. Observe Node A `ServiceLevel` progression:
|
||||
- ~0 s: 1 (`NoData`) briefly while HostedServices start.
|
||||
- Startup: 180 (`RecoveringPrimary`) — recovery dwell gate active.
|
||||
- After >= 60 s dwell + one positive publish witness: 255 (`AuthoritativePrimary`).
|
||||
3. Observe Node B:
|
||||
- Returns to 100 (`AuthoritativeBackup`) once it sees Node A peer probe succeed.
|
||||
4. Record dwell duration and whether the client (UaExpert/Kepware) switches back.
|
||||
|
||||
### Step 7 — Execute mid-apply dip (A6 scenario)
|
||||
|
||||
1. Via Admin UI, create a trivial draft change and publish.
|
||||
2. Watch Node A `ServiceLevel` during apply.
|
||||
3. Expected: drops to 200 (`PrimaryMidApply`) for the apply duration
|
||||
(typically seconds); returns to 255 when `GenerationRefreshHostedService`
|
||||
releases the lease.
|
||||
|
||||
### Step 8 — Record results
|
||||
|
||||
Copy the following block into a tracking doc:
|
||||
|
||||
```
|
||||
Run date: YYYY-MM-DD
|
||||
Release SHA: <git sha>
|
||||
Cluster: <cluster-id> Primary: node-a Backup: node-b
|
||||
Config DB: 10.100.0.35,14330
|
||||
|
||||
A1: [PASS/FAIL] evidence: <screenshot or CLI output>
|
||||
A2: [PASS/FAIL]
|
||||
A3: [PASS/FAIL] time-to-IsolatedPrimary: <N>s
|
||||
A4: [PASS/FAIL]
|
||||
A5: [PASS/FAIL/DEFERRED - ServerUriArray upgrade pending]
|
||||
A6: [PASS/FAIL] mid-apply duration: <N>s
|
||||
A7: [PASS/FAIL] CLI output attached
|
||||
A8: [PASS/FAIL] CLI reconnect observed
|
||||
B1: [PASS/FAIL]
|
||||
B2: [PASS/FAIL] reconnect time: <N>s
|
||||
B3: [PASS/FAIL] dwell observed: <N>s
|
||||
B4: [PASS/FAIL] (Kepware)
|
||||
B5: [PASS/FAIL] (OI Gateway — if available)
|
||||
C1: [PASS/FAIL/SKIP - Galaxy not available]
|
||||
C2: [PASS/FAIL/SKIP]
|
||||
C3: [PASS/FAIL/SKIP]
|
||||
```
|
||||
|
||||
One pass of every non-SKIP row is the v2 GA acceptance criterion.
|
||||
|
||||
## Known limitations
|
||||
|
||||
### A5 — ServerUriArray node not yet writable
|
||||
|
||||
The OPC UA .NET Standard SDK's default `Server.ServerRedundancy` object is the
|
||||
base `ServerRedundancyState`, which has no `ServerUriArray` child node.
|
||||
`ServerRedundancyNodeWriter.ApplyServerUriArray` currently logs a warning and
|
||||
skips. The operator obtains `ServerUriArray` by reading `ClusterNode` rows
|
||||
directly until the non-transparent redundancy-type upgrade follow-up ships.
|
||||
|
||||
### Recovery dwell is 60 s by default
|
||||
|
||||
`RecoveryStateManager.DwellTime` defaults to `TimeSpan.FromSeconds(60)` in
|
||||
`Program.cs`. Step 6 of the runbook will block for at least 60 s waiting for
|
||||
Node A to return to `AuthoritativePrimary`. This is intentional per
|
||||
decision #154 (thrash prevention) — do not lower it for the test run.
|
||||
|
||||
### IsolatedBackup (80) does not auto-promote
|
||||
|
||||
Per decision #154, the Backup at band 80 does not self-elevate. If the operator
|
||||
needs authoritative service from Node B while Node A is down, they must write
|
||||
`RedundancyRole=Primary` on the `ClusterNode` row for Node B and publish a
|
||||
draft generation. The Admin UI `RedundancyTab` exposes this flow.
|
||||
|
||||
## Dependency on existing tests
|
||||
|
||||
The cutover runbook validates the end-to-end wire path. The math and edge cases
|
||||
are already locked by the unit/integration tests enumerated in the first section.
|
||||
A failing runbook step that contradicts a passing unit test indicates a
|
||||
deployment configuration error or an SDK version mismatch — not a logic bug.
|
||||
Check `PeerHttpProbeLoop` logs first (look for `PeerProbe` Serilog events).
|
||||
@@ -0,0 +1,307 @@
|
||||
# v2 GA Lab Gates Plan
|
||||
|
||||
> **Canonical tracker**: `docs/v2/v2-release-readiness.md` — all code-path
|
||||
> release blockers are closed as of 2026-04-24. This document maps the
|
||||
> remaining exit-criteria from that tracker to concrete commands, automation
|
||||
> boundaries, operator procedures, and pass criteria.
|
||||
>
|
||||
> **Status**: RELEASE-READY (code-path). Manual/lab gates remain open.
|
||||
|
||||
## The gate list
|
||||
|
||||
From `docs/v2/v2-release-readiness.md` §"Release-readiness exit criteria":
|
||||
|
||||
| # | Gate | Kind | Automatable here |
|
||||
|---|------|------|-----------------|
|
||||
| G1 | All four Phase 6.N compliance scripts exit 0 | Script | Yes — run on this box |
|
||||
| G2 | `dotnet test ZB.MOM.WW.OtOpcUa.slnx` passes with <= 1 known-flake failure | Script | Yes — run on this box |
|
||||
| G3 | Release blockers closed | Audit | Already closed (code-path) |
|
||||
| G4 | Phase 5 driver complement shipped | Audit | Already closed |
|
||||
| G5 | Production deployment checklist signed off by Fleet Admin | Operator | No — separate doc, human signoff |
|
||||
| G6 | At least one end-to-end integration run against live Galaxy succeeds | Dev rig | No — requires AVEVA platform |
|
||||
| G7 | FOCAS live-CNC wire-level smoke (#54) passes against a real FANUC control | Lab hardware | No — requires FANUC CNC |
|
||||
| G8 | OPC UA CTT / UA Compliance Test Tool passes against the live endpoint | Operator tool | No — requires CTT binary + live endpoint |
|
||||
| G9 | Non-transparent redundancy cutover validated with >= 1 production client | Lab | No — see `docs/plans/phase-6-3-redundancy-interop-plan.md` |
|
||||
|
||||
---
|
||||
|
||||
## G1 — Phase 6 compliance scripts
|
||||
|
||||
### Command
|
||||
|
||||
```powershell
|
||||
pwsh ./scripts/compliance/phase-6-all.ps1
|
||||
```
|
||||
|
||||
This meta-runner at `scripts/compliance/phase-6-all.ps1` invokes each
|
||||
sub-script in a separate `powershell.exe` process to isolate exit codes:
|
||||
|
||||
| Sub-script | Phase | What it checks |
|
||||
|-----------|-------|---------------|
|
||||
| `phase-6-1-compliance.ps1` | 6.1 Resilience & Observability | Polly resilience classes, health endpoints, LiteDB sealed cache, observability sinks |
|
||||
| `phase-6-2-compliance.ps1` | 6.2 Authorization runtime | `AuthorizationGate`, `TriePermissionEvaluator`, `NodeScopeResolver`, dispatch wiring in `DriverNodeManager` |
|
||||
| `phase-6-3-compliance.ps1` | 6.3 Redundancy runtime | `ServiceLevelCalculator` 8-state band values, `RecoveryStateManager`, `ApplyLeaseRegistry`, `ServerRedundancyNodeWriter`; also invokes `dotnet test` with a baseline of 1097 |
|
||||
| `phase-6-4-compliance.ps1` | 6.4 Admin UI completion | Data-layer types, Identification folder, deferred Blazor items marked `[DEFERRED]` |
|
||||
|
||||
### Pass criterion
|
||||
|
||||
```
|
||||
Phase 6 aggregate: PASS
|
||||
```
|
||||
|
||||
Exit code 0. Any `[FAIL]` line is a blocker. `[DEFERRED]` lines are expected
|
||||
for the known-deferred surfaces listed in the implementation docs; they do not
|
||||
fail the run.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- SQL Server `10.100.0.35,14330` reachable (Config DB tests use it).
|
||||
- `dotnet` SDK on PATH (`.NET 10`).
|
||||
- Run from repo root.
|
||||
|
||||
---
|
||||
|
||||
## G2 — Full solution test suite
|
||||
|
||||
### Command
|
||||
|
||||
```powershell
|
||||
dotnet test ZB.MOM.WW.OtOpcUa.slnx --logger "console;verbosity=minimal"
|
||||
```
|
||||
|
||||
For a more targeted run of integration suites that need their fixtures up:
|
||||
|
||||
```powershell
|
||||
# bring modbus fixture up first
|
||||
lmxopcua-fix up modbus standard
|
||||
|
||||
dotnet test ZB.MOM.WW.OtOpcUa.slnx --logger "console;verbosity=minimal"
|
||||
```
|
||||
|
||||
### Pass criterion
|
||||
|
||||
- Passed count >= 1159 (2026-04-19 baseline after Phase 5 driver complement).
|
||||
- Failed count <= 1 (the pre-existing
|
||||
`SubscribeCommandTests.Execute_PrintsSubscriptionMessage` flake in
|
||||
`Client.CLI` is the only tolerated failure).
|
||||
- No new `[FAILED]` tests relative to the baseline.
|
||||
|
||||
### Known flake
|
||||
|
||||
`ZB.MOM.WW.OtOpcUa.Client.CLI.Tests::SubscribeCommandTests.Execute_PrintsSubscriptionMessage`
|
||||
is a timing-sensitive subscribe-then-cancel test. Rerun the specific project
|
||||
if it appears:
|
||||
|
||||
```powershell
|
||||
dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests `
|
||||
--filter "FullyQualifiedName~SubscribeCommandTests.Execute_PrintsSubscriptionMessage" `
|
||||
--count 3
|
||||
```
|
||||
|
||||
If it fails all three runs, investigate; otherwise treat as flake.
|
||||
|
||||
### Docker fixtures needed for integration suites
|
||||
|
||||
| Driver | Command | Endpoint used |
|
||||
|--------|---------|---------------|
|
||||
| Modbus | `lmxopcua-fix up modbus standard` | `10.100.0.35:5020` |
|
||||
| AB CIP | `lmxopcua-fix up abcip controllogix` | `10.100.0.35:44818` |
|
||||
| S7 | `lmxopcua-fix up s7 s7_1500` | `10.100.0.35:1102` |
|
||||
| OPC UA Client | `lmxopcua-fix up opcuaclient` | `opc.tcp://10.100.0.35:50000` |
|
||||
| FOCAS | `lmxopcua-fix up focas` (mock server) | `10.100.0.35:8193` |
|
||||
|
||||
TwinCAT integration tests require the TCBSD ESXi VM at `10.100.0.128`
|
||||
(AmsNetId `41.169.163.43.1.1`). Set env var before running:
|
||||
|
||||
```powershell
|
||||
$env:TWINCAT_TARGET_HOST = "10.100.0.128"
|
||||
$env:TWINCAT_TARGET_NETID = "41.169.163.43.1.1"
|
||||
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests
|
||||
```
|
||||
|
||||
Galaxy integration tests run against the live mxaccessgw on the dev box
|
||||
(gate G6).
|
||||
|
||||
---
|
||||
|
||||
## G3 — Release blockers closed (audit, already satisfied)
|
||||
|
||||
All three code-path release blockers are closed per `v2-release-readiness.md`:
|
||||
|
||||
- Authorization dispatch wiring (task #143, PR #94) — CLOSED.
|
||||
- Config fallback Phase 6.1 Stream D (task #136, PR #96) — CLOSED.
|
||||
- Redundancy Phase 6.3 Streams A/C core (tasks #145/#147, PRs #98-99) — CLOSED.
|
||||
|
||||
No action required. Record the PR numbers in the release notes.
|
||||
|
||||
---
|
||||
|
||||
## G4 — Driver complement (audit, already satisfied)
|
||||
|
||||
All eight drivers shipped:
|
||||
|
||||
Galaxy, Modbus (+ DL205/S7/MELSEC profiles), S7 native, OPC UA Client, AB CIP,
|
||||
AB Legacy, TwinCAT ADS, FOCAS (managed wire client — Tier-C isolation retired,
|
||||
FOCAS is now Tier A in-process via `WireFocasClient`).
|
||||
|
||||
No action required.
|
||||
|
||||
---
|
||||
|
||||
## G5 — Production deployment checklist (operator action)
|
||||
|
||||
The deployment checklist is a separate document covering:
|
||||
|
||||
- Windows service install (`scripts/install/Install-Services.ps1`)
|
||||
- Config DB migration (`scripts/db/Apply-Migrations.ps1`)
|
||||
- Certificate provisioning and trust
|
||||
- LDAP / GLAuth configuration for production AD target
|
||||
- mxaccessgw API key provisioning (`apikey create-key` in the sibling repo)
|
||||
- Service account permissions
|
||||
- Prometheus / OpenTelemetry export configuration
|
||||
- Firewall rules (port 4840 OPC UA, port 5120 gRPC to mxaccessgw,
|
||||
Admin port 5000/5001)
|
||||
|
||||
**Sign-off party**: Fleet Admin (operator). Not automatable.
|
||||
|
||||
Record sign-off as a comment on the v2 release issue.
|
||||
|
||||
---
|
||||
|
||||
## G6 — Live Galaxy end-to-end integration run
|
||||
|
||||
**Requires**: AVEVA System Platform installed on dev box (confirmed available
|
||||
per project memory `project_aveva_platform_installed.md`); mxaccessgw running
|
||||
with a provisioned API key; at least one Galaxy object deployed.
|
||||
|
||||
### Procedure
|
||||
|
||||
1. Start mxaccessgw:
|
||||
|
||||
```powershell
|
||||
# in sibling repo C:\Users\dohertj2\Desktop\mxaccessgw\
|
||||
dotnet run --project src/MxGateway.Server -- --apikey-path .local/api-key.txt
|
||||
```
|
||||
|
||||
2. Start OtOpcUa server with Galaxy driver instance configured:
|
||||
|
||||
```powershell
|
||||
sc start OtOpcUa
|
||||
```
|
||||
|
||||
3. Browse via Client CLI:
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
browse -u opc.tcp://localhost:4840 -r -d 3
|
||||
```
|
||||
|
||||
4. Read a known Galaxy tag (e.g. a deployed `$UserDefined` object attribute):
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
read -u opc.tcp://localhost:4840 -n "ns=2;s=<tag_name.AttributeName>"
|
||||
```
|
||||
|
||||
5. Subscribe and verify live updates:
|
||||
|
||||
```powershell
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- `
|
||||
subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=<tag_name.AttributeName>" -i 1000
|
||||
```
|
||||
|
||||
### Pass criterion
|
||||
|
||||
- Browse returns a non-empty node tree mirroring the Galaxy hierarchy.
|
||||
- Read returns `Good` quality with a non-null value.
|
||||
- Subscribe receives at least one data-change notification within 5 s
|
||||
(or within the configured publishing interval).
|
||||
- No `BadNoCommunication` or `BadTimeout` errors in the server log.
|
||||
|
||||
Record: Galaxy version, deployed object count, OtOpcUa git SHA.
|
||||
|
||||
---
|
||||
|
||||
## G7 — FOCAS live-CNC smoke (task #54)
|
||||
|
||||
**Requires**: real FANUC CNC with Ethernet option, accessible on TCP port 8193
|
||||
from the dev box; CNC series known (e.g. 0i-F, 30i-B).
|
||||
|
||||
See `docs/plans/live-hardware-validation-runbooks.md` §FOCAS for the full
|
||||
runbook.
|
||||
|
||||
### Pass criterion
|
||||
|
||||
- `WireFocasClient` opens a FOCAS2 session (`cnc_allclibhndl3` succeeds).
|
||||
- Identity nodes (`Identity/SeriesNumber`, `Identity/MaxAxes`) return non-null
|
||||
values matching the physical control panel display.
|
||||
- At least one axis position (`Axes/X/AbsolutePosition` or similar) returns
|
||||
`Good` quality with a plausible double value.
|
||||
- Subscribe on a polled tag delivers at least three updates within 5 s.
|
||||
- No `EW_SOCKET` (-1) or `EW_HANDLE` (-7) errors in the server log during a
|
||||
2-minute soak.
|
||||
|
||||
Record: CNC series, firmware version, test date, OtOpcUa git SHA.
|
||||
|
||||
---
|
||||
|
||||
## G8 — OPC UA Conformance Test Tool (CTT) pass
|
||||
|
||||
**Requires**: OPC Foundation OPC UA Compliance Test Tool (CTT) or the
|
||||
open-source UA Compliance Test Tool installed on the client machine;
|
||||
live OtOpcUa server endpoint.
|
||||
|
||||
### Recommended minimum profile set
|
||||
|
||||
- `Attribute Read`
|
||||
- `Attribute Write`
|
||||
- `Browse`
|
||||
- `Subscription` (DataChange)
|
||||
- `Server-side monitoring`
|
||||
- `Security — None profile` (if server configured with `Security:Profiles=[None]`)
|
||||
|
||||
### Procedure
|
||||
|
||||
1. Launch CTT. Add server endpoint: `opc.tcp://localhost:4840`.
|
||||
2. Run the profile set above.
|
||||
3. Capture the CTT report HTML/XML.
|
||||
|
||||
### Pass criterion
|
||||
|
||||
All mandatory test cases in each profile: **PASS** or **NOT APPLICABLE**.
|
||||
|
||||
Zero mandatory failures. Advisory failures may be documented with rationale
|
||||
(e.g. optional capability not implemented).
|
||||
|
||||
Record: CTT version, profile set, OtOpcUa git SHA, report artifact.
|
||||
|
||||
---
|
||||
|
||||
## G9 — Non-transparent redundancy cutover with production client
|
||||
|
||||
See `docs/plans/phase-6-3-redundancy-interop-plan.md` for the full runbook.
|
||||
|
||||
**Minimum acceptable result**: one complete pass of the A-block (UaExpert
|
||||
OPC UA signal verification) plus scenario B2 (UaExpert failover on Primary
|
||||
kill).
|
||||
|
||||
Ignition 8.3 is the recommended production client per decision #85. If
|
||||
Ignition is not available on the lab machine, UaExpert is accepted for v2 GA.
|
||||
|
||||
Record: client name + version, OtOpcUa git SHA, test date.
|
||||
|
||||
---
|
||||
|
||||
## Gate summary table
|
||||
|
||||
| Gate | Command / Procedure | Pass criterion | Owner |
|
||||
|------|---------------------|----------------|-------|
|
||||
| G1 | `pwsh ./scripts/compliance/phase-6-all.ps1` | Exit 0, no `[FAIL]` | Dev |
|
||||
| G2 | `dotnet test ZB.MOM.WW.OtOpcUa.slnx` | >= 1159 passing, <= 1 failure | Dev |
|
||||
| G3 | Audit PR list in release-readiness.md | All blockers show CLOSED | Dev |
|
||||
| G4 | Audit driver table | All 8 drivers listed as shipped | Dev |
|
||||
| G5 | Run deployment checklist doc | All items checked; Fleet Admin signs off | Fleet Admin |
|
||||
| G6 | Browse/read/subscribe against live Galaxy | Good quality, non-empty tree | Dev (dev box) |
|
||||
| G7 | FOCAS CNC smoke — see live-hardware runbook | Session open, Good quality reads | Dev + lab hardware |
|
||||
| G8 | CTT profile run against live endpoint | Zero mandatory failures | Dev + CTT tool |
|
||||
| G9 | Redundancy cutover runbook | A-block + B2 pass with >= 1 client | Dev + two instances |
|
||||
+10
-10
@@ -95,7 +95,7 @@ The Server accepts three OPC UA identity-token types:
|
||||
| Token | Handler | Notes |
|
||||
|---|---|---|
|
||||
| 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. |
|
||||
|
||||
### LDAP bind flow (`LdapUserAuthenticator`)
|
||||
@@ -164,7 +164,7 @@ ACLs are evaluated against the UNS path:
|
||||
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
|
||||
|
||||
@@ -196,7 +196,7 @@ The three Write tiers map to Galaxy's v1 `SecurityClassification` — `FreeAcces
|
||||
|
||||
### Evaluator — `PermissionTrie`
|
||||
|
||||
`src/ZB.MOM.WW.OtOpcUa.Core/Authorization/`:
|
||||
`src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/`:
|
||||
|
||||
| Class | Role |
|
||||
|---|---|
|
||||
@@ -209,7 +209,7 @@ The three Write tiers map to Galaxy's v1 `SecurityClassification` — `FreeAcces
|
||||
|
||||
### 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:
|
||||
|
||||
@@ -219,7 +219,7 @@ Key properties:
|
||||
|
||||
### 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
|
||||
|
||||
@@ -235,7 +235,7 @@ Per decision #150 control-plane roles are **deliberately independent of data-pla
|
||||
|
||||
### Roles
|
||||
|
||||
`src/ZB.MOM.WW.OtOpcUa.Admin/Services/AdminRoles.cs`:
|
||||
`src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/AdminRoles.cs`:
|
||||
|
||||
| Role | Capabilities |
|
||||
|---|---|
|
||||
@@ -255,17 +255,17 @@ Razor pages and API endpoints gate with `[Authorize(Policy = "CanEdit")]` / `"Ca
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
|
||||
|
||||
+16
-16
@@ -8,7 +8,7 @@
|
||||
> See [docs/AlarmTracking.md](../AlarmTracking.md) for the v2 final
|
||||
> architecture — that is the document to read for current behaviour.
|
||||
|
||||
Alarm surfacing is an optional driver capability exposed via `IAlarmSource` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs`). Drivers whose backends have an alarm concept implement it — today: Galaxy (MXAccess alarms), FOCAS (CNC alarms), OPC UA Client (A&C events from the upstream server). Modbus / S7 / AB CIP / AB Legacy / TwinCAT do not implement the interface and the feature is simply absent from their subtrees.
|
||||
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
|
||||
|
||||
@@ -25,7 +25,7 @@ The driver fires `OnAlarmEvent` for every transition (`Active`, `Acknowledged`,
|
||||
|
||||
## 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.
|
||||
- `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`
|
||||
|
||||
`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
|
||||
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`.
|
||||
|
||||
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`
|
||||
|
||||
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.
|
||||
|
||||
@@ -114,23 +114,23 @@ Dead-letter retention defaults to 30 days (plan decision #21). `PurgeAgedDeadLet
|
||||
|
||||
### 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
|
||||
|
||||
`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
|
||||
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs` — capability contract + `AlarmEventArgs`
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs` — per-host fan-out + no-retry ack
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — `CapturingBuilder` + alarm forwarder
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `VariableHandle.MarkAsAlarmCondition` + `ConditionSink`
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs` — capability contract + `AlarmEventArgs`
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs` — per-host fan-out + no-retry ack
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs` — `CapturingBuilder` + alarm forwarder
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `VariableHandle.MarkAsAlarmCondition` + `ConditionSink`
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/Alarms/GalaxyAlarmTracker.cs` — Galaxy-specific alarm-event production
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs` — historian sink intake contract + `AlarmHistorianEvent` + `HistorianSinkStatus` + `IAlarmHistorianWriter`
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs` — durable queue + drain worker + backoff ladder + dead-letter retention
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — `RouteToHistorianAsync` wires scripted-alarm emissions into the sink
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` — `ResolveHistorianSink` selects `SqliteStoreAndForwardSink` vs `NullAlarmHistorianSink`
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Admin/Services/HistorianDiagnosticsService.cs` — Admin UI `/alarms/historian` status + retry-dead-lettered operator action
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs` — historian sink intake contract + `AlarmHistorianEvent` + `HistorianSinkStatus` + `IAlarmHistorianWriter`
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs` — durable queue + drain worker + backoff ladder + dead-letter retention
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7EngineComposer.cs` — `RouteToHistorianAsync` wires scripted-alarm emissions into the sink
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/Phase7Composer.cs` — `ResolveHistorianSink` selects `SqliteStoreAndForwardSink` vs `NullAlarmHistorianSink`
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/HistorianDiagnosticsService.cs` — Admin UI `/alarms/historian` status + retry-dead-lettered operator action
|
||||
|
||||
@@ -17,7 +17,7 @@ The rule: if the setting describes *how the process connects to the rest of the
|
||||
|
||||
Each of the three processes (Server, Admin, Galaxy.Host) reads its own `appsettings.json` plus environment overrides.
|
||||
|
||||
### 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:
|
||||
|
||||
@@ -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 |
|
||||
|---|---|
|
||||
@@ -73,7 +73,7 @@ Standard .NET config layering applies: `appsettings.{Environment}.json`, then en
|
||||
|
||||
## 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
|
||||
|
||||
@@ -103,7 +103,7 @@ Old generations are retained; rollback is "publish older generation as new". `Co
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
+10
-10
@@ -1,10 +1,10 @@
|
||||
# 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
|
||||
|
||||
`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 |
|
||||
|---|---|
|
||||
@@ -23,8 +23,8 @@ The enum also carries `Int16 / Int64 / UInt16 / UInt32 / UInt64 / Reference` mem
|
||||
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`.
|
||||
- **AB CIP** — `src/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`.
|
||||
- **AB CIP** — `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDataType.cs` maps CIP tag type codes.
|
||||
- **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.
|
||||
|
||||
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` 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:
|
||||
|
||||
@@ -57,9 +57,9 @@ Drivers whose backend has no notion of classification (Modbus, most PLCs) defaul
|
||||
|
||||
## Key source files
|
||||
|
||||
- `src/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/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs` — per-attribute descriptor
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `MapDataType` translation
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs` — classification-to-role policy
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs` — driver-agnostic type enum
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/SecurityClassification.cs` — write-authz tier metadata
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs` — per-attribute descriptor
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs` — `MapDataType` translation
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs` — classification-to-role policy
|
||||
- Per-driver mappers in each `Driver.*` project
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
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`
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ For current architecture see:
|
||||
|---|---|
|
||||
| `AlarmTracking.md` | v1 alarm-tracking flow through the in-process MXAccess client |
|
||||
| `Configuration.md` | v1 server configuration (`OTOPCUA_GALAXY_*` env vars now live in mxaccessgw config) |
|
||||
| `DataTypeMapping.md` | Galaxy `mx_data_type` → OPC UA type mapping (still accurate as a reference; the live mapping logic is in `src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`) |
|
||||
| `DataTypeMapping.md` | Galaxy `mx_data_type` → OPC UA type mapping (still accurate as a reference; the live mapping logic is in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`) |
|
||||
| `HistoricalDataAccess.md` | v1 IHistoryProvider on the Host side; current path is the server-level HistoryRouter + Wonderware sidecar |
|
||||
| `Subscriptions.md` | v1 MXAccess subscription mechanics; current path uses gateway StreamEvents |
|
||||
| `drivers/Galaxy-Repository.md` | v1 Host-side ZB SQL repository client; the gateway owns this path now |
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 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
|
||||
|
||||
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.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.
|
||||
|
||||
@@ -63,7 +63,7 @@ When an OPC UA session is resumed (client reconnect with `TransferSubscriptions`
|
||||
|
||||
## Key source files
|
||||
|
||||
- `src/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.Abstractions/ISubscribable.cs` — capability contract
|
||||
- `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
|
||||
- Per-driver subscribe implementations in each `Driver.*` project
|
||||
|
||||
@@ -147,10 +147,10 @@ Dev credentials in this inventory are convenience defaults, not secrets. Change
|
||||
| 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 |
|
||||
| **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) |
|
||||
| **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) |
|
||||
| **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) |
|
||||
| **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) |
|
||||
| **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/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/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/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 |
|
||||
| **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) |
|
||||
@@ -165,10 +165,10 @@ init + skip cleanly when nothing's running.
|
||||
|
||||
| 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` |
|
||||
| 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` |
|
||||
| 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` |
|
||||
| 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` |
|
||||
| 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/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/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/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml` | `docker compose -f <compose> up -d` |
|
||||
|
||||
First build of a local-build image takes 1–5 minutes; subsequent runs use
|
||||
layer cache. `ab_server` is the slowest (multi-stage build clones
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
### 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
|
||||
|
||||
@@ -29,7 +29,7 @@ History reads + alarm condition tracking now live in the server-layer `IHistoryR
|
||||
|
||||
### 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
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# FOCAS version / capability matrix
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
- 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
|
||||
boundaries + unknown-series permissiveness + rejection-message
|
||||
content + case-insensitivity.
|
||||
|
||||
@@ -72,7 +72,7 @@ takes the form of the per-driver test suites + e2e scripts:
|
||||
- [x] **Integration tests** — `Driver.*.IntegrationTests` stands up Docker-hosted simulators (pymodbus, ab_server, python-snap7, opc-plc) at collection init and exercises real wire-level read/write/subscribe/probe per driver.
|
||||
- [x] **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] **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.
|
||||
|
||||
### Live-boot verification
|
||||
|
||||
@@ -49,7 +49,7 @@ Covered by `scripts/compliance/phase-7-compliance.ps1`:
|
||||
|
||||
Originally kept out of the capstone so the gate could close cleanly. Each landed as a targeted follow-up PR; audit this session verified them against the repo:
|
||||
|
||||
- [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] **sp_ComputeGenerationDiff extension** (task #241) — **CLOSED**. Migration `20260420232000_ExtendComputeGenerationDiffWithPhase7.cs` extends the stored proc to emit Script / VirtualTag / ScriptedAlarm sections alongside the existing NodeAcl / Tag / Equipment / DriverInstance / Namespace output. Admin DiffViewer picks them up through its existing section-plugin architecture (Phase 6.4 Stream C).
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ itself is verifiable without Fwlib32 actually being called:
|
||||
assert rejection.
|
||||
- **Fwlib32 integration itself**: still untestable without hardware.
|
||||
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`.
|
||||
|
||||
## Decisions to confirm before starting
|
||||
|
||||
@@ -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
|
||||
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`
|
||||
|
||||
@@ -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
|
||||
+ licensed Fwlib64.dll + real CNC are all available. See the implementer's
|
||||
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
|
||||
|
||||
@@ -283,8 +283,8 @@ Once Phase 2 proves Fwlib64 can talk to the mock:
|
||||
|
||||
## 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/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/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/FwlibNative.cs`](../../../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibNative.cs) — P/Invoke surface, authoritative struct layouts
|
||||
- [`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/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
|
||||
- `strangesast/fwlib` on GitHub — redistributes `fwlib32.h` + runtime binaries; no wire protocol docs
|
||||
|
||||
@@ -74,9 +74,9 @@ Save the result to `docs/v2/implementation/phase-0-rename-inventory.md` (gitigno
|
||||
Per project (11 projects total — 5 src + 6 tests):
|
||||
|
||||
```bash
|
||||
git mv src/ZB.MOM.WW.LmxOpcUa.Client.CLI src/ZB.MOM.WW.OtOpcUa.Client.CLI
|
||||
git mv src/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
|
||||
git mv src/ZB.MOM.WW.LmxOpcUa.Client.CLI src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI
|
||||
git mv src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.LmxOpcUa.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.
|
||||
@@ -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:
|
||||
|
||||
```bash
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
|
||||
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 2
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
|
||||
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 2
|
||||
```
|
||||
|
||||
**Acceptance**:
|
||||
|
||||
@@ -63,7 +63,7 @@ Phase 1 is large — broken into 5 work streams (A–E) that can partly overlap.
|
||||
|
||||
#### Task A.1 — Define driver capability interfaces
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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:
|
||||
- `ServerCluster`, `ClusterNode`, `ClusterNodeCredential`
|
||||
@@ -146,7 +146,7 @@ Implement DbContext with entities matching `config-db-schema.md` exactly:
|
||||
Generate the initial migration:
|
||||
|
||||
```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**:
|
||||
@@ -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)
|
||||
|
||||
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`
|
||||
- `Components/Layout/MainLayout.razor`, `Components/Layout/NavMenu.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
|
||||
# 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
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
>
|
||||
> **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 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`.
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
> **Status**: **SHIPPED (mostly)** 2026-04-19; audit 2026-04-23 confirms what landed separately after the data-layer PR #91:
|
||||
>
|
||||
> **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 #156 Stream C** — `DiffViewer.razor` + `DiffSection.razor` refactor in place.
|
||||
> - Admin UI `IdentificationFields.razor` surface shipped (part of #157).
|
||||
>
|
||||
> **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.**
|
||||
>
|
||||
|
||||
@@ -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` |
|
||||
| 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 |
|
||||
| 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.
|
||||
|
||||
@@ -21,7 +21,7 @@ End-to-end validation that the Phase 7 production wiring chain (#243 / #244 / #2
|
||||
### 1. Migrate the Config DB
|
||||
|
||||
```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;"
|
||||
```
|
||||
|
||||
@@ -126,7 +126,7 @@ Dev-box GLAuth ships `writeop` / `writeop123` in the `WriteOperate` group, `admi
|
||||
### 5. Start the Server
|
||||
|
||||
```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):
|
||||
@@ -146,7 +146,7 @@ Any line missing = follow up the failure surface (each step has its own log sign
|
||||
### 6. Validate via Client.CLI
|
||||
|
||||
```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.
|
||||
@@ -154,7 +154,7 @@ Expect to see under the namespace root: `lab-floor → galaxy-line → reactor-1
|
||||
#### Read the virtual tag
|
||||
|
||||
```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 `
|
||||
-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
|
||||
|
||||
```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 `
|
||||
-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
|
||||
# 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 `
|
||||
-n "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/Source" `
|
||||
-v 75 -U writeop -P writeop123
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
# LMX Galaxy bridge — remaining follow-ups
|
||||
|
||||
State after PR 19: the Galaxy driver is functionally at v1 parity through the
|
||||
`IDriver` abstraction; the OPC UA server runs with LDAP-authenticated
|
||||
Basic256Sha256 endpoints and alarms are observable through
|
||||
`AlarmConditionState.ReportEvent`. The items below are what remains LMX-
|
||||
specific before the stack can fully replace the v1 deployment, in
|
||||
rough priority order.
|
||||
|
||||
## 1. Proxy-side `IHistoryProvider` for `ReadAtTime` / `ReadEvents` — **DONE (PRs 35 + 38)**
|
||||
|
||||
PR 35 extended `IHistoryProvider` with `ReadAtTimeAsync` + `ReadEventsAsync`
|
||||
(default throwing implementations so existing impls keep compiling), added the
|
||||
`HistoricalEvent` + `HistoricalEventsResult` records to `Core.Abstractions`,
|
||||
and implemented both methods in `GalaxyProxyDriver` on top of the PR 10 / PR 11
|
||||
IPC messages.
|
||||
|
||||
PR 38 wired the OPC UA HistoryRead service-handler through
|
||||
`DriverNodeManager` by overriding `CustomNodeManager2`'s four per-kind hooks —
|
||||
`HistoryReadRawModified` / `HistoryReadProcessed` / `HistoryReadAtTime` /
|
||||
`HistoryReadEvents`. Each walks `nodesToProcess`, resolves the driver-side
|
||||
full reference from `NodeId.Identifier`, dispatches to the right
|
||||
`IHistoryProvider` method, and populates the paired results + errors lists
|
||||
(both must be set — the MasterNodeManager merges them and a Good result with
|
||||
an unset error slot serializes as `BadHistoryOperationUnsupported` on the
|
||||
wire). Historized variables gain `AccessLevels.HistoryRead` so the stack
|
||||
dispatches; the driver root folder gains `EventNotifiers.HistoryRead` so
|
||||
`HistoryReadEvents` can target it.
|
||||
|
||||
Aggregate translation uses a small `MapAggregate` helper that handles
|
||||
`Average` / `Minimum` / `Maximum` / `Total` / `Count` (the enum surface the
|
||||
driver exposes) and returns null for unsupported aggregates so the handler
|
||||
can surface `BadAggregateNotSupported`. Raw+Processed+AtTime wrap driver
|
||||
samples as `HistoryData` in an `ExtensionObject`; Events emits a
|
||||
`HistoryEvent` with the standard BaseEventType field list (EventId /
|
||||
SourceName / Message / Severity / Time / ReceiveTime) — custom
|
||||
`SelectClause` evaluation is an explicit follow-up.
|
||||
|
||||
**Tests**:
|
||||
|
||||
- `DriverNodeManagerHistoryMappingTests` — 12 unit cases pinning
|
||||
`MapAggregate`, `BuildHistoryData`, `BuildHistoryEvent`, `ToDataValue`.
|
||||
- `HistoryReadIntegrationTests` — 5 end-to-end cases drive a real OPC UA
|
||||
client (`Session.HistoryRead`) against a fake `IHistoryProvider` driver
|
||||
through the running stack. Covers raw round-trip, processed with Average
|
||||
aggregate, unsupported aggregate → `BadAggregateNotSupported`, at-time
|
||||
timestamp forwarding, and events field-list shape.
|
||||
|
||||
**Deferred**:
|
||||
- Continuation-point plumbing via `Session.Save/RestoreHistoryContinuationPoint`.
|
||||
Driver returns null continuations today so the pass-through is fine.
|
||||
- Per-`SelectClause` evaluation in HistoryReadEvents — clients that send a
|
||||
custom field selection currently get the standard BaseEventType layout.
|
||||
|
||||
## 2. Write-gating by role — **DONE (PR 26)**
|
||||
|
||||
Landed in PR 26. `WriteAuthzPolicy` in `Server/Security/` maps
|
||||
`SecurityClassification` → required role (`FreeAccess` → no role required,
|
||||
`Operate`/`SecuredWrite` → `WriteOperate`, `Tune` → `WriteTune`,
|
||||
`Configure`/`VerifiedWrite` → `WriteConfigure`, `ViewOnly` → deny regardless).
|
||||
`DriverNodeManager` caches the classification per variable during discovery and
|
||||
checks the session's roles (via `IRoleBearer`) in `OnWriteValue` before calling
|
||||
`IWritable.WriteAsync`. Roles do not cascade — a session with `WriteOperate`
|
||||
can't write a `Tune` attribute unless it also carries `WriteTune`.
|
||||
|
||||
See `feedback_acl_at_server_layer.md` in memory for the architectural directive
|
||||
that authz stays at the server layer and never delegates to driver-specific auth.
|
||||
|
||||
## 3. Admin UI client-cert trust management — **DONE (PR 28)**
|
||||
|
||||
PR 28 shipped `/certificates` in the Admin UI. `CertTrustService` reads the OPC
|
||||
UA server's PKI store root (`OpcUaServerOptions.PkiStoreRoot` — default
|
||||
`%ProgramData%\OtOpcUa\pki`) and lists rejected + trusted certs by parsing the
|
||||
`.der` files directly, so it has no `Opc.Ua` dependency and runs on any
|
||||
Admin host that can reach the shared PKI directory.
|
||||
|
||||
Operator actions: Trust (moves `rejected/certs/*.der` → `trusted/certs/*.der`),
|
||||
Delete rejected, Revoke trust. The OPC UA stack re-reads the trusted store on
|
||||
each new client handshake, so no explicit reload signal is needed —
|
||||
operators retry the rejected client's connection after trusting.
|
||||
|
||||
Deferred: flipping `AutoAcceptUntrustedClientCertificates` to `false` as the
|
||||
deployment default. That's a production-hardening config change, not a code
|
||||
gap — the Admin UI is now ready to be the trust gate.
|
||||
|
||||
## 4. Live-LDAP integration test — **DONE (PR 31)**
|
||||
|
||||
PR 31 shipped `Server.Tests/LdapUserAuthenticatorLiveTests.cs` — 6 live-bind
|
||||
tests against the dev GLAuth instance at `localhost:3893`, skipped cleanly
|
||||
when the port is unreachable. Covers: valid bind, wrong password, unknown
|
||||
user, empty credentials, single-group → WriteOperate mapping, multi-group
|
||||
admin user surfacing all mapped roles.
|
||||
|
||||
Also added `UserNameAttribute` to `LdapOptions` (default `uid` for RFC 2307
|
||||
compat) so Active Directory deployments can configure `sAMAccountName` /
|
||||
`userPrincipalName` without code changes. `LdapUserAuthenticatorAdCompatTests`
|
||||
(5 unit guards) pins the AD-shape DN parsing + filter escape behaviors. See
|
||||
`docs/security.md` §"Active Directory configuration" for the AD appsettings
|
||||
snippet.
|
||||
|
||||
Deferred: asserting `session.Identity` end-to-end on the server side (i.e.
|
||||
drive a full OPC UA session with username/password, then read an
|
||||
`IHostConnectivityProbe`-style "whoami" node to verify the role surfaced).
|
||||
That needs a test-only address-space node and is a separate PR.
|
||||
|
||||
## 5. Full Galaxy live-service smoke test against the merged v2 stack — **IN PROGRESS (PRs 36 + 37)**
|
||||
|
||||
PR 36 shipped the prerequisites helper (`AvevaPrerequisites`) that probes
|
||||
every dependency a live smoke test needs and produces actionable skip
|
||||
messages.
|
||||
|
||||
PR 37 shipped the live-stack smoke test project structure:
|
||||
`tests/Driver.Galaxy.Proxy.Tests/LiveStack/` with `LiveStackFixture` (connects
|
||||
to the *already-running* `OtOpcUaGalaxyHost` Windows service via named pipe;
|
||||
never spawns the Host process) and `LiveStackSmokeTests` covering:
|
||||
|
||||
- Fixture initializes successfully (IPC handshake succeeds end-to-end).
|
||||
- Driver reports `DriverState.Healthy` post-handshake.
|
||||
- `DiscoverAsync` returns at least one variable from the live Galaxy.
|
||||
- `GetHostStatuses` reports at least one Platform/AppEngine host.
|
||||
- `ReadAsync` on a discovered variable round-trips through
|
||||
Proxy → Host pipe → MXAccess → back without a BadInternalError.
|
||||
|
||||
Shared secret + pipe name resolve from `OTOPCUA_GALAXY_SECRET` /
|
||||
`OTOPCUA_GALAXY_PIPE` env vars, falling back to reading the service's
|
||||
registry-stored Environment values (requires elevated test host).
|
||||
|
||||
**PR 40** added the write + subscribe facts targeting
|
||||
`DelmiaReceiver_001.TestAttribute` (the writable Boolean UDA the dev Galaxy
|
||||
ships under TestMachine_001) — write-then-read with a 5s scan-window poll +
|
||||
restore-on-finally, and subscribe-then-write asserting both an initial-value
|
||||
OnDataChange and a post-write OnDataChange. PR 39 added the elevated-shell
|
||||
short-circuit so a developer running from an admin window gets an actionable
|
||||
skip instead of `UnauthorizedAccessException`.
|
||||
|
||||
**Run the live tests** (from a NORMAL non-admin PowerShell):
|
||||
|
||||
```powershell
|
||||
$env:OTOPCUA_GALAXY_SECRET = Get-Content C:\Users\dohertj2\Desktop\lmxopcua\.local\galaxy-host-secret.txt
|
||||
cd C:\Users\dohertj2\Desktop\lmxopcua
|
||||
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests --filter "FullyQualifiedName~LiveStackSmokeTests"
|
||||
```
|
||||
|
||||
Expected: 7/7 pass against the running `OtOpcUaGalaxyHost` service.
|
||||
|
||||
**Remaining for #5 in production-grade form**:
|
||||
- Confirm the suite passes from a non-elevated shell (operator action).
|
||||
- Add similar facts for an alarm-source attribute once `TestMachine_001` (or
|
||||
a sibling) carries a deployed alarm condition — the current dev Galaxy's
|
||||
TestAttribute isn't alarm-flagged.
|
||||
|
||||
## 6. Second driver instance on the same server — **DONE (PR 32)**
|
||||
|
||||
`Server.Tests/MultipleDriverInstancesIntegrationTests.cs` registers two
|
||||
drivers with distinct `DriverInstanceId`s on one `DriverHost`, spins up the
|
||||
full OPC UA server, and asserts three behaviors: (1) each driver's namespace
|
||||
URI (`urn:OtOpcUa:{id}`) resolves to a distinct index in the client's
|
||||
NamespaceUris, (2) browsing one subtree returns that driver's folder and
|
||||
does NOT leak the other driver's folder, (3) reads route to the correct
|
||||
driver — the alpha instance returns 42 while beta returns 99, so a misroute
|
||||
would surface at the assertion layer.
|
||||
|
||||
Deferred: the alarm-event multi-driver parity case (two drivers each raising
|
||||
a `GalaxyAlarmEvent`, assert each condition lands on its owning instance's
|
||||
condition node). Alarm tracking already has its own integration test
|
||||
(`AlarmSubscription*`); the multi-driver alarm case would need a stub
|
||||
`IAlarmSource` that's worth its own focused PR.
|
||||
|
||||
## 7. Host-status per-AppEngine granularity → Admin UI dashboard — **DONE (PRs 33 + 34)**
|
||||
|
||||
**PR 33** landed the data layer: `DriverHostStatus` entity + migration with
|
||||
composite key `(NodeId, DriverInstanceId, HostName)` and two query-supporting
|
||||
indexes (per-cluster drill-down on `NodeId`, stale-row detection on
|
||||
`LastSeenUtc`).
|
||||
|
||||
**PR 34** wired the publisher + consumer. `HostStatusPublisher` is a
|
||||
`BackgroundService` in the Server process that walks every registered
|
||||
`IHostConnectivityProbe`-capable driver every 10s, calls
|
||||
`GetHostStatuses()`, and upserts rows (`LastSeenUtc` advances each tick;
|
||||
`State` + `StateChangedUtc` update on transitions). Admin UI `/hosts` page
|
||||
groups by cluster, shows four summary cards (Hosts / Running / Stale /
|
||||
Faulted), and flags rows whose `LastSeenUtc` is older than 30s as Stale so
|
||||
operators see crashed Servers without waiting for a state change.
|
||||
|
||||
Deferred as follow-ups:
|
||||
|
||||
- Event-driven push (subscribe to `OnHostStatusChanged` per driver for
|
||||
sub-heartbeat latency). Adds DriverHost lifecycle-event plumbing;
|
||||
10s polling is fine for operator-scale use.
|
||||
- Failure-count column — needs the publisher to track a transition history
|
||||
per host, not just current-state.
|
||||
- SignalR fan-out to the Admin page (currently the page polls the DB, not
|
||||
a hub). The DB-polled version is fine at current cadence but a hub push
|
||||
would eliminate the 10s race where a new row sits in the DB before the
|
||||
Admin page notices.
|
||||
@@ -7,7 +7,7 @@ populations disagree with the spec in small, device-specific ways, and a driver
|
||||
passes textbook tests can still misbehave against actual equipment.
|
||||
|
||||
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
|
||||
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**:
|
||||
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
|
||||
`localhost:5020`; override via `MODBUS_SIM_ENDPOINT` for a real PLC on its
|
||||
native port 502.
|
||||
@@ -116,7 +116,7 @@ vendors get promoted into driver defaults or opt-in options:
|
||||
## Next concrete PRs
|
||||
|
||||
- **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
|
||||
endpoint is unreachable), `DL205/DL205Profile.cs` (tag map stub), and
|
||||
`DL205/DL205SmokeTests.cs` (write-then-read round-trip).
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
# Phase 7 Status — Scripting Runtime, Virtual Tags, Scripted Alarms, Historian Sink
|
||||
|
||||
> **Reconciliation date**: 2026-05-18
|
||||
> **Based on**: `docs/v2/implementation/phase-7-scripting-and-alarming.md` (the plan) and
|
||||
> `docs/v2/implementation/exit-gate-phase-7.md` (the exit-gate audit) cross-checked against
|
||||
> the actual repository files. See "Evidence sources" at the bottom.
|
||||
|
||||
## Summary verdict
|
||||
|
||||
**Phase 7 core is fully shipped and the exit gate is closed.** All eight plan streams
|
||||
(A–H, where H = exit gate) plus the three deferred follow-ups (tasks #239 / #240 / #241)
|
||||
landed before the 2026-04-23 exit-gate audit. The `v2-release-readiness.md` note
|
||||
"Phase 7 — out of scope for v2 GA" is a stale label: the work shipped after that doc
|
||||
was last updated. The four `Core.*` Phase 7 projects exist, have tests, and are wired
|
||||
into the running server. Five targeted gaps remain open (see section below).
|
||||
|
||||
---
|
||||
|
||||
## Work-item status by plan stream
|
||||
|
||||
### Stream A — `Core.Scripting` (Roslyn engine, sandbox, AST inference, logger)
|
||||
|
||||
| Plan item | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| A.1 — Project scaffold + `ScriptContext` base class (`GetTag` / `SetVirtualTag` / `Logger` / `Now` / `Deadband`) | **Done** | `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptContext.cs`, `ScriptGlobals.cs` |
|
||||
| A.2 — `DependencyExtractor : CSharpSyntaxWalker` — literal-only path check, `Inputs` + `Outputs` sets | **Done** | `DependencyExtractor.cs`; literal-reject logic exercised by 7 test files in `Core.Scripting.Tests` |
|
||||
| A.3 — Compile cache keyed on `SHA-256(source)` | **Done** | `CompiledScriptCache.cs` (`ConcurrentDictionary<string, Lazy<ScriptEvaluator<...>>>`) |
|
||||
| A.4 — Per-evaluation timeout (250 ms default) | **Done** | `TimedScriptEvaluator.cs`; `TimedScriptEvaluatorTests.cs` |
|
||||
| A.5 — Serilog sink wiring; `scripts-*.log` companion mirror to main log at WARN on ERROR | **Done** | `ScriptLoggerFactory.cs`, `ScriptLogCompanionSink.cs`; `ScriptLogCompanionSinkTests.cs` |
|
||||
| A.6 — Tests (AST extraction, sandbox escape, exception isolation, timeout, logger binding) | **Done** | `ScriptSandboxTests.cs`, `DependencyExtractorTests.cs`, `CompiledScriptCacheTests.cs`, `ScriptLoggerFactoryTests.cs`, `TimedScriptEvaluatorTests.cs` — 7 test files |
|
||||
|
||||
Shipped as PRs #177–#179 (63 tests).
|
||||
|
||||
### Stream B — Virtual tag engine
|
||||
|
||||
| Plan item | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| B.1 — `VirtualTagEngine` + `DependencyGraph` | **Done** | `VirtualTagEngine.cs`, `DependencyGraph.cs` |
|
||||
| B.2 — `ChangeTriggerDispatcher` (subscribe to referenced driver tags via `ITagUpstreamSource`) | **Done** | `VirtualTagEngine.OnUpstreamChange` internal subscriber path |
|
||||
| B.3 — `TimerTriggerDispatcher` (per-tag `IntervalMs` via timer-wheel) | **Done** | `TimerTriggerScheduler.cs` |
|
||||
| B.4 — `EvaluationPipeline` (serial, per-tag isolation, `_evalGate` semaphore) | **Done** | `VirtualTagEngine.EvaluateInternalAsync`; `_evalGate SemaphoreSlim(1,1)` |
|
||||
| B.5 — `IVirtualTagSource` implementing `IReadable` + `ISubscribable` | **Done** | `VirtualTagSource.cs` |
|
||||
| B.6 — History routing (`IHistoryWriter.Record` when `Historize=true`) | **Partial** | `IHistoryWriter.cs` + `NullHistoryWriter` present; no production writer is wired into the virtual-tag path. `docs/VirtualTags.md` §"Upstream reads + history" explicitly notes: "no production writer is currently wired for virtual tags". Virtual-tag historization is functional at the engine level but has no live sink. |
|
||||
| B.7 — Tests: dependency graph, cascade, timer, change+timer combined, error propagation, historize | **Done** | `DependencyGraphTests.cs`, `VirtualTagEngineTests.cs`, `TimerTriggerSchedulerTests.cs`, `VirtualTagSourceTests.cs` — 5 test files |
|
||||
|
||||
Shipped as PR #180 (36 tests).
|
||||
|
||||
### Stream C — Scripted alarm engine + Part 9 state machine + template messages
|
||||
|
||||
| Plan item | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| C.1 — `ScriptedAlarmEngine` skeleton + alarm config model | **Done** | `ScriptedAlarmEngine.cs`, `ScriptedAlarmDefinition.cs` |
|
||||
| C.2 — `Part9StateMachine` (Enable/Disable/Active/Ack/Confirm/Shelve/Unshelve/Comment/ShelvingCheck) | **Done** | `Part9StateMachine.cs`; `Part9StateMachineTests.cs` |
|
||||
| C.3 — Predicate evaluation on input change; activate/clear transitions | **Done** | `ScriptedAlarmEngine.ReevaluateAsync`; `_alarmsReferencing` inverse index |
|
||||
| C.4 — Startup recovery (`ActiveState` re-derived; Enabled/Ack/Confirm/Shelve loaded from store) | **Done** | `ScriptedAlarmEngine.LoadAsync`; `IAlarmStateStore.LoadAsync` |
|
||||
| C.5 — Template substitution (`{TagPath}` tokens resolved at emission time) | **Done** | `MessageTemplate.cs`; `MessageTemplateTests.cs` |
|
||||
| C.6 — OPC UA method binding (Acknowledge / Confirm / AddComment / OneShotShelve / TimedShelve / Unshelve) | **Partial** | Engine methods exist and are tested. `ScriptedAlarmSource.AcknowledgeAsync` defaults the user to `"opcua-client"`. The plan's Stream G wiring of these methods to OPC UA `MethodCall` dispatch on the condition nodes (so OPC UA client method calls reach the engine with the authenticated principal) is noted in the e2e smoke doc as "not yet wired through `DriverNodeManager.MethodCall` dispatch." Operators acknowledge through Admin UI today; the Part 9 method-call path is a follow-up. |
|
||||
| C.7 — `IAlarmSource` implementation / fan-out registration | **Done** | `ScriptedAlarmSource.cs` |
|
||||
| C.8 — Tests: all state transitions, startup recovery, template substitution, shelving timer expiry | **Done** | `Part9StateMachineTests.cs`, `ScriptedAlarmEngineTests.cs`, `ScriptedAlarmSourceTests.cs`, `MessageTemplateTests.cs` — 5 test files |
|
||||
|
||||
Shipped as PR #181 (47 tests).
|
||||
|
||||
### Stream D — Historian alarm sink (SQLite store-and-forward + Wonderware IPC)
|
||||
|
||||
| Plan item | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| D.1 — `Core.AlarmHistorian` project; `IAlarmHistorianSink`; `SqliteStoreAndForwardSink` (backoff, dead-letter, capacity) | **Done** | `IAlarmHistorianSink.cs`, `SqliteStoreAndForwardSink.cs`; `SqliteStoreAndForwardSinkTests.cs` |
|
||||
| D.2 — Live-historian smoke against dev box Aveva Historian; document the exact SDK entry point | **Partial** | The smoke (`docs/v2/implementation/phase-7-e2e-smoke.md`) ran but the IPC path via Galaxy.Host to `aahClientManaged` was the original plan. That path changed: the production implementation uses `Driver.Historian.Wonderware.Client` (`WonderwareHistorianClient.WriteBatchAsync`) over a named-pipe sidecar, not Galaxy.Host. There is no separate `docs/v2/historian-alarm-api.md` artifact documenting the SDK entry point as the plan called for; the implementation detail is in `WonderwareHistorianClient.cs` inline. |
|
||||
| D.3 — `Driver.Galaxy.Shared` contract additions (`HistorianAlarmEventRequest` / `Response` / `ConnectivityStatusNotification`) | **Changed** | The plan routed alarm writes through Galaxy.Host IPC. The shipped implementation uses `Driver.Historian.Wonderware.Client` (a standalone sidecar project) instead. `HistorianAlarmEventRequest` / `HistorianAlarmEventResponse` as named protos never shipped; the equivalent contract is the `AlarmHistorianEventDto` / `WriteAlarmEventsRequest` / `WriteAlarmEventsReply` MessagePack DTOs in `Driver.Historian.Wonderware.Client/Ipc/`. Galaxy.Host still exists as the mxaccessgw entry point but does not carry historian writes. |
|
||||
| D.4 — `Driver.Galaxy.Host` handler for alarm writes | **Changed** | Not shipped via Galaxy.Host. The sidecar (`Driver.Historian.Wonderware.Client`) is the production path. `IAlarmHistorianWriter` is implemented by `WonderwareHistorianClient`, not by a Galaxy.Host frame handler. |
|
||||
| D.5 — Drain worker in main server (poll SQLite queue, batch 100 events, exponential backoff) | **Done** | `SqliteStoreAndForwardSink.StartDrainLoop`; backoff ladder 1s → 2s → 5s → 15s → 60s; `Phase7Composer.ResolveHistorianSink` starts it with a 2-second drain cadence |
|
||||
| D.6 — Per-alarm `HistorizeToAveva` toggle; `AlarmHistorizationPolicy` per source | **Done** | `ScriptedAlarm.HistorizeToAveva` column (default `true`); `Phase7EngineComposer.RouteToHistorianAsync` checks it; Galaxy defaults `false` |
|
||||
| D.7 — `/alarms/historian` diagnostics view in Admin (queue depth, drain rate, last error, retry dead-lettered) | **Done** | `AlarmsHistorian.razor`; `HistorianDiagnosticsService.cs` |
|
||||
| D.8 — Tests | **Done** | `SqliteStoreAndForwardSinkTests.cs`; `Phase7ComposerWriterSelectionTests.cs` covers historian-writer resolution |
|
||||
|
||||
Shipped as PR #182 (14 tests). Architecture deviated from the plan (Wonderware sidecar instead of Galaxy.Host IPC) but the functional goals are met.
|
||||
|
||||
### Stream E — Config DB schema + generation-sealed cache extensions
|
||||
|
||||
| Plan item | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| E.1 — EF migration for `Script` / `VirtualTag` / `ScriptedAlarm` / `ScriptedAlarmState` tables | **Done** | Migration `20260420231641_AddPhase7ScriptingTables.cs`; entities in `Configuration/Entities/` |
|
||||
| E.2 — `sp_PublishGeneration` extension (sealed-cache snapshot includes Phase 7 rows) | **Done** | Migration `20260420232000_ExtendComputeGenerationDiffWithPhase7.cs` |
|
||||
| E.3 — CRUD services: `VirtualTagService`, `ScriptedAlarmService`, `ScriptService`, `ScriptedAlarmStateService` | **Done** | All four exist in `Admin/Services/`; `GetStateAsync` on `ScriptedAlarmService` serves the state query |
|
||||
| E.4 — Tests: migration up/down; publish atomicity; audit trail | **Done** | `Phase7ServicesTests.cs` (13 tests covering CRUD + hash behavior + harness) |
|
||||
|
||||
Shipped as PR #183 (12 tests in configuration; 13 more in Admin.Tests).
|
||||
|
||||
### Stream F — Admin UI scripting tab
|
||||
|
||||
| Plan item | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| F.1 — Monaco editor Razor component (CDN bundle + textarea fallback) | **Done** | `ScriptEditor.razor` (textarea with Monaco JS interop, `otOpcUaScriptEditor.attach`) |
|
||||
| F.2 — `/virtual-tags` tab (list view, edit pane, dependency preview, publish gate) | **Partial** | The `ScriptsTab.razor` is the single tab covering script CRUD, dependency preview, and harness. There is no separate `/virtual-tags` tab UI — virtual tags are managed through the script service alone; no VirtualTag list/edit form exists in the Admin UI. The per-tag fields (`EquipmentId`, `DataType`, `ChangeTriggered`, `TimerIntervalMs`, `Historize`) are accessible via the `VirtualTagService` backend but have no corresponding UI form. |
|
||||
| F.3 — `/scripted-alarms` tab (alarm type, severity, message template, `HistorizeToAveva`, detail page with shelve/ack state read-only) | **Partial** | No dedicated scripted-alarms tab razor page exists (confirmed by Glob + Grep searches). Scripted alarm CRUD (`ScriptedAlarmService`) exists as a service but has no Admin UI page. |
|
||||
| F.4 — Test harness (modal, synthetic inputs, output + logger display) | **Partial** | `ScriptTestHarnessService.cs` is complete and tested. `ScriptsTab.razor` calls `Harness.RunVirtualTagAsync` with zero-value synthetic inputs derived from the extractor. A full interactive input-form modal was not shipped — the harness zeroes all inputs automatically rather than prompting the operator per-tag. |
|
||||
| F.5 — Script log viewer (SignalR tail of `scripts-*.log` filtered by `ScriptName`, load-more) | **Not started** | No SignalR stream of the scripts log is wired in the Admin UI. The `AlertHub` / `FleetStatusHub` exist but there is no `ScriptLogHub`. |
|
||||
| F.6 — `/alarms/historian` diagnostics view | **Done** | `AlarmsHistorian.razor` + `HistorianDiagnosticsService.cs` |
|
||||
| F.7 — Playwright smoke (author calc tag, verify in equipment tree; author alarm, verify in `AlarmsAndConditions`) | **Not started** | `tests/Server/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/` exists but its `UnsTabDragDropE2ETests.cs` is the only Playwright test; no Phase 7 Admin UI playwright scenario. |
|
||||
|
||||
Shipped as PR #185 (13 Admin service tests; UI completeness is partial — see gaps section).
|
||||
|
||||
### Stream G — Address-space integration
|
||||
|
||||
| Plan item | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| G.1 — `EquipmentNodeWalker` extension emits `NodeSourceKind.Virtual` + `NodeSourceKind.ScriptedAlarm` variables | **Done** | PR #184; `NodeSourceKind` discriminator confirmed in exit gate |
|
||||
| G.2 — `DriverNodeManager` dispatch routes reads by source; writes to non-Driver rejected with `BadUserAccessDenied` | **Done** | PR #186 follow-up; `OpcUaApplicationHost.SetPhase7Sources` threads `_virtualReadable` + `_scriptedAlarmReadable` into the node manager |
|
||||
| G.3 — `AlarmTracker` composition (`ScriptedAlarmEngine` registers as additional `IAlarmSource`) | **Done** | `ScriptedAlarmSource` adapts engine to `IAlarmSource`; `Phase7EngineComposer.Compose` wires it |
|
||||
| G.4 — Tests: mixed equipment folder browsable via Client.CLI; read/subscribe round-trip; alarm transitions in event stream | **Done** | `Phase7ComposerMappingTests.cs`, `Phase7EngineComposerTests.cs`, `ScriptedAlarmReadableTests.cs`, `CachedTagUpstreamSourceTests.cs`, `DriverSubscriptionBridgeTests.cs` — 6 test files in `Server.Tests/Phase7/` |
|
||||
| OPC UA method binding for alarm Ack/Confirm/Shelve | **Not started** | Noted explicitly in `phase-7-e2e-smoke.md` §"Known limitations": `DriverNodeManager.MethodCall` dispatch for scripted alarm methods is not wired. Engine has the methods; the OPC UA call path does not reach them. |
|
||||
|
||||
Shipped across PRs #184 + #186 (5 + 7 tests).
|
||||
|
||||
### Stream H — Exit gate
|
||||
|
||||
| Plan item | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| H.1 — Compliance script real-checks | **Done** | `scripts/compliance/phase-7-compliance.ps1` |
|
||||
| H.2 — Full solution `dotnet test` baseline | **Done** | Exit gate records ~197 new tests + solution baseline |
|
||||
| H.3 — `plan.md` Migration Strategy §6 update | **Not verified** | Not explicitly confirmed; minor — the plan doc is not the primary status artifact |
|
||||
| H.4 — Phase-status memory update | **Done** | Memory updated (see `project_alarms_over_gateway_epic.md` + `project_server_history_alarm_subsystems.md`) |
|
||||
| H.5 — Merge `v2/phase-7-scripting-and-alarming` → `v2` | **Done** | All PRs (#177–#186) merged |
|
||||
|
||||
### Post-gate follow-ups (tasks #239 / #240 / #241)
|
||||
|
||||
All three are verified closed in the 2026-04-23 exit-gate audit:
|
||||
|
||||
| Task | Item | Status |
|
||||
|------|------|--------|
|
||||
| #239 | `SealedBootstrap` composition root — `Phase7Composer.PrepareAsync` + `OpcUaServerService` wiring | **Done** |
|
||||
| #240 | Live OPC UA e2e smoke — `scripts/e2e/test-phase7-virtualtags.ps1` | **Done** (partial pass: 3/7 stages reach `PASS`; writer/subscribe/alarm stages blocked by live Galaxy attribute activity + Historian SDK environment) |
|
||||
| #241 | `sp_ComputeGenerationDiff` extension for Script / VirtualTag / ScriptedAlarm diff sections | **Done** — migration `20260420232000_ExtendComputeGenerationDiffWithPhase7` |
|
||||
|
||||
---
|
||||
|
||||
## What genuinely remains
|
||||
|
||||
These are real open items, not issues with the plan reconciliation.
|
||||
|
||||
### Gap 1 — OPC UA method-call dispatch for scripted alarm Ack/Confirm/Shelve (Stream G / C.6)
|
||||
|
||||
`DriverNodeManager.MethodCall` does not route OPC UA `Acknowledge` / `Confirm` / `OneShotShelve` / `TimedShelve` / `Unshelve` / `AddComment` method invocations to the `ScriptedAlarmEngine`. Operators can acknowledge scripted alarms through the Admin UI today; OPC UA HMI clients expecting to use Part 9 method nodes directly cannot. Explicit in `phase-7-e2e-smoke.md` §"Known limitations".
|
||||
|
||||
### Gap 2 — Admin UI: no `/virtual-tags` tab or form (Stream F.2)
|
||||
|
||||
`VirtualTagService` CRUD is fully tested but no razor page exposes it. Operators must author virtual tags through direct SQL or Admin API calls. `ScriptsTab.razor` covers script CRUD only; virtual-tag fields (`EquipmentId`, `DataType`, trigger config, `Historize`) have no UI form.
|
||||
|
||||
### Gap 3 — Admin UI: no `/scripted-alarms` tab or form (Stream F.3)
|
||||
|
||||
`ScriptedAlarmService` CRUD is fully tested but no razor page exists. Only `ScriptsTab.razor` under the cluster detail view is present; there is no `ScriptedAlarmsTab.razor` or equivalent.
|
||||
|
||||
### Gap 4 — Script log viewer not shipped (Stream F.5)
|
||||
|
||||
The SignalR tail of `scripts-*.log` filtered by `ScriptName` was not implemented. `ScriptsTab.razor` shows script output from the in-process harness but has no live-log panel for production emissions.
|
||||
|
||||
### Gap 5 — Virtual-tag historization has no production sink (Stream B.6)
|
||||
|
||||
`IHistoryWriter` + `NullHistoryWriter` are present; `VirtualTagEngine` calls `IHistoryWriter.Record` per evaluation when `Historize=true`. `Phase7EngineComposer.Compose` passes `NullHistoryWriter` — no live writer is wired. Virtual-tag values are computed and served correctly but never persisted to any historian. Explicitly documented in `docs/VirtualTags.md` §"Upstream reads + history".
|
||||
|
||||
---
|
||||
|
||||
## What is definitely done
|
||||
|
||||
- All four `Core.*` projects (`Core.Scripting`, `Core.VirtualTags`, `Core.ScriptedAlarms`, `Core.AlarmHistorian`) ship with full implementation and test coverage.
|
||||
- Roslyn sandbox (allow-list + `ForbiddenTypeAnalyzer` defense-in-depth + 250 ms timeout + per-script Serilog sink + compile cache) is complete.
|
||||
- Virtual tag engine: dependency graph with iterative Tarjan SCC, topo-sort, change-trigger cascade, timer trigger, `IReadable` + `ISubscribable` adapter, per-tag error isolation.
|
||||
- Scripted alarm engine: full Part 9 state machine, startup recovery, template substitution, `IAlarmSource` fan-out, 5-second shelving timer, `IAlarmStateStore` (in-memory default; DB-backed via Config DB entities).
|
||||
- SQLite store-and-forward historian sink: drain loop with exponential backoff, dead-letter retention, bounded capacity, `RetryDeadLettered` operator action.
|
||||
- Config DB schema: `Script`, `VirtualTag`, `ScriptedAlarm`, `ScriptedAlarmState` tables with EF migrations and generation-diff extension.
|
||||
- Admin services: `ScriptService`, `VirtualTagService`, `ScriptedAlarmService`, `ScriptTestHarnessService`, `HistorianDiagnosticsService` — all backed by unit tests.
|
||||
- Admin UI: `ScriptsTab.razor` (Monaco-backed editor, dependency preview, test harness), `AlarmsHistorian.razor` (queue depth, drain state, retry dead-lettered).
|
||||
- Server-side composition: `Phase7Composer`, `Phase7EngineComposer`, `CachedTagUpstreamSource`, `DriverSubscriptionBridge`, `ScriptedAlarmReadable` — fully wired into `OpcUaServerService` startup sequence before `OpcUaApplicationHost.StartAsync`.
|
||||
- `EquipmentNodeWalker` emits `NodeSourceKind.Virtual` and `NodeSourceKind.ScriptedAlarm` variables; `DriverNodeManager` dispatches reads and rejects writes to virtual nodes.
|
||||
- `WonderwareHistorianClient.WriteBatchAsync` implements `IAlarmHistorianWriter` as the alarm-event write path (deviation from plan's Galaxy.Host route, but functionally equivalent).
|
||||
- Compliance script `scripts/compliance/phase-7-compliance.ps1` and e2e smoke `scripts/e2e/test-phase7-virtualtags.ps1` both present.
|
||||
|
||||
---
|
||||
|
||||
## Evidence sources
|
||||
|
||||
| Source | Path |
|
||||
|--------|------|
|
||||
| Phase 7 plan | `docs/v2/implementation/phase-7-scripting-and-alarming.md` |
|
||||
| Phase 7 exit gate | `docs/v2/implementation/exit-gate-phase-7.md` |
|
||||
| E2E smoke runbook | `docs/v2/implementation/phase-7-e2e-smoke.md` |
|
||||
| Virtual tags reference doc | `docs/VirtualTags.md` |
|
||||
| Scripted alarms reference doc | `docs/ScriptedAlarms.md` |
|
||||
| `Core.Scripting` sources | `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/` |
|
||||
| `Core.VirtualTags` sources | `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/` |
|
||||
| `Core.ScriptedAlarms` sources | `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/` |
|
||||
| `Core.AlarmHistorian` sources | `src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/` |
|
||||
| Server Phase7 composition | `src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/` |
|
||||
| Admin services | `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/Script*.cs`, `VirtualTagService.cs`, `HistorianDiagnosticsService.cs` |
|
||||
| Admin UI pages | `src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptsTab.razor`, `AlarmsHistorian.razor` |
|
||||
| Historian sidecar writer | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/WonderwareHistorianClient.cs` |
|
||||
| EF migrations | `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260420231641_AddPhase7ScriptingTables.cs`, `20260420232000_ExtendComputeGenerationDiffWithPhase7.cs` |
|
||||
@@ -121,7 +121,7 @@ flips A4 from "deferred" to "expected pass").
|
||||
redundancy implementations we don't control.
|
||||
- For the sub-set of scenarios that *can* be automated — the self-loopback
|
||||
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`
|
||||
(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
|
||||
|
||||
@@ -191,7 +191,7 @@ Modbus has no native String, DateTime, or Int64 — those rows are skipped on th
|
||||
|
||||
### 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`).
|
||||
- **`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
|
||||
shell: pwsh
|
||||
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
|
||||
# Wait for :44818 to accept connections (compose healthcheck-equivalent)
|
||||
for ($i = 0; $i -lt 30; $i++) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Phase 6.1 exit-gate compliance check. Each check either passes or records a
|
||||
failure; non-zero exit = fail.
|
||||
@@ -64,50 +64,51 @@ Write-Host "=== Phase 6.1 compliance - Resilience & Observability runtime ===" -
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Stream A - Resilience layer"
|
||||
Assert-FileExists "Pipeline builder present" "src/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs"
|
||||
Assert-FileExists "CapabilityInvoker present" "src/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs"
|
||||
Assert-FileExists "WriteIdempotentAttribute present" "src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/WriteIdempotentAttribute.cs"
|
||||
Assert-TextFound "Pipeline key includes HostName (per-device isolation)" "PipelineKey\(.+HostName" @("src/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs")
|
||||
Assert-TextFound "OnReadValue routes through invoker" "DriverCapability\.Read," @("src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
|
||||
Assert-TextFound "OnWriteValue routes through invoker" "ExecuteWriteAsync" @("src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
|
||||
Assert-TextFound "HistoryRead routes through invoker" "DriverCapability\.HistoryRead" @("src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
|
||||
Assert-FileExists "Galaxy supervisor CircuitBreaker preserved" "src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/Supervisor/CircuitBreaker.cs"
|
||||
Assert-FileExists "Galaxy supervisor Backoff preserved" "src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/Supervisor/Backoff.cs"
|
||||
Assert-FileExists "Pipeline builder present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs"
|
||||
Assert-FileExists "CapabilityInvoker present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs"
|
||||
Assert-FileExists "WriteIdempotentAttribute present" "src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/WriteIdempotentAttribute.cs"
|
||||
Assert-TextFound "Pipeline key includes HostName (per-device isolation)" "PipelineKey\(.+HostName" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs")
|
||||
Assert-TextFound "OnReadValue routes through invoker" "DriverCapability\.Read," @("src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
|
||||
Assert-TextFound "OnWriteValue routes through invoker" "ExecuteWriteAsync" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
|
||||
Assert-TextFound "HistoryRead routes through invoker" "DriverCapability\.HistoryRead" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
|
||||
# The pre-PR-7.2 Galaxy.Proxy supervisor (CircuitBreaker/Backoff) was retired with the legacy
|
||||
# in-process Galaxy stack; circuit-breaker + backoff resilience is now the Core pipeline checked
|
||||
# above (DriverResiliencePipelineBuilder, per-device-keyed). No Galaxy.Proxy assertions remain.
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream B - Tier A/B/C runtime"
|
||||
Assert-FileExists "DriverTier enum present" "src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTier.cs"
|
||||
Assert-TextFound "DriverTypeMetadata requires Tier" "DriverTier Tier" @("src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs")
|
||||
Assert-FileExists "MemoryTracking present" "src/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryTracking.cs"
|
||||
Assert-FileExists "MemoryRecycle present" "src/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryRecycle.cs"
|
||||
Assert-TextFound "MemoryRecycle is Tier C gated" "_tier == DriverTier\.C" @("src/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryRecycle.cs")
|
||||
Assert-FileExists "ScheduledRecycleScheduler present" "src/ZB.MOM.WW.OtOpcUa.Core/Stability/ScheduledRecycleScheduler.cs"
|
||||
Assert-TextFound "Scheduler ctor rejects Tier A/B" "tier != DriverTier\.C" @("src/ZB.MOM.WW.OtOpcUa.Core/Stability/ScheduledRecycleScheduler.cs")
|
||||
Assert-FileExists "WedgeDetector present" "src/ZB.MOM.WW.OtOpcUa.Core/Stability/WedgeDetector.cs"
|
||||
Assert-TextFound "WedgeDetector is demand-aware" "HasPendingWork" @("src/ZB.MOM.WW.OtOpcUa.Core/Stability/WedgeDetector.cs")
|
||||
Assert-FileExists "DriverTier enum present" "src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTier.cs"
|
||||
Assert-TextFound "DriverTypeMetadata requires Tier" "DriverTier Tier" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs")
|
||||
Assert-FileExists "MemoryTracking present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryTracking.cs"
|
||||
Assert-FileExists "MemoryRecycle present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryRecycle.cs"
|
||||
Assert-TextFound "MemoryRecycle is Tier C gated" "_tier == DriverTier\.C" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryRecycle.cs")
|
||||
Assert-FileExists "ScheduledRecycleScheduler present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/ScheduledRecycleScheduler.cs"
|
||||
Assert-TextFound "Scheduler ctor rejects Tier A/B" "tier != DriverTier\.C" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/ScheduledRecycleScheduler.cs")
|
||||
Assert-FileExists "WedgeDetector present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/WedgeDetector.cs"
|
||||
Assert-TextFound "WedgeDetector is demand-aware" "HasPendingWork" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/WedgeDetector.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream C - Health + logging"
|
||||
Assert-FileExists "DriverHealthReport present" "src/ZB.MOM.WW.OtOpcUa.Core/Observability/DriverHealthReport.cs"
|
||||
Assert-FileExists "HealthEndpointsHost present" "src/ZB.MOM.WW.OtOpcUa.Server/Observability/HealthEndpointsHost.cs"
|
||||
Assert-TextFound "State matrix: Healthy = 200" "ReadinessVerdict\.Healthy => 200" @("src/ZB.MOM.WW.OtOpcUa.Core/Observability/DriverHealthReport.cs")
|
||||
Assert-TextFound "State matrix: Faulted = 503" "ReadinessVerdict\.Faulted => 503" @("src/ZB.MOM.WW.OtOpcUa.Core/Observability/DriverHealthReport.cs")
|
||||
Assert-FileExists "LogContextEnricher present" "src/ZB.MOM.WW.OtOpcUa.Core/Observability/LogContextEnricher.cs"
|
||||
Assert-TextFound "Enricher pushes DriverInstanceId property" "DriverInstanceId" @("src/ZB.MOM.WW.OtOpcUa.Core/Observability/LogContextEnricher.cs")
|
||||
Assert-TextFound "JSON sink opt-in via Serilog:WriteJson" "Serilog:WriteJson" @("src/ZB.MOM.WW.OtOpcUa.Server/Program.cs")
|
||||
Assert-FileExists "DriverHealthReport present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/DriverHealthReport.cs"
|
||||
Assert-FileExists "HealthEndpointsHost present" "src/Server/ZB.MOM.WW.OtOpcUa.Server/Observability/HealthEndpointsHost.cs"
|
||||
Assert-TextFound "State matrix: Healthy = 200" "ReadinessVerdict\.Healthy => 200" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/DriverHealthReport.cs")
|
||||
Assert-TextFound "State matrix: Faulted = 503" "ReadinessVerdict\.Faulted => 503" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/DriverHealthReport.cs")
|
||||
Assert-FileExists "LogContextEnricher present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/LogContextEnricher.cs"
|
||||
Assert-TextFound "Enricher pushes DriverInstanceId property" "DriverInstanceId" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/LogContextEnricher.cs")
|
||||
Assert-TextFound "JSON sink opt-in via Serilog:WriteJson" "Serilog:WriteJson" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Program.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream D - LiteDB generation-sealed cache"
|
||||
Assert-FileExists "GenerationSealedCache present" "src/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/GenerationSealedCache.cs"
|
||||
Assert-TextFound "Sealed files marked ReadOnly" "FileAttributes\.ReadOnly" @("src/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/GenerationSealedCache.cs")
|
||||
Assert-TextFound "Corruption fails closed with GenerationCacheUnavailableException" "GenerationCacheUnavailableException" @("src/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/GenerationSealedCache.cs")
|
||||
Assert-FileExists "ResilientConfigReader present" "src/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/ResilientConfigReader.cs"
|
||||
Assert-FileExists "StaleConfigFlag present" "src/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/StaleConfigFlag.cs"
|
||||
Assert-FileExists "GenerationSealedCache present" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/GenerationSealedCache.cs"
|
||||
Assert-TextFound "Sealed files marked ReadOnly" "FileAttributes\.ReadOnly" @("src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/GenerationSealedCache.cs")
|
||||
Assert-TextFound "Corruption fails closed with GenerationCacheUnavailableException" "GenerationCacheUnavailableException" @("src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/GenerationSealedCache.cs")
|
||||
Assert-FileExists "ResilientConfigReader present" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/ResilientConfigReader.cs"
|
||||
Assert-FileExists "StaleConfigFlag present" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/StaleConfigFlag.cs"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream E - Admin /hosts (data layer)"
|
||||
Assert-FileExists "DriverInstanceResilienceStatus entity" "src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstanceResilienceStatus.cs"
|
||||
Assert-FileExists "DriverResilienceStatusTracker present" "src/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs"
|
||||
Assert-FileExists "DriverInstanceResilienceStatus entity" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstanceResilienceStatus.cs"
|
||||
Assert-FileExists "DriverResilienceStatusTracker present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs"
|
||||
Assert-Deferred "FleetStatusHub SignalR push + Blazor /hosts column refresh" "Phase 6.1 Stream E.2/E.3 visual-compliance follow-up"
|
||||
|
||||
Write-Host ""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Phase 6.2 exit-gate compliance check. Each check either passes or records a
|
||||
failure; non-zero exit = fail.
|
||||
@@ -73,51 +73,51 @@ Write-Host "=== Phase 6.2 compliance - Authorization runtime ===" -ForegroundCol
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Stream A - LdapGroupRoleMapping (control plane)"
|
||||
Assert-FileExists "LdapGroupRoleMapping entity present" "src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs"
|
||||
Assert-FileExists "AdminRole enum present" "src/ZB.MOM.WW.OtOpcUa.Configuration/Enums/AdminRole.cs"
|
||||
Assert-FileExists "ILdapGroupRoleMappingService present" "src/ZB.MOM.WW.OtOpcUa.Configuration/Services/ILdapGroupRoleMappingService.cs"
|
||||
Assert-FileExists "LdapGroupRoleMappingService impl present" "src/ZB.MOM.WW.OtOpcUa.Configuration/Services/LdapGroupRoleMappingService.cs"
|
||||
Assert-TextFound "Write-time invariant: IsSystemWide XOR ClusterId" "IsSystemWide=true requires ClusterId" @("src/ZB.MOM.WW.OtOpcUa.Configuration/Services/LdapGroupRoleMappingService.cs")
|
||||
Assert-FileExists "EF migration for LdapGroupRoleMapping" "src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419131444_AddLdapGroupRoleMapping.cs"
|
||||
Assert-FileExists "LdapGroupRoleMapping entity present" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs"
|
||||
Assert-FileExists "AdminRole enum present" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/AdminRole.cs"
|
||||
Assert-FileExists "ILdapGroupRoleMappingService present" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Services/ILdapGroupRoleMappingService.cs"
|
||||
Assert-FileExists "LdapGroupRoleMappingService impl present" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Services/LdapGroupRoleMappingService.cs"
|
||||
Assert-TextFound "Write-time invariant: IsSystemWide XOR ClusterId" "IsSystemWide=true requires ClusterId" @("src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Services/LdapGroupRoleMappingService.cs")
|
||||
Assert-FileExists "EF migration for LdapGroupRoleMapping" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419131444_AddLdapGroupRoleMapping.cs"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream B - Permission-trie evaluator (Core.Authorization)"
|
||||
Assert-FileExists "OpcUaOperation enum present" "src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/OpcUaOperation.cs"
|
||||
Assert-FileExists "NodeScope record present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/NodeScope.cs"
|
||||
Assert-FileExists "AuthorizationDecision tri-state" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/AuthorizationDecision.cs"
|
||||
Assert-TextFound "Verdict has Denied member (reserved for v2.1)" "Denied" @("src/ZB.MOM.WW.OtOpcUa.Core/Authorization/AuthorizationDecision.cs")
|
||||
Assert-FileExists "IPermissionEvaluator present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs"
|
||||
Assert-FileExists "PermissionTrie present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrie.cs"
|
||||
Assert-FileExists "PermissionTrieBuilder present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieBuilder.cs"
|
||||
Assert-FileExists "PermissionTrieCache present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs"
|
||||
Assert-TextFound "Cache keyed on GenerationId" "GenerationId" @("src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs")
|
||||
Assert-FileExists "UserAuthorizationState present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs"
|
||||
Assert-TextFound "MembershipFreshnessInterval default 15 min" "FromMinutes\(15\)" @("src/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs")
|
||||
Assert-TextFound "AuthCacheMaxStaleness default 5 min" "FromMinutes\(5\)" @("src/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs")
|
||||
Assert-FileExists "TriePermissionEvaluator impl present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs"
|
||||
Assert-TextFound "HistoryRead maps to NodePermissions.HistoryRead" "HistoryRead.+NodePermissions\.HistoryRead" @("src/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs")
|
||||
Assert-FileExists "OpcUaOperation enum present" "src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/OpcUaOperation.cs"
|
||||
Assert-FileExists "NodeScope record present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/NodeScope.cs"
|
||||
Assert-FileExists "AuthorizationDecision tri-state" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/AuthorizationDecision.cs"
|
||||
Assert-TextFound "Verdict has Denied member (reserved for v2.1)" "Denied" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/AuthorizationDecision.cs")
|
||||
Assert-FileExists "IPermissionEvaluator present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs"
|
||||
Assert-FileExists "PermissionTrie present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrie.cs"
|
||||
Assert-FileExists "PermissionTrieBuilder present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieBuilder.cs"
|
||||
Assert-FileExists "PermissionTrieCache present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs"
|
||||
Assert-TextFound "Cache keyed on GenerationId" "GenerationId" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs")
|
||||
Assert-FileExists "UserAuthorizationState present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs"
|
||||
Assert-TextFound "MembershipFreshnessInterval default 15 min" "FromMinutes\(15\)" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs")
|
||||
Assert-TextFound "AuthCacheMaxStaleness default 5 min" "FromMinutes\(5\)" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs")
|
||||
Assert-FileExists "TriePermissionEvaluator impl present" "src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs"
|
||||
Assert-TextFound "HistoryRead maps to NodePermissions.HistoryRead" "HistoryRead.+NodePermissions\.HistoryRead" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Control/data-plane separation (decision #150)"
|
||||
Assert-TextAbsent "Evaluator has zero references to LdapGroupRoleMapping" "LdapGroupRoleMapping" @(
|
||||
"src/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs",
|
||||
"src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrie.cs",
|
||||
"src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieBuilder.cs",
|
||||
"src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs",
|
||||
"src/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs")
|
||||
"src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs",
|
||||
"src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrie.cs",
|
||||
"src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieBuilder.cs",
|
||||
"src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs",
|
||||
"src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream C foundation (dispatch-wiring gate)"
|
||||
Assert-FileExists "ILdapGroupsBearer present" "src/ZB.MOM.WW.OtOpcUa.Server/Security/ILdapGroupsBearer.cs"
|
||||
Assert-FileExists "AuthorizationGate present" "src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs"
|
||||
Assert-TextFound "Gate has StrictMode knob" "StrictMode" @("src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs")
|
||||
Assert-FileExists "ILdapGroupsBearer present" "src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/ILdapGroupsBearer.cs"
|
||||
Assert-FileExists "AuthorizationGate present" "src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs"
|
||||
Assert-TextFound "Gate has StrictMode knob" "StrictMode" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs")
|
||||
Assert-Deferred "DriverNodeManager dispatch-path wiring (11 surfaces)" "Phase 6.2 Stream C follow-up task #143"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream D data layer (ValidatedNodeAclAuthoringService)"
|
||||
Assert-FileExists "ValidatedNodeAclAuthoringService present" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/ValidatedNodeAclAuthoringService.cs"
|
||||
Assert-TextFound "InvalidNodeAclGrantException present" "class InvalidNodeAclGrantException" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/ValidatedNodeAclAuthoringService.cs")
|
||||
Assert-TextFound "Rejects None permissions" "Permission set cannot be None" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/ValidatedNodeAclAuthoringService.cs")
|
||||
Assert-FileExists "ValidatedNodeAclAuthoringService present" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/ValidatedNodeAclAuthoringService.cs"
|
||||
Assert-TextFound "InvalidNodeAclGrantException present" "class InvalidNodeAclGrantException" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/ValidatedNodeAclAuthoringService.cs")
|
||||
Assert-TextFound "Rejects None permissions" "Permission set cannot be None" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/ValidatedNodeAclAuthoringService.cs")
|
||||
Assert-Deferred "RoleGrantsTab + AclsTab Probe-this-permission + SignalR invalidation + draft diff section" "Phase 6.2 Stream D follow-up task #144"
|
||||
|
||||
Write-Host ""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Phase 6.3 exit-gate compliance check. Each check either passes or records a
|
||||
failure; non-zero exit = fail.
|
||||
@@ -47,33 +47,33 @@ Write-Host "=== Phase 6.3 compliance - Redundancy runtime ===" -ForegroundColor
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Stream B - ServiceLevel 8-state matrix (decision #154)"
|
||||
Assert-FileExists "ServiceLevelCalculator present" "src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs"
|
||||
Assert-FileExists "ServiceLevelBand enum present" "src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs"
|
||||
Assert-TextFound "Maintenance = 0 (reserved per OPC UA Part 5)" "Maintenance\s*=\s*0" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "NoData = 1 (reserved per OPC UA Part 5)" "NoData\s*=\s*1" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "InvalidTopology = 2 (detected-inconsistency band)" "InvalidTopology\s*=\s*2" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "AuthoritativePrimary = 255" "AuthoritativePrimary\s*=\s*255" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "IsolatedPrimary = 230 (retains authority)" "IsolatedPrimary\s*=\s*230" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "PrimaryMidApply = 200" "PrimaryMidApply\s*=\s*200" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "RecoveringPrimary = 180" "RecoveringPrimary\s*=\s*180" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "AuthoritativeBackup = 100" "AuthoritativeBackup\s*=\s*100" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "IsolatedBackup = 80 (does NOT auto-promote)" "IsolatedBackup\s*=\s*80" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "BackupMidApply = 50" "BackupMidApply\s*=\s*50" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "RecoveringBackup = 30" "RecoveringBackup\s*=\s*30" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-FileExists "ServiceLevelCalculator present" "src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs"
|
||||
Assert-FileExists "ServiceLevelBand enum present" "src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs"
|
||||
Assert-TextFound "Maintenance = 0 (reserved per OPC UA Part 5)" "Maintenance\s*=\s*0" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "NoData = 1 (reserved per OPC UA Part 5)" "NoData\s*=\s*1" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "InvalidTopology = 2 (detected-inconsistency band)" "InvalidTopology\s*=\s*2" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "AuthoritativePrimary = 255" "AuthoritativePrimary\s*=\s*255" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "IsolatedPrimary = 230 (retains authority)" "IsolatedPrimary\s*=\s*230" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "PrimaryMidApply = 200" "PrimaryMidApply\s*=\s*200" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "RecoveringPrimary = 180" "RecoveringPrimary\s*=\s*180" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "AuthoritativeBackup = 100" "AuthoritativeBackup\s*=\s*100" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "IsolatedBackup = 80 (does NOT auto-promote)" "IsolatedBackup\s*=\s*80" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "BackupMidApply = 50" "BackupMidApply\s*=\s*50" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
Assert-TextFound "RecoveringBackup = 30" "RecoveringBackup\s*=\s*30" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ServiceLevelCalculator.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream B - RecoveryStateManager"
|
||||
Assert-FileExists "RecoveryStateManager present" "src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/RecoveryStateManager.cs"
|
||||
Assert-TextFound "Dwell + publish-witness gate" "_witnessed" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/RecoveryStateManager.cs")
|
||||
Assert-TextFound "Default dwell 60 s" "FromSeconds\(60\)" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/RecoveryStateManager.cs")
|
||||
Assert-FileExists "RecoveryStateManager present" "src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/RecoveryStateManager.cs"
|
||||
Assert-TextFound "Dwell + publish-witness gate" "_witnessed" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/RecoveryStateManager.cs")
|
||||
Assert-TextFound "Default dwell 60 s" "FromSeconds\(60\)" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/RecoveryStateManager.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream D - Apply-lease registry (decision #162)"
|
||||
Assert-FileExists "ApplyLeaseRegistry present" "src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ApplyLeaseRegistry.cs"
|
||||
Assert-TextFound "BeginApplyLease returns IAsyncDisposable" "IAsyncDisposable" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ApplyLeaseRegistry.cs")
|
||||
Assert-TextFound "Lease key includes PublishRequestId" "PublishRequestId" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ApplyLeaseRegistry.cs")
|
||||
Assert-TextFound "Watchdog PruneStale present" "PruneStale" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ApplyLeaseRegistry.cs")
|
||||
Assert-TextFound "Default ApplyMaxDuration 10 min" "FromMinutes\(10\)" @("src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ApplyLeaseRegistry.cs")
|
||||
Assert-FileExists "ApplyLeaseRegistry present" "src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ApplyLeaseRegistry.cs"
|
||||
Assert-TextFound "BeginApplyLease returns IAsyncDisposable" "IAsyncDisposable" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ApplyLeaseRegistry.cs")
|
||||
Assert-TextFound "Lease key includes PublishRequestId" "PublishRequestId" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ApplyLeaseRegistry.cs")
|
||||
Assert-TextFound "Watchdog PruneStale present" "PruneStale" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ApplyLeaseRegistry.cs")
|
||||
Assert-TextFound "Default ApplyMaxDuration 10 min" "FromMinutes\(10\)" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/ApplyLeaseRegistry.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Deferred surfaces"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Phase 6.4 exit-gate compliance check. Each check either passes or records a
|
||||
failure; non-zero exit = fail.
|
||||
@@ -47,20 +47,20 @@ Write-Host "=== Phase 6.4 compliance - Admin UI completion ===" -ForegroundColor
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Stream A data layer - UnsImpactAnalyzer"
|
||||
Assert-FileExists "UnsImpactAnalyzer present" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/UnsImpactAnalyzer.cs"
|
||||
Assert-TextFound "DraftRevisionToken present" "record DraftRevisionToken" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/UnsImpactAnalyzer.cs")
|
||||
Assert-TextFound "Cross-cluster move rejected per decision #82" "CrossClusterMoveRejectedException" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/UnsImpactAnalyzer.cs")
|
||||
Assert-TextFound "LineMove + AreaRename + LineMerge covered" "UnsMoveKind\.LineMerge" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/UnsImpactAnalyzer.cs")
|
||||
Assert-FileExists "UnsImpactAnalyzer present" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/UnsImpactAnalyzer.cs"
|
||||
Assert-TextFound "DraftRevisionToken present" "record DraftRevisionToken" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/UnsImpactAnalyzer.cs")
|
||||
Assert-TextFound "Cross-cluster move rejected per decision #82" "CrossClusterMoveRejectedException" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/UnsImpactAnalyzer.cs")
|
||||
Assert-TextFound "LineMove + AreaRename + LineMerge covered" "UnsMoveKind\.LineMerge" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/UnsImpactAnalyzer.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream B data layer - EquipmentCsvImporter"
|
||||
Assert-FileExists "EquipmentCsvImporter present" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs"
|
||||
Assert-TextFound "CSV header version marker '# OtOpcUaCsv v1'" "OtOpcUaCsv v1" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
Assert-TextFound "Required columns match decision #117" "ZTag.+MachineCode.+SAPID.+EquipmentId.+EquipmentUuid" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
Assert-TextFound "Optional columns match decision #139 (Manufacturer)" "Manufacturer" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
Assert-TextFound "Optional columns include DeviceManualUri" "DeviceManualUri" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
Assert-TextFound "Rejects duplicate ZTag within file" "Duplicate ZTag" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
Assert-TextFound "Rejects unknown column" "unknown column" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
Assert-FileExists "EquipmentCsvImporter present" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs"
|
||||
Assert-TextFound "CSV header version marker '# OtOpcUaCsv v1'" "OtOpcUaCsv v1" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
Assert-TextFound "Required columns match decision #117" "ZTag.+MachineCode.+SAPID.+EquipmentId.+EquipmentUuid" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
Assert-TextFound "Optional columns match decision #139 (Manufacturer)" "Manufacturer" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
Assert-TextFound "Optional columns include DeviceManualUri" "DeviceManualUri" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
Assert-TextFound "Rejects duplicate ZTag within file" "Duplicate ZTag" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
Assert-TextFound "Rejects unknown column" "unknown column" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentCsvImporter.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Deferred surfaces"
|
||||
|
||||
@@ -47,74 +47,74 @@ Write-Host "=== Phase 7 compliance - scripting + virtual tags + scripted alarms
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Stream A - Core.Scripting (Roslyn + sandbox + AST inference + logger)"
|
||||
Assert-FileExists "Core.Scripting project" "src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ZB.MOM.WW.OtOpcUa.Core.Scripting.csproj"
|
||||
Assert-TextFound "ScriptSandbox allow-list anchored on ScriptContext assembly" "contextType\.Assembly" @("src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs")
|
||||
Assert-TextFound "ForbiddenTypeAnalyzer defense-in-depth (plan decision #6)" "class ForbiddenTypeAnalyzer" @("src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs")
|
||||
Assert-TextFound "DependencyExtractor rejects non-literal paths (plan decision #7)" "class DependencyExtractor" @("src/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs")
|
||||
Assert-TextFound "Per-script log companion sink forwards Error+ to main log (plan decision #12)" "class ScriptLogCompanionSink" @("src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogCompanionSink.cs")
|
||||
Assert-TextFound "ScriptContext static Deadband helper" "static bool Deadband" @("src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptContext.cs")
|
||||
Assert-FileExists "Core.Scripting project" "src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ZB.MOM.WW.OtOpcUa.Core.Scripting.csproj"
|
||||
Assert-TextFound "ScriptSandbox allow-list anchored on ScriptContext assembly" "contextType\.Assembly" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs")
|
||||
Assert-TextFound "ForbiddenTypeAnalyzer defense-in-depth (plan decision #6)" "class ForbiddenTypeAnalyzer" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs")
|
||||
Assert-TextFound "DependencyExtractor rejects non-literal paths (plan decision #7)" "class DependencyExtractor" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs")
|
||||
Assert-TextFound "Per-script log companion sink forwards Error+ to main log (plan decision #12)" "class ScriptLogCompanionSink" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogCompanionSink.cs")
|
||||
Assert-TextFound "ScriptContext static Deadband helper" "static bool Deadband" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptContext.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream B - Core.VirtualTags (dependency graph + change/timer + source)"
|
||||
Assert-FileExists "Core.VirtualTags project" "src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.csproj"
|
||||
Assert-TextFound "DependencyGraph iterative Tarjan SCC (no stack overflow on 10k chains)" "class DependencyGraph" @("src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs")
|
||||
Assert-TextFound "VirtualTagEngine SemaphoreSlim async-safe cascade" "SemaphoreSlim" @("src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs")
|
||||
Assert-TextFound "VirtualTagSource IReadable + ISubscribable per ADR-002" "class VirtualTagSource" @("src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs")
|
||||
Assert-TextFound "TimerTriggerScheduler groups by interval" "class TimerTriggerScheduler" @("src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/TimerTriggerScheduler.cs")
|
||||
Assert-FileExists "Core.VirtualTags project" "src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.csproj"
|
||||
Assert-TextFound "DependencyGraph iterative Tarjan SCC (no stack overflow on 10k chains)" "class DependencyGraph" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs")
|
||||
Assert-TextFound "VirtualTagEngine SemaphoreSlim async-safe cascade" "SemaphoreSlim" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs")
|
||||
Assert-TextFound "VirtualTagSource IReadable + ISubscribable per ADR-002" "class VirtualTagSource" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs")
|
||||
Assert-TextFound "TimerTriggerScheduler groups by interval" "class TimerTriggerScheduler" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/TimerTriggerScheduler.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream C - Core.ScriptedAlarms (Part 9 state machine + predicate engine + IAlarmSource)"
|
||||
Assert-FileExists "Core.ScriptedAlarms project" "src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.csproj"
|
||||
Assert-TextFound "Part9StateMachine pure functions" "class Part9StateMachine" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs")
|
||||
Assert-TextFound "Alarm condition state with GxP audit Comments list" "Comments" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs")
|
||||
Assert-TextFound "MessageTemplate {TagPath} substitution (plan decision #13)" "class MessageTemplate" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs")
|
||||
Assert-TextFound "AlarmPredicateContext rejects SetVirtualTag (predicates must be pure)" "class AlarmPredicateContext" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmPredicateContext.cs")
|
||||
Assert-TextFound "ScriptedAlarmSource implements IAlarmSource" "class ScriptedAlarmSource" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs")
|
||||
Assert-TextFound "IAlarmStateStore abstraction + in-memory default" "class InMemoryAlarmStateStore" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs")
|
||||
Assert-FileExists "Core.ScriptedAlarms project" "src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.csproj"
|
||||
Assert-TextFound "Part9StateMachine pure functions" "class Part9StateMachine" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs")
|
||||
Assert-TextFound "Alarm condition state with GxP audit Comments list" "Comments" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs")
|
||||
Assert-TextFound "MessageTemplate {TagPath} substitution (plan decision #13)" "class MessageTemplate" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs")
|
||||
Assert-TextFound "AlarmPredicateContext rejects SetVirtualTag (predicates must be pure)" "class AlarmPredicateContext" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmPredicateContext.cs")
|
||||
Assert-TextFound "ScriptedAlarmSource implements IAlarmSource" "class ScriptedAlarmSource" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs")
|
||||
Assert-TextFound "IAlarmStateStore abstraction + in-memory default" "class InMemoryAlarmStateStore" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream D - Core.AlarmHistorian (SQLite store-and-forward; alarm-event sidecar IPC moved to Driver.Historian.Wonderware.Client in PR 3.4)"
|
||||
Assert-FileExists "Core.AlarmHistorian project" "src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"
|
||||
Assert-TextFound "SqliteStoreAndForwardSink backoff ladder (1s..60s cap)" "BackoffLadder" @("src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs")
|
||||
Assert-TextFound "Default 1M row capacity + 30-day dead-letter retention (plan decision #21)" "DefaultDeadLetterRetention" @("src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs")
|
||||
Assert-TextFound "Per-event outcomes (Ack/RetryPlease/PermanentFail)" "HistorianWriteOutcome" @("src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs")
|
||||
Assert-FileExists "Core.AlarmHistorian project" "src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"
|
||||
Assert-TextFound "SqliteStoreAndForwardSink backoff ladder (1s..60s cap)" "BackoffLadder" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs")
|
||||
Assert-TextFound "Default 1M row capacity + 30-day dead-letter retention (plan decision #21)" "DefaultDeadLetterRetention" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs")
|
||||
Assert-TextFound "Per-event outcomes (Ack/RetryPlease/PermanentFail)" "HistorianWriteOutcome" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs")
|
||||
# Galaxy.Shared pipe-IPC contracts retired in PR 7.2 alongside the rest of the legacy
|
||||
# Galaxy projects. Wonderware sidecar contracts live in Driver.Historian.Wonderware.Client.
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream E - Config DB schema"
|
||||
Assert-FileExists "Script entity" "src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs"
|
||||
Assert-FileExists "VirtualTag entity" "src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs"
|
||||
Assert-FileExists "ScriptedAlarm entity" "src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarm.cs"
|
||||
Assert-FileExists "ScriptedAlarmState entity (logical-id keyed per plan decision #14)" "src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarmState.cs"
|
||||
Assert-TextFound "VirtualTag trigger check constraint (change OR timer)" "CK_VirtualTag_Trigger_AtLeastOne" @("src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
|
||||
Assert-TextFound "ScriptedAlarm severity range check" "CK_ScriptedAlarm_Severity_Range" @("src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
|
||||
Assert-TextFound "ScriptedAlarm type enum check" "CK_ScriptedAlarm_AlarmType" @("src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
|
||||
Assert-TextFound "ScriptedAlarmState.CommentsJson is ISJSON (GxP audit)" "CK_ScriptedAlarmState_CommentsJson_IsJson" @("src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
|
||||
Assert-FileExists "Phase 7 migration present" "src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260420231641_AddPhase7ScriptingTables.cs"
|
||||
Assert-FileExists "Script entity" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs"
|
||||
Assert-FileExists "VirtualTag entity" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs"
|
||||
Assert-FileExists "ScriptedAlarm entity" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarm.cs"
|
||||
Assert-FileExists "ScriptedAlarmState entity (logical-id keyed per plan decision #14)" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarmState.cs"
|
||||
Assert-TextFound "VirtualTag trigger check constraint (change OR timer)" "CK_VirtualTag_Trigger_AtLeastOne" @("src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
|
||||
Assert-TextFound "ScriptedAlarm severity range check" "CK_ScriptedAlarm_Severity_Range" @("src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
|
||||
Assert-TextFound "ScriptedAlarm type enum check" "CK_ScriptedAlarm_AlarmType" @("src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
|
||||
Assert-TextFound "ScriptedAlarmState.CommentsJson is ISJSON (GxP audit)" "CK_ScriptedAlarmState_CommentsJson_IsJson" @("src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
|
||||
Assert-FileExists "Phase 7 migration present" "src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260420231641_AddPhase7ScriptingTables.cs"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream F - Admin UI (services + Monaco editor + test harness + historian diagnostics)"
|
||||
Assert-FileExists "ScriptService" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptService.cs"
|
||||
Assert-FileExists "VirtualTagService" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/VirtualTagService.cs"
|
||||
Assert-FileExists "ScriptedAlarmService" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptedAlarmService.cs"
|
||||
Assert-FileExists "ScriptTestHarnessService" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptTestHarnessService.cs"
|
||||
Assert-FileExists "HistorianDiagnosticsService" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/HistorianDiagnosticsService.cs"
|
||||
Assert-FileExists "ScriptEditor Razor component" "src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptEditor.razor"
|
||||
Assert-FileExists "ScriptsTab Razor component" "src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptsTab.razor"
|
||||
Assert-FileExists "AlarmsHistorian diagnostics page" "src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/AlarmsHistorian.razor"
|
||||
Assert-FileExists "Monaco loader (CDN progressive enhancement)" "src/ZB.MOM.WW.OtOpcUa.Admin/wwwroot/js/monaco-loader.js"
|
||||
Assert-TextFound "Scripts tab wired into DraftEditor" "ScriptsTab " @("src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DraftEditor.razor")
|
||||
Assert-TextFound "Harness enforces declared-inputs-only contract (plan decision #22)" "UnknownInputs" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptTestHarnessService.cs")
|
||||
Assert-FileExists "ScriptService" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptService.cs"
|
||||
Assert-FileExists "VirtualTagService" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/VirtualTagService.cs"
|
||||
Assert-FileExists "ScriptedAlarmService" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptedAlarmService.cs"
|
||||
Assert-FileExists "ScriptTestHarnessService" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptTestHarnessService.cs"
|
||||
Assert-FileExists "HistorianDiagnosticsService" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/HistorianDiagnosticsService.cs"
|
||||
Assert-FileExists "ScriptEditor Razor component" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptEditor.razor"
|
||||
Assert-FileExists "ScriptsTab Razor component" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptsTab.razor"
|
||||
Assert-FileExists "AlarmsHistorian diagnostics page" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/AlarmsHistorian.razor"
|
||||
Assert-FileExists "Monaco loader (CDN progressive enhancement)" "src/Server/ZB.MOM.WW.OtOpcUa.Admin/wwwroot/js/monaco-loader.js"
|
||||
Assert-TextFound "Scripts tab wired into DraftEditor" "ScriptsTab " @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DraftEditor.razor")
|
||||
Assert-TextFound "Harness enforces declared-inputs-only contract (plan decision #22)" "UnknownInputs" @("src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptTestHarnessService.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream G - Address-space integration"
|
||||
Assert-TextFound "NodeSourceKind discriminator in DriverAttributeInfo" "enum NodeSourceKind" @("src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs")
|
||||
Assert-TextFound "Walker emits VirtualTag variables with Source=Virtual" "AddVirtualTagVariable" @("src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs")
|
||||
Assert-TextFound "Walker emits ScriptedAlarm variables with Source=ScriptedAlarm + IsAlarm" "AddScriptedAlarmVariable" @("src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs")
|
||||
Assert-TextFound "EquipmentNamespaceContent carries VirtualTags + ScriptedAlarms" "VirtualTags" @("src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs")
|
||||
Assert-TextFound "DriverNodeManager selects IReadable by source kind" "SelectReadable" @("src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
|
||||
Assert-TextFound "Virtual/ScriptedAlarm writes rejected (plan decision #6)" "IsWriteAllowedBySource" @("src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
|
||||
Assert-TextFound "NodeSourceKind discriminator in DriverAttributeInfo" "enum NodeSourceKind" @("src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs")
|
||||
Assert-TextFound "Walker emits VirtualTag variables with Source=Virtual" "AddVirtualTagVariable" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs")
|
||||
Assert-TextFound "Walker emits ScriptedAlarm variables with Source=ScriptedAlarm + IsAlarm" "AddScriptedAlarmVariable" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs")
|
||||
Assert-TextFound "EquipmentNamespaceContent carries VirtualTags + ScriptedAlarms" "VirtualTags" @("src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs")
|
||||
Assert-TextFound "DriverNodeManager selects IReadable by source kind" "SelectReadable" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
|
||||
Assert-TextFound "Virtual/ScriptedAlarm writes rejected (plan decision #6)" "IsWriteAllowedBySource" @("src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Deferred surfaces"
|
||||
|
||||
@@ -54,7 +54,7 @@ read-only tag.
|
||||
## Status
|
||||
|
||||
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
|
||||
materialise any `DriverType` row from the central Config DB into a
|
||||
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
|
||||
have no public simulator; they are gated with env-var skip flags
|
||||
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.
|
||||
3. **PowerShell 7+**. The runner uses null-coalescing + `Set-StrictMode`;
|
||||
the Windows-PowerShell-5.1 shell will not parse `test-all.ps1`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#Requires -Version 7.0
|
||||
#Requires -Version 7.0
|
||||
<#
|
||||
.SYNOPSIS
|
||||
End-to-end CLI test for the AB CIP driver (ControlLogix / CompactLogix /
|
||||
@@ -44,10 +44,10 @@ $ErrorActionPreference = "Stop"
|
||||
. "$PSScriptRoot/_common.ps1"
|
||||
|
||||
$abcipCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli" `
|
||||
-ProjectFolder "src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli" `
|
||||
-ExeName "otopcua-abcip-cli"
|
||||
$opcUaCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ProjectFolder "src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ExeName "otopcua-cli"
|
||||
|
||||
$commonAbCip = @("-g", $Gateway, "-f", $Family)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#Requires -Version 7.0
|
||||
#Requires -Version 7.0
|
||||
<#
|
||||
.SYNOPSIS
|
||||
End-to-end CLI test for the AB Legacy (PCCC) driver.
|
||||
@@ -48,10 +48,10 @@ $ErrorActionPreference = "Stop"
|
||||
# accepts an empty path — use `ab://host:44818/` when pointing at real PLCs.
|
||||
|
||||
$abLegacyCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli" `
|
||||
-ProjectFolder "src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli" `
|
||||
-ExeName "otopcua-ablegacy-cli"
|
||||
$opcUaCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ProjectFolder "src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ExeName "otopcua-cli"
|
||||
|
||||
$commonAbLegacy = @("-g", $Gateway, "-P", $PlcType)
|
||||
|
||||
@@ -112,10 +112,10 @@ if (-not [string]::IsNullOrWhiteSpace($Series)) {
|
||||
}
|
||||
|
||||
$focasCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli" `
|
||||
-ProjectFolder "src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli" `
|
||||
-ExeName "otopcua-focas-cli"
|
||||
$opcUaCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ProjectFolder "src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ExeName "otopcua-cli"
|
||||
|
||||
$allResults = @()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#Requires -Version 7.0
|
||||
#Requires -Version 7.0
|
||||
<#
|
||||
.SYNOPSIS
|
||||
End-to-end CLI test for the Modbus-TCP driver bridged through the OtOpcUa server.
|
||||
@@ -48,10 +48,10 @@ $hostPart, $portPart = $ModbusHost.Split(":")
|
||||
$port = [int]$portPart
|
||||
|
||||
$modbusCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli" `
|
||||
-ProjectFolder "src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli" `
|
||||
-ExeName "otopcua-modbus-cli"
|
||||
$opcUaCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ProjectFolder "src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ExeName "otopcua-cli"
|
||||
|
||||
$commonModbus = @("-h", $hostPart, "-p", $port)
|
||||
|
||||
@@ -166,7 +166,7 @@ $ErrorActionPreference = "Stop"
|
||||
. "$PSScriptRoot/_common.ps1"
|
||||
|
||||
$opcUaCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ProjectFolder "src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ExeName "otopcua-cli"
|
||||
|
||||
$results = @()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#Requires -Version 7.0
|
||||
#Requires -Version 7.0
|
||||
<#
|
||||
.SYNOPSIS
|
||||
End-to-end test for Phase 7 virtual tags + scripted alarms, driven via the
|
||||
@@ -57,10 +57,10 @@ $hostPart, $portPart = $ModbusHost.Split(":")
|
||||
$port = [int]$portPart
|
||||
|
||||
$modbusCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli" `
|
||||
-ProjectFolder "src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli" `
|
||||
-ExeName "otopcua-modbus-cli"
|
||||
$opcUaCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ProjectFolder "src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ExeName "otopcua-cli"
|
||||
|
||||
$commonModbus = @("-h", $hostPart, "-p", $port)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#Requires -Version 7.0
|
||||
#Requires -Version 7.0
|
||||
<#
|
||||
.SYNOPSIS
|
||||
End-to-end CLI test for the Siemens S7 driver bridged through the OtOpcUa server.
|
||||
@@ -49,10 +49,10 @@ $hostPart, $portPart = $S7Host.Split(":")
|
||||
$port = [int]$portPart
|
||||
|
||||
$s7Cli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli" `
|
||||
-ProjectFolder "src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli" `
|
||||
-ExeName "otopcua-s7-cli"
|
||||
$opcUaCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ProjectFolder "src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ExeName "otopcua-cli"
|
||||
|
||||
$commonS7 = @("-h", $hostPart, "-p", $port, "-c", $Cpu, "--slot", $Slot)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#Requires -Version 7.0
|
||||
#Requires -Version 7.0
|
||||
<#
|
||||
.SYNOPSIS
|
||||
End-to-end CLI test for the TwinCAT (Beckhoff ADS) driver.
|
||||
@@ -48,10 +48,10 @@ if (-not ($env:TWINCAT_TRUST_WIRE -eq "1" -or $env:TWINCAT_TRUST_WIRE -eq "true"
|
||||
}
|
||||
|
||||
$twinCatCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli" `
|
||||
-ProjectFolder "src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli" `
|
||||
-ExeName "otopcua-twincat-cli"
|
||||
$opcUaCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ProjectFolder "src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ExeName "otopcua-cli"
|
||||
|
||||
$commonTc = @("-n", $AmsNetId, "-p", $AmsPort)
|
||||
|
||||
@@ -112,9 +112,9 @@ Run {
|
||||
Step "Publishing OtOpcUa server + Wonderware historian sidecar from $RepoRoot"
|
||||
|
||||
Run {
|
||||
& dotnet publish "$RepoRoot\src\ZB.MOM.WW.OtOpcUa.Server" `
|
||||
& dotnet publish "$RepoRoot\src\Server\ZB.MOM.WW.OtOpcUa.Server" `
|
||||
-c Release -o (Join-Path $PublishRoot "lmxopcua") | Out-Null
|
||||
& dotnet publish "$RepoRoot\src\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware" `
|
||||
& dotnet publish "$RepoRoot\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware" `
|
||||
-c Release -o (Join-Path $PublishRoot "lmxopcua\WonderwareHistorian") | Out-Null
|
||||
} "dotnet publish (Server + sidecar)"
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ pwsh .\scripts\integration\run-focas.ps1 -Profile powermotion
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ Set-StrictMode -Version 3.0
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||
$integTests = Join-Path $repoRoot "tests\ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests"
|
||||
$integTests = Join-Path $repoRoot "tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests"
|
||||
$dockerYml = Join-Path $integTests "Docker\docker-compose.yml"
|
||||
|
||||
function Write-Step { param([string]$Msg) Write-Host ""; Write-Host "=== $Msg ===" -ForegroundColor Cyan }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-- AB CIP e2e smoke seed — closes #211 (umbrella #209).
|
||||
--
|
||||
-- One-cluster seed pointing at the ab_server ControlLogix fixture
|
||||
-- (`docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml --profile controllogix up -d`).
|
||||
-- (`docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml --profile controllogix up -d`).
|
||||
-- Publishes a single `TestDINT:DInt` tag under NodeId `ns=<N>;s=TestDINT`
|
||||
-- (ab_server seeds this tag by default).
|
||||
--
|
||||
@@ -124,5 +124,5 @@ PRINT 'Next steps:';
|
||||
PRINT ' 1. Set src/.../Server/appsettings.json Node:NodeId = "abcip-smoke-node"';
|
||||
PRINT ' Node:ClusterId = "abcip-smoke"';
|
||||
PRINT ' 2. docker compose -f tests/.../AbCip.IntegrationTests/Docker/docker-compose.yml --profile controllogix up -d';
|
||||
PRINT ' 3. dotnet run --project src/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' 3. dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' 4. ./scripts/e2e/test-abcip.ps1 -BridgeNodeId "ns=2;s=TestDINT"';
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
-- after) before running the seed for that setup.
|
||||
--
|
||||
-- Usage:
|
||||
-- docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml --profile slc500 up -d
|
||||
-- docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml --profile slc500 up -d
|
||||
-- sqlcmd -S "localhost,14330" -d OtOpcUaConfig -U sa -P "OtOpcUaDev_2026!" \
|
||||
-- -i scripts/smoke/seed-ablegacy-smoke.sql
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
-- Node:ClusterId = "modbus-smoke"
|
||||
--
|
||||
-- Then start the simulator + server + run the e2e script:
|
||||
-- docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/docker-compose.yml --profile standard up -d
|
||||
-- dotnet run --project src/ZB.MOM.WW.OtOpcUa.Server
|
||||
-- docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/docker-compose.yml --profile standard up -d
|
||||
-- dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server
|
||||
-- ./scripts/e2e/test-modbus.ps1 -BridgeNodeId "ns=2;s=HR200"
|
||||
|
||||
SET NOCOUNT ON;
|
||||
@@ -152,5 +152,5 @@ PRINT 'Next steps:';
|
||||
PRINT ' 1. Set src/.../Server/appsettings.json Node:NodeId = "modbus-smoke-node"';
|
||||
PRINT ' Node:ClusterId = "modbus-smoke"';
|
||||
PRINT ' 2. docker compose -f tests/.../Modbus.IntegrationTests/Docker/docker-compose.yml --profile standard up -d';
|
||||
PRINT ' 3. dotnet run --project src/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' 3. dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' 4. ./scripts/e2e/test-modbus.ps1 -BridgeNodeId "ns=2;s=HR200"';
|
||||
|
||||
@@ -174,7 +174,7 @@ PRINT ' Node: ' + @NodeId + ' (set Node:NodeId in appsettings.json)';
|
||||
PRINT ' Generation: ' + CONVERT(nvarchar(20), @Gen);
|
||||
PRINT '';
|
||||
PRINT 'Next steps:';
|
||||
PRINT ' 1. Edit src/ZB.MOM.WW.OtOpcUa.Server/appsettings.json:';
|
||||
PRINT ' 1. Edit src/Server/ZB.MOM.WW.OtOpcUa.Server/appsettings.json:';
|
||||
PRINT ' Node:NodeId = "p7-smoke-node"';
|
||||
PRINT ' Node:ClusterId = "p7-smoke"';
|
||||
PRINT ' 2. Edit the placeholder Galaxy attribute in dbo.Tag.TagConfig above';
|
||||
@@ -182,5 +182,5 @@ PRINT ' so it points at a real attribute on this Galaxy — replace';
|
||||
PRINT ' REPLACE_WITH_REAL_GALAXY_ATTRIBUTE with e.g. "Plant1.Reactor1.Temp".';
|
||||
PRINT ' 3. Start the Server in a non-elevated shell so the Galaxy.Host pipe ACL';
|
||||
PRINT ' accepts the connection:';
|
||||
PRINT ' dotnet run --project src/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' 4. Validate via Client.CLI per docs/v2/implementation/phase-7-e2e-smoke.md';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-- S7 e2e smoke seed — closes #212 (umbrella #209).
|
||||
--
|
||||
-- One-cluster seed pointing at the python-snap7 fixture
|
||||
-- (`docker compose -f tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/docker-compose.yml --profile s7_1500 up -d`).
|
||||
-- (`docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/docker-compose.yml --profile s7_1500 up -d`).
|
||||
-- python-snap7 listens on port 1102 (non-priv); real S7 CPUs listen on 102.
|
||||
-- Publishes one Int16 tag at DB1.DBW0 under `ns=<N>;s=DB1_DBW0` (driver
|
||||
-- sanitises the dot for browse names — see S7Driver.DiscoverAsync).
|
||||
@@ -123,5 +123,5 @@ PRINT 'Next steps:';
|
||||
PRINT ' 1. Set src/.../Server/appsettings.json Node:NodeId = "s7-smoke-node"';
|
||||
PRINT ' Node:ClusterId = "s7-smoke"';
|
||||
PRINT ' 2. docker compose -f tests/.../S7.IntegrationTests/Docker/docker-compose.yml --profile s7_1500 up -d';
|
||||
PRINT ' 3. dotnet run --project src/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' 3. dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' 4. ./scripts/e2e/test-s7.ps1 -BridgeNodeId "ns=2;s=DB1_DBW0" -S7Host "127.0.0.1:1102"';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user