feat(mtconnect): CommsLossConfirmMs AdminUI field, refused-connection fast path reaches native conditions, dedicated live-gate Agent (#852)

- AdminUI MTConnect driver form: "Comms-loss confirm (ms)" in the Reconnect section, always written
  (0 = first refusal, negative = off is never dropped), case-insensitive load, unknown keys preserved,
  advisory at >= 90000 ms (the watchdog fires first). Form output byte-pinned and parsed through
  MTConnectDriver.ParseOptions on the driver side.
- Driver: a confirmed refused-connection outage now annotates every told CONDITION quality-only
  BadNoCommunication (state/ack kept, once per outage, UNAVAILABLE-Bad skipped); the first delivery
  afterwards re-diffs all subscribed conditions so unchanged ones get a quality-only Good.
- docker-dev/docker-compose.mtconnect-gate.yml: throwaway Agent in its adapter's network namespace so a
  stop is a genuine ConnectionRefused (a plain stopped container is a DNS failure = ambiguous).
- Docs: MTConnect.md comms-loss section + live-gate recipe, docker-dev README, drivers.md rule.

Refs #852
This commit is contained in:
Joseph Doherty
2026-11-11 06:23:18 -05:00
parent d1b6cad299
commit 340d9d26b7
11 changed files with 635 additions and 35 deletions
+19
View File
@@ -198,6 +198,25 @@ dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.Integration
Tear down with `docker compose -f docker-dev/docker-compose.yml -f docker-dev/docker-compose.questdb.yml rm -sf questdb` (add `docker volume rm otopcua-questdb-data`
to drop the data). The `admin`/`quest` credentials are QuestDB's stock defaults, not secrets.
## MTConnect refused-connection gate overlay (#852)
`docker-compose.mtconnect-gate.yml` is an **optional overlay** that adds a dedicated, throwaway MTConnect Agent
(`mtconnect-gate-agent`, `mtconnect/agent:2.7.0.12`) plus its SHDR adapter (`mtconnect-gate`), reusing the integration
fixture's `agent.cfg` / `Devices.xml` / `adapter.py` — so the #846/#852 fast path can be gated by stopping an Agent
**without** touching the shared fixture on `10.100.0.35`. Driver URI from inside the rig: `http://mtconnect-gate:5000`;
from the Mac: `http://127.0.0.1:5155`. The Agent runs in the adapter's network namespace so that stopping it yields a
genuine connection **refusal** (a plainly stopped container drops out of Docker DNS — an ambiguous DNS failure the fast
path deliberately ignores).
```bash
docker compose -f docker-dev/docker-compose.yml -f docker-dev/docker-compose.mtconnect-gate.yml up -d --no-deps mtconnect-gate mtconnect-gate-agent
docker compose -f docker-dev/docker-compose.yml -f docker-dev/docker-compose.mtconnect-gate.yml stop mtconnect-gate-agent # refuse
docker compose -f docker-dev/docker-compose.yml -f docker-dev/docker-compose.mtconnect-gate.yml start mtconnect-gate-agent # recover
docker compose -f docker-dev/docker-compose.yml -f docker-dev/docker-compose.mtconnect-gate.yml rm -sf mtconnect-gate-agent mtconnect-gate
```
Full gate recipe (driver config, Client CLI, expectations): [`docs/drivers/MTConnect.md`](../docs/drivers/MTConnect.md#live-gate-refused-connection-fast-path-846852--recipe).
## Tear down
```bash
@@ -0,0 +1,66 @@
# Dedicated, THROWAWAY MTConnect Agent for the #846/#852 refused-connection live gate.
#
# Adds two services to the docker-dev rig's own network so a driver deployed on
# central-1/central-2 can reach an Agent that the gate is free to stop and start —
# never the shared fixture on 10.100.0.35, which other work relies on. It reuses the
# integration fixture's agent.cfg / Devices.xml / adapter.py (read-only binds), so
# the device model is the familiar `OtFixtureCnc` one (fixture_x_pos, fixture_execution,
# the fixture_x_travel / fixture_logic CONDITIONs...).
#
# WHY THE AGENT SHARES THE ADAPTER'S NETWORK NAMESPACE (the whole point of this file):
# the fast path fires ONLY on SocketError.ConnectionRefused. Stopping an ordinary
# compose container also drops its name from Docker's embedded DNS, so the driver
# would see a DNS failure (HostNotFound) instead — an AMBIGUOUS failure that, by
# design, waits for the ~90 s watchdog, and the gate would "fail" for the wrong
# reason. Here the name the driver dials (`mtconnect-gate`) belongs to the ADAPTER
# container, which keeps running; the Agent joins its namespace
# (network_mode: service:...), so stopping the Agent leaves the name resolving to an
# IP with nothing on :5000 — the kernel answers RST, i.e. a genuine refusal.
# (Stopping BOTH services is the ambiguous control leg: DNS failure, watchdog only.)
#
# up: docker compose -p otopcua-dev -f docker-dev/docker-compose.yml \
# -f docker-dev/docker-compose.mtconnect-gate.yml up -d mtconnect-gate mtconnect-gate-agent
# refuse: docker compose -p otopcua-dev -f docker-dev/docker-compose.yml \
# -f docker-dev/docker-compose.mtconnect-gate.yml stop mtconnect-gate-agent
# recover: ... start mtconnect-gate-agent
# remove: ... rm -sf mtconnect-gate-agent mtconnect-gate
#
# Driver AgentUri (from inside the rig) -> http://mtconnect-gate:5000
# From the Mac (curl /probe) -> http://127.0.0.1:5155 (macOS squats :5000 — AirPlay)
#
# Starting only these two services touches nothing else in the rig (no depends_on
# either way). Recipe: docs/drivers/MTConnect.md §"Live gate: refused-connection fast path".
services:
# Network-namespace owner + SHDR data source. Must be started first and must stay
# up for the whole gate; restarting it orphans the Agent's namespace (then restart both).
mtconnect-gate:
image: python:3.13-alpine
restart: "no"
labels:
project: lmxopcua
purpose: mtconnect-refused-connection-gate
volumes:
- ../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/adapter.py:/fixtures/adapter.py:ro
command: ["python", "-u", "/fixtures/adapter.py", "--host", "0.0.0.0", "--port", "7878"]
networks:
default:
# agent.cfg dials its adapter as `adapter`; inside the shared namespace that resolves here.
aliases: [adapter]
ports:
# Published on the namespace OWNER — the Agent cannot publish ports of its own in this mode.
- "5155:5000"
mtconnect-gate-agent:
image: mtconnect/agent:2.7.0.12
restart: "no"
labels:
project: lmxopcua
purpose: mtconnect-refused-connection-gate
network_mode: "service:mtconnect-gate"
depends_on:
mtconnect-gate:
condition: service_started
volumes:
- ../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/agent.cfg:/mtconnect/config/agent.cfg:ro
- ../tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/Devices.xml:/mtconnect/config/Devices.xml:ro