Files
lmxopcua/docs
Joseph Doherty 72be19225d feat(mqtt): write-through (IWritable) — Plain publish + Sparkplug NCMD/DCMD (#508)
MqttDriver implemented no IWritable, so every MQTT node materialized read-only
regardless of what the operator authored. It now publishes.

  Plain      -> the tag's writeTopic; UTF-8 text (Raw/Scalar) or a bare JSON
                literal (Json)
  Sparkplug  -> DCMD for a device metric, NCMD for an edge-node metric, carrying
                the metric BY NAME with an explicit Datatype and no seq

Both at QoS 0, retain:false. Retain is deliberately not configurable — a retained
command is replayed by the broker on every future subscribe, including the
device's own reconnect, so one stale write would be re-applied forever.

writeTopic is a new, separate TagConfig key, and its ABSENCE is meaningful: the
write is refused BadNotWritable and the node is discovered ViewOnly. The issue's
"do not skip" warning drove the design — the one outcome worse than read-only
nodes is a node that looks writable, returns Good and publishes nothing. Reusing
the read topic would be worse still: it would echo back and read as a confirmed
device value. Browse-time writability is computed by the SAME encoder the write
path uses, so a node cannot advertise writability it would then refuse.

Good means published, not applied — MQTT has no application-level write ack, so
this is Galaxy's optimistic-Good posture and inherits its property: a failed
DEVICE write can never be surfaced. A failed PUBLISH is reported Bad; the
optimism covers what the device did with the command, never whether it was sent.
WriteIdempotent stays default-off.

The encoder is pure and lives in .Contracts, so both wire shapes are testable
without a broker. Unrepresentable values and types with no Sparkplug encoding are
refused rather than coerced — a Reference must not silently become a String on
the wire, and "not-a-number" must not become 0 on a live plant.

LIVE GATE PASSED against the real Mosquitto fixture on 10.100.0.35: 21/21
integration tests, including 6 new ones that observe the published message with
an INDEPENDENT broker subscriber — a fake asserting "publish was called" could
not tell the feature working from the feature publishing to the wrong topic.

The gate earned its keep: the NCMD test initially failed because NCMD is a SHARED
channel — the driver publishes its own rebirth request (Node Control/Rebirth) on
the very topic an edge-node metric write uses, and it arrived first. Correct
driver behaviour, wrong test; the watcher now matches on metric name. Documented,
because anything consuming that topic needs to know. DCMD has no contention.

Offline: 24 encoder + 9 driver tests. The four new status constants are NAMED so
StatusCodeParityTests can see them (#497/#519) — it went 258 -> 262 and validated
each against the pinned SDK.

Also corrects the Mqtt.md "Known gaps" table: prunes the six rows closed in this
round, and fixes the #507 row, which still described rediscovery as inert after
that issue was closed as superseded and its injection path deleted.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
2026-07-31 06:33:23 -04:00
..

OtOpcUa documentation

Two tiers of documentation live here:

  • Current reference at the top level (docs/*.md) — describes what's shipped today. Start here for operator + integrator reference.
  • Implementation history + design notes at docs/v2/*.md — the authoritative plan + decision log the current reference is built from. Start here when you need the why behind an architectural choice, or when a top-level doc says "see plan.md § X".

The project was originally called LmxOpcUa (a single-driver Galaxy/MXAccess OPC UA server) and has since become OtOpcUa, a multi-driver OPC UA server platform. Any lingering LmxOpcUa-string in a path you see in docs is a deliberate residual (executable name lmxopcua-cli, client PKI folder {LocalAppData}/LmxOpcUaClient/) — fixing those requires migration shims + is tracked as follow-ups.

Platform overview

v2 (2026-05-26): the separate OtOpcUa.Server + OtOpcUa.Admin services fused into a single role-gated OtOpcUa.Host binary, joined by an Akka.NET cluster. See v2 design for the architectural decision.

  • Core owns shared abstractions (driver capability contracts, scripting, virtual tags, alarm historian).
  • Drivers plug in via capability interfaces in ZB.MOM.WW.OtOpcUa.Core.Abstractions: IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IAlarmSource, IHistoryProvider, IPerCallHostResolver. Each driver opts into whichever it supports.
  • Host (src/Server/ZB.MOM.WW.OtOpcUa.Host) is the single fused binary (.NET 10, AnyCPU). OTOPCUA_ROLES env decides what to mount: admin (Blazor + control-plane singletons), driver (OPC UA endpoint + per-node actors), or both. See ServiceHosting.md.
  • Cluster + ControlPlane + Runtime + AdminUI + Security sit between Core and Host. The cluster glues per-node actors into one logical fleet; the control-plane singletons (deploy coordinator, audit writer, redundancy state) live on the admin role-leader. See Redundancy.md.
  • The Galaxy driver still reaches MXAccess via gRPC to a separately-installed mxaccessgw sidecar (sibling repo).

Where to find what

Architecture + data-path reference

Doc Covers
OpcUaServer.md Top-level server architecture — Core, driver dispatch, Config DB, generations
AddressSpace.md GenericDriverNodeManager + ITagDiscovery + IAddressSpaceBuilder
ReadWriteOperations.md OPC UA Read/Write → CapabilityInvoker → IReadable/IWritable
DriverLifecycle.md Server-side driver lifecycle + infrastructure contracts (IDriverFactory, IDriverProbe, IDriverHealthPublisher, IDriverConfigEditor, IHistorianDataSource) + the Commons library
Subscriptions.md Monitored items → ISubscribable + per-driver subscription refcount (v1 archive)
AlarmTracking.md IAlarmSource + AlarmSurfaceInvoker + OPC UA alarm conditions — native Galaxy alarms end-to-end (live)
AlarmTracking.md Original alarm-tracking write-up (v1 archive)
AlarmHistorian.md Core.AlarmHistorian store-and-forward sink — LocalDbStoreAndForwardSink, the replicated alarm_sf_events buffer + Primary-only drain, IAlarmHistorianWriter, dead-letter/retry/eviction
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 Address-space rebuild on redeploy + sp_ComputeGenerationDiff
HistoricalDataAccess.md IHistoryProvider as a per-driver optional capability (v1 archive)
VirtualTags.md Core.Scripting + Core.VirtualTags — Roslyn script sandbox, engine, dispatch alongside driver tags
ScriptEditor.md Monaco script editor — Roslyn-backed IntelliSense (completions, diagnostics, hover, tag-path) for C# virtual-tag scripts; AdminUI/ScriptAnalysis/ backend
ScriptedAlarms.md Core.ScriptedAlarms — script-predicate IAlarmSource + Part 9 state machine

One Core subsystem is shipped without a dedicated top-level doc; see the section in the linked doc:

Project See
Analyzers (Roslyn OTOPCUA0001) security.md § OTOPCUA0001 Analyzer

Drivers

Doc Covers
drivers/README.md Index of the eight shipped drivers + capability matrix
drivers/Galaxy.md Galaxy driver — in-process gRPC client to the mxaccessgw sidecar
v1/drivers/Galaxy-Repository.md Galaxy-specific discovery via the ZB SQL database (v1 archive — the gateway owns this path now)

For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics, see v2/driver-specs.md.

Operational

Doc Covers
Configuration.md Live appsettings + environment-variable reference (current state)
Configuration.md appsettings bootstrap + Config DB + Admin UI draft/publish (v1 archive — OTOPCUA_GALAXY_* env vars now live in mxaccessgw config)
Uns.md The global /uns master tree — manage Area/Line/Equipment/Tag/VirtualTag fleet-wide; replaces the per-cluster UNS/Equipment/Tags tabs
security.md Transport security profiles, LDAP auth, ACL trie, role grants, OTOPCUA0001 analyzer
Redundancy.md RedundancyStateActor (cluster-scoped singleton), ServiceLevelCalculator, IRedundancyRoleView, per-cluster meshes, Prometheus metrics
Reservations.md Fleet-wide ZTag / SAPID external-ID reservations — publish-time claim, release flow
ServiceHosting.md Single fused OtOpcUa.Host binary install/uninstall with OTOPCUA_ROLES gating; the historian backend is the external HistorianGateway
StatusDashboard.md Pointer — superseded by v2/admin-ui.md

Client tooling

Doc Covers
Client.CLI.md otopcua-cli — OPC UA command-line client
Client.UI.md Avalonia desktop client
DriverClis.md Driver test-client CLIs — index + shared commands
Driver.Modbus.Cli.md otopcua-modbus-cli — Modbus-TCP
Driver.AbCip.Cli.md otopcua-abcip-cli — ControlLogix / CompactLogix / Micro800 / GuardLogix
Driver.AbLegacy.Cli.md otopcua-ablegacy-cli — SLC / MicroLogix / PLC-5 (PCCC)
Driver.S7.Cli.md otopcua-s7-cli — Siemens S7-300 / S7-400 / S7-1200 / S7-1500
Driver.TwinCAT.Cli.md otopcua-twincat-cli — Beckhoff TwinCAT 2/3 ADS
Driver.FOCAS.Cli.md otopcua-focas-cli — Fanuc FOCAS/2 CNC

Requirements

Doc Covers
reqs/HighLevelReqs.md HLRs — numbered system-level requirements
reqs/OpcUaServerReqs.md OPC UA server-layer reqs
v1/reqs/ServiceHostReqs.md Per-process hosting reqs (v1 archive — only OtOpcUa server hosting remains in scope post-PR-7.2)
reqs/ClientRequirements.md Client CLI + UI reqs
v1/reqs/GalaxyRepositoryReqs.md Galaxy-scoped repository reqs (v1 archive — owned by mxaccessgw today)
v1/reqs/MxAccessClientReqs.md Galaxy-scoped MXAccess reqs (v1 archive — owned by mxaccessgw today)
reqs/StatusDashboardReqs.md Pointer — superseded by Admin UI

Implementation history (docs/v2/)

Design decisions + phase plans + execution notes. Load-bearing cross-references from the top-level docs:

v1 archive

The v1 in-process MXAccess architecture (Galaxy.Host + Galaxy.Proxy + Galaxy.Shared, .NET 4.8 x86 COM, the OtOpcUaGalaxyHost Windows service) was retired in PR 7.2 (2026-04-30, commit ae7106d). Docs that described that shape are kept under v1/ as historical record — see v1/README.md for the index.