diff --git a/docker/README.md b/docker/README.md index 49d8d419..0a01ed7b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -95,6 +95,8 @@ docker/ ├── deploy.sh # Build + deploy all containers ├── seed-sites.sh # Create test sites with Akka + gRPC addresses ├── seed-live-probe.sh # Seed one deployable live-data instance on site-a (stream traffic) +├── seed-delmia-sim.sh # Seed the in-memory Delmia machine stand-in (DelmiaSim01) on site-a +├── seed-delmia-api.sh # Seed the DelmiaRecipeDownload inbound method + a scoped API key (token printed once) ├── teardown.sh # Stop and remove containers ├── central-node-a/ │ ├── appsettings.Central.json # Central node A configuration diff --git a/docker/seed-delmia-sim.sh b/docker/seed-delmia-sim.sh new file mode 100755 index 00000000..f1d9779b --- /dev/null +++ b/docker/seed-delmia-sim.sh @@ -0,0 +1,112 @@ +#!/bin/bash +set -euo pipefail + +# Seed an IN-MEMORY stand-in for a vd03-style machine with a composed DelmiaReceiver on +# site-a, so DelmiaNotifier can be exercised end-to-end on the docker rig without a Galaxy. +# +# What it creates (all static attributes — no data connection, no bindings): +# - Template DelmiaReceiverSim — the ten DelmiaReceiver attributes from prod +# (docs/api-specs/Delmia-Recipe-Download-API.md §5), ReadyFlag defaulting to true. +# - Template SimMachine — composes DelmiaReceiverSim as `DelmiaReceiver` and carries +# * ProcessRecipeDownload (Call) — prod's handshake ported to per-attribute +# SetAsync/WaitAsync (WriteBatchAndWaitAsync rejects static attributes). +# * DelmiaReceiverSimulator (ValueChange on DelmiaReceiver.RecipeDownloadFlag) — +# plays the Galaxy object: accepts unless DownloadPath ends in ".bad" or the work +# order is blank, writes RecipeProcessResult/Text, clears the trigger flag, then +# sets RecipeProcessedFlag=true. +# - Instance DelmiaSim01 on site-a (id 1), deployed. +# +# Not idempotent: names must be unique. To re-seed: +# scadabridge instance delete --id ; scadabridge template delete --id ; +# scadabridge template delete --id +# Prerequisites: bash docker/deploy.sh; docker/seed-sites.sh; central healthy on :9000. + +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}" +INSTANCE_NAME="${INSTANCE_NAME:-DelmiaSim01}" + +json_id() { python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])'; } + +echo "1/6 Creating template DelmiaReceiverSim..." +RECV_ID=$("$CLI" template create "${A[@]}" --name DelmiaReceiverSim \ + --description "In-memory stand-in for the Galaxy DelmiaReceiver object (ten static attributes)" | json_id) +echo " id=$RECV_ID" + +echo "2/6 Adding the ten receiver attributes (static)..." +"$CLI" template attribute add "${A[@]}" --template-id "$RECV_ID" --name ReadyFlag --data-type Boolean --value true --description "Precondition; true = receiver accepts downloads" > /dev/null +"$CLI" template attribute add "${A[@]}" --template-id "$RECV_ID" --name RecipeDownloadFlag --data-type Boolean --value false --description "Trigger: set true after the payload is written" > /dev/null +"$CLI" template attribute add "${A[@]}" --template-id "$RECV_ID" --name RecipeProcessedFlag --data-type Boolean --value false --description "Cleared by the caller, set true by the receiver when done" > /dev/null +"$CLI" template attribute add "${A[@]}" --template-id "$RECV_ID" --name RecipeProcessResult --data-type Boolean --value false --description "Outcome -> Result" > /dev/null +for s in DownloadPath WorkOrderNumber PartNumber JobStepNumber Username RecipeProcessResultText; do + "$CLI" template attribute add "${A[@]}" --template-id "$RECV_ID" --name "$s" --data-type String --description "Recipe-download payload / outcome text" > /dev/null +done + +echo "3/6 Creating template SimMachine + composing DelmiaReceiver..." +MACHINE_ID=$("$CLI" template create "${A[@]}" --name SimMachine \ + --description "vd03-style machine stand-in: composes DelmiaReceiverSim as DelmiaReceiver; in-memory only" | json_id) +"$CLI" template composition add "${A[@]}" --template-id "$MACHINE_ID" --composed-template-id "$RECV_ID" --instance-name DelmiaReceiver > /dev/null +echo " id=$MACHINE_ID" + +echo "4/6 Adding ProcessRecipeDownload (Call) — prod handshake on static attributes..." +read -r -d '' PROCESS_CODE <<'CS' || true +// Port of prod CvdReactor.ProcessRecipeDownload for STATIC attributes: WriteBatchAndWaitAsync only +// accepts data-sourced attributes, so the payload is written with SetAsync and the handshake waited +// with WaitAsync (same trigger/response flags, same 25 s budget, same result mapping). +var recv = Children["DelmiaReceiver"]; +System.Func IsTrue = v => { var s = v?.ToString(); return (v is bool b && b) || s == "1" || string.Equals(s, "true", System.StringComparison.OrdinalIgnoreCase); }; +System.Func P = k => Parameters.ContainsKey(k) ? Parameters[k] : null; +if (!IsTrue(await recv.Attributes.GetAsync("ReadyFlag"))) + return new { Result = false, ResultText = "Receiver ready flag not set to true" }; +try { + await recv.Attributes.SetAsync("RecipeProcessedFlag", false); + await recv.Attributes.SetAsync("DownloadPath", P("DownloadPath")); + await recv.Attributes.SetAsync("WorkOrderNumber", P("WorkOrderNumber")); + await recv.Attributes.SetAsync("PartNumber", P("PartNumber")); + await recv.Attributes.SetAsync("JobStepNumber", P("JobStepNumber")); + await recv.Attributes.SetAsync("Username", P("Username")); + await recv.Attributes.SetAsync("RecipeDownloadFlag", true); + if (!await recv.Attributes.WaitAsync("RecipeProcessedFlag", true, System.TimeSpan.FromSeconds(25))) + return new { Result = false, ResultText = "Timeout waiting for recipe to be processed" }; + return new { + Result = IsTrue(await recv.Attributes.GetAsync("RecipeProcessResult")), + ResultText = (await recv.Attributes.GetAsync("RecipeProcessResultText"))?.ToString() ?? "" + }; +} catch (Exception ex) { + return new { Result = false, ResultText = "RecipeDownload failed: " + ex.Message }; +} +CS +"$CLI" template script add "${A[@]}" --template-id "$MACHINE_ID" --name ProcessRecipeDownload --trigger-type Call \ + --parameters '{"type":"object","properties":{"DownloadPath":{"type":"string"},"WorkOrderNumber":{"type":"string"},"PartNumber":{"type":"string"},"JobStepNumber":{"type":"string"},"Username":{"type":"string"}},"required":[]}' \ + --return-def '{"type":"object","properties":{"Result":{"type":"boolean"},"ResultText":{"type":"string"}},"required":["Result","ResultText"]}' \ + --code "$PROCESS_CODE" > /dev/null + +echo "5/6 Adding DelmiaReceiverSimulator (ValueChange on DelmiaReceiver.RecipeDownloadFlag)..." +read -r -d '' SIM_CODE <<'CS' || true +// Plays the Galaxy DelmiaReceiver object. Fires on every write to RecipeDownloadFlag (root-scope +// trigger, canonical name) and acts only on the rising edge; its own reset to false re-triggers +// this script, which then returns at the guard. +var recv = Children["DelmiaReceiver"]; +var flag = await recv.Attributes.GetAsync("RecipeDownloadFlag"); +var fs = flag?.ToString(); +if (!((flag is bool b && b) || fs == "1" || string.Equals(fs, "true", System.StringComparison.OrdinalIgnoreCase))) return null; +var path = (await recv.Attributes.GetAsync("DownloadPath"))?.ToString() ?? ""; +var wo = (await recv.Attributes.GetAsync("WorkOrderNumber"))?.ToString() ?? ""; +var ok = path.Length > 0 && wo.Length > 0 && !path.EndsWith(".bad", System.StringComparison.OrdinalIgnoreCase); +await recv.Attributes.SetAsync("RecipeProcessResult", ok); +await recv.Attributes.SetAsync("RecipeProcessResultText", ok ? "Simulated download accepted for " + wo : "Simulated reject: bad path or missing work order"); +await recv.Attributes.SetAsync("RecipeDownloadFlag", false); +await recv.Attributes.SetAsync("RecipeProcessedFlag", true); +return null; +CS +"$CLI" template script add "${A[@]}" --template-id "$MACHINE_ID" --name DelmiaReceiverSimulator --trigger-type ValueChange \ + --trigger-config '{"attributeName":"DelmiaReceiver.RecipeDownloadFlag"}' --code "$SIM_CODE" > /dev/null + +echo "6/6 Creating + deploying instance $INSTANCE_NAME on site $SITE_ID..." +INSTANCE_ID=$("$CLI" instance create "${A[@]}" --name "$INSTANCE_NAME" --template-id "$MACHINE_ID" --site-id "$SITE_ID" | json_id) +"$CLI" instance deploy "${A[@]}" --id "$INSTANCE_ID" +echo "" +echo "DELMIA_SIM_TEMPLATE_ID=$RECV_ID SIM_MACHINE_TEMPLATE_ID=$MACHINE_ID INSTANCE_ID=$INSTANCE_ID INSTANCE_NAME=$INSTANCE_NAME" +echo "Verify: $CLI debug snapshot --id $INSTANCE_ID --url http://localhost:9000 --username multi-role --password password"