docs(#36): rig live gate PASS (two runs, reset echo held 751 ms on run 2); seed retargeted to String nodes; register row 120 — OPC UA scalar writes go out as String variants

Claude-Session: https://claude.ai/code/session_01XPsuy2ZZ5Gbyfpt8PrK5BV
This commit is contained in:
Joseph Doherty
2026-09-10 15:53:51 -04:00
parent 3595f00dad
commit 29ca6b15ab
2 changed files with 24 additions and 15 deletions
+23 -15
View File
@@ -9,12 +9,19 @@ set -euo pipefail
#
# 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
# * 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
# * 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:
@@ -37,18 +44,19 @@ TEMPLATE_ID=$("$CLI" template create "${A[@]}" --name HandshakeDemo \
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
"$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.
var values = new Dictionary<string, object?> { ["Payload"] = 42.5 };
// 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));
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
@@ -64,8 +72,8 @@ read -r -d '' DEVICE_CODE <<'CS' || true
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);
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 \
@@ -74,7 +82,7 @@ CS
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
--bindings "[[\"Trigger\",$CONNECTION_ID],[\"Complete\",$CONNECTION_ID]]" > /dev/null
"$CLI" instance deploy "${A[@]}" --id "$INSTANCE_ID" > /dev/null
echo " instance id=$INSTANCE_ID"