feat(mtconnect): current/sample parse + sequence-gap detection (Task 7)

Adds the /current and /sample legs to MTConnectAgentClient:

- MTConnectStreamsParser: hand-rolled System.Xml.Linq parse of an
  MTConnectStreams document into MTConnectStreamsResult. Observations are
  every element child of <Samples>/<Events>/<Condition>, keyed by
  dataItemId (the element NAME is the data item's type in PascalCase, so
  a fixed name set would drop observations). A CONDITION's value is its
  element name, not its text - <Normal/> is empty - and <Unavailable/>
  normalizes onto the same UNAVAILABLE sentinel a Sample/Event carries as
  text, so Task 8's no-comms mapping sees one token. Timestamps are
  normalized to DateTimeKind.Utc explicitly.
- IsSequenceGap(requestedFrom, chunk) => chunk.FirstSequence >
  requestedFrom, so Task 11's pump can re-baseline after a ring-buffer
  overflow. Equality is NOT a gap.
- SampleAsync: ResponseHeadersRead + a hand-rolled
  multipart/x-mixed-replace frame reader (Content-length fast path,
  boundary-scan fallback), bounded by a heartbeat watchdog derived from
  HeartbeatMs so a silent Agent faults instead of wedging the pump - the
  S7 frozen-peer shape (arch-review R2-01). The long poll gets its own
  HttpClient with an infinite Timeout so the unary /probe + /current
  deadline stays bounded.

Task 0 package decision, option (c): both TrakHound PackageReferences and
their PackageVersions are dropped. MTConnectHttpClientStream takes a URL
and dials its own socket (no handler/stream seam, so it cannot serve a
socket-free unit suite), and it emits already-parsed documents through the
formatter lookup Task 6 proved is missing - framing-only reuse was never
actually on offer. The driver now depends on nothing but the BCL. Recorded
in the plan's Task 0 CORRECTION block.

Also folds in two Task 6 review carry-overs: the stale
IMTConnectAgentClient doc comment claiming a TrakHound-backed
implementation, and a probe test constructing a mixed-namespace vendor
extension (<x:Pump> with default-namespace <DataItems> children) that
backs the parser's ignore-the-namespace claim with a test.

187/187 green.
This commit is contained in:
Joseph Doherty
2026-07-24 14:32:36 -04:00
parent d3aaa4a411
commit cf43f8d0ab
9 changed files with 1740 additions and 59 deletions
@@ -17,13 +17,12 @@
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts\ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts.csproj"/>
</ItemGroup>
<ItemGroup>
<!-- Task 0 decision (verified 2026-07-24 against real nuget.org): TrakHound MTConnect.NET,
MIT, netstandard2.0 lib (resolves on net10 via the in-box compat mapping). Pulls
MTConnect.NET-TLS 6.9.0.2 transitively via -HTTP — expected, not a rogue dependency. -->
<PackageReference Include="MTConnect.NET-Common"/>
<PackageReference Include="MTConnect.NET-HTTP"/>
</ItemGroup>
<!-- No backend NuGet: the Agent surface is plain HTTP + XML, served entirely by the BCL
(HttpClient + System.Xml.Linq). The TrakHound MTConnect.NET-Common/-HTTP references Task 0
added were removed in Task 7 — they can neither parse an MTConnect document (no XML
formatter ships in either package) nor frame the /sample stream socket-free (their client
type dials its own URL). See the Task 0 CORRECTION block in
docs/plans/2026-07-24-mtconnect-driver.md. -->
<ItemGroup>
<InternalsVisibleTo Include="ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests"/>