S7 integration — AbCip/Modbus already have real-simulator integration suites; S7 had zero wire-level coverage despite being a Tier-A driver (all unit tests mocked IS7Client). Picked python-snap7's `snap7.server.Server` over raw Snap7 C library because `pip install` beats per-OS binary-pin maintenance, the package ships a Python __main__ shim that mirrors our existing pymodbus serve.ps1 + *.json pattern structurally, and the python-snap7 project is actively maintained. New project `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/` with four moving parts: (a) `Snap7ServerFixture` — collection-scoped TCP probe on `localhost:1102` that sets `SkipReason` when the simulator's not running, matching the `ModbusSimulatorFixture` shape one directory over (same S7_SIM_ENDPOINT env var override convention for pointing at a real S7 CPU on port 102); (b) `PythonSnap7/` — `serve.ps1` wrapper + `server.py` shim + `s7_1500.json` seed profile + `README.md` documenting install / run / known limitations; (c) `S7_1500/S7_1500Profile.cs` — driver-side `S7DriverOptions` whose tag addresses map 1:1 to the JSON profile's seed offsets (DB1.DBW0 u16, DB1.DBW10 i16, DB1.DBD20 i32, DB1.DBD30 f32, DB1.DBX50.3 bool, DB1.DBW100 scratch); (d) `S7_1500SmokeTests` — three tests proving typed reads + write-then-read round-trip work through real S7netplus + real ISO-on-TCP + real snap7 server. Picked port 1102 default instead of S7-standard 102 because 102 is privileged on Linux + triggers Windows Firewall prompt; S7netplus 0.20 has a 5-arg `Plc(CpuType, host, port, rack, slot)` ctor that lets the driver honour `S7DriverOptions.Port`, but the existing driver code called the 4-arg overload + silently hardcoded 102. One-line driver fix (S7Driver.cs:87) threads `_options.Port` through — the S7 unit suite (58/58) still passes unchanged because every unit test uses a fake IS7Client that never sees the real ctor. Server seed-type matrix in `server.py` covers u8 / i8 / u16 / i16 / u32 / i32 / f32 / bool-with-bit / ascii (S7 STRING with max_len header). register_area takes the SrvArea enum value, not the string name — a 15-minute debug after the first test run caught that; documented inline.
Per-driver test-fixture coverage docs — eight new files in `docs/drivers/` laying out what each driver's harness actually benchmarks vs. what's trusted from field deployments. Pattern mirrors the AbServer-Test-Fixture.md doc that shipped earlier in this arc: TL;DR → What the fixture is → What it actually covers → What it does NOT cover → When-to-trust table → Follow-up candidates → Key files. Ugly truth the survey made visible: Galaxy + Modbus + (now) S7 + AB CIP have real wire-level coverage; AB Legacy / TwinCAT / FOCAS / OpcUaClient are still contract-only because their libraries ship no fake + no open-source simulator exists (AB Legacy PCCC), no public simulator exists (FOCAS), the vendor SDK has no in-process fake (TwinCAT/ADS.NET), or the test wiring just hasn't happened yet (OpcUaClient could trivially loopback against this repo's own server — flagged as #215). Each doc names the specific follow-up route: Snap7 server for S7 (done), TwinCAT 3 developer-runtime auto-restart for TwinCAT, Tier-C out-of-process Host for FOCAS, lab rigs for AB Legacy + hardware-gated bits of the others. `docs/drivers/README.md` gains a coverage-map section linking all eight. Tracking tasks #215-#222 filed for each PR-able follow-up.
Build clean (driver + integration project + docs); S7.Tests 58/58 (unchanged); S7.IntegrationTests 3/3 (new, verified end-to-end against a live python-snap7 server: `driver_reads_seeded_u16_through_real_S7comm`, `driver_reads_seeded_typed_batch`, `driver_write_then_read_round_trip_on_scratch_word`). Next fixture follow-up is #215 (OpcUaClient loopback against own server) — highest ROI of the remaining set, zero external deps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five operational docs rewritten for v2 (multi-process, multi-driver, Config-DB authoritative):
- docs/Configuration.md — replaced appsettings-only story with the two-layer model.
appsettings.json is bootstrap only (Node identity, Config DB connection string,
transport security, LDAP bind, logging). Authoritative config (clusters, namespaces,
UNS, equipment, tags, driver instances, ACLs, role grants, poll groups) lives in
the Config DB accessed via OtOpcUaConfigDbContext and edited through the Admin UI
draft/publish workflow. Added v1-to-v2 migration index so operators can locate where
each old section moved. Cross-links to docs/v2/config-db-schema.md + docs/v2/admin-ui.md.
- docs/Redundancy.md — Phase 6.3 rewrite. Named every class under
src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/: RedundancyCoordinator, RedundancyTopology,
ApplyLeaseRegistry (publish fencing), PeerReachabilityTracker, RecoveryStateManager,
ServiceLevelCalculator (pure function), RedundancyStatePublisher. Documented the
full 11-band ServiceLevel matrix (Maintenance=0 through AuthoritativePrimary=255)
from ServiceLevelCalculator.cs and the per-ClusterNode fields (RedundancyRole,
ServiceLevelBase, ApplicationUri). Covered metrics
(otopcua.redundancy.role_transition counter + primary/secondary/stale_count gauges
on meter ZB.MOM.WW.OtOpcUa.Redundancy) and SignalR RoleChanged push from
FleetStatusPoller to RedundancyTab.razor.
- docs/security.md — preserved the transport-security section (still accurate) and
added Phase 6.2 authorization. Four concerns now documented in one place:
(1) transport security profiles, (2) OPC UA auth via LdapUserAuthenticator
(note: task spec called this LdapAuthenticationProvider — actual class name is
LdapUserAuthenticator in Server/Security/), (3) data-plane authorization via
NodeAcl + PermissionTrie + AuthorizationGate — additive-only model per decision
#129, ClusterId → Namespace → UnsArea → UnsLine → Equipment → Tag hierarchy,
NodePermissions bundle, PermissionProbeService in Admin for "probe this permission",
(4) control-plane authorization via LdapGroupRoleMapping + AdminRole
(ConfigViewer / ConfigEditor / FleetAdmin, CanEdit / CanPublish policies) —
deliberately independent of data-plane ACLs per decision #150. Documented the
OTOPCUA0001 Roslyn analyzer (UnwrappedCapabilityCallAnalyzer) as the compile-time
guard ensuring every driver-capability async call is wrapped by CapabilityInvoker.
- docs/ServiceHosting.md — three-process rewrite: OtOpcUa Server (net10 x64,
BackgroundService + AddWindowsService, hosts OPC UA endpoint + all non-Galaxy
drivers), OtOpcUa Admin (net10 x64, Blazor Server + SignalR + /metrics via
OpenTelemetry Prometheus exporter), OtOpcUa Galaxy.Host (.NET Framework 4.8 x86,
NSSM-wrapped, env-variable driven, STA thread + MXAccess COM). Pipe ACL
denies-Admins detail + non-elevated shell requirement captured from feedback memory.
Divergence from CLAUDE.md: task spec said "TopShelf is still the service-installer
wrapper per CLAUDE.md note" but no csproj in the repo references TopShelf — decision
#30 replaced it with the generic host's AddWindowsService wrapper (per the doc
comment on OpcUaServerService). Reflected the actual state + flagged this divergence
here so someone can update CLAUDE.md separately.
- docs/StatusDashboard.md — replaced the full v1 reference (dashboard endpoints,
health check rules, StatusData DTO, etc.) with a short "superseded by Admin UI"
pointer that preserves git-blame continuity + avoids broken links from other docs
that reference it.
Class references verified by reading:
src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/{RedundancyCoordinator, ServiceLevelCalculator,
ApplyLeaseRegistry, RedundancyStatePublisher}.cs
src/ZB.MOM.WW.OtOpcUa.Core/Authorization/{PermissionTrie, PermissionTrieBuilder,
PermissionTrieCache, TriePermissionEvaluator, AuthorizationGate}.cs
src/ZB.MOM.WW.OtOpcUa.Server/Security/{AuthorizationGate, LdapUserAuthenticator}.cs
src/ZB.MOM.WW.OtOpcUa.Admin/{Program.cs, Services/AdminRoles.cs,
Services/RedundancyMetrics.cs, Hubs/FleetStatusPoller.cs}
src/ZB.MOM.WW.OtOpcUa.Server/Program.cs + appsettings.json
src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/{Program.cs, Ipc/PipeServer.cs}
src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/{ClusterNode, NodeAcl,
LdapGroupRoleMapping}.cs
src/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructure the driver-facing docs to match the OtOpcUa v2 multi-driver
reality (Galaxy, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, OPC UA Client
— 8 drivers total; Galaxy ships as three projects) and the capability-interface
architecture where every driver opts into IDriver + whichever of IReadable /
IWritable / ITagDiscovery / ISubscribable / IHostConnectivityProbe /
IPerCallHostResolver / IAlarmSource / IHistoryProvider / IRediscoverable it
supports. Doc scope follows the code: one-driver-specific docs scoped to that
driver, cross-driver concerns live once at the top level, per-driver specs
cross-link to docs/v2/driver-specs.md rather than duplicate.
What changed per file:
- docs/MxAccessBridge.md -> docs/drivers/Galaxy.md (git mv + rewrite): retitled
"Galaxy Driver", reframed as one of seven drivers. Added Project Split table
(Shared .NET Standard 2.0 / Host .NET 4.8 x86 / Proxy .NET 10) and Why
Out-of-Process section citing both the MXAccess bitness constraint and Tier C
stability isolation per docs/v2/plan.md section 4. Added IPC Transport
section covering pipe naming, MessagePack framing, DACL that denies Admins,
shared-secret handshake, heartbeat, and CallAsync<TReq,TResp> dispatch.
Moved file paths from src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/* to
src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/MxAccess/* and added the
Shared + Proxy key-file tables. Added CapabilityInvoker + OTOPCUA0001
analyzer callout. Cross-linked to drivers/README.md, Galaxy-Repository.md,
HistoricalDataAccess.md.
- docs/GalaxyRepository.md -> docs/drivers/Galaxy-Repository.md (git mv +
rewrite): retitled "Galaxy Repository — Tag Discovery for the Galaxy
Driver", opened with a comparison table showing how every driver's
ITagDiscovery source is different (AB CIP @tags walker, TwinCAT
SymbolLoaderFactory, FOCAS CNC queries, OPC UA Client Session.Browse, etc).
Repositioned GalaxyRepositoryService as the Galaxy driver's
ITagDiscovery.DiscoverAsync implementation. Updated paths to
Driver.Galaxy.Host/Backend/GalaxyRepository/*. Added IRediscoverable section
covering the on-change-redeploy IPC path.
- docs/drivers/README.md (new): index with ground-truth driver table —
project path, stability tier, wire library, capability-interface list, and
one notable quirk per driver. Verified against the driver csproj files and
class declarations on focas-pr3-remaining-capabilities (the most recent
branch containing every driver). Galaxy gets its own dedicated docs; the
other seven drivers cross-link to docs/v2/driver-specs.md. Lists the full
Core.Abstractions capability surface, DriverTypeRegistry, CapabilityInvoker,
and OTOPCUA0001 analyzer.
- docs/HistoricalDataAccess.md (rewrite): reframed around IHistoryProvider as
a per-driver optional capability interface. Replaced v1 HistorianPluginLoader
/ AvevaHistorianPluginEntry plugin architecture with the v2 story —
Historian.Aveva was merged into Driver.Galaxy.Host/Backend/Historian/ and
IPC-forwarded through GalaxyProxyDriver. Documented all four IHistoryProvider
methods (ReadRawAsync / ReadProcessedAsync / ReadAtTimeAsync /
ReadEventsAsync), CapabilityInvoker wrapping with DriverCapability.HistoryRead,
and the per-driver coverage matrix (Galaxy + OPC UA Client implement; the
six protocol drivers don't and return BadHistoryOperationUnsupported). Kept
the cluster-failover + health-counter + quality-mapping detail for the
Galaxy Historian implementation. Flagged one gap: Proxy forwards all four
history message kinds but the Host-side HistoryAggregateType -> AnalogSummary
column mapping may surface GalaxyIpcException{Code="not-implemented"} on a
given branch until the Phase 2 Galaxy out-of-process gate lands.
Driver list built against ground truth (src on focas-pr3-remaining-capabilities):
Driver.Galaxy.{Shared,Host,Proxy}, Driver.Modbus, Driver.S7, Driver.AbCip,
Driver.AbLegacy, Driver.TwinCAT, Driver.FOCAS, Driver.OpcUaClient.
Capability interface lists verified against each *Driver.cs class declaration.
Aveva Historian ported to Driver.Galaxy.Host/Backend/Historian/; no separate
Historian.Aveva assembly on v2 branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrite seven core-architecture docs to match the shipped multi-driver platform.
The v1 single-driver LmxNodeManager framing is replaced with the Core +
capability-interface model — Galaxy is now one driver of seven, and each doc
points at the current class names + source paths.
What changed per file:
- OpcUaServer.md — OtOpcUaServer as StandardServer host; per-driver
DriverNodeManager + CapabilityInvoker wiring; Config-DB-driven configuration
(sp_PublishGeneration, DraftRevisionToken, Admin UI); Phase 6.2
AuthorizationGate integration.
- AddressSpace.md — GenericDriverNodeManager.BuildAddressSpaceAsync walks
ITagDiscovery.DiscoverAsync and streams DriverAttributeInfo through
IAddressSpaceBuilder; CapturingBuilder registers alarm-condition sinks;
per-driver NodeId schemes replace the fixed ns=1;s=ZB root.
- ReadWriteOperations.md — OnReadValue / OnWriteValue dispatch to
IReadable.ReadAsync / IWritable.WriteAsync through CapabilityInvoker,
honoring WriteIdempotentAttribute (#143); two-layer authorization
(WriteAuthzPolicy + Phase 6.2 AuthorizationGate).
- Subscriptions.md — ISubscribable.SubscribeAsync/UnsubscribeAsync is the
capability surface; STA-thread story is now Galaxy-specific (StaPump inside
Driver.Galaxy.Host), other drivers are free-threaded.
- AlarmTracking.md — IAlarmSource is optional; AlarmSurfaceInvoker wraps
Subscribe/Ack/Unsubscribe with fan-out by IPerCallHostResolver and the
no-retry AlarmAcknowledge pipeline (#143); CapturingBuilder registers sinks
at build time.
- DataTypeMapping.md — DriverDataType + SecurityClassification are the
driver-agnostic enums; per-driver mappers (GalaxyProxyDriver inline,
AbCipDataType, ModbusDriver, etc.); SecurityClassification is metadata only,
ACL enforcement is at the server layer.
- IncrementalSync.md — IRediscoverable covers backend-change signals;
sp_ComputeGenerationDiff + DiffViewer drive generation-level change
detection; IDriver.ReinitializeAsync is the in-process recovery path.