From 465a9d711ab7aa143edefcfd56ee415810bd7f68 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 10 Sep 2026 15:28:00 -0400 Subject: [PATCH] =?UTF-8?q?docker:=20seed-handshake-demo.sh=20=E2=80=94=20?= =?UTF-8?q?live=20gate=20for=20the=20reset-aware=20handshake=20over=20real?= =?UTF-8?q?=20OPC=20UA=20flags=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01XPsuy2ZZ5Gbyfpt8PrK5BV --- docker/seed-handshake-demo.sh | 85 +++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100755 docker/seed-handshake-demo.sh diff --git a/docker/seed-handshake-demo.sh b/docker/seed-handshake-demo.sh new file mode 100755 index 00000000..de9ab35a --- /dev/null +++ b/docker/seed-handshake-demo.sh @@ -0,0 +1,85 @@ +#!/bin/bash +set -euo pipefail + +# Seed a DATA-SOURCED flag handshake on site-a to prove the reset-aware +# Attributes.WriteBatchAndWaitAsync overload (Gitea #36) against a real OPC UA +# subscription — the reset echo must round-trip through the device before the +# trigger is written, and a SECOND run must not fast-path on the first run's +# cached Complete=true. +# +# What it creates (site-a / site id 1, connection id 1 "OPC PLC Simulator"): +# - Template HandshakeDemo +# * Trigger (Boolean) <- ns=3;s=Motor.Running writable, NOT simulated (holds what is written) +# * Complete (Boolean) <- ns=3;s=Pump.Running writable, NOT simulated +# * Payload (Double) <- ns=3;s=Motor.Speed writable, NOT simulated +# * Handshake (Call) — the reset-aware overload; returns the HandshakeResult fields +# * Device (ValueChange on Trigger) — plays the PLC: on Trigger==true sets Complete=true +# and clears Trigger (so the next run's trigger is a fresh edge) +# - Instance handshake-demo-1, bound + deployed +# +# Not idempotent (names must be unique). To re-seed: +# scadabridge instance delete --id && scadabridge template delete --id +# Prerequisites: infra (scadabridge-opcua) up, docker/deploy.sh, docker/seed-sites.sh. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +CLI="$PROJECT_ROOT/src/ZB.MOM.WW.ScadaBridge.CLI/bin/Debug/net10.0/scadabridge" +A=(--url "${SCADABRIDGE_URL:-http://localhost:9000}" --format=json --username "${SCADABRIDGE_USERNAME:-multi-role}" --password "${SCADABRIDGE_PASSWORD:-password}") +SITE_ID="${SITE_ID:-1}" +CONNECTION_ID="${CONNECTION_ID:-1}" +INSTANCE_NAME="${INSTANCE_NAME:-handshake-demo-1}" + +json_id() { python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])'; } + +echo "1/5 Creating template HandshakeDemo..." +TEMPLATE_ID=$("$CLI" template create "${A[@]}" --name HandshakeDemo \ + --description "Gitea #36 live gate: reset-aware WriteBatchAndWaitAsync over real OPC UA flags" | json_id) +echo " id=$TEMPLATE_ID" + +echo "2/5 Adding data-sourced attributes..." +"$CLI" template attribute add "${A[@]}" --template-id "$TEMPLATE_ID" --name Trigger --data-type Boolean --data-source "ns=3;s=Motor.Running" --description "Handshake trigger (writable, not simulated)" > /dev/null +"$CLI" template attribute add "${A[@]}" --template-id "$TEMPLATE_ID" --name Complete --data-type Boolean --data-source "ns=3;s=Pump.Running" --description "Handshake completion flag (writable, not simulated)" > /dev/null +"$CLI" template attribute add "${A[@]}" --template-id "$TEMPLATE_ID" --name Payload --data-type Double --data-source "ns=3;s=Motor.Speed" --description "Batch payload" > /dev/null + +echo "3/5 Adding the Handshake (Call) script — reset-aware overload..." +read -r -d '' HANDSHAKE_CODE <<'CS' || true +// Gitea #36: reset Complete=false, WAIT for the device to echo it, then batch+trigger, then wait +// for Complete=true. Run this twice: the second run is the one the plain overload got wrong. +var values = new Dictionary { ["Payload"] = 42.5 }; +var hs = await Attributes.WriteBatchAndWaitAsync( + values, "Trigger", true, "Complete", true, TimeSpan.FromSeconds(20), + "Complete", false, TimeSpan.FromSeconds(10)); +Log.Info($"handshake matched={hs.Matched} phase={hs.Phase} timedOut={hs.TimedOut} resetFailed={hs.ResetFailed}"); +return new { Matched = hs.Matched, Phase = hs.Phase.ToString(), TimedOut = hs.TimedOut, ResetFailed = hs.ResetFailed, Error = hs.ErrorMessage ?? "" }; +CS +"$CLI" template script add "${A[@]}" --template-id "$TEMPLATE_ID" --name Handshake --trigger-type Call \ + --return-def '{"type":"object","properties":{"Matched":{"type":"boolean"},"Phase":{"type":"string"},"TimedOut":{"type":"boolean"},"ResetFailed":{"type":"boolean"},"Error":{"type":"string"}},"required":["Matched","Phase"]}' \ + --code "$HANDSHAKE_CODE" > /dev/null + +echo "4/5 Adding the Device (ValueChange on Trigger) script — plays the PLC..." +read -r -d '' DEVICE_CODE <<'CS' || true +// Plays the device: on the rising edge of Trigger, complete the request and clear the trigger +// so the NEXT run's Trigger=true is a fresh change on the subscription. Its own Trigger=false +// write re-triggers this script, which returns at the guard. +var t = await Attributes.GetAsync("Trigger"); +var ts = t?.ToString(); +if (!((t is bool b && b) || ts == "1" || string.Equals(ts, "true", StringComparison.OrdinalIgnoreCase))) return null; +await Attributes.SetAsync("Complete", true); +await Attributes.SetAsync("Trigger", false); +return null; +CS +"$CLI" template script add "${A[@]}" --template-id "$TEMPLATE_ID" --name Device --trigger-type ValueChange \ + --trigger-config '{"attributeName":"Trigger"}' --code "$DEVICE_CODE" > /dev/null + +echo "5/5 Creating, binding and deploying instance $INSTANCE_NAME..." +INSTANCE_ID=$("$CLI" instance create "${A[@]}" --name "$INSTANCE_NAME" --template-id "$TEMPLATE_ID" --site-id "$SITE_ID" | json_id) +"$CLI" instance set-bindings "${A[@]}" --id "$INSTANCE_ID" \ + --bindings "[[\"Trigger\",$CONNECTION_ID],[\"Complete\",$CONNECTION_ID],[\"Payload\",$CONNECTION_ID]]" > /dev/null +"$CLI" instance deploy "${A[@]}" --id "$INSTANCE_ID" > /dev/null +echo " instance id=$INSTANCE_ID" + +echo "" +echo "=== Done. Live gate (run it TWICE — the second run is the #36 case): ===" +echo " $CLI flow-debug run-script --instance-id $INSTANCE_ID --script Handshake --show-logs ${A[*]}" +echo "Expect on BOTH runs: Matched=true, Phase=Response; the trace shows Complete write ->" +echo "Complete wait (reset echo, >0 ms on the 2nd run) -> batch+Trigger write -> Complete wait."