docs(driver-expansion): resolve all open design concerns from the review pass
Every concern the 7-agent review parked is now decided and integrated: - universal browser: in-flight capture coalescing keyed (driverType, config-hash) + global cap of 4 concurrent captures; CanBrowse catches a throwing TryCreate and defensively shuts down the throwaway instance; cleanup ShutdownAsync bounded at 10s (R2-01); picker Refresh = close + re-capture. Coalesced sessions share an immutable tree, so DisposeAsync drops the session's reference, not the tree. - mtconnect: UNAVAILABLE pinned to BadNoCommunication (fleet's BadCommunicationError stays reserved for the driver's own transport failures); TIME_SERIES sampleCount flows into ArrayDim; Subscribe timeout bounds only the stream-start handshake; library version/TFM folded into the license checklist. - mqtt: browse rebirth is an explicit DriverOperator-gated "Request rebirth" button (RequestRebirthAsync(scope)) - never fired by open/root/expand; hand-rolled reconnect loop committed for P1 (MQTTnet v5 dropped ManagedMqttClient); Wave-2-start library re-verification checkbox; implement from the Sparkplug v3.0 spec text. - bacnet: P1 opens with a first-spike checklist (package TFM, BBMD/ segmented-RPM/COV API smoke, unicast-I-Am fixture behavior); AdminUI-node browse registers a second foreign device - BBMD FD-table sizing note. - sql-poll: split-node browse needs env parity for Sql__ConnectionStrings__ refs on admin nodes (actionable error + session-only pasted literal); one-row-per-key query contract (last-wins + rate-limited warning); absent key -> BadNoData; operationTimeout > commandTimeout authoring rule; Browser->Driver.Sql SqlClient transitive on AdminUI accepted on record. - omron: FINS framer gated on W227 + live golden vectors; CIP string layout is the first P1 live-gate item; AbCip harness static-init anti-pattern note; writable-defaults-true operator warning. - modbus-rtu: first P1 step confirms pymodbus exposes framer=rtu on a TCP server (custom-script fallback otherwise). - program doc: new cross-cutting rule - driver ctors must be connection-free (the universal browser's CanBrowse throwaway instances depend on it).
This commit is contained in:
@@ -184,7 +184,18 @@ line in the program doc's cross-cutting rules when P1 lands).
|
||||
Serves entirely from the in-memory captured tree (the shape the MTConnect research report
|
||||
proposed for its cached-`/probe` session — now subsumed by this generic component): `RootAsync` = top-level nodes; `ExpandAsync(nodeId)` = the captured children of that
|
||||
node; `AttributesAsync(nodeId)` = the leaf's `AttributeInfo` (+ any captured properties).
|
||||
`Token`/`LastUsedUtc` per the interface; `DisposeAsync` drops the tree. No I/O after open.
|
||||
`Token`/`LastUsedUtc` per the interface; `DisposeAsync` drops the **session's reference** to
|
||||
the tree (the tree is immutable and may be shared by coalesced sessions, §4.2 — disposing one
|
||||
session never affects another). No I/O after open.
|
||||
|
||||
**Point-in-time snapshot + Refresh.** The captured tree is a snapshot taken at open time and
|
||||
never updates for the session's whole TTL — a tag added on the controller after open does not
|
||||
appear. The picker therefore exposes a **Refresh** action = close the session + re-open it
|
||||
(`CloseAsync` then `OpenAsync` with the same driverType/configJson): a fresh capture that flows
|
||||
through the same coalescing/cap path as any open. Because the coalescing map holds in-flight
|
||||
captures only (no result caching), Refresh always yields a new capture — or joins one already
|
||||
in flight, which is equally fresh. Cheap to build (the picker already has both calls); the UI
|
||||
should label browse results as "snapshot taken at open/refresh time".
|
||||
|
||||
## 5. The online-discovery capability gate
|
||||
|
||||
@@ -276,9 +287,10 @@ turns the FixedTree on; the settle waits for its cache to fill.)
|
||||
1. **Eager, not lazy.** `DiscoverAsync` runs the whole enumeration at open. For huge trees
|
||||
(ControlLogix 10 k+ tags, OPC UA 100 k nodes) the node-cap truncates and the UX is worse than
|
||||
a bespoke per-click `ExpandAsync`. Graduate those to Tier 2.
|
||||
2. **Runs the real driver in-process** (construct + connect + one-shot discover + shutdown) —
|
||||
heavier than the lightweight ad-hoc bespoke session, and requires the driver assemblies in the
|
||||
host (fused Host: yes).
|
||||
2. **Runs the real driver in-process** (construct + connect + one-shot discover + bounded
|
||||
shutdown) — heavier than the lightweight ad-hoc bespoke session, and requires the driver
|
||||
assemblies in the host (fused Host: yes). The §4.2 coalescing + `MaxConcurrentCaptures` cap
|
||||
bound the aggregate load, but each individual capture is still a full device walk.
|
||||
3. **No incrementally-fetched attribute panel** beyond what `DriverAttributeInfo` +
|
||||
`AddProperty` carry (fine for the picker's needs; Galaxy's richer two-stage pick stays bespoke).
|
||||
|
||||
@@ -288,10 +300,29 @@ turns the FixedTree on; the settle waits for its cache to fill.)
|
||||
known Folder/Variable/AddProperty/alarm graph; assert the `BrowseNode` tree, leaf `NodeId ==
|
||||
FullName`, `AttributeInfo` mapping, node-cap truncation, and the no-op alarm sink.
|
||||
- **Unit — `DiscoveryDriverBrowser`:** fake `IDriverFactory` returning a fake driver; assert
|
||||
Initialize→Discover→Shutdown ordering, open-timeout, `PatchForBrowse` merge, `CanBrowse` gate
|
||||
Initialize→Discover→Shutdown ordering (shutdown always runs, even on discovery failure),
|
||||
open-timeout, `PatchForBrowse` merge, `CanBrowse` gate
|
||||
(true for `SupportsOnlineDiscovery`, false otherwise / `TryCreate`→null), and the
|
||||
`UntilStable` settle (a fake `UntilStable` driver whose tree only fills on the second
|
||||
discovery pass must yield the full tree; a never-stable one must stop at the open-timeout).
|
||||
- **Unit — capture coalescing:** two concurrent `OpenAsync` calls with the same
|
||||
(driverType, configJson) run **one** capture (fake driver counts Initialize/Discover
|
||||
invocations) and both callers receive **distinct session tokens** over the shared tree;
|
||||
disposing one session leaves the other browsable; a failed capture fails both awaiters; a
|
||||
subsequent open after completion (Refresh path) runs a **new** capture (in-flight-only map,
|
||||
no result caching).
|
||||
- **Unit — capture cap queueing:** with `MaxConcurrentCaptures = 4` and >4 concurrent opens on
|
||||
**distinct** keys (fake drivers gated on a test signal), at most 4 captures are in flight at
|
||||
once; queued opens complete after a slot frees; a queued open observes caller cancellation.
|
||||
- **Unit — `CanBrowse` hardening:** a fake factory whose `TryCreate` **throws** on malformed
|
||||
configJson yields `CanBrowse == false` (no exception escapes); when `TryCreate` returns a
|
||||
non-discovery (or flag-false) instance, `CanBrowse` is false **and** the created instance
|
||||
was shut down/disposed.
|
||||
- **Unit — bounded shutdown:** a fake driver whose `ShutdownAsync` never completes — the open
|
||||
still returns within open-timeout + the 10 s shutdown bound, on the **discovery** outcome,
|
||||
for both legs: successful capture ⇒ a usable session is returned; failed discovery ⇒ the
|
||||
discovery error (not a shutdown timeout) surfaces. The wedged instance is abandoned, not
|
||||
awaited.
|
||||
- **Unit — `BrowserSessionService` resolution:** bespoke-first, universal-fallback, and
|
||||
manual-entry-when-neither; confirm no `ToDictionary` collision.
|
||||
- **Integration (fixture-gated):** point the universal browser at the existing driver fixtures on
|
||||
@@ -306,9 +337,12 @@ turns the FixedTree on; the settle waits for its cache to fill.)
|
||||
- **P1 — universal browser end-to-end:** `CapturingAddressSpaceBuilder` +
|
||||
`CapturedTreeBrowseSession` + `DiscoveryDriverBrowser` + `IUniversalDriverBrowser` +
|
||||
`BrowserSessionService` fallback + `ITagDiscovery.SupportsOnlineDiscovery` default member +
|
||||
the AbCip/TwinCAT/FOCAS patches + the `UntilStable` settle loop (§4.2) + DI line + AdminUI
|
||||
`CanBrowse`→Browse-button gate. Set `SupportsOnlineDiscovery=true` on
|
||||
OpcUaClient(n/a—bespoke), AbCip, TwinCAT, FOCAS. **~3–5 days.**
|
||||
the AbCip/TwinCAT/FOCAS patches + the `UntilStable` settle loop (§4.2) + capture coalescing
|
||||
and the `MaxConcurrentCaptures` cap + the bounded (10 s) shutdown + hardened `CanBrowse`
|
||||
(catch-throwing-`TryCreate`, defensive teardown) + DI line + AdminUI
|
||||
`CanBrowse`→Browse-button gate + the picker **Refresh** action (§4.3). Set
|
||||
`SupportsOnlineDiscovery=true` on OpcUaClient(n/a—bespoke), AbCip, TwinCAT, FOCAS.
|
||||
**~3–5 days.**
|
||||
- **P2 — light up new drivers for free:** as MTConnect/BACnet land, each sets
|
||||
`SupportsOnlineDiscovery=true` (+ patch if config-gated) and gets a working picker with no
|
||||
browser code. (MQTT/Sparkplug and SQL poll ship bespoke browsers instead — program doc §4.)
|
||||
|
||||
Reference in New Issue
Block a user