#!/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, System.TimeSpan.FromSeconds(20), "Complete", false, System.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", System.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."