From e5c536c6b73f1adfa9377d2aee1489ac158ddea9 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 10 Sep 2026 15:18:10 -0400 Subject: [PATCH] feat(commons): HandshakePhase + HandshakeResult for the reset-aware handshake (#36) Claude-Session: https://claude.ai/code/session_01XPsuy2ZZ5Gbyfpt8PrK5BV --- .../2026-09-10-handshake-reset-echo-design.md | 4 ++ .../Types/HandshakeResult.cs | 47 +++++++++++++++++++ .../Types/HandshakeResultTests.cs | 23 +++++++++ 3 files changed, 74 insertions(+) create mode 100644 src/ZB.MOM.WW.ScadaBridge.Commons/Types/HandshakeResult.cs create mode 100644 tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/HandshakeResultTests.cs diff --git a/docs/plans/2026-09-10-handshake-reset-echo-design.md b/docs/plans/2026-09-10-handshake-reset-echo-design.md index 4d008f04..3ccff0f0 100644 --- a/docs/plans/2026-09-10-handshake-reset-echo-design.md +++ b/docs/plans/2026-09-10-handshake-reset-echo-design.md @@ -80,6 +80,10 @@ public readonly record struct HandshakeResult( HandshakePhase Phase, string? ErrorMessage); ``` +`ResetFailed` (`!Matched && Phase == Reset`) is a computed convenience: script bodies import only +`System`, `System.Collections.Generic`, `System.Linq` and `System.Threading.Tasks`, so a script +branching on the enum would have to spell `ZB.MOM.WW.ScadaBridge.Commons.Types.HandshakePhase`. + Semantics of the new overload, in execution order: 1. **Reset write** — `resetKey = resetValue` through the existing `SetAttribute` path (static or diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/HandshakeResult.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/HandshakeResult.cs new file mode 100644 index 00000000..1698b389 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/HandshakeResult.cs @@ -0,0 +1,47 @@ +namespace ZB.MOM.WW.ScadaBridge.Commons.Types; + +/// +/// Which leg of a reset-aware Attributes.WriteBatchAndWaitAsync handshake a +/// describes (Gitea #36, design 2026-09-10 §4). +/// +public enum HandshakePhase +{ + /// The reset write + reset-echo wait that precedes the batch and trigger. + Reset, + + /// The batch + trigger write and the response wait that follow an observed reset echo. + Response, +} + +/// +/// Result of the reset-aware Attributes.WriteBatchAndWaitAsync overload (Gitea #36): +/// like , plus the the outcome belongs to so a +/// script can report "timed out waiting for the reset echo" separately from "timed out +/// waiting for the device to complete". A reset that is never echoed means the batch and +/// trigger were NEVER written. +/// +/// +/// when the response attribute reached its target within the timeout +/// (which implies the reset echo was observed first). +/// +/// The matched value of the phase's attribute; on timeout / error. +/// The attribute quality at match time; on the non-match paths. +/// when the phase's wait timed out before a match. +/// The leg this result describes — only when that leg failed. +/// Why the phase did not match, when known; on success. +public readonly record struct HandshakeResult( + bool Matched, + object? Value, + string? Quality, + bool TimedOut, + HandshakePhase Phase, + string? ErrorMessage) +{ + /// + /// when the reset leg did not match (timeout or error), i.e. the + /// device never echoed the reset value and nothing else was written. Provided so a + /// script can branch without spelling the type name, which + /// is not in the script imports. + /// + public bool ResetFailed => !Matched && Phase == HandshakePhase.Reset; +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/HandshakeResultTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/HandshakeResultTests.cs new file mode 100644 index 00000000..c6427e13 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/HandshakeResultTests.cs @@ -0,0 +1,23 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Types; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types; + +/// +/// Gitea #36: lets a script tell WHICH leg of a reset-aware +/// handshake failed without naming the enum (script bodies +/// import only System/System.Collections.Generic/System.Linq/System.Threading.Tasks). +/// +public class HandshakeResultTests +{ + [Fact] + public void ResetFailed_IsTrue_OnlyForAnUnmatchedResetPhase() + { + var resetTimeout = new HandshakeResult(false, null, null, true, HandshakePhase.Reset, "reset timed out"); + var responseTimeout = new HandshakeResult(false, null, null, true, HandshakePhase.Response, "response timed out"); + var matched = new HandshakeResult(true, true, "Good", false, HandshakePhase.Response, null); + + Assert.True(resetTimeout.ResetFailed); + Assert.False(responseTimeout.ResetFailed); + Assert.False(matched.ResetFailed); + } +}