diff --git a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs
index 8055f80f..acce7a3c 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs
@@ -332,6 +332,19 @@ public sealed class ScriptCompileSurface
/// The maximum time to wait for the response.
/// true if the response value was observed before the timeout elapsed; otherwise false.
public Task WriteBatchAndWaitAsync(IReadOnlyDictionary values, string flagKey, object? flagValue, string responseKey, object? responseValue, TimeSpan timeout) => throw new NotSupportedException(CompileOnly);
+
+ /// Mirrors the reset-aware AttributeAccessor.WriteBatchAndWaitAsync overload (Gitea #36).
+ /// The set of attribute values to write as a batch.
+ /// The name of the flag attribute signaling the write.
+ /// The value written to the flag attribute.
+ /// The name of the attribute to observe for a response.
+ /// The value to wait for on the response attribute.
+ /// The maximum time to wait for the response.
+ /// The attribute written first and waited on before the batch.
+ /// The reset value the device must echo before the batch is written.
+ /// The maximum time to wait for the reset echo; null means .
+ /// The phase-aware handshake outcome.
+ public Task WriteBatchAndWaitAsync(IReadOnlyDictionary values, string flagKey, object? flagValue, string responseKey, object? responseValue, TimeSpan timeout, string resetKey, object? resetValue, TimeSpan? resetTimeout = null) => throw new NotSupportedException(CompileOnly);
}
/// Compile-only mirror of ChildrenAccessor.
diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs
index da523a5f..c581a5ec 100644
--- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs
@@ -111,6 +111,47 @@ public class AttributeAccessor
timeout);
}
+ ///
+ /// Reset-aware handshake (Gitea #36): writes =,
+ /// waits (event-driven, bounded by , default = )
+ /// for the device to ECHO that reset, and only then performs the ordinary
+ ///
+ /// legs (batch → trigger → response wait). Returns a so a
+ /// script can tell a reset-echo timeout (; nothing
+ /// else was written) from a completion timeout. Use this for every level-triggered
+ /// flag handshake: without an observed reset the plain overload can be satisfied
+ /// instantly by the previous run's cached response value. Throws
+ /// if the reset write or the batch/trigger
+ /// write itself fails; never throws on either timeout.
+ ///
+ /// Attribute key → value to batch-write (data-sourced, one connection).
+ /// Trigger attribute key written AFTER the batch.
+ /// Value to write to the trigger.
+ /// Attribute key to wait on after the trigger.
+ /// Target value to wait for (null ⇒ any change).
+ /// How long to wait for the response before giving up.
+ /// Attribute written FIRST (usually ); static or data-sourced.
+ /// Value the device must echo back before the batch is written.
+ /// How long to wait for the reset echo; null ⇒ .
+ /// The phase-aware handshake outcome.
+ public Task WriteBatchAndWaitAsync(
+ IReadOnlyDictionary values, string flagKey, object? flagValue,
+ string responseKey, object? responseValue, TimeSpan timeout,
+ string resetKey, object? resetValue, TimeSpan? resetTimeout = null)
+ {
+ var encoded = new Dictionary(values.Count);
+ foreach (var kv in values)
+ encoded[Resolve(kv.Key)] = AttributeValueCodec.Encode(kv.Value);
+
+ return _ctx.WriteBatchResetAndWait(
+ encoded,
+ Resolve(flagKey), AttributeValueCodec.Encode(flagValue),
+ Resolve(responseKey), AttributeValueCodec.Encode(responseValue),
+ timeout,
+ Resolve(resetKey), AttributeValueCodec.Encode(resetValue),
+ resetTimeout ?? timeout);
+ }
+
///
/// WaitForAttribute (spec §3-§5): waits event-driven until the attribute equals
/// (value-equality, codec-normalized), bounded by
diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScopeAccessorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScopeAccessorTests.cs
index 6d885f2b..03b9846d 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScopeAccessorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScopeAccessorTests.cs
@@ -294,4 +294,51 @@ public class AttributeAccessorWaitAsyncTests : TestKit, IDisposable
Assert.True(result.TimedOut);
Assert.Null(result.Value);
}
+
+ // ── Reset-aware WriteBatchAndWaitAsync overload (Gitea #36) ──────────────
+
+ ///
+ /// The reset-aware overload must scope-resolve and codec-encode the reset key/value
+ /// exactly like every other accessor, must default resetTimeout to
+ /// timeout, and — when the reset echo never arrives — must return a
+ /// result WITHOUT writing the batch or
+ /// the trigger (the ExpectNoMsg at the end is that assertion).
+ ///
+ [Fact]
+ public async Task WriteBatchAndWaitAsync_ResetAware_ResolvesScopeAndDefaultsResetTimeout()
+ {
+ var probe = CreateTestProbe();
+ var ctx = MakeContext(probe.Ref);
+
+ // Composed scope "RightMESReceiver" — every key is prefixed before it leaves the accessor.
+ var acc = new AttributeAccessor(ctx, "RightMESReceiver");
+
+ var call = acc.WriteBatchAndWaitAsync(
+ new Dictionary { ["MoveOutBatchID"] = 28950 },
+ "MoveOutFlag", true, "MoveOutCompleteFlag", true, TimeSpan.FromSeconds(25),
+ "MoveOutCompleteFlag", false);
+
+ // Leg 1 — the reset write.
+ var reset = probe.ExpectMsg(TimeSpan.FromSeconds(5));
+ Assert.Equal("RightMESReceiver.MoveOutCompleteFlag", reset.AttributeName);
+ Assert.Equal(AttributeValueCodec.Encode(false), reset.Value);
+ probe.Reply(new SetStaticAttributeResponse(
+ reset.CorrelationId, "Pump1", reset.AttributeName, true, null, DateTimeOffset.UtcNow));
+
+ // Leg 2 — the reset echo wait, bounded by the defaulted resetTimeout.
+ var resetWait = probe.ExpectMsg(TimeSpan.FromSeconds(5));
+ Assert.Equal("RightMESReceiver.MoveOutCompleteFlag", resetWait.AttributeName);
+ Assert.Equal(AttributeValueCodec.Encode(false), resetWait.TargetValueEncoded);
+ Assert.Equal(TimeSpan.FromSeconds(25), resetWait.Timeout); // defaulted to `timeout`
+ probe.Reply(new WaitForAttributeResponse(
+ resetWait.CorrelationId, Matched: false, Value: null, Quality: null, TimedOut: true));
+
+ var result = await call;
+ Assert.True(result.ResetFailed);
+ Assert.False(result.Matched);
+ Assert.True(result.TimedOut);
+
+ // Nothing else was written: no batch, no trigger.
+ probe.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
+ }
}