diff --git a/docs/drivers/MTConnect.md b/docs/drivers/MTConnect.md
index 3731bbb23..81641b708 100644
--- a/docs/drivers/MTConnect.md
+++ b/docs/drivers/MTConnect.md
@@ -372,6 +372,11 @@ Degraded across further re-baselines until a chunk is delivered. Values keep flo
off the re-baselines and `LastSuccessfulRead` keeps advancing — each `/current` is the
Agent answering — so the comms-loss watchdog does not reconnect-loop a configuration
problem. Size `SampleCount` below the Agent's `bufferSize` (in the `/probe` header).
+**It is caught before it happens (#825):** the `/probe` header's `bufferSize` is parsed
+(`MTConnectProbeModel.BufferSize`, advisory — missing/malformed is null, never a parse failure); **Test
+Connect** reads `sampleCount` (default 1000 when absent) and answers Ok **with a warning** (an amber
+"OK with warning" chip showing the text) when it is at or above the buffer, and the driver's **initialize**
+(every deploy) logs the same Warning and puts `agentBufferSize` on the `/hosts` diagnostics bag.
**An Agent `instanceId` change means the Agent restarted**, and is checked
**before** the sequence-gap check (a restart also usually trips the gap, so
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.SampleLag.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.SampleLag.cs
index 210bf153e..3d6594792 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.SampleLag.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.SampleLag.cs
@@ -55,6 +55,27 @@ public sealed partial class MTConnectDriver : IDriverDiagnostics
/// Times a pump raised its count. Diagnostics only.
private long _sampleCountRaises;
+ /// The Agent's /probe bufferSize as of the last initialize, or 0 when unknown. Diagnostics only.
+ private long _agentBufferSize;
+
+ ///
+ /// lmxopcua#825 — the configuration warning for a the Agent
+ /// will refuse. An Agent answers /sample?count=N with OUT_OF_RANGE ("'count' must be less than
+ /// bufferSize") when N ≥ bufferSize, so such a driver never streams: it re-baselines from
+ /// /current and reports Degraded after three of them. Shared by
+ /// (Test Connect) and the driver's initialize (deploy), so both say the same thing.
+ ///
+ /// The configured count.
+ /// The Agent's buffer size, or null when its header did not say.
+ /// The warning text, or when the count fits (or the buffer size is unknown).
+ internal static string? SampleCountBufferWarning(int sampleCount, long? bufferSize) =>
+ bufferSize is { } size && sampleCount >= size
+ ? $"SampleCount {sampleCount.ToString(CultureInfo.InvariantCulture)} is at or above the Agent's bufferSize "
+ + $"{size.ToString(CultureInfo.InvariantCulture)}: the Agent refuses every /sample stream (OUT_OF_RANGE), so "
+ + "values will only re-baseline from /current and the driver reports Degraded. Set SampleCount below "
+ + $"{size.ToString(CultureInfo.InvariantCulture)}."
+ : null;
+
/// The ceiling a pump started with may raise its count to.
/// The configured .
/// The ceiling — never below .
@@ -77,6 +98,12 @@ public sealed partial class MTConnectDriver : IDriverDiagnostics
bag["sampleLag"] = lag.ToString(CultureInfo.InvariantCulture);
}
+ var bufferSize = Interlocked.Read(ref _agentBufferSize);
+ if (bufferSize > 0)
+ {
+ bag["agentBufferSize"] = bufferSize.ToString(CultureInfo.InvariantCulture);
+ }
+
bag["sampleSaturatedChunks"] = Interlocked.Read(ref _saturatedChunks).ToString(CultureInfo.InvariantCulture);
bag["sampleCountRaises"] = Interlocked.Read(ref _sampleCountRaises).ToString(CultureInfo.InvariantCulture);
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs
index dd6b53940..352220eb5 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs
@@ -3333,6 +3333,15 @@ public sealed partial class MTConnectDriver
// ---- commit point: everything below is non-throwing bookkeeping ----
Volatile.Write(ref _options, options);
+ // lmxopcua#825 — a SampleCount the Agent will refuse is a configuration error; say so at deploy rather than
+ // leaving it to surface as three progress-free re-baselines and a Degraded stream.
+ Interlocked.Exchange(ref _agentBufferSize, probe.BufferSize ?? 0);
+ if (SampleCountBufferWarning(options.SampleCount, probe.BufferSize) is { } bufferWarning)
+ {
+ _logger.LogWarning("MTConnect driver {DriverInstanceId} ({AgentUri}): {Warning}",
+ _driverInstanceId, AgentDisplay(options), bufferWarning);
+ }
+
// Derived from the options AND the /probe model just fetched: a raw tag whose TagConfig
// blob declares no type takes the type the Agent itself declares for that DataItem, so a
// browse-committed tag (whose blob carries only the address) publishes as the number it
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriverProbe.cs
index 0e668b404..cc402eb07 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriverProbe.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriverProbe.cs
@@ -132,8 +132,18 @@ public sealed class MTConnectDriverProbe : IDriverProbe
/// driver's tag/timing configuration — see the "reachable ≠ deployable" remarks on the type.
///
public string? DeviceName { get; set; }
+
+ ///
+ /// The /sample count (lmxopcua#825). Not needed to reach the Agent, but read so Test Connect
+ /// can warn when it is at or above the Agent's bufferSize — a config the Agent will refuse on every
+ /// stream. Absent ⇒ the driver's own default.
+ ///
+ public int? SampleCount { get; set; }
}
+ /// The driver's default SampleCount (MTConnectDriver.ParseOptions) when the config omits it.
+ private const int DefaultSampleCount = 1_000;
+
///
public async Task ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct)
{
@@ -195,10 +205,17 @@ public sealed class MTConnectDriverProbe : IDriverProbe
try
{
using var client = new MTConnectAgentClient(options, _handler);
- _ = await client.ProbeAsync(cts.Token).ConfigureAwait(false);
+ var model = await client.ProbeAsync(cts.Token).ConfigureAwait(false);
sw.Stop();
- return new(true, $"MTConnect /probe OK ({safeUri})", sw.Elapsed);
+ // Reachable but misconfigured stays Ok (the endpoint answered — Test Connect's question), with the
+ // warning in the message so the operator sees it before deploying (lmxopcua#825).
+ var warning = MTConnectDriver.SampleCountBufferWarning(dto.SampleCount ?? DefaultSampleCount, model.BufferSize);
+ return new(true,
+ warning is null
+ ? $"MTConnect /probe OK ({safeUri})"
+ : $"MTConnect /probe OK ({safeUri}) — WARNING: {warning}",
+ sw.Elapsed);
}
catch (OperationCanceledException)
{
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDtos.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDtos.cs
index d9992bf01..a7b6ef792 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDtos.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDtos.cs
@@ -8,7 +8,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect;
/// XElement.
///
/// Every device the Agent's probe response declares.
-public sealed record MTConnectProbeModel(IReadOnlyList Devices);
+///
+/// The Agent's observation ring-buffer size (the /probe Header's bufferSize), or
+/// when the header omits it or it is not a positive integer. A /sample
+/// count at or above it is refused OUT_OF_RANGE by the Agent (lmxopcua#825).
+///
+public sealed record MTConnectProbeModel(IReadOnlyList Devices, long? BufferSize = null);
///
/// Common shape shared by and :
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectProbeParser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectProbeParser.cs
index d50d66bfa..e9561a22b 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectProbeParser.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectProbeParser.cs
@@ -132,7 +132,21 @@ internal static class MTConnectProbeParser
"MTConnect /probe response declares no devices; refusing to report an empty device model as a successful probe.");
}
- return new MTConnectProbeModel(devices);
+ return new MTConnectProbeModel(devices, ReadBufferSize(root));
+ }
+
+ ///
+ /// The Header's bufferSize (lmxopcua#825). Advisory only — a missing, malformed or non-positive
+ /// value is , never a parse failure, because nothing but a configuration warning reads it.
+ ///
+ private static long? ReadBufferSize(XElement root)
+ {
+ var header = MTConnectXml.ChildrenNamed(root, "Header").FirstOrDefault();
+ var raw = header is null ? null : MTConnectXml.OptionalAttribute(header, "bufferSize");
+ return long.TryParse(raw, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out var size)
+ && size > 0
+ ? size
+ : null;
}
private static MTConnectDevice ReadDevice(XElement element) =>
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverTestConnectButton.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverTestConnectButton.razor
index 06ec04ec1..f133dae8f 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverTestConnectButton.razor
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverTestConnectButton.razor
@@ -12,7 +12,16 @@
@if (_result is not null)
{
- @if (_result.Ok)
+ @if (_result.Ok && WarningOf(_result.Message) is { } warning)
+ {
+ @* Reachable but misconfigured (e.g. MTConnect SampleCount >= the Agent's bufferSize, #825): a probe
+ that answers Ok with a "WARNING:" message is shown amber with the warning, never a plain green OK. *@
+
+ OK with warning · @_result.LatencyMs?.ToString("F0") ms
+
+ @warning
+ }
+ else if (_result.Ok)
{
OK · @_result.LatencyMs?.ToString("F0") ms
@@ -28,6 +37,16 @@
@code {
+ /// The marker a probe puts in an Ok message to flag a warning (see MTConnectDriverProbe).
+ internal const string WarningMarker = "WARNING:";
+
+ /// The warning text after , or null when the message carries none.
+ internal static string? WarningOf(string? message)
+ {
+ var at = message?.IndexOf(WarningMarker, StringComparison.Ordinal) ?? -1;
+ return at < 0 ? null : message![(at + WarningMarker.Length)..].Trim();
+ }
+
/// Driver type key — must match an installed IDriverProbe.DriverType.
[Parameter, EditorRequired] public string DriverType { get; set; } = "";
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/MTConnectAgentIntegrationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/MTConnectAgentIntegrationTests.cs
index 47a57aa6d..85639ebcd 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/MTConnectAgentIntegrationTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/MTConnectAgentIntegrationTests.cs
@@ -947,6 +947,35 @@ public sealed class MTConnectAgentIntegrationTests(MTConnectAgentFixture agent)
seen.Skip(before).ShouldContain(e => e.Snapshot.StatusCode == Good);
}
+ ///
+ /// #825 — Test Connect reads the live Agent's /probe bufferSize and warns (still Ok) when the
+ /// configured SampleCount would be refused; one below the buffer is the falsifiability control.
+ ///
+ [Fact(Timeout = TestTimeoutMs)]
+ public async Task Test_Connect_warns_when_SampleCount_reaches_the_live_agents_buffer_size()
+ {
+ SkipIfDown();
+
+ var bufferSize = (int?)agent.ProbeDocument?.Root?.Elements().FirstOrDefault(e => e.Name.LocalName == "Header")
+ ?.Attribute("bufferSize");
+ if (bufferSize is not > 1)
+ {
+ Assert.Skip($"The Agent at {agent.AgentUri} declares no usable bufferSize in its /probe header.");
+ }
+
+ var refused = await new MTConnectDriverProbe().ProbeAsync(
+ JsonSerializer.Serialize(new { agentUri = agent.AgentUri, sampleCount = bufferSize.Value }),
+ TimeSpan.FromSeconds(15), Ct);
+ refused.Ok.ShouldBeTrue(refused.Message);
+ refused.Message!.ShouldContain($"WARNING: SampleCount {bufferSize.Value} is at or above the Agent's bufferSize {bufferSize.Value}");
+
+ var fits = await new MTConnectDriverProbe().ProbeAsync(
+ JsonSerializer.Serialize(new { agentUri = agent.AgentUri, sampleCount = bufferSize.Value - 1 }),
+ TimeSpan.FromSeconds(15), Ct);
+ fits.Ok.ShouldBeTrue(fits.Message);
+ fits.Message!.ShouldNotContain("WARNING");
+ }
+
private static bool Is(string? value, string expected) =>
string.Equals(value?.Trim(), expected, StringComparison.OrdinalIgnoreCase);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectBufferSizeWarningTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectBufferSizeWarningTests.cs
new file mode 100644
index 000000000..1f6c72e67
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectBufferSizeWarningTests.cs
@@ -0,0 +1,128 @@
+using System.Net;
+using System.Net.Http;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests;
+
+///
+/// lmxopcua#825 — a SampleCount at or above the Agent's bufferSize is refused OUT_OF_RANGE on
+/// every /sample stream. The /probe header carries bufferSize, so Test Connect and the
+/// driver's initialize (deploy) both warn instead of leaving it to surface as a Degraded stream.
+///
+[Trait("Category", "Unit")]
+public sealed class MTConnectBufferSizeWarningTests
+{
+ private static string ProbeXml(string? bufferSizeAttribute) =>
+ $"""
+
+
+
+ """;
+
+ [Theory]
+ [InlineData("16384", 16384L)]
+ [InlineData(null, null)]
+ [InlineData("0", null)]
+ [InlineData("-5", null)]
+ [InlineData("lots", null)]
+ public void The_probe_parser_reads_the_header_buffer_size_and_never_fails_on_it(string? attribute, long? expected) =>
+ MTConnectProbeParser.Parse(ProbeXml(attribute)).BufferSize.ShouldBe(expected);
+
+ [Theory]
+ [InlineData(16383, 16384L, false)]
+ [InlineData(16384, 16384L, true)]
+ [InlineData(20000, 16384L, true)]
+ [InlineData(1_000_000, null, false)]
+ public void The_warning_fires_exactly_at_and_above_the_buffer_size(int sampleCount, long? bufferSize, bool warns)
+ {
+ var warning = MTConnectDriver.SampleCountBufferWarning(sampleCount, bufferSize);
+ (warning is not null).ShouldBe(warns);
+ if (warns) warning!.ShouldContain($"below {bufferSize}");
+ }
+
+ [Fact]
+ public async Task Test_Connect_stays_ok_but_carries_the_warning_when_SampleCount_does_not_fit()
+ {
+ var probe = new MTConnectDriverProbe(new StubHandler(ProbeXml("16384")));
+ var r = await probe.ProbeAsync(
+ "{\"agentUri\":\"http://stub-agent:5000\",\"sampleCount\":16384}", TimeSpan.FromSeconds(3),
+ TestContext.Current.CancellationToken);
+
+ r.Ok.ShouldBeTrue("the Agent answered — reachability is not the problem");
+ r.Message!.ShouldContain("WARNING:");
+ r.Message!.ShouldContain("bufferSize 16384");
+ }
+
+ [Fact]
+ public async Task Test_Connect_checks_the_driver_default_when_SampleCount_is_absent()
+ {
+ // The default SampleCount is 1000 — an Agent with a tiny buffer refuses it too.
+ var probe = new MTConnectDriverProbe(new StubHandler(ProbeXml("512")));
+ var r = await probe.ProbeAsync(
+ "{\"agentUri\":\"http://stub-agent:5000\"}", TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
+
+ r.Ok.ShouldBeTrue();
+ r.Message!.ShouldContain("SampleCount 1000 is at or above the Agent's bufferSize 512");
+ }
+
+ [Fact]
+ public async Task Test_Connect_message_is_unchanged_when_SampleCount_fits()
+ {
+ var probe = new MTConnectDriverProbe(new StubHandler(ProbeXml("16384")));
+ var r = await probe.ProbeAsync(
+ "{\"agentUri\":\"http://stub-agent:5000\",\"sampleCount\":1000}", TimeSpan.FromSeconds(3),
+ TestContext.Current.CancellationToken);
+
+ r.Ok.ShouldBeTrue();
+ r.Message!.ShouldNotContain("WARNING");
+ }
+
+ [Theory]
+ [InlineData(131072, true)]
+ [InlineData(1000, false)]
+ public async Task Initialize_warns_and_surfaces_the_buffer_size_on_the_diagnostics_bag(int sampleCount, bool warns)
+ {
+ var client = CannedAgentClient.FromFixtures(); // Fixtures/probe.xml: bufferSize="131072"
+ client.Probe.BufferSize.ShouldBe(131072L);
+ var logger = new RecordingDriverLogger();
+ var driver = new MTConnectDriver(
+ new MTConnectDriverOptions
+ {
+ AgentUri = "http://fixture-agent:5000",
+ RequestTimeoutMs = 5000,
+ SampleCount = sampleCount,
+ Tags = [new MTConnectTagDefinition("dev1_pos", DriverDataType.Float64)],
+ },
+ "mt-825",
+ _ => client,
+ logger);
+ try
+ {
+ await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
+
+ logger.Warnings.Any(w => w.Contains("bufferSize", StringComparison.Ordinal)).ShouldBe(warns);
+ driver.GetDiagnostics()["agentBufferSize"].ShouldBe("131072");
+ }
+ finally
+ {
+ await driver.ShutdownAsync(TestContext.Current.CancellationToken);
+ }
+ }
+
+ private sealed class StubHandler(string probeXml) : HttpMessageHandler
+ {
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken ct) =>
+ Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(probeXml, System.Text.Encoding.UTF8, "application/xml"),
+ });
+ }
+}