Per user decision 2026-07-15: the direct-serial transport (ModbusRtuTransport,
System.IO.Ports, serial config fields, socat live rig) is not being built.
Serial RS-485 buses are reached exclusively via serial->Ethernet gateways
(Moxa NPort etc.) using the new ModbusRtuOverTcpTransport — zero new package
deps, no container device mapping, reuses the hardened socket lifecycle.
ModbusTransportMode shrinks to Tcp|RtuOverTcp (Rtu member reserved). The
direct-serial design is kept in the RTU doc's §2b as a marked record,
including the R2-01 SerialPort-BaseStream-ignores-ReadTimeout trap.
Adds the driver-expansion program design (umbrella: universal Discover-backed
browser + MTConnect, MQTT/Sparkplug B, BACnet/IP, SQL poll, Omron, Modbus RTU;
MELSEC deferred) plus the per-driver research reports.
All docs went through a 7-agent parallel review against the codebase before
this commit. Highlights fixed in review:
- universal browser: FOCAS FixedTree fills post-connect -> UntilStable settle
+ FixedTree.Enabled patch; MQTT reconciled to bespoke (was contradicting the
program doc's SupportsOnlineDiscovery=false verdict)
- modbus-rtu: SerialPort.ReadTimeout doesn't bound async BaseStream reads ->
linked-CTS per-op deadline (R2-01 class); BCL enum reuse would leak
System.IO.Ports into Contracts
- bacnet: DiscoveryRediscoverPolicy enum name; UDP 47808 contention; live
suite rewritten around unicast Who-Is + BBMD (broadcast doesn't cross VMs)
- sql-poll: real tier registration via DriverFactoryRegistry.Register;
blackhole gate must not docker-pause the shared central SQL Server
- mqtt: Sparkplug v3.0 STATE topic form; first-in-repo proto codegen noted
- omron: host hardcodes isIdempotent:false today (retry seam unshipped);
v1 scopes UDTs to dotted-leaf access
- mtconnect: SecurityClassification.ViewOnly; factory ParseEnum<T> pattern
- program doc: both valid enum-serialization patterns; IRediscoverable is
change-signal-gated; RTU P2 adds System.IO.Ports; label is host-side
#420 (filed 2026-05-01 as '4 inert scaffolds + D.1 smoke') is stale. Current
master + the mxaccessgw sibling repo show the feature landed on both sides:
- mxaccessgw A.2/A.4: native alarms served via the wnwrap consumer
(WnWrapAlarmConsumer over WNWRAPCONSUMERLib.wwAlarmConsumerClass) +
FailoverAlarmConsumer + AlarmDispatcher; StreamAlarms active-alarm snapshot
(20 alarms verified live). The WM_APP-pump premise this issue was written
against was ruled out by the 2026-05-01 probes; the design pivoted to wnwrap.
- mxaccessgw A.3: NotWiredAlarmRpcDispatcher gone (only in generated proto);
ack routes MxGatewayClient.AcknowledgeAlarmAsync -> AlarmClient.AlarmAckByGUID.
- lmxopcua: GalaxyDriver : IAlarmSource wires GatewayGalaxyAlarmFeed
(-> OnAlarmEvent, native metadata incl. operator-comment) + inbound ack via
GatewayGalaxyAlarmAcknowledger; native feed live-proven by
GatewayGalaxyAlarmFeedLiveTests.
- C.1 (SdkAlarmHistorianWriteBackend / aahClientManaged): SUPERSEDED. The whole
Wonderware sidecar was retired; alarm-history writes cut over to the
HistorianGateway (GatewayAlarmHistorianWriter, SendEvent path), live-verified
2026-07-15 (R2-06/R2-08, 6/6). The target no longer exists in the tree.
Updated the alarms-over-gateway.md banner (final reconciliation) + the
alarms-d1-smoke-artifact.md status note. Sole residual = a Windows-parity-rig
running-server OPC UA A&C round-trip (needs live Galaxy + human IDE).
- pending.md, current.md: git rm --cached (untrack, keep on disk) + root-anchored
.gitignore entries — making the files' own declared 'never stage' rule true.
- looseends.md: deleted (2026-05-18 snapshot; sole open item + all follow-ups
target the retired Wonderware historian sidecar rig, moot since the gateway
cutover; recoverable from history).
- stillpending.md -> docs/plans/2026-06-18-stillpending-backlog-audit.md (git mv;
referenced by docs/Historian.md + memory index, preserved under dated-plan convention).
- HISTORIAN-GATEWAY-INTEGRATION-ISSUES.md -> docs/plans/2026-06-27-historian-gateway-integration-issues.md (git mv).
- docs/Historian.md §'Closed backlog §2 items' repointed at the archived path.
Historical docs/plans/* references to the bare stillpending.md filename are frozen
point-in-time records, left untouched per the documentation-audit convention.
CLAUDE.md: add scadaproj sister-project index, fix mxaccessgw path to
~/Desktop/MxAccessGateway, note the temporary CVE-2025-6965 NuGetAuditSuppress,
and correct the driver-typed tag-editor list (OpcUaClient is now mapped; only
Galaxy falls back). Also captures docker-dev data-plane GroupToRole + ServerHistorian
live-verify config and the HistorianGateway integration/backlog notes.
Record the execution outcomes in the follow-up plan: FU-1 resolved as a documented
protocol limitation (gateway pending.md C4; not fixable without histsdk wire-capture
evidence), FU-2 done + live-validated (exact round-trip), FU-3 done (mux-ref vs
historian-name decoupled via HistorizedTagRef), FU-4 done. FU-5 (pre-existing Modbus
failure) and FU-6 (post-merge propagation) remain tracked.
Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
First real FOCAS hardware contact (Makino Pro 5 / 31i-B @ 10.201.31.5). A full
v3 data-PDU capture corrected the initial diagnosis: the v3 block envelope is
identical to v1, so only specific payload structs / request math / one client
robustness gap were wrong — not "framing rewrites".
Fixes (all re-validated live through the fixed driver):
- version gate: accept inbound PDU {1,3}, keep emitting v1 (FocasWireProtocol).
- cnc_rdtimer: 8-byte {minute,msec} payload is little-endian (ParseTimer) — the
only decode with an in-range msec field.
- pmc_rdpmcrng: request range widened to the data-type byte width
(end = start + width - 1) so a Word/Long isn't truncated to 0 values
(was spurious BadOutOfRange); decode extracted to ParsePmcRange.
- cnc_rdsvmeter: per-axis LOADELM is 8 bytes (not 12) and names come from the
0x0089 block — ParseServoMeters fixes the misaligned 655360 garbage. Also the
"hang" was NetworkStream.ReadAsync not aborting a stalled socket: ReadExactlyAsync
now disposes the stream on cancellation so a stalled peer can't wedge a poll loop.
- cnc_rddynamic2: contract guard rejecting axis < 1 (driver poll already 1-based).
- FocasDriverProbe: run a real wire session (initiate + cnc_statinfo) instead of
degrading to Ok=true "TCP reachability only" when FWLIB is absent — a bare TCP
listener no longer reports HEALTHY.
cnc_rdparam (0x000e) is unsupported on this control — EW_FUNC across 14
request-framing variants x 4 known-present params; needs a reference FWLIB trace
or is restricted. Deferred (deployed config uses macros, not parameters).
Tests: FOCAS suite 234 green (+16), full solution builds 0 errors. Raw v3
captures checked in under tests/.../Fixtures/v3/. Capture tools under scripts/focas/.
Docs: docs/plans/2026-06-25-focas-pdu-v3-{30i-b-support,implementation-plan}.md,
docs/drivers/FOCAS.md, docs/v2/focas-version-matrix.md,
docs/deployments/wonder-app-vd03-makino-z-34184.md.
Driver-internal IHistoryProvider.ReadEventsAsync passthrough to the upstream
OPC UA server's HistoryReadEvents service. No interface change: the driver
builds a fixed canonical 6-clause EventFilter and maps the upstream HistoryEvent
onto the existing HistoricalEvent record (the server re-projects only those 6
BaseEventType fields, so richer clauses are discarded anyway).