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).
26 KiB
MTConnect (Agent-first) driver — executable implementation design
Status: design, build-ready. 2026-07-15.
Research input: docs/research/drivers/mtconnect-agent.md — this doc turns that research into a build spec; it does not re-argue the protocol findings.
House-style references: 2026-06-12-galaxy-standard-driver-design.md, 2026-07-15-universal-discovery-browser-design.md.
1. Motivation + scope
MTConnect is the dominant open read-only telemetry standard for machine tools; adding a DriverType = "MTConnect" Equipment-kind driver lets OtOpcUa surface a machine's self-describing device model (positions, spindle speed, execution state, availability, conditions) alongside — and complementing — the lower-level FOCAS driver (FOCAS reaches the Fanuc CNC directly; MTConnect reaches the vendor-neutral Agent that often front-ends that same machine). v1 is agent-first, read-only (Discover + Read + Subscribe, no Write) because the mainstream MTConnect surface (/probe, /current, /sample) is read-only by design. Full rationale, protocol details, library survey, and risk register live in the research report §1–§8; this doc assumes them.
2. Project layout
Two new projects, mirroring the Modbus split (contracts DTOs isolated from runtime so the AdminUI probe/editor can reference config shapes without the driver's NuGet deps). No .Browser project — see §4.
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/
MTConnectDriverOptions.cs // strongly-typed runtime options (like ModbusDriverOptions)
MTConnectTagDefinition.cs // one record per authored tag (FullName=dataItemId + mt* metadata)
MTConnectDataTypeInference.cs // pure category/type/units → DriverDataType table (§3.3) — shared by driver, factory, editor, browser-commit
ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts.csproj // refs Core.Abstractions only
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/
MTConnectDriver.cs // IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe, IRediscoverable
MTConnectAgentClient.cs // thin seam over MTConnectHttpClient (probe/current/sample); testable via IMTConnectAgentClient
IMTConnectAgentClient.cs // seam interface — canned-XML fake in unit tests
MTConnectObservationIndex.cs // dataItemId → latest DataValueSnapshot, updated by /current + /sample
MTConnectDriverFactoryExtensions.cs // Register(registry, loggerFactory) + CreateInstance
MTConnectDriverProbe.cs // IDriverProbe: one-shot /probe reachability
ZB.MOM.WW.OtOpcUa.Driver.MTConnect.csproj // refs Core, Core.Abstractions, .Contracts + TrakHound pkgs
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/
Fixtures/*.xml // canned probe/current/sample docs (§8)
...Tests.cs
.csproj boilerplate copies Modbus verbatim: net10.0, Nullable=enable, TreatWarningsAsErrors=true, GenerateDocumentationFile, InternalsVisibleTo the Tests project.
NuGet dependency
Consume TrakHound MTConnect.NET client packages (MIT) — pin an exact version:
<PackageReference Include="MTConnect.NET-Common" Version="6.9.0.2" />
<PackageReference Include="MTConnect.NET-HTTP" Version="6.9.0.2" />
<!-- MTConnect.NET-XML / -JSON pulled transitively for serialization -->
These target netstandard2.0 (load cleanly on .NET 10) and give MTConnectHttpClient (probe/current/sample with polling and the multipart long-poll stream, gzip, XML+JSON) plus the model types (IDevice/IComponent/IDataItem/IObservation) — removing the multipart-framing / version-negotiation / buffer-overflow-rebaseline grind.
Library verification checklist (from research §1.4 — research-sourced facts, unverifiable offline). Older TrakHound releases carried mixed MIT / Apache-2.0 / "all rights reserved" strings, and the version/TFM facts above came from research, not from a restore. At implementation start, verify against the actual NuGet packages: (1) the exact package version pin — confirm
6.9.0.2(or the then-current pin) exists on nuget.org and restores; (2) its TFM set — confirm the packages shipnetstandard2.0(or a net-10-compatible) target and load cleanly on .NET 10; (3) the license of the pinned version — confirm the embeddedLICENSEis MIT (the before-merge legal-review checkbox, not a blocker). Hand-roll fallback if review fails:HttpClient+System.Xml.Linqfor probe/current, amultipart/x-mixed-replaceboundary reader for sample (~300–500 LoC behind the sameIMTConnectAgentClientseam — the driver above the seam is unchanged, so the fallback is a drop-in of one class).
3. Capability mapping (concrete seam wiring)
MTConnectDriver implements IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe, IRediscoverable — not IWritable (§3.5). One MTConnectAgentClient + one MTConnectObservationIndex per driver instance; one shared sample stream (the Agent streams the whole device model, so per-tag streams would be wasteful).
3.1 IDriver
InitializeAsync(json, ct): deserializeMTConnectDriverOptions(via factory), constructMTConnectAgentClientfromAgentUri, run one/probeunder a per-call deadline (§7). Cache the parsedIDevice[]model +Header.instanceId. Prime theMTConnectObservationIndexwith one/current. SetDriverState.Healthyon success;Faulted(rethrow) on failure — the actor marks the instance Faulted, nodes go Bad, process stays up.ReinitializeAsync: tear down the sample stream, re-run Initialize. Config-only change (no address-space rebuild) unlessAgentUri/DeviceNamechanged.ShutdownAsync: stop the sample stream, dispose the HTTP client.GetHealth:DriverHealth(State, LastSuccessfulRead, LastError)—LastSuccessfulRead= last/currentor/samplechunk time.GetMemoryFootprint/FlushOptionalCachesAsync: footprint ≈ cached probe model + observation index; flush drops the browse/probe-model cache (re-fetchable), keeps the observation index (correctness state).
3.2 ITagDiscovery — plugs into the universal browser (§4)
public bool SupportsOnlineDiscovery => true; // /probe enumerates from the device
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once; // probe is synchronous + complete
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken ct)
DiscoverAsync fetches (or reuses the Initialize-cached) /probe model and streams the full Device→Component→DataItem tree into the builder:
- each
Device→builder.Folder(device.Name, device.Name)→ child builder; - each nested
Component→ recursiveFolder(component.Name, component.DisplayName)on the parent's child builder (component nesting becomes folder nesting — the builder-graph is the tree); - each
DataItem→child.Variable(browseName, displayName, attr)whereattr = new DriverAttributeInfo(FullName: dataItem.Id, DriverDataType: MTConnectDataTypeInference.Infer(category, type, units), IsArray: representation==TIME_SERIES, ArrayDim: representation==TIME_SERIES && dataItem.SampleCount is int n and > 0 ? n : null, SecurityClass: SecurityClassification.ViewOnly, IsHistorized: false, IsAlarm: category==CONDITION). (ForTIME_SERIESitems the probe-declaredsampleCountattribute flows intoArrayDim— the record's doc-comment definesArrayDimas the declared array length whenIsArrayis true.nullonly when the device model doesn't declaresampleCount; many agents omit it for variable-length series.) (ViewOnlyis the read-only-from-OPC-UA tier —SecurityClassificationhas noReadOnlymember.)browseName=dataItem.Name ?? dataItem.Id(dataItemnameis optional in MTConnect; fall back toid, which is guaranteed unique).
Critical: DriverAttributeInfo.FullName = dataItem.Id. This is (a) the value the universal browser commits as TagConfig.FullName, and (b) the key the read/subscribe paths resolve against — the two align by construction (research §3). If DeviceName scopes to one device, only that device's subtree is streamed.
DeviceName scoping and the whole-model-in-one-call nature make this a natural fit for eager one-shot discovery — exactly what the universal browser assumes.
3.3 Data-type mapping (MTConnectDataTypeInference.Infer)
Pure function in .Contracts (shared by driver, browser-commit, and the typed editor so all three agree). Weak wire typing means this is a heuristic, stored per-tag and author-overridable (§5):
| MTConnect | DriverDataType |
|---|---|
SAMPLE numeric (has units) |
Float64 |
SAMPLE representation=TIME_SERIES |
Float64 array (IsArray=true; ArrayDim = probe-declared sampleCount when present, else null — §3.2) |
EVENT numeric type (PartCount, Line, …) |
Int64 |
EVENT controlled-vocab (Execution, ControllerMode, Availability, …) |
String |
EVENT free text (Program, Block, Message) |
String |
CONDITION |
String (state word; §3.6) |
Quality mapping (DataValueSnapshot.StatusCode): an observation value of UNAVAILABLE (MTConnect's explicit no-data sentinel) → BadNoCommunication (0x80310000u) with a null value, not the literal string — this is the UNAVAILABLE code, used everywhere in this design (read, subscribe, and the §8 fixtures). Rationale + fleet fit: semantically, UNAVAILABLE means the Agent is reachable but has no device-backed value (the adapter↔device link is down or the item has never reported) — exactly OPC UA's BadNoCommunication ("communication to the data source has failed"). The fleet grep shows no competing convention for this case: every existing driver's BadCommunicationError (0x80050000u, Modbus/FOCAS/TwinCAT/OpcUaClient/S7/AbCip/AbLegacy/Galaxy) marks the driver's own transport failure to its peer — a different failure (and the code this driver also uses when the Agent HTTP call fails, per §7) — while BadNoData (0x800E0000u) is historian-domain only (Historian.Gateway/Mapping/SampleMapper.cs). BadNoCommunication is already in the CLI's SnapshotFormatter name table (src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/SnapshotFormatter.cs), so it renders by name, not hex. Declare it as a private const uint in MTConnectDriver (the Modbus StatusBadCommunicationError pattern). Missing dataItem / empty condition → Bad. observation.timestamp → SourceTimestamp (not a separate node).
3.4 IReadable — /current
ReadAsync(fullReferences, ct): issue one /current under a per-call deadline, index the returned MTConnectStreams observations by dataItemId into MTConnectObservationIndex, then return one DataValueSnapshot per requested ref in order. A ref absent from the response → a Bad-coded snapshot (not a throw). Reads are idempotent (matches IReadable contract; DriverCapability.Read auto-retries). /current is the whole-device snapshot regardless of how many refs are requested, so batch reads cost one round-trip.
3.5 ISubscribable — /sample multipart long-poll
SubscribeAsync(fullReferences, publishingInterval, ct): on the first subscription, start the sharedMTConnectAgentClientsample stream (/sample?from=<nextSequence>&interval=<SampleIntervalMs>&count=<SampleCount>). Record the subscribed ref set. Immediately fireOnDataChangefor each subscribed ref from the primed/currentvalues (OPC UA initial-data convention). Return anISubscriptionHandle(monotonic id +DiagnosticId).- Stream pump: each received
MTConnectStreamschunk → for each observation whosedataItemId∈ subscribed set, update the index and raiseOnDataChange(handle, dataItemId, snapshot). Advancefrom = Header.nextSequencefor the next request (contiguous, gap-free). - Ring-buffer overflow: if the Agent reports a sequence gap /
fromolder thanfirstSequence, re-/currentto re-baseline, then resume the stream from the newnextSequence. (TrakHound handles this internally; the hand-roll fallback must replicate it — unit-tested via a fixture with a forced gap.) UnsubscribeAsync(handle, ct): drop that handle's refs from the subscribed set; stop the shared stream when the set empties.publishingIntervalmaps to the/sampleintervalwhen it is the only/first subscription; the driver polls at the finest requested interval and fans out (one stream per instance, not per tag).
3.6 IWritable — not implemented (v1)
Justified per research §2.1: the MTConnect Agent surface is read-only by design; write-back exists only via optional, rarely-deployed MTConnect Interfaces (a request/response state-machine handshake, not a "set value" — modelling it as IWritable would mislead). Capability interfaces are composable, so omitting IWritable is idiomatic (Galaxy's write path is even fire-and-forget). Nodes materialize without the AccessLevels.CurrentWrite bit automatically. Revisit Interfaces only if a concrete deployment needs it.
3.7 IHostConnectivityProbe + IRediscoverable
IHostConnectivityProbe: expose oneHostConnectivityStatusper Agent (HostName =AgentUri). A cheap periodic/probe(or reuse the sample-stream heartbeat) flipsRunning ↔ Stopped; raiseOnHostStatusChangedon transition. Enable/interval fromMTConnectDriverOptions.Probe(mirrorsModbusProbeOptions).IRediscoverable: watch the AgentHeader.instanceIdon every/current//samplechunk. A change means the Agent restarted / its model changed → raiseOnRediscoveryNeededsoDriverHostrebuilds the address space (mirrors Galaxy'sDeployWatcher). This is whyRediscoverPolicy = Onceis safe: instanceId change, not polling, drives re-discovery.
3.8 CONDITION modelling
v1 simple (this design): each CONDITION DataItem is a String variable whose value is the current state word (Normal/Warning/Fault/Unavailable), optionally suffixed with nativeCode. Zero alarm plumbing. IsAlarm=true is still set on the DriverAttributeInfo so the browser side-panel flags it and a future upgrade needn't re-author tags.
v1.5 (fast-follow, not in this build): implement IAlarmSource + emit a TagConfig alarm object so Fault/Warning become native OPC UA Part 9 alarms, routing on the authored dotted ConditionId per the Galaxy/Phase-B native-alarm pattern.
4. Browse — plugs into the universal browser (reconciliation)
The research report (§4.2) proposed a bespoke ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Browser project with a hand-written MTConnectBrowseSession. This design supersedes that: build NO bespoke browser. With the universal discovery browser in place (the Wave-0 gate, which lands before this Wave-2 driver), MTConnect browse is covered for free by the generic DiscoveryDriverBrowser, because MTConnect browse comes from discovery (/probe → the device model → the same DiscoverAsync stream). Concretely, the universal browser already does exactly what the proposed bespoke session would have done:
DiscoveryDriverBrowser.CanBrowse("MTConnect", cfg)returns true becauseTryCreatesucceeds and the instance isITagDiscovery { SupportsOnlineDiscovery: true }(§3.2) → the AdminUI renders the Browse button.OpenAsyncconstructs the driver,InitializeAsync(the/probeconnect), runsDiscoverAsyncinto aCapturingAddressSpaceBuilder, thenShutdownAsync— the captured tree (Device folders → Component folders → DataItem leaves) is served byCapturedTreeBrowseSession. Each leaf'sNodeId == DriverAttributeInfo.FullName == dataItemId, committed directly asTagConfig.FullName;IsAlarm/DriverDataTypeflow to the side-panel and pre-fill the typed editor.
No PatchForBrowse entry is needed — MTConnect discovery is unconditional (not config-gated like AbCip/TwinCAT's EnableControllerBrowse); it belongs in the "no patch" set alongside OpcUaClient/BACnet. No IUniversalDriverBrowser/BrowserSessionService change is needed — MTConnect lights up purely by setting SupportsOnlineDiscovery => true. The single required action to enable browse is that one default-member override in §3.2.
The only limit (universal-browser §8) is that discovery is eager one-shot; /probe is already a whole-model single call, so eager is the right fit and graduating to bespoke is unlikely.
5. Typed tag editor + validator (AdminUI)
Avoid the raw-JSON fallback by adding a typed editor (mirrors the Modbus template exactly).
src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/MTConnectTagConfigModel.cs— pureFromJson/ToJson/Validate, preserves unknown keys via theTagConfigJsonbag helper (copyModbusTagConfigModel). Fields:FullName(dataItemId, required),MtCategory/MtType/MtSubType(enums / strings, from probe — read-only in UI),DataType(DriverDataTypeoverride dropdown),Units,MtDevice/MtComponent(author context).Validate()returns an error ifFullNameis blank..../Components/Shared/Uns/TagEditors/MTConnectTagConfigEditor.razor— thin shell:FullNametext, read-onlymtCategory/mtType, aDataTypeoverride<select>. Because most tags arrive via the browse picker (which fills the model), the editor is mostly a confirm/override surface.- Register
["MTConnect"] = typeof(Components.Shared.Uns.TagEditors.MTConnectTagConfigEditor)inUns/TagEditors/TagConfigEditorMap.cs, and["MTConnect"] = j => MTConnectTagConfigModel.FromJson(j).Validate()inTagConfigValidator.cs.
JsonStringEnumConverterenum-serialization trap (project-wide gotcha — MEMORY). Every driver's AdminUI serialization path must round-trip enums as name strings, never numerics. The factory/probe DTOs are string-typed (ParseEnum<T>), so a numerically-serialized enum FAULTS the driver at deploy. Two concrete requirements: (1)MTConnectTagConfigModel.ToJsonwritesMtCategory/DataTypeviaTagConfigJson.Set(bag, "dataType", DataType)which emits the enum name (the Modbus helper already does this — do not hand-rollJsonSerializerwithoutJsonStringEnumConverter); (2)MTConnectDriverProbeparses withnew JsonStringEnumConverter()in itsJsonSerializerOptions(copyModbusDriverProbe._opts); the factory keeps enum-carrying DTO fieldsstring?and parses them viaParseEnum<T>— the Modbus factory pattern (itsJsonOptionscarries no enum converter). The.ContractsMTConnectDataTypeInferencereturns aDriverDataTypeenum; when it's written to JSON it must be the string.
6. Factory + registration
MTConnectDriverFactoryExtensions(copy Modbus):public const string DriverTypeName = "MTConnect";Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null)→registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory)).CreateInstancedeserializes aMTConnectDriverConfigDto(nullable-init DTO withTags,Probe), validatesAgentUripresent, buildsMTConnectDriverOptions, returnsnew MTConnectDriver(options, id, agentClientFactory: null, logger). Copy the Modbus factoryJsonOptions(PropertyNameCaseInsensitive,ReadCommentHandling=Skip,AllowTrailingCommas— no enum converter: enum-carrying DTO fields staystring?and go throughParseEnum<T>, per §5's gotcha).- Host wiring — 3 one-line edits in
src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs:Register(...)body: addDriver.MTConnect.MTConnectDriverFactoryExtensions.Register(registry, loggerFactory);- add
using MTConnectProbe = Driver.MTConnect.MTConnectDriverProbe; AddOtOpcUaDriverProbes: addservices.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, MTConnectProbe>());The probe MUST be inAddOtOpcUaDriverProbes(not only the factory path) so it reaches admin-only nodes — theadmin-operationssingleton that backs Test-Connect is admin-role-pinned (the MEMORY "driver probes on admin nodes" gotcha).TryAddEnumerablekeeps a fused admin,driver node from double-registering (a dup makes the singleton'sToDictionary(p=>p.DriverType)throw).
MTConnectDriverProbe : IDriverProbe,DriverType => "MTConnect": parse the config DTO, one-shotGET {AgentUri}/probeundertimeout,Ok=true+latency on any validMTConnectDevicesresponse,Ok=false+message on TCP/HTTP/timeout failure. Never throw (perIDriverProbecontract).- No AdminUI
IDriverBrowserDI line — browse is the universal browser (§4), already registered.
7. Resilience / timeout (the R2-01 frozen-peer lesson)
Every agent HTTP request MUST carry a per-call deadline — never an unbounded wait (the R2-01 S7 finding: an async read that ignores its socket timeout wedges the poll loop against a frozen peer). Concretely:
/probeand/current: wrap each call in aCancellationTokenSource(RequestTimeoutMs)linked to the caller'sct; a timeout surfaces as aBadCommunicationError-coded snapshot (read — the fleet-standard driver-transport-failure code, distinct from the §3.3BadNoCommunicationUNAVAILABLE mapping) or a Faulted init (probe), never a hang. SetHttpClient.Timeoutand a linked CTS (belt-and-suspenders —HttpClient.Timeoutdoesn't cover the response-body read of a streamed multipart)./samplelong-poll stream watchdog: the stream is intentionally long-lived, soHttpClient.Timeoutcannot bound it. Instead run a watchdog: if no chunk and no keep-alive heartbeat arrives withinHeartbeatMs × N(e.g. 3× the Agent heartbeat), treat the stream as dead → cancel it, transitionHostState.Stopped, and reconnect. The Agent's ownheartbeaton the multipart boundary is the liveness signal; absence past the watchdog window is the frozen-peer detector.- Reconnect / backoff: geometric backoff (
MinBackoffMs→MaxBackoffMs) on stream drop or connect failure, mirroringModbusReconnectOptions. On reconnect, re-baseline via/currentthen resume/samplefrom the freshnextSequence. - All capability calls flow through the existing Phase-6.1
IDriverCapabilityInvokerresilience pipeline (Read/Discover/Subscribe/Probe auto-retry); the per-call deadlines above are the driver-internal floor beneath that pipeline. Subscribe-timeout interplay: the pipeline's Subscribe timeout bounds only theSubscribeAsynccall itself — the/samplestream-start handshake must complete within it, andSubscribeAsyncreturns once the stream is established; the long-lived pump then runs outside that timeout, guarded by the heartbeat watchdog above instead.
8. Test fixtures
- Canned XML unit fixtures (bulk of coverage, no network). Capture one
MTConnectDevices(probe) + a couple ofMTConnectStreams(current + sample, incl. one with a forcednextSequencegap) from a demo agent intotests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/Fixtures/. Drive through theIMTConnectAgentClientfake to pin:DiscoverAsynctree shape + leafFullName==dataItemId,MTConnectDataTypeInferencetable, observation indexing,UNAVAILABLE→BadNoCommunication(§3.3), CONDITION→state-word, multipart chunk framing, and ring-buffer re-baseline paging. - Dockerized
mtconnect/cppagent(reproducible integration fixture). Addtests/.../Docker/docker-compose.ymlwith theproject: lmxopcualabel on every service, seeded with a cannedDevices.xml(+ an SHDR simulator or the built-in adapter), exposed on the shared docker host10.100.0.35; drive it vialmxopcua-fix up mtconnect+sync. Env-gated integration suite (*.IntegrationTests), skips cleanly when the fixture is down — the analog of the Modbus/S7 sims. - Public demo agent (manual live smoke only).
https://demo.mtconnect.org/(or NISTsmstestbedDevices.xml) for a real-shape browse/read/stream smoke. Internet-dependent, not in CI. Live-verify the browse picker on docker-dev per the universal-browser discipline: open the/unsTagModal picker for an MTConnect driver, confirm the Device→Component→DataItem tree renders and a picked leaf commitsTagConfig.FullName = <dataItemId>(Razor binding bugs pass unit tests — always/runit).
9. Phasing + effort
- P1 (this design) — Agent MVP, ~1–1.5 wk with TrakHound (~2.5–3 wk hand-rolled):
.Contracts+Driver.MTConnect(IDriver+ITagDiscovery+IReadable+ISubscribable+IHostConnectivityProbe+IRediscoverable),MTConnectDriverProbe,SupportsOnlineDiscovery=true(browse is free via universal browser — no browser code), typed editor + map/validator entries, 3-line Host registration, canned-XML unit suite + cppagent fixture. CONDITION asString. - P1.5 fast-follow: CONDITION → native Part-9 alarms via
IAlarmSource(Galaxy pattern);TIME_SERIESSAMPLE arrays; EVENT controlled-vocab → OPC UA enumerations. - P2 (on demand) — SHDR adapter ingest: add
SourceMode: "Agent" | "Shdr"; SHDR opens the raw pipe-delimited TCP socket (:7878). Loses auto-discovery (no device model →SupportsOnlineDiscoverymust returnfalsein SHDR mode → picker falls back to manual entry; tags authored like Modbus). Niche.
Detailed risk register (weak typing, CONDITION analog, TrakHound license, demo-agent CI flakiness, ring-buffer overflow, netstandard2.0-on-.NET10) is in research §7 — not repeated here.