From 34cc1825ad4301e1a08f951c9602508694cb9cd8 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 24 Jul 2026 13:50:07 -0400 Subject: [PATCH] feat(mtconnect): driver options + tag definition records (Task 2) --- .../MTConnectDriverOptions.cs | 91 +++++++++++++++++++ .../MTConnectTagDefinition.cs | 49 ++++++++++ .../MTConnectDriverOptionsTests.cs | 80 ++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs create mode 100644 src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectTagDefinition.cs create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectDriverOptionsTests.cs diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs new file mode 100644 index 00000000..8b9a9031 --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs @@ -0,0 +1,91 @@ +namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect; + +/// +/// MTConnect Agent driver configuration. Bound from the driver's DriverConfig JSON at +/// DriverHost.RegisterAsync. The driver polls a remote MTConnect Agent's REST endpoints +/// (/probe, /current, /sample) rooted at and maps +/// each returned DataItem into an OPC UA variable per . +/// +public sealed class MTConnectDriverOptions +{ + /// + /// Gets the MTConnect Agent's base URI (e.g. http://agent:5000). Required — the + /// driver appends the standard Agent request paths (/probe, /current, + /// /sample) to this base. + /// + public required string AgentUri { get; init; } + + /// + /// Optional device-name scope. When set, requests are narrowed to + /// {AgentUri}/{DeviceName}/... so a multi-device Agent only serves one device's + /// DataItems through this driver instance. Default null = agent-wide (all devices). + /// + public string? DeviceName { get; init; } + + /// + /// Per-call HTTP deadline, in milliseconds, applied to every Agent request + /// (/probe, /current, /sample). Default 5000 — long enough for + /// a large device probe response over a LAN, short enough that a hung Agent surfaces as a + /// failed poll rather than wedging the driver. + /// + public int RequestTimeoutMs { get; init; } = 5000; + + /// + /// The interval query parameter (milliseconds) passed to the Agent's /sample + /// streaming request — the minimum time the Agent waits between successive chunks of the + /// response. Default 1000; must be non-zero so the sample pump cannot spin the + /// connection into a busy-loop. + /// + public int SampleIntervalMs { get; init; } = 1000; + + /// + /// The count query parameter passed to the Agent's /sample request — the + /// maximum number of DataItem observations returned per chunk. Default 1000, the + /// MTConnect Agent's own conventional default. + /// + public int SampleCount { get; init; } = 1000; + + /// + /// The heartbeat query parameter (milliseconds) passed to the Agent's /sample + /// streaming request — the Agent sends an empty chunk after this much idle time so the + /// client can detect a silently-stalled connection. Default 10000, matching the + /// MTConnect Agent's own conventional default; must be non-zero or a quiet connection can + /// never be distinguished from a dead one. + /// + public int HeartbeatMs { get; init; } = 10000; + + /// + /// Background connectivity-probe settings. When + /// is true the driver periodically issues a cheap /probe request and raises + /// OnHostStatusChanged on Running ↔ Stopped transitions. + /// + public MTConnectProbeOptions Probe { get; init; } = new(); + + /// + /// Reconnect backoff settings used after a failed Agent request or a dropped + /// /sample streaming connection. + /// + public MTConnectReconnectOptions Reconnect { get; init; } = new(); +} + +/// Background connectivity-probe knobs, mirroring ModbusProbeOptions. +public sealed class MTConnectProbeOptions +{ + /// Gets a value indicating whether probing is enabled. + public bool Enabled { get; init; } = true; + /// Gets the interval between probe requests. + public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5); + /// Gets the probe request timeout. + public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2); +} + +/// Geometric-backoff settings for the post-failure reconnect loop, mirroring ModbusReconnectOptions. +public sealed class MTConnectReconnectOptions +{ + /// Delay before the first reconnect attempt, in milliseconds. Default 0 = immediate. + public int MinBackoffMs { get; init; } = 0; + /// Upper bound on the geometric backoff sequence, in milliseconds. + public int MaxBackoffMs { get; init; } = 30000; + /// Multiplier applied each retry. Default 2.0 doubles each step. + public double BackoffMultiplier { get; init; } = 2.0; +} diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectTagDefinition.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectTagDefinition.cs new file mode 100644 index 00000000..c956f809 --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectTagDefinition.cs @@ -0,0 +1,49 @@ +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; + +namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect; + +/// +/// One MTConnect-backed OPC UA variable. Each authored tag binds a single Agent DataItem by +/// its id attribute (); the driver looks up the observation's +/// current value from the Agent's /current snapshot / /sample stream by that id. +/// +/// +/// The MTConnect DataItem's id attribute (the Agent's /probe response). This is +/// the driver's reference for read/subscribe — MTConnect is a read-only source protocol, so +/// there is no corresponding write path. +/// +/// Logical data type the DataItem's value is coerced to. +/// +/// When true, the tag is exposed as an OPC UA array (e.g. an MTConnect +/// PathPosition / vector-valued sample). Default false = scalar. +/// +/// +/// Element count when is true; ignored for scalar tags. Default +/// 0. +/// +/// +/// The DataItem's MTConnect category attribute — SAMPLE, EVENT, or +/// CONDITION. Optional metadata carried through for browse / display; not consulted by +/// the value-mapping path. Default null. +/// +/// +/// The DataItem's MTConnect type attribute (e.g. POSITION, EXECUTION). +/// Optional metadata. Default null. +/// +/// +/// The DataItem's MTConnect subType attribute (e.g. ACTUAL, COMMANDED). +/// Optional metadata. Default null. +/// +/// +/// The DataItem's MTConnect units attribute (e.g. MILLIMETER, +/// REVOLUTION/MINUTE). Optional metadata. Default null. +/// +public sealed record MTConnectTagDefinition( + string FullName, + DriverDataType DriverDataType, + bool IsArray = false, + int ArrayDim = 0, + string? MtCategory = null, + string? MtType = null, + string? MtSubType = null, + string? Units = null); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectDriverOptionsTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectDriverOptionsTests.cs new file mode 100644 index 00000000..0b77db18 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectDriverOptionsTests.cs @@ -0,0 +1,80 @@ +using Shouldly; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests; + +/// +/// Task 2 coverage for / : +/// defaults are sane (non-zero timers, probe on) and the required-field shape (AgentUri) binds. +/// +[Trait("Category", "Unit")] +public sealed class MTConnectDriverOptionsTests +{ + /// Default sample/heartbeat timers are non-zero and the probe is enabled by default. + [Fact] + public void Options_default_sample_and_heartbeat_are_sane() + { + var o = new MTConnectDriverOptions { AgentUri = "http://agent:5000" }; + o.SampleIntervalMs.ShouldBeGreaterThan(0); + o.HeartbeatMs.ShouldBeGreaterThan(0); + o.Probe.Enabled.ShouldBeTrue(); + } + + /// AgentUri is the only required field; DeviceName scope defaults to unset (agent-wide). + [Fact] + public void Options_agent_uri_is_required_device_name_defaults_to_null() + { + var o = new MTConnectDriverOptions { AgentUri = "http://agent:5000" }; + o.AgentUri.ShouldBe("http://agent:5000"); + o.DeviceName.ShouldBeNull(); + } + + /// RequestTimeoutMs and SampleCount default to sane, non-zero, non-wedging values. + [Fact] + public void Options_request_timeout_and_sample_count_are_non_zero() + { + var o = new MTConnectDriverOptions { AgentUri = "http://agent:5000" }; + o.RequestTimeoutMs.ShouldBeGreaterThan(0); + o.SampleCount.ShouldBeGreaterThan(0); + } + + /// Probe sub-options mirror ModbusProbeOptions' shape: Enabled/Interval/Timeout, both non-zero. + [Fact] + public void Probe_defaults_have_non_zero_interval_and_timeout() + { + var probe = new MTConnectDriverOptions { AgentUri = "http://agent:5000" }.Probe; + probe.Interval.ShouldBeGreaterThan(TimeSpan.Zero); + probe.Timeout.ShouldBeGreaterThan(TimeSpan.Zero); + } + + /// Reconnect sub-options default to a bounded, non-degenerate geometric backoff. + [Fact] + public void Reconnect_defaults_bound_backoff_growth() + { + var reconnect = new MTConnectDriverOptions { AgentUri = "http://agent:5000" }.Reconnect; + reconnect.MaxBackoffMs.ShouldBeGreaterThan(reconnect.MinBackoffMs); + reconnect.BackoffMultiplier.ShouldBeGreaterThan(1.0); + } + + /// MTConnectTagDefinition round-trips its positional fields, including MTConnect metadata. + [Fact] + public void TagDefinition_carries_dataItemId_and_mtconnect_metadata() + { + var tag = new MTConnectTagDefinition( + FullName: "dtop_2", + DriverDataType: ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float64, + MtCategory: "SAMPLE", + MtType: "POSITION", + MtSubType: "ACTUAL", + Units: "MILLIMETER"); + + tag.FullName.ShouldBe("dtop_2"); + tag.DriverDataType.ShouldBe(ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float64); + tag.IsArray.ShouldBeFalse(); + tag.ArrayDim.ShouldBe(0); + tag.MtCategory.ShouldBe("SAMPLE"); + tag.MtType.ShouldBe("POSITION"); + tag.MtSubType.ShouldBe("ACTUAL"); + tag.Units.ShouldBe("MILLIMETER"); + } +}