Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDtos.cs
T

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>&lt;Entry key="…"&gt;</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>&lt;Entry key="…"&gt;</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>&lt;Entry&gt;</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>&lt;Condition&gt;</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>&lt;Cell key="…"&gt;</c> of a <c>TABLE</c> row (#650).</summary>
public sealed record MTConnectDataSetCell(string Key, string Value);
/// <summary>
/// One <c>&lt;Entry key="…"&gt;</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;
}
}
}
}