From b5e39e1f328ad86ec33146885e157b4069669e90 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 5 Aug 2026 06:13:30 -0400 Subject: [PATCH] feat(mtconnect): parse the write.enabled config block --- .../MTConnectDriverOptions.cs | 19 ++++++++ .../MTConnectDriver.cs | 14 ++++++ .../MTConnectDriverOptionsTests.cs | 14 ++++++ .../MTConnectFactoryTests.cs | 47 +++++++++++++++++++ 4 files changed, 94 insertions(+) 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 index da548efa..6c601e76 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs @@ -109,6 +109,12 @@ public sealed class MTConnectDriverOptions /// /sample streaming connection. /// public MTConnectReconnectOptions Reconnect { get; init; } = new(); + + /// + /// Write-back settings. Off by default — the driver shipped read-only, and enabling this is + /// only half the gate: the Agent's own AllowPut must be on too. + /// + public MTConnectWriteOptions Write { get; init; } = new(); } /// Background connectivity-probe knobs, mirroring ModbusProbeOptions. @@ -132,3 +138,16 @@ public sealed class MTConnectReconnectOptions /// Multiplier applied each retry. Default 2.0 doubles each step. public double BackoffMultiplier { get; init; } = 2.0; } + +/// +/// Write-back knobs. A write is observation injection via the Agent's AllowPut +/// surface — it sets what the Agent reports for a DataItem, not the machine itself (design §2.2). +/// +public sealed class MTConnectWriteOptions +{ + /// + /// Gets a value indicating whether write-back is enabled. Default false — an existing + /// deployment that never authors this block keeps today's read-only behaviour exactly. + /// + public bool Enabled { get; init; } +} 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 e811179c..53e41b9d 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs @@ -2276,6 +2276,13 @@ public sealed class MTConnectDriver : IDriver, IReadable, ISubscribable, ITagDis MaxBackoffMs = dto.Reconnect?.MaxBackoffMs ?? 30_000, BackoffMultiplier = dto.Reconnect?.BackoffMultiplier ?? 2.0, }, + + // Absent block == disabled: write-back is opt-in, and the agent's own AllowPut has to be + // on as well, so neither layer can arm the other by omission. + Write = new MTConnectWriteOptions + { + Enabled = dto.Write?.Enabled ?? false, + }, }; } @@ -3272,6 +3279,7 @@ public sealed class MTConnectDriver : IDriver, IReadable, ISubscribable, ITagDis public List? RawTags { get; init; } public MTConnectProbeDto? Probe { get; init; } public MTConnectReconnectDto? Reconnect { get; init; } + public MTConnectWriteDto? Write { get; init; } } /// @@ -3347,4 +3355,10 @@ public sealed class MTConnectDriver : IDriver, IReadable, ISubscribable, ITagDis public int? MaxBackoffMs { get; init; } public double? BackoffMultiplier { get; init; } } + + /// Write-back knobs — the driver-side half of the two-layer opt-in on observation injection. + private sealed class MTConnectWriteDto + { + public bool? Enabled { get; init; } + } } 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 index 91d1c4ca..aa2d989f 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectDriverOptionsTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectDriverOptionsTests.cs @@ -56,6 +56,20 @@ public sealed class MTConnectDriverOptionsTests reconnect.BackoffMultiplier.ShouldBeGreaterThan(1.0); } + /// + /// Write-back is off unless authored: the driver shipped read-only, and the agent's + /// PUT surface is itself off by default, so a deployment that never mentions write + /// must keep exactly today's behaviour. + /// + [Fact] + public void Write_defaults_to_disabled() + { + var write = new MTConnectDriverOptions { AgentUri = "http://agent:5000" }.Write; + + write.ShouldNotBeNull(); + write.Enabled.ShouldBeFalse(); + } + /// /// defaults to an empty collection, never /// null — a null collection here is an operator-authorable NullReferenceException at diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectFactoryTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectFactoryTests.cs index 49dea03c..6184bc5f 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectFactoryTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectFactoryTests.cs @@ -349,4 +349,51 @@ public sealed class MTConnectFactoryTests // above) hold for both entry points. MTConnectDriverFactoryExtensions.CreateInstance("mt1", json).DriverInstanceId.ShouldBe("mt1"); } + + /// + /// The write block reaches the options — the opt-in half of the two-layer gate on + /// write-back (the agent's own AllowPut is the other). Authored true arms it; + /// an authored false and an omitted block are the same thing, and both leave the + /// driver read-only. + /// + [Fact] + public void Write_section_round_trips_and_defaults_to_disabled_when_absent() + { + MTConnectDriver.ParseOptions( + "mt1", """{ "agentUri": "http://x:5000", "write": { "enabled": true } }""") + .Write.Enabled.ShouldBeTrue(); + + MTConnectDriver.ParseOptions( + "mt1", """{ "agentUri": "http://x:5000", "write": { "enabled": false } }""") + .Write.Enabled.ShouldBeFalse(); + + MTConnectDriver.ParseOptions("mt1", """{ "agentUri": "http://x:5000" }""") + .Write.Enabled.ShouldBeFalse(); + + // The factory shares that parse, so an armed instance is authorable through it too. + MTConnectDriverFactoryExtensions.CreateInstance( + "mt1", """{ "agentUri": "http://x:5000", "write": { "enabled": true } }""") + .DriverInstanceId.ShouldBe("mt1"); + } + + /// + /// A malformed write block fails exactly as its siblings do — the whole + /// document is one JsonSerializer.Deserialize, so a non-boolean enabled faults + /// the parse rather than silently defaulting to off. The probe leg is the control: it + /// pins that this is the surrounding convention and not stricter behaviour invented here. + /// + [Fact] + public void Malformed_write_block_faults_exactly_like_the_sibling_probe_block() + { + Should.Throw(() => MTConnectDriver.ParseOptions( + "mt1", """{ "agentUri": "http://x:5000", "probe": { "enabled": "yes" } }""")); + + Should.Throw(() => MTConnectDriver.ParseOptions( + "mt1", """{ "agentUri": "http://x:5000", "write": { "enabled": "yes" } }""")); + + // A null block is not malformed — the same "omitted means default" reading the sibling + // blocks get, since every DTO property is nullable. + MTConnectDriver.ParseOptions("mt1", """{ "agentUri": "http://x:5000", "write": null }""") + .Write.Enabled.ShouldBeFalse(); + } }