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:
@@ -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"
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ All 7 fix-now items landed via PLAN-04/05/06/07/08 (verified in review 08 round
|
||||
| 118 | **Pre-existing bUnit flake: `CentralUI.Tests.Pages.NotificationReportDetailModalTests.CloseButton_DismissesModal` (~10 %)** *(Low)*. Fails intermittently with `UnknownEventHandlerIdException` — the test resolves the close button once and clicks it after a re-render has already retired that element's handler id. Not caused by the step-logs work: the file is untouched since `40944a0d`. The fix is a one-liner — re-`Find` the button immediately before the click (the pattern the other modal tests already use) rather than reusing an element captured earlier in the test. | Observed during the step-logs full-suite runs, 2026-09-09 | It is a test-only race with a known cause and a known fix; it was out of scope for a remediation pass whose rule was to touch only the files under review. | Next time `CentralUI.Tests` is worked on, or the first time the flake blocks a gate run. |
|
||||
| 68 | ~~**`deploy.sh` run from a git worktree silently rehomes every node's bind-mount data dir**~~ **RESOLVED 2026-08-15 (guard added in the same close-out).** The compose files' bind mounts are relative (`./site-a-node-a/data`), so a deploy from a worktree checkout mounts the worktree's copies — the rig keeps running and looks healthy, but its SQLite stores split from the main checkout's and everything written there is orphaned by the next deploy from main. Bit the rows-46–53 close-out live: the seeding agent's `rig-probe-1` deploy persisted into worktree-homed dirs and "vanished" after the orchestrator's redeploy (no tombstone, no product bug — LWW/tombstone forensics ruled out deletion; the row sat in `.claude/worktrees/…/docker/site-a-node-a/data/site-localdb.db` the whole time). Both `docker/deploy.sh` and `docker-env2/deploy.sh` now refuse to run from a `.claude/worktrees/` path (override: `SCADABRIDGE_ALLOW_WORKTREE_DEPLOY=1`). | Found during the rows-46–53 close-out | Closed 2026-08-15. | — |
|
||||
| 119 | **Port the QA/prod MES receiver scripts onto the reset-aware `WriteBatchAndWaitAsync` overload (Gitea #36)** *(Medium)*. Template scripts CvdReactor MesMoveIn (13) / MesMoveOut (14) / MesSampleData (36) and base MESReceiver MoveIn/MoveOut (15, 16, 25, 26, 37, 38) still reset `<X>CompleteFlag` with a plain write and rely on `Task.Delay(1000)`; QA carries a config-only `WaitAsync(false, 10 s)` patch on 13/14 (deployment record 110). Rewrite: `var hs = await rx.Attributes.WriteBatchAndWaitAsync(values, "<X>Flag", true, "<X>CompleteFlag", true, 25 s, "<X>CompleteFlag", false, 10 s); if (!hs.Matched) return new { WasSuccessful=false, ErrorText = hs.ResetFailed ? "Timeout waiting for <X>CompleteFlag to reset" : "Timeout waiting for <X> to complete", BatchID=0 };` and delete the delay. Requires the #36 build on the target central + sites first (the overload does not exist on older sites → deploy-gate compile failure, by design). | Gitea #36, design `2026-09-10-handshake-reset-echo-design.md` §8 | Repo-only scope was the user's decision for the #36 session; the script bodies live only in the QA/prod databases. | Next QA deployment of a build ≥ this change (write a deployment record; then prod). |
|
||||
| 120 | **Data-sourced SCALAR writes reach the OPC UA server as String Variants — every non-String node rejects them with BadTypeMismatch (0x80740000)** *(Medium)*. `InstanceActor.HandleSetDataAttribute` / `HandleWriteAttributeBatch` decode only List values before the DCL write ("Scalars are unchanged"), so the codec-encoded string (`"True"`, `"42.5"`) reaches `RealOpcUaClient.WriteValuesAsync`, which wraps it in `new Variant(value)` — a String variant sent to a Boolean/Double node. Found live 2026-09-10 running `docker/seed-handshake-demo.sh` against opc-plc `Motor.Running`/`Pump.Running`/`Motor.Speed` (`SetAttribute('Complete') failed: OPC UA write failed with status: 0x80740000`); the seed was retargeted onto the rig's only two writable String nodes. The MxGateway path is unaffected in practice: prod's Delmia/MES handshakes write the same strings through the gateway today. Fix candidates: (a) decode scalars by declared `DataType` in the InstanceActor before the DCL write — also changes what MxGateway receives, so it needs a gateway-side compatibility check; (b) adapter-local: `RealOpcUaClient` coerces to the node's DataType (it already resolves `DataTypeIds` in `EnrichVariableTypeInfoAsync`), leaving the MxGateway wire untouched — preferred. Pin with a `RealOpcUaClientArrayWriteTests`-style test for a Boolean scalar. | Found live 2026-09-10 during the #36 rig gate | Out of scope for #36 (handshake ordering); touches the production write path and needs the MxGateway compatibility question answered first. | Before any OPC UA-bound template relies on script writes to non-String nodes (none does today — until this gate the rig's script writes to OPC UA were static-only). |
|
||||
|
||||
## Resolved (verified against the code 2026-07-10)
|
||||
Rows removed from the Deferred table above once confirmed shipped. Kept here for traceability.
|
||||
|
||||
Reference in New Issue
Block a user