29ca6b15ab
Claude-Session: https://claude.ai/code/session_01XPsuy2ZZ5Gbyfpt8PrK5BV
94 lines
6.1 KiB
Bash
Executable File
94 lines
6.1 KiB
Bash
Executable File
#!/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 (String) <- ns=3;s=TestChildObject.TestString writable, NOT simulated (holds what is written)
|
|
# * Complete (String) <- ns=3;s=JoeAppEngine.BTCS 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)
|
|
#
|
|
# WHY STRING NODES, NOT THE Boolean/Double ones (Motor.Running, Pump.Running, Motor.Speed):
|
|
# a data-sourced SCALAR write leaves the InstanceActor as its codec-encoded STRING ("True",
|
|
# "42.5") and RealOpcUaClient wraps it in a plain Variant, so opc-plc answers BadTypeMismatch
|
|
# (0x80740000) for every non-String node — observed live 2026-09-10 (deferred-work register
|
|
# row 120). Only List values are decoded before the write. The two String nodes above are
|
|
# the rig's only writable String scalars, so the flags are the strings "True"/"False";
|
|
# the reset-echo ordering this gate proves is identical either way.
|
|
# - Instance handshake-demo-1, bound + deployed
|
|
#
|
|
# Not idempotent (names must be unique). To re-seed:
|
|
# scadabridge instance delete --id <id> && scadabridge template delete --id <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 String --data-source "ns=3;s=TestChildObject.TestString" --description "Handshake trigger flag as a string (writable String node, not simulated; see header)" > /dev/null
|
|
"$CLI" template attribute add "${A[@]}" --template-id "$TEMPLATE_ID" --name Complete --data-type String --data-source "ns=3;s=JoeAppEngine.BTCS" --description "Handshake completion flag as a string (writable String node, not simulated; see header)" > /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.
|
|
// String flags because the rig's OPC UA sim rejects non-String scalar writes (register row 120);
|
|
// the batch carries no payload values — the trigger alone rides the batch leg.
|
|
var values = new Dictionary<string, object?>();
|
|
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]]" > /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."
|