9631a40f1f
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
263 lines
14 KiB
C#
263 lines
14 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>
|
|
/// <param name="SampleCount">
|
|
/// The observation's own <c>sampleCount</c> attribute — the number of values a <c>TIME_SERIES</c>
|
|
/// vector claims to carry (#627). The index checks the parsed vector against it. <c>null</c> when absent.
|
|
/// </param>
|
|
/// <param name="SampleRate">
|
|
/// The observation's own <c>sampleRate</c> attribute (samples per second) on a <c>TIME_SERIES</c>
|
|
/// observation. Carried for the parser contract; not yet surfaced on the served node (see
|
|
/// docs/drivers/MTConnect.md). <c>null</c> when absent.
|
|
/// </param>
|
|
/// <param name="Entries">
|
|
/// The parsed <c><Entry key="…"></c> children of a <c>DATA_SET</c> / <c>TABLE</c> observation (#650), in
|
|
/// document order, or <c>null</c> when the observation had none. A <c>TABLE</c> entry carries
|
|
/// <see cref="MTConnectDataSetEntry.Cells"/>. The observation index folds these into a JSON String — see
|
|
/// <see cref="MTConnectObservationIndex"/>. <see cref="IsStructured"/> stays <c>true</c> beside them: it is the
|
|
/// wire-shape fact, this is the content.
|
|
/// </param>
|
|
/// <param name="ResetTriggered">
|
|
/// <c>true</c> when the observation carried a non-empty <c>resetTriggered</c> attribute — the Agent's statement that
|
|
/// the set was reset, so this observation <b>replaces</b> the accumulated set rather than merging into it.
|
|
/// </param>
|
|
/// <param name="IsDataSet">
|
|
/// <c>true</c> when the element is data-set shaped: it carries the <c>count</c> attribute every DATA_SET / TABLE
|
|
/// observation has (and no SAMPLE or ordinary EVENT does) or has <c><Entry></c> children. This — not
|
|
/// <see cref="ResetTriggered"/> — is what lets the index serve an entry-less set as <c>{}</c> rather than as
|
|
/// an absent value; <c>resetTriggered</c> also appears on ordinary accumulating SAMPLEs and must not matter there.
|
|
/// </param>
|
|
/// <param name="Condition">
|
|
/// The CONDITION-only detail (#626) — set exactly when the observation came from a
|
|
/// <c><Condition></c> container, <c>null</c> for every Sample/Event. Its presence is what marks an
|
|
/// observation as a condition for the alarm path; <see cref="Value"/> still carries the state word.
|
|
/// </param>
|
|
public sealed record MTConnectObservation(
|
|
string DataItemId,
|
|
string Value,
|
|
DateTime TimestampUtc,
|
|
bool IsStructured = false,
|
|
int? SampleCount = null,
|
|
double? SampleRate = null,
|
|
MTConnectConditionDetail? Condition = null,
|
|
IReadOnlyList<MTConnectDataSetEntry>? Entries = null,
|
|
bool ResetTriggered = false,
|
|
bool IsDataSet = false);
|
|
|
|
/// <summary>One <c><Cell key="…"></c> of a <c>TABLE</c> row (#650).</summary>
|
|
public sealed record MTConnectDataSetCell(string Key, string Value);
|
|
|
|
/// <summary>
|
|
/// One <c><Entry key="…"></c> of a <c>DATA_SET</c> / <c>TABLE</c> observation (#650).
|
|
/// </summary>
|
|
/// <param name="Key">The entry's <c>key</c> attribute, verbatim.</param>
|
|
/// <param name="Value">A <c>DATA_SET</c> entry's trimmed text; <c>null</c> for a <c>TABLE</c> row or a removed entry.</param>
|
|
/// <param name="Removed"><c>true</c> when the entry carried <c>removed="true"</c>: the key is deleted from the set.</param>
|
|
/// <param name="Cells">A <c>TABLE</c> row's cells in document order; <c>null</c> for a <c>DATA_SET</c> entry.</param>
|
|
public sealed record MTConnectDataSetEntry(
|
|
string Key,
|
|
string? Value,
|
|
bool Removed,
|
|
IReadOnlyList<MTConnectDataSetCell>? Cells = null);
|
|
|
|
/// <summary>
|
|
/// What an MTConnect CONDITION observation says beyond its state word (#626). Every field is the
|
|
/// Agent's own text, trimmed, or <c>null</c> when absent or blank — none is interpreted at this layer.
|
|
/// </summary>
|
|
/// <param name="Text">The element's text content: the operator-facing condition message.</param>
|
|
/// <param name="NativeCode">The <c>nativeCode</c> attribute — the controller's own alarm number/code.</param>
|
|
/// <param name="NativeSeverity">The <c>nativeSeverity</c> attribute — the controller's own severity, verbatim.</param>
|
|
/// <param name="Qualifier">The <c>qualifier</c> attribute (e.g. <c>HIGH</c>, <c>LOW</c>).</param>
|
|
/// <param name="ConditionType">The element's <c>type</c> attribute (e.g. <c>SYSTEM</c>, <c>TEMPERATURE</c>).</param>
|
|
public sealed record MTConnectConditionDetail(
|
|
string? Text,
|
|
string? NativeCode,
|
|
string? NativeSeverity,
|
|
string? Qualifier,
|
|
string? ConditionType);
|
|
|
|
/// <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;
|
|
}
|
|
}
|
|
}
|
|
}
|