diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/IMTConnectAgentClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/IMTConnectAgentClient.cs new file mode 100644 index 00000000..6f54ef98 --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/IMTConnectAgentClient.cs @@ -0,0 +1,44 @@ +namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect; + +/// +/// The seam between the MTConnect driver and an MTConnect Agent's HTTP surface +/// (/probe, /current, /sample). This interface is transport-neutral by +/// design — every method returns/yields the plain DTOs in MTConnectDtos.cs, never a +/// TrakHound type, an XElement, or any other wire/library artifact. That is what lets +/// every driver behaviour above this seam (Tasks 6–13) be unit-tested against a fake serving +/// canned probe/current/sample XML, with no sockets involved. +/// +/// +/// The production implementation (Task 6/7) is built on the TrakHound MTConnect.NET client +/// libraries (Task 0's decision); a fake for tests only needs to implement these three members. +/// +public interface IMTConnectAgentClient +{ + /// + /// Issues an Agent /probe request and returns the parsed device hierarchy. Called + /// once at driver initialization; the result is cached and later walked to build the OPC UA + /// browse tree, where each becomes a tag's FullName. + /// + /// Cancellation token. + Task ProbeAsync(CancellationToken ct); + + /// + /// Issues an Agent /current request and returns the latest observed value for every + /// data item. Used both to prime the observation index at startup and to re-baseline after + /// a detected sequence gap in . + /// + /// Cancellation token. + Task CurrentAsync(CancellationToken ct); + + /// + /// Opens an Agent /sample long-poll stream starting at sequence + /// and yields one per multipart chunk the Agent sends, + /// indefinitely, until is cancelled. Each yielded result's + /// is load-bearing — the caller compares + /// it against the requested to detect a ring-buffer sequence gap and + /// re-baseline via . + /// + /// The sequence number to resume streaming from. + /// Cancellation token; cancelling ends the stream. + IAsyncEnumerable SampleAsync(long from, CancellationToken ct); +} diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDtos.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDtos.cs new file mode 100644 index 00000000..76bd41c9 --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDtos.cs @@ -0,0 +1,170 @@ +namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect; + +/// +/// Result of an Agent /probe request: the full device hierarchy the Agent exposes. +/// Produced by . This is the neutral shape both +/// the TrakHound-backed implementation and a hand-rolled XML fallback must produce — nothing +/// downstream of ever sees a TrakHound type or an +/// XElement. +/// +/// Every device the Agent's probe response declares. +public sealed record MTConnectProbeModel(IReadOnlyList Devices); + +/// +/// Common shape shared by and : +/// each may directly own data items and/or nest further components, to arbitrary depth. Backs +/// the recursive walk. +/// +public interface IMTConnectComponentContainer +{ + /// Components nested directly under this device/component (may be empty). + IReadOnlyList Components { get; } + + /// Data items declared directly on this device/component (may be empty). + IReadOnlyList DataItems { get; } +} + +/// +/// One MTConnect device from the Agent's probe response — the root of a nested +/// component/data-item tree. +/// +/// The device's id attribute. +/// The device's name attribute, when the Agent supplies one. +/// Child components declared directly under this device. +/// Data items declared directly on this device (outside any component). +public sealed record MTConnectDevice( + string Id, + string? Name, + IReadOnlyList Components, + IReadOnlyList DataItems) : IMTConnectComponentContainer; + +/// +/// One MTConnect component (e.g. Axes, Controller, Path) from the Agent's +/// probe response. Components nest to arbitrary depth — a component may itself contain further +/// child components as well as data items. +/// +/// The component's id attribute. +/// The component's name attribute, when the Agent supplies one. +/// Child components nested directly under this component. +/// Data items declared directly on this component. +public sealed record MTConnectComponent( + string Id, + string? Name, + IReadOnlyList Components, + IReadOnlyList DataItems) : IMTConnectComponentContainer; + +/// +/// One MTConnect DataItem declaration from the Agent's probe response. This is pure metadata — +/// it carries no observed value; values arrive separately via +/// / +/// and are correlated back to a data item by . +/// +/// +/// The DataItem's id attribute — globally unique within the Agent's probe response and +/// the correlation key used against . +/// +/// The DataItem's name attribute, when the Agent supplies one. +/// The DataItem's category attribute — SAMPLE, EVENT, or CONDITION. +/// The DataItem's type attribute (e.g. POSITION, EXECUTION). +/// The DataItem's subType attribute (e.g. ACTUAL, COMMANDED), when present. +/// The DataItem's units attribute (e.g. MILLIMETER, REVOLUTION/MINUTE), when present. +/// +/// The DataItem's representation attribute (e.g. TIME_SERIES, DISCRETE); +/// absent means the MTConnect default (VALUE). +/// +/// +/// The DataItem's sampleCount attribute — element count for a TIME_SERIES +/// representation. Present only on data items that carry one. +/// +public sealed record MTConnectDataItem( + string Id, + string? Name, + string Category, + string Type, + string? SubType, + string? Units, + string? Representation, + int? SampleCount); + +/// +/// Result of an Agent /current request, or of a single multipart chunk from a +/// /sample stream. Produced by and +/// . +/// +/// +/// The Agent's current instance id (its MTConnectStreams header instanceId). 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. +/// +/// +/// The header nextSequence — the sequence number to resume a subsequent +/// call from. +/// +/// +/// The header firstSequence — the oldest sequence number still held in the Agent's +/// circular buffer. Load-bearing for sequence-gap detection: a caller that requested +/// from a sequence older than this chunk's has fallen out of +/// the Agent's buffer and must re-baseline via . +/// +/// +/// The observations carried in this snapshot/chunk, in Agent-supplied order. Never null; +/// empty when the chunk carries no new observations (e.g. a heartbeat). +/// +public sealed record MTConnectStreamsResult( + long InstanceId, + long NextSequence, + long FirstSequence, + IReadOnlyList Observations); + +/// +/// One observed value for a single DataItem, as reported in a /current snapshot or a +/// /sample stream chunk. Deliberately dumb: is carried as the raw +/// string the Agent reported (including the literal UNAVAILABLE) with no interpretation +/// applied here — coercing UNAVAILABLE to a bad-quality status and converting the string +/// to the tag's DriverDataType are the observation index's job, not this layer's. +/// +/// +/// The reporting DataItem's id — correlates back to +/// from the probe model. +/// +/// +/// The observed value exactly as the Agent reported it, or the literal string +/// "UNAVAILABLE" when the Agent has no current value for the data item. Never +/// pre-parsed into a nullable or an enum at this layer. +/// +/// +/// The observation's Agent-reported timestamp, normalized to UTC ( +/// is always ). Becomes the OPC UA variable's SourceTimestamp. +/// +public sealed record MTConnectObservation( + string DataItemId, + string Value, + DateTime TimestampUtc); + +/// +/// Recursive helpers over the device/component tree. +/// +public static class MTConnectComponentTreeExtensions +{ + /// + /// 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. + /// + /// The device or component to walk. + public static IEnumerable 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; + } + } + } +}