712635ce8c
Code review of c46540ae approved the framing algorithm but moved the risk onto the contract around it. C1 (critical). SampleAsync returned normally on three different events: EOF, the closing --boundary--, and a response that was not multipart at all (where it yielded one parsed snapshot and finished). The seam documents the stream as yielding until ct is cancelled, so a Task 11 pump written to that contract would silently drop its subscription or hot-loop reconnecting; the non-multipart leg reported a configuration error as a healthy finished stream, which is the #485 quiet-successful-termination shape aimed at the very next task. Every non-cancellation end now throws: MTConnectStreamEndedException (ConnectionClosed / ClosingBoundary - transient) or MTConnectStreamNotSupportedException (configuration - reconnecting reproduces it forever), sharing one catchable base. The non-multipart body is still parsed first so an Agent's own MTConnectError text wins, but the document is NOT yielded. I1. IsSequenceGap moved to IMTConnectAgentClient (static) and its first parameter is now expectedFrom. The old name and doc were wrong for every chunk after the first - the correct comparand is the PREVIOUS chunk's NextSequence, and comparing against the opening "from" argument reports a gap on every chunk once the ring buffer rolls, i.e. an endless /current re-baseline storm. Both doc blocks also now record that cppagent answers from < firstSequence with an OUT_OF_RANGE MTConnectError under HTTP 200, which surfaces as InvalidDataException and NOT as a gap - so Task 11 must treat that parse failure as a re-baseline trigger too. I2. Every multipart test served the whole body from one buffer, so every framing test completed in a single ReadAsync - the split-boundary path, the split-header path and the multi-fill loop were correct by inspection only. A ChunkedStream double (N bytes per read, N = 1/3/7) now drives the fixtures through a split transport, plus a >16 KB document that forces a buffer resize. Falsifiability: removing either scan overlap, or collapsing the fill loop, fails ONLY the new chunked tests. I3. Disposal mid-enumeration surfaces as OperationCanceledException via an internal dispose-linked token, not a raw ObjectDisposedException that Task 9's re-init would inflict on an enumerating pump. I5. HeartbeatMs, SampleIntervalMs and SampleCount join RequestTimeoutMs in construction-time positive-value validation. All three reach the query string and an Agent answers heartbeat=0 with an HTTP-200 MTConnectError that names no config key. I4/M2. The no-Content-length framing fallback logs a one-shot warning (the client takes an optional ILogger); a multipart response with no boundary parameter fails fast instead of degrading into a watchdog timeout. M5/M6. Shared element/attribute reading rules extracted to MTConnectXml; the probe parser documents why it alone does not trim BOM/whitespace. The literal U+FEFF in source became a '' escape. Also, from Task 8: MTConnectObservation gained IsStructured (default false), set from element.HasElements. A DATA_SET/TABLE observation's <Entry> children concatenate through element.Value to nonsense ("12" for two entries) that the index would publish as Good, and only the parser can tell that apart from a legitimate space-bearing Message. False for CONDITION observations - their value comes from the element name, so child content cannot corrupt it. 275/275 green in this suite's own files.
194 lines
10 KiB
C#
194 lines
10 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect;
|
|
|
|
/// <summary>
|
|
/// Result of an Agent <c>/probe</c> request: the full device hierarchy the Agent exposes.
|
|
/// Produced by <see cref="IMTConnectAgentClient.ProbeAsync"/>. This is the neutral shape both
|
|
/// the TrakHound-backed implementation and a hand-rolled XML fallback must produce — nothing
|
|
/// downstream of <see cref="IMTConnectAgentClient"/> ever sees a TrakHound type or an
|
|
/// <c>XElement</c>.
|
|
/// </summary>
|
|
/// <param name="Devices">Every device the Agent's probe response declares.</param>
|
|
public sealed record MTConnectProbeModel(IReadOnlyList<MTConnectDevice> Devices);
|
|
|
|
/// <summary>
|
|
/// Common shape shared by <see cref="MTConnectDevice"/> and <see cref="MTConnectComponent"/>:
|
|
/// each may directly own data items and/or nest further components, to arbitrary depth. Backs
|
|
/// the <see cref="MTConnectComponentTreeExtensions.AllDataItems"/> recursive walk.
|
|
/// </summary>
|
|
public interface IMTConnectComponentContainer
|
|
{
|
|
/// <summary>Components nested directly under this device/component (may be empty).</summary>
|
|
IReadOnlyList<MTConnectComponent> Components { get; }
|
|
|
|
/// <summary>Data items declared directly on this device/component (may be empty).</summary>
|
|
IReadOnlyList<MTConnectDataItem> DataItems { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// One MTConnect device from the Agent's probe response — the root of a nested
|
|
/// component/data-item tree.
|
|
/// </summary>
|
|
/// <param name="Id">The device's <c>id</c> attribute.</param>
|
|
/// <param name="Name">The device's <c>name</c> attribute, when the Agent supplies one.</param>
|
|
/// <param name="Components">Child components declared directly under this device.</param>
|
|
/// <param name="DataItems">Data items declared directly on this device (outside any component).</param>
|
|
public sealed record MTConnectDevice(
|
|
string Id,
|
|
string? Name,
|
|
IReadOnlyList<MTConnectComponent> Components,
|
|
IReadOnlyList<MTConnectDataItem> DataItems) : IMTConnectComponentContainer;
|
|
|
|
/// <summary>
|
|
/// One MTConnect component (e.g. <c>Axes</c>, <c>Controller</c>, <c>Path</c>) from the Agent's
|
|
/// probe response. Components nest to arbitrary depth — a component may itself contain further
|
|
/// child components as well as data items.
|
|
/// </summary>
|
|
/// <param name="Id">The component's <c>id</c> attribute.</param>
|
|
/// <param name="Name">The component's <c>name</c> attribute, when the Agent supplies one.</param>
|
|
/// <param name="Components">Child components nested directly under this component.</param>
|
|
/// <param name="DataItems">Data items declared directly on this component.</param>
|
|
public sealed record MTConnectComponent(
|
|
string Id,
|
|
string? Name,
|
|
IReadOnlyList<MTConnectComponent> Components,
|
|
IReadOnlyList<MTConnectDataItem> DataItems) : IMTConnectComponentContainer;
|
|
|
|
/// <summary>
|
|
/// One MTConnect DataItem declaration from the Agent's probe response. This is pure metadata —
|
|
/// it carries no observed value; values arrive separately via
|
|
/// <see cref="IMTConnectAgentClient.CurrentAsync"/> / <see cref="IMTConnectAgentClient.SampleAsync"/>
|
|
/// and are correlated back to a data item by <see cref="Id"/>.
|
|
/// </summary>
|
|
/// <param name="Id">
|
|
/// The DataItem's <c>id</c> attribute — globally unique within the Agent's probe response and
|
|
/// the correlation key used against <see cref="MTConnectObservation.DataItemId"/>.
|
|
/// </param>
|
|
/// <param name="Name">The DataItem's <c>name</c> attribute, when the Agent supplies one.</param>
|
|
/// <param name="Category">The DataItem's <c>category</c> attribute — <c>SAMPLE</c>, <c>EVENT</c>, or <c>CONDITION</c>.</param>
|
|
/// <param name="Type">The DataItem's <c>type</c> attribute (e.g. <c>POSITION</c>, <c>EXECUTION</c>).</param>
|
|
/// <param name="SubType">The DataItem's <c>subType</c> attribute (e.g. <c>ACTUAL</c>, <c>COMMANDED</c>), when present.</param>
|
|
/// <param name="Units">The DataItem's <c>units</c> attribute (e.g. <c>MILLIMETER</c>, <c>REVOLUTION/MINUTE</c>), when present.</param>
|
|
/// <param name="Representation">
|
|
/// The DataItem's <c>representation</c> attribute (e.g. <c>TIME_SERIES</c>, <c>DISCRETE</c>);
|
|
/// absent means the MTConnect default (<c>VALUE</c>).
|
|
/// </param>
|
|
/// <param name="SampleCount">
|
|
/// The DataItem's <c>sampleCount</c> attribute — element count for a <c>TIME_SERIES</c>
|
|
/// representation. Present only on data items that carry one.
|
|
/// </param>
|
|
public sealed record MTConnectDataItem(
|
|
string Id,
|
|
string? Name,
|
|
string Category,
|
|
string Type,
|
|
string? SubType,
|
|
string? Units,
|
|
string? Representation,
|
|
int? SampleCount);
|
|
|
|
/// <summary>
|
|
/// Result of an Agent <c>/current</c> request, or of a single multipart chunk from a
|
|
/// <c>/sample</c> stream. Produced by <see cref="IMTConnectAgentClient.CurrentAsync"/> and
|
|
/// <see cref="IMTConnectAgentClient.SampleAsync"/>.
|
|
/// </summary>
|
|
/// <param name="InstanceId">
|
|
/// The Agent's current instance id (its MTConnectStreams header <c>instanceId</c>). Changes
|
|
/// whenever the Agent restarts or its underlying device model changes; the driver watches this
|
|
/// for change to raise rediscovery rather than trusting a stale observation snapshot.
|
|
/// </param>
|
|
/// <param name="NextSequence">
|
|
/// The header <c>nextSequence</c> — the sequence number to resume a subsequent
|
|
/// <see cref="IMTConnectAgentClient.SampleAsync"/> call <c>from</c>.
|
|
/// </param>
|
|
/// <param name="FirstSequence">
|
|
/// The header <c>firstSequence</c> — the oldest sequence number still held in the Agent's
|
|
/// circular buffer. Load-bearing for sequence-gap detection: a caller that requested
|
|
/// <c>from</c> a sequence older than this chunk's <see cref="FirstSequence"/> has fallen out of
|
|
/// the Agent's buffer and must re-baseline via <see cref="IMTConnectAgentClient.CurrentAsync"/>.
|
|
/// </param>
|
|
/// <param name="Observations">
|
|
/// The observations carried in this snapshot/chunk, in Agent-supplied order. Never <c>null</c>;
|
|
/// empty when the chunk carries no new observations (e.g. a heartbeat).
|
|
/// </param>
|
|
public sealed record MTConnectStreamsResult(
|
|
long InstanceId,
|
|
long NextSequence,
|
|
long FirstSequence,
|
|
IReadOnlyList<MTConnectObservation> Observations);
|
|
|
|
/// <summary>
|
|
/// One observed value for a single DataItem, as reported in a <c>/current</c> snapshot or a
|
|
/// <c>/sample</c> stream chunk. Deliberately dumb: <see cref="Value"/> is carried as the raw
|
|
/// string the Agent reported (including the literal <c>UNAVAILABLE</c>) with no interpretation
|
|
/// applied here — coercing <c>UNAVAILABLE</c> to a bad-quality status and converting the string
|
|
/// to the tag's <c>DriverDataType</c> are the observation index's job, not this layer's.
|
|
/// </summary>
|
|
/// <param name="DataItemId">
|
|
/// The reporting DataItem's <c>id</c> — correlates back to <see cref="MTConnectDataItem.Id"/>
|
|
/// from the probe model.
|
|
/// </param>
|
|
/// <param name="Value">
|
|
/// The observed value exactly as the Agent reported it, or the literal string
|
|
/// <c>"UNAVAILABLE"</c> when the Agent has no current value for the data item. Never
|
|
/// pre-parsed into a nullable or an enum at this layer.
|
|
/// </param>
|
|
/// <param name="TimestampUtc">
|
|
/// The observation's Agent-reported timestamp, normalized to UTC (<see cref="DateTime.Kind"/>
|
|
/// is always <see cref="DateTimeKind.Utc"/>). Becomes the OPC UA variable's SourceTimestamp.
|
|
/// </param>
|
|
/// <param name="IsStructured">
|
|
/// <c>true</c> when the Agent carried this observation's real content in <b>child elements</b>
|
|
/// rather than as text — an MTConnect 2.0 <c>DATA_SET</c> / <c>TABLE</c> observation, whose
|
|
/// content is a list of <c><Entry key="…"></c> elements.
|
|
/// <para>
|
|
/// <b>Why the flag exists, and why only the parser can set it.</b> <see cref="Value"/> is
|
|
/// the element's concatenated descendant text, so a two-entry data set reads as the single
|
|
/// token <c>"12"</c> — keys discarded, values run together. The observation index cannot
|
|
/// detect that after the fact: neither this record nor the tag definition carries the
|
|
/// DataItem's <c>representation</c>, and no value-shape heuristic can work, because
|
|
/// concatenated entries are indistinguishable from a legitimate space-bearing EVENT such as
|
|
/// a <c>Message</c> reading <c>"Coolant level low"</c>. The one reliable discriminator —
|
|
/// that the observation element had element children — exists only while the XML is still
|
|
/// XML.
|
|
/// </para>
|
|
/// <para>
|
|
/// Deliberately a <b>neutral fact about the wire shape</b>, not a status: mapping it to a
|
|
/// quality code is the observation index's job (it codes these
|
|
/// <c>BadNotSupported</c> rather than publishing concatenated noise as Good). Defaults to
|
|
/// <c>false</c>, the shape of every ordinary scalar observation.
|
|
/// </para>
|
|
/// </param>
|
|
public sealed record MTConnectObservation(
|
|
string DataItemId,
|
|
string Value,
|
|
DateTime TimestampUtc,
|
|
bool IsStructured = false);
|
|
|
|
/// <summary>
|
|
/// Recursive helpers over the <see cref="IMTConnectComponentContainer"/> device/component tree.
|
|
/// </summary>
|
|
public static class MTConnectComponentTreeExtensions
|
|
{
|
|
/// <summary>
|
|
/// Enumerates every data item owned by this device or component, plus every data item owned
|
|
/// by any component nested beneath it, to arbitrary depth. Order is depth-first: a node's
|
|
/// own data items first, then each child component's data items in turn.
|
|
/// </summary>
|
|
/// <param name="container">The device or component to walk.</param>
|
|
public static IEnumerable<MTConnectDataItem> AllDataItems(this IMTConnectComponentContainer container)
|
|
{
|
|
foreach (var dataItem in container.DataItems)
|
|
{
|
|
yield return dataItem;
|
|
}
|
|
|
|
foreach (var child in container.Components)
|
|
{
|
|
foreach (var dataItem in child.AllDataItems())
|
|
{
|
|
yield return dataItem;
|
|
}
|
|
}
|
|
}
|
|
}
|