21b4f00f46
Subscription throughput was hard-capped at SampleCount / SampleIntervalMs with no lag detection: a machine producing more fell behind the ring buffer forever, re-baselining every buffer-length with every value backlog-stale. - The parser carries the header's optional lastSequence; each chunk measures lag = lastSequence + 1 - nextSequence and whether it was saturated (full count). - Three consecutive saturated chunks with a positive (or unknown) lag reopen the stream at the cursor reached with count doubled, up to 16x SampleCount (max 65536), through a new IMTConnectAgentClient.SampleAsync(from, count, ct) overload (default implementation ignores count). A raised stream that delivers nothing steps back to the last good count; at the ceiling one Warning per pump. - MTConnectDriver implements IDriverDiagnostics: sampleCount, sampleLag, sampleSaturatedChunks, sampleCountRaises on the /hosts bag. The cap is documented on SampleCount. Refs #743 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
168 lines
9.5 KiB
C#
168 lines
9.5 KiB
C#
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||
|
||
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.
|
||
/// <para>
|
||
/// <b>It caps subscription throughput</b> at <c>SampleCount / SampleIntervalMs</c> observations per
|
||
/// millisecond (default 1000 a second): a machine producing more falls behind the Agent's ring buffer. The
|
||
/// pump measures that lag and, when it is persistently behind, reopens the stream with the count doubled — up
|
||
/// to 16× this value, at most 65 536 — and reports the effective count and the lag on the <c>/hosts</c>
|
||
/// diagnostics (review 2026-10-21 T-5). Each new stream starts again from this value.
|
||
/// </para>
|
||
/// </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>
|
||
/// The authored tags this driver instance serves — one <see cref="MTConnectTagDefinition"/>
|
||
/// per tag, keyed by the DataItem <c>id</c> it binds
|
||
/// (<see cref="MTConnectTagDefinition.FullName"/>). The factory config DTO carries these and
|
||
/// builds them into the options; the driver indexes them to know each tag's target
|
||
/// <see cref="MTConnectTagDefinition.DriverDataType"/> when coercing an Agent observation
|
||
/// (whose wire form is always text) into a published value.
|
||
/// <para>
|
||
/// Defaults to <b>empty, never <c>null</c></b>: a driver instance may legitimately be
|
||
/// authored with no tags yet, and a null collection here would surface as a
|
||
/// NullReferenceException at deploy time from operator-authored config.
|
||
/// </para>
|
||
/// </summary>
|
||
public IReadOnlyList<MTConnectTagDefinition> Tags { get; init; } = [];
|
||
|
||
/// <summary>
|
||
/// <b>The v3 data-plane binding: the authored raw tags the deploy artifact delivers.</b> Each
|
||
/// <see cref="RawTagEntry"/> pairs the tag's <b>RawPath</b> — the driver's wire reference for
|
||
/// read / subscribe / publish, and the key <c>DriverHostActor</c> routes published values by —
|
||
/// with the driver-specific <c>TagConfig</c> blob naming the Agent DataItem <c>id</c> it binds.
|
||
/// Injected into every driver's merged config by <c>DriverDeviceConfigMerger</c>, so this is the
|
||
/// collection a deployed instance actually serves from.
|
||
/// <para>
|
||
/// <b>Relationship to <see cref="Tags"/>.</b> The driver builds ONE observation index and
|
||
/// ONE <c>RawPath → dataItemId</c> table from both collections:
|
||
/// <list type="bullet">
|
||
/// <item>A tag in <c>RawTags</c> is reachable by its RawPath (production). Its coercion
|
||
/// type comes from the blob's <c>driverDataType</c>; failing that, from a
|
||
/// <see cref="Tags"/> entry naming the same DataItem id; failing that, from the Agent's
|
||
/// own <c>/probe</c> declaration (<see cref="MTConnectDataTypeInference"/>); failing
|
||
/// that, <see cref="DriverDataType.String"/> — the coercion that cannot fail.
|
||
/// The <c>/probe</c> step is not reached by a browse-committed tag — the AdminUI's
|
||
/// commit mapper writes a full typed blob, so the first step always answers — but
|
||
/// the type it writes IS the browsed leaf's inference, so the answer
|
||
/// is the same one. It previously wrote the model's <c>String</c> default and silently
|
||
/// mistyped every browsed tag (<c>code-reviews/Driver.MTConnect/findings.md</c>).
|
||
/// The step still serves a hand-authored / CSV / API blob that
|
||
/// states no type.</item>
|
||
/// <item>A tag in <see cref="Tags"/> only is reachable by its DataItem <c>id</c>
|
||
/// (the driver CLI's authoring surface, and every pre-v3 test) and still contributes
|
||
/// its coercion type to the index.</item>
|
||
/// </list>
|
||
/// Both default to empty, so neither collection is required and a driver instance
|
||
/// authored with no tags yet starts cleanly.
|
||
/// </para>
|
||
/// </summary>
|
||
public IReadOnlyList<RawTagEntry> RawTags { get; init; } = [];
|
||
|
||
/// <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>
|
||
/// Write-back settings. Off by default — the driver shipped read-only, and enabling this is
|
||
/// only half the gate: the Agent's own <c>AllowPut</c> must be on too.
|
||
/// </summary>
|
||
public MTConnectWriteOptions Write { 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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Write-back knobs. A write is <b>observation injection</b> via the Agent's <c>AllowPut</c>
|
||
/// surface — it sets what the Agent reports for a DataItem, not the machine itself (design §2.2).
|
||
/// </summary>
|
||
public sealed class MTConnectWriteOptions
|
||
{
|
||
/// <summary>
|
||
/// Gets a value indicating whether write-back is enabled. Default <c>false</c> — an existing
|
||
/// deployment that never authors this block keeps today's read-only behaviour exactly.
|
||
/// </summary>
|
||
public bool Enabled { get; init; }
|
||
}
|