feat(mtconnect): driver options + tag definition records (Task 2)
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect;
|
||||
|
||||
/// <summary>
|
||||
/// MTConnect Agent driver configuration. Bound from the driver's <c>DriverConfig</c> JSON at
|
||||
/// <c>DriverHost.RegisterAsync</c>. The driver polls a remote MTConnect Agent's REST endpoints
|
||||
/// (<c>/probe</c>, <c>/current</c>, <c>/sample</c>) rooted at <see cref="AgentUri"/> and maps
|
||||
/// each returned DataItem into an OPC UA variable per <see cref="MTConnectTagDefinition"/>.
|
||||
/// </summary>
|
||||
public sealed class MTConnectDriverOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the MTConnect Agent's base URI (e.g. <c>http://agent:5000</c>). Required — the
|
||||
/// driver appends the standard Agent request paths (<c>/probe</c>, <c>/current</c>,
|
||||
/// <c>/sample</c>) to this base.
|
||||
/// </summary>
|
||||
public required string AgentUri { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional device-name scope. When set, requests are narrowed to
|
||||
/// <c>{AgentUri}/{DeviceName}/...</c> so a multi-device Agent only serves one device's
|
||||
/// DataItems through this driver instance. Default <c>null</c> = agent-wide (all devices).
|
||||
/// </summary>
|
||||
public string? DeviceName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Per-call HTTP deadline, in milliseconds, applied to every Agent request
|
||||
/// (<c>/probe</c>, <c>/current</c>, <c>/sample</c>). Default <c>5000</c> — 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.
|
||||
/// </summary>
|
||||
public int RequestTimeoutMs { get; init; } = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// The <c>interval</c> query parameter (milliseconds) passed to the Agent's <c>/sample</c>
|
||||
/// streaming request — the minimum time the Agent waits between successive chunks of the
|
||||
/// response. Default <c>1000</c>; must be non-zero so the sample pump cannot spin the
|
||||
/// connection into a busy-loop.
|
||||
/// </summary>
|
||||
public int SampleIntervalMs { get; init; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// The <c>count</c> query parameter passed to the Agent's <c>/sample</c> request — the
|
||||
/// maximum number of DataItem observations returned per chunk. Default <c>1000</c>, the
|
||||
/// MTConnect Agent's own conventional default.
|
||||
/// </summary>
|
||||
public int SampleCount { get; init; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// The <c>heartbeat</c> query parameter (milliseconds) passed to the Agent's <c>/sample</c>
|
||||
/// streaming request — the Agent sends an empty chunk after this much idle time so the
|
||||
/// client can detect a silently-stalled connection. Default <c>10000</c>, matching the
|
||||
/// MTConnect Agent's own conventional default; must be non-zero or a quiet connection can
|
||||
/// never be distinguished from a dead one.
|
||||
/// </summary>
|
||||
public int HeartbeatMs { get; init; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Background connectivity-probe settings. When <see cref="MTConnectProbeOptions.Enabled"/>
|
||||
/// is true the driver periodically issues a cheap <c>/probe</c> request and raises
|
||||
/// <c>OnHostStatusChanged</c> on Running ↔ Stopped transitions.
|
||||
/// </summary>
|
||||
public MTConnectProbeOptions Probe { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Reconnect backoff settings used after a failed Agent request or a dropped
|
||||
/// <c>/sample</c> streaming connection.
|
||||
/// </summary>
|
||||
public MTConnectReconnectOptions Reconnect { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>Background connectivity-probe knobs, mirroring <c>ModbusProbeOptions</c>.</summary>
|
||||
public sealed class MTConnectProbeOptions
|
||||
{
|
||||
/// <summary>Gets a value indicating whether probing is enabled.</summary>
|
||||
public bool Enabled { get; init; } = true;
|
||||
/// <summary>Gets the interval between probe requests.</summary>
|
||||
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
|
||||
/// <summary>Gets the probe request timeout.</summary>
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
|
||||
}
|
||||
|
||||
/// <summary>Geometric-backoff settings for the post-failure reconnect loop, mirroring <c>ModbusReconnectOptions</c>.</summary>
|
||||
public sealed class MTConnectReconnectOptions
|
||||
{
|
||||
/// <summary>Delay before the first reconnect attempt, in milliseconds. Default <c>0</c> = immediate.</summary>
|
||||
public int MinBackoffMs { get; init; } = 0;
|
||||
/// <summary>Upper bound on the geometric backoff sequence, in milliseconds.</summary>
|
||||
public int MaxBackoffMs { get; init; } = 30000;
|
||||
/// <summary>Multiplier applied each retry. Default <c>2.0</c> doubles each step.</summary>
|
||||
public double BackoffMultiplier { get; init; } = 2.0;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect;
|
||||
|
||||
/// <summary>
|
||||
/// One MTConnect-backed OPC UA variable. Each authored tag binds a single Agent DataItem by
|
||||
/// its <c>id</c> attribute (<see cref="FullName"/>); the driver looks up the observation's
|
||||
/// current value from the Agent's <c>/current</c> snapshot / <c>/sample</c> stream by that id.
|
||||
/// </summary>
|
||||
/// <param name="FullName">
|
||||
/// The MTConnect DataItem's <c>id</c> attribute (the Agent's <c>/probe</c> response). This is
|
||||
/// the driver's reference for read/subscribe — MTConnect is a read-only source protocol, so
|
||||
/// there is no corresponding write path.
|
||||
/// </param>
|
||||
/// <param name="DriverDataType">Logical data type the DataItem's value is coerced to.</param>
|
||||
/// <param name="IsArray">
|
||||
/// When <c>true</c>, the tag is exposed as an OPC UA array (e.g. an MTConnect
|
||||
/// <c>PathPosition</c> / vector-valued sample). Default <c>false</c> = scalar.
|
||||
/// </param>
|
||||
/// <param name="ArrayDim">
|
||||
/// Element count when <see cref="IsArray"/> is <c>true</c>; ignored for scalar tags. Default
|
||||
/// <c>0</c>.
|
||||
/// </param>
|
||||
/// <param name="MtCategory">
|
||||
/// The DataItem's MTConnect <c>category</c> attribute — <c>SAMPLE</c>, <c>EVENT</c>, or
|
||||
/// <c>CONDITION</c>. Optional metadata carried through for browse / display; not consulted by
|
||||
/// the value-mapping path. Default <c>null</c>.
|
||||
/// </param>
|
||||
/// <param name="MtType">
|
||||
/// The DataItem's MTConnect <c>type</c> attribute (e.g. <c>POSITION</c>, <c>EXECUTION</c>).
|
||||
/// Optional metadata. Default <c>null</c>.
|
||||
/// </param>
|
||||
/// <param name="MtSubType">
|
||||
/// The DataItem's MTConnect <c>subType</c> attribute (e.g. <c>ACTUAL</c>, <c>COMMANDED</c>).
|
||||
/// Optional metadata. Default <c>null</c>.
|
||||
/// </param>
|
||||
/// <param name="Units">
|
||||
/// The DataItem's MTConnect <c>units</c> attribute (e.g. <c>MILLIMETER</c>,
|
||||
/// <c>REVOLUTION/MINUTE</c>). Optional metadata. Default <c>null</c>.
|
||||
/// </param>
|
||||
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);
|
||||
@@ -0,0 +1,80 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Task 2 coverage for <see cref="MTConnectDriverOptions"/> / <see cref="MTConnectTagDefinition"/>:
|
||||
/// defaults are sane (non-zero timers, probe on) and the required-field shape (AgentUri) binds.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class MTConnectDriverOptionsTests
|
||||
{
|
||||
/// <summary>Default sample/heartbeat timers are non-zero and the probe is enabled by default.</summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>AgentUri is the only required field; DeviceName scope defaults to unset (agent-wide).</summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>RequestTimeoutMs and SampleCount default to sane, non-zero, non-wedging values.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Probe sub-options mirror ModbusProbeOptions' shape: Enabled/Interval/Timeout, both non-zero.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Reconnect sub-options default to a bounded, non-degenerate geometric backoff.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>MTConnectTagDefinition round-trips its positional fields, including MTConnect metadata.</summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user