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();
+ }
}