b3021e7c5f
Script file access (design docs/plans/2026-09-28-script-file-access-design.md): Files.ReadTextAsync / ReadLinesAsync / ExistsAsync on site ScriptGlobals (+ Instance.Files), backed by the site-singleton ScriptFileReader. The one host-mediated exception to the System.IO ban - scripts still never name a System.IO type. OFF by default: ScadaBridge:SiteRuntime:ScriptFileAccess:AllowedRoots (empty = every call throws Disabled) + MaxFileBytes (1 MiB, validated 1..64 MiB, roots must be absolute). Guard: fully qualified, no device paths / ADS, canonical path strictly inside a root (directory-boundary match), no reparse point on the file or any directory below the root, size cap before and during the read. Errors are one Commons type, ScriptFileAccessException(+ScriptFileAccessFailure); Commons.Types.Scripts joins the default script imports on every surface so it binds by short name. Mirrored on ScriptCompileSurface (CompileFiles, parity-pinned) and the Test Run sandbox (binds, refuses Disabled). Inbound (central) scripts do not get it. Rig: docker/recipes (sample RecipeValueName,RecipeValue files incl. an invalid one) bind-mounted read-only at /app/recipes on both site-a nodes with AllowedRoots [/app/recipes]. The TestMesMachine SimDelmiaRecipe simulator now reads + parses the downloaded file, converts each value to its recipe tag's type and loads it all-or-nothing (14 new TestDelmiaReceiver attributes); verify-mes-test-machine.sh group A covers accept / re-download / invalid lines / NotFound / OutsideAllowedRoots, VERIFY_GROUPS selects groups (rig: AD).
605 lines
42 KiB
Bash
Executable File
605 lines
42 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# Seed an IN-MEMORY MES / DELMIA / ALARM-API TEST MACHINE — a standalone stand-in for a CvdReactor-style
|
|
# machine that needs no Galaxy, no MxGateway and no bindings. Built for wonder-app-vd03 (DEV) but works
|
|
# against any ScadaBridge central (defaults to the docker rig on :9000).
|
|
#
|
|
# SCADABRIDGE_URL=http://wonder-app-vd03.zmr.zimmer.com:8085 SITE_ID=2 bash docker/seed-mes-test-machine.sh
|
|
#
|
|
# What it creates (everything STATIC — writes land in the instance's in-memory/SQLite state):
|
|
# - Template TestMESReceiver — the 26 prod MESReceiver attribute names (MoveIn*/MoveOut*/ScanStateCmd),
|
|
# ready flags default true.
|
|
# - Template TestDelmiaReceiver — the 10 prod DelmiaReceiver attribute names, ReadyFlag default true.
|
|
# - Template TestMesMachine (standalone base, NOT derived from CvdReactor) composing
|
|
# LeftMESReceiver + RightMESReceiver (TestMESReceiver) and DelmiaReceiver (TestDelmiaReceiver), with
|
|
# * simulator knobs SimNextBatchID, SimMoveInReject, SimMoveOutReject, SimRecipeReject, SimNoResponse
|
|
# * alarm source tags VacuumPressure, ArgonPressure, FurnaceTemp, DoorOpen, SensorStatus
|
|
# * computed alarms HighVacuumFault / LowArgonPressure / FurnaceOverTemp (MES-reportable),
|
|
# DoorOpenAlarm / SensorFault (not reportable on the template)
|
|
# * Call scripts MesMoveIn, MesMoveOut, ProcessRecipeDownload, AlarmStatus — same names,
|
|
# parameters and return shapes as the CvdReactor scripts the inbound methods
|
|
# route to, so the REAL MesMoveIn/MesMoveOut/DelmiaRecipeDownload methods work
|
|
# unchanged. WriteBatchAndWaitAsync only accepts data-sourced attributes, so the
|
|
# handshakes are ported to per-attribute SetAsync + WaitAsync.
|
|
# * Galaxy simulators SimLeft/RightMoveIn, SimLeft/RightMoveOut, SimDelmiaRecipe (ValueChange on the
|
|
# trigger flags) — they play the machine side and complete each handshake.
|
|
# SimDelmiaRecipe READS + PARSES the downloaded recipe file via the Files global
|
|
# and loads it into the TestDelmiaReceiver recipe tags (see docker/recipes/).
|
|
# - Instances SBTEST01 and SBTEST02 (SBTEST02 overrides SensorFault to MES-reportable), deployed.
|
|
# - Inbound methods SimpleAlarmStatusRequest (updated in place) and AlarmStatusRequest (legacy
|
|
# /mes/simplealarmstatus + /mes/alarmstatus shapes). Both resolve the machine through BTDB.dbo.Machine,
|
|
# so they need the "BTDB" database connection — present on vd03, absent on the docker rig.
|
|
#
|
|
# Recipe files: the site needs ScadaBridge:SiteRuntime:ScriptFileAccess:AllowedRoots covering the folder the
|
|
# DownloadPath points into (rig: docker/recipes bind-mounted at /app/recipes on site-a; vd03:
|
|
# E:\ApiInstall\ScadaBridge\recipes). Without it every download is rejected "recipe file not readable (Disabled)".
|
|
#
|
|
# The MES methods resolve SAPID -> BTDB.dbo.Machine.Code, so each instance needs a BTDB row
|
|
# (Code = instance name). On vd03 those were inserted 2026-09-28: SBTEST01 = SAPID 990001 (MachineID 203),
|
|
# SBTEST02 = SAPID 990002 (MachineID 204), LastUpdateBy 'scadabridge-test-seed'.
|
|
#
|
|
# IDEMPOTENT: objects are looked up by name; scripts and methods are UPDATED in place; attributes, alarms,
|
|
# compositions and instances are only added when missing (edit those by hand, or delete + re-seed).
|
|
#
|
|
# Test recipes: docs/deployment/deployment-records/2026-09-28-wonder-app-vd03-fe89363b.md.
|
|
|
|
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}"
|
|
INSTANCES=(SBTEST01 SBTEST02)
|
|
|
|
json_id() { python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])'; }
|
|
# stdin = JSON array, $1 = key, $2 = value -> the matching row's id, or "" when absent
|
|
find_id_opt() {
|
|
python3 -c 'import json,sys
|
|
k,v=sys.argv[1],sys.argv[2]
|
|
print(next((str(x["id"]) for x in json.load(sys.stdin) if str(x.get(k))==v), ""))' "$1" "$2"
|
|
}
|
|
# stdin = template get JSON, $1 = child collection, $2 = name -> id or ""
|
|
child_id() {
|
|
python3 -c 'import json,sys
|
|
t=json.load(sys.stdin); coll,name=sys.argv[1],sys.argv[2]
|
|
key="instanceName" if coll=="compositions" else "name"
|
|
print(next((str(c["id"]) for c in (t.get(coll) or []) if c.get(key)==name and not c.get("isInherited")), ""))' "$1" "$2"
|
|
}
|
|
|
|
ensure_template() { # $1 name, $2 description -> id
|
|
local id
|
|
id=$("$CLI" template list "${A[@]}" | python3 -c 'import json,sys
|
|
n=sys.argv[1]
|
|
print(next((str(x["id"]) for x in json.load(sys.stdin) if x.get("name")==n and not x.get("parentTemplateId")), ""))' "$1")
|
|
if [ -z "$id" ]; then
|
|
id=$("$CLI" template create "${A[@]}" --name "$1" --description "$2" | json_id)
|
|
echo " created template $1 id=$id" >&2
|
|
else
|
|
echo " template $1 exists id=$id" >&2
|
|
fi
|
|
echo "$id"
|
|
}
|
|
|
|
ensure_attr() { # $1 template id, $2 name, then extra add args
|
|
local tid="$1" name="$2"; shift 2
|
|
if [ -z "$("$CLI" template get "${A[@]}" --id "$tid" | child_id attributes "$name")" ]; then
|
|
"$CLI" template attribute add "${A[@]}" --template-id "$tid" --name "$name" "$@" > /dev/null
|
|
echo " + attribute $name" >&2
|
|
fi
|
|
}
|
|
|
|
ensure_alarm() { # $1 template id, $2 name, then extra add args
|
|
local tid="$1" name="$2"; shift 2
|
|
if [ -z "$("$CLI" template get "${A[@]}" --id "$tid" | child_id alarms "$name")" ]; then
|
|
"$CLI" template alarm add "${A[@]}" --template-id "$tid" --name "$name" "$@" > /dev/null
|
|
echo " + alarm $name" >&2
|
|
fi
|
|
}
|
|
|
|
upsert_script() { # $1 template id, $2 name, then script args (--trigger-type ... --code ...)
|
|
local tid="$1" name="$2" sid; shift 2
|
|
sid=$("$CLI" template get "${A[@]}" --id "$tid" | child_id scripts "$name")
|
|
if [ -n "$sid" ]; then
|
|
"$CLI" template script update "${A[@]}" --id "$sid" --name "$name" "$@" > /dev/null
|
|
echo " ~ script $name (updated id=$sid)" >&2
|
|
else
|
|
"$CLI" template script add "${A[@]}" --template-id "$tid" --name "$name" "$@" > /dev/null
|
|
echo " + script $name" >&2
|
|
fi
|
|
}
|
|
|
|
ensure_composition() { # $1 template id, $2 composed template id, $3 instance name
|
|
if [ -z "$("$CLI" template get "${A[@]}" --id "$1" | child_id compositions "$3")" ]; then
|
|
"$CLI" template composition add "${A[@]}" --template-id "$1" --composed-template-id "$2" --instance-name "$3" > /dev/null
|
|
echo " + composition $3" >&2
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------------------------------
|
|
echo "1/7 Template TestMESReceiver (26 static attributes)..."
|
|
RX_ID=$(ensure_template TestMESReceiver "In-memory stand-in for the Galaxy MESReceiver object (prod attribute names, static)")
|
|
for dir in MoveIn MoveOut; do
|
|
ensure_attr "$RX_ID" "${dir}ReadyFlag" --data-type Boolean --value true --description "Precondition; true = machine accepts a ${dir}"
|
|
for f in Flag CompleteFlag SuccessfulFlag; do
|
|
ensure_attr "$RX_ID" "${dir}${f}" --data-type Boolean --value false --description "${dir} handshake flag"
|
|
done
|
|
ensure_attr "$RX_ID" "${dir}BatchID" --data-type Int32 --value 0 --description "${dir} batch id (written by the simulator)"
|
|
ensure_attr "$RX_ID" "${dir}NumberWorkOrders" --data-type Int32 --value 0 --description "${dir} payload"
|
|
for s in MesContainerNum ErrorText OperatorName Type; do
|
|
ensure_attr "$RX_ID" "${dir}${s}" --data-type String --description "${dir} payload / result text"
|
|
done
|
|
ensure_attr "$RX_ID" "${dir}WorkOrderNumbers" --data-type List --element-type String --description "${dir} payload"
|
|
ensure_attr "$RX_ID" "${dir}PartNumbers" --data-type List --element-type String --description "${dir} payload"
|
|
done
|
|
ensure_attr "$RX_ID" MoveInJobSequenceNumber --data-type String --description "MoveIn payload"
|
|
ensure_attr "$RX_ID" ScanStateCmd --data-type Boolean --value false --description "Unused by the simulator (prod parity)"
|
|
|
|
echo "2/7 Template TestDelmiaReceiver (10 handshake + 14 recipe static attributes)..."
|
|
DR_ID=$(ensure_template TestDelmiaReceiver "In-memory stand-in for the Galaxy DelmiaReceiver object (prod attribute names, static)")
|
|
ensure_attr "$DR_ID" ReadyFlag --data-type Boolean --value true --description "Precondition; true = receiver accepts downloads"
|
|
ensure_attr "$DR_ID" RecipeDownloadFlag --data-type Boolean --value false --description "Trigger: set true after the payload is written"
|
|
ensure_attr "$DR_ID" RecipeProcessedFlag --data-type Boolean --value false --description "Cleared by the caller, set true by the receiver when done"
|
|
ensure_attr "$DR_ID" RecipeProcessResult --data-type Boolean --value false --description "Outcome -> Result"
|
|
for s in DownloadPath WorkOrderNumber PartNumber JobStepNumber Username RecipeProcessResultText; do
|
|
ensure_attr "$DR_ID" "$s" --data-type String --description "Recipe-download payload / outcome text"
|
|
done
|
|
# Recipe tags the simulator loads from the downloaded file (the Galaxy object's recipe schema stand-in).
|
|
# The type here is what each file value is converted to — keep it in step with RECIPE_SCHEMA in SimDelmiaRecipe.
|
|
ensure_attr "$DR_ID" RecipeName --data-type String --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" RecipeRevision --data-type Int32 --value 0 --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" CoatingType --data-type String --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" ReactorTempSP --data-type Double --value 0 --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" PotTempSP --data-type Int32 --value 0 --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" ArgonFlowSP --data-type Double --value 0 --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" HydrogenFlowSP --data-type Int32 --value 0 --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" ChlorineFlowSP --data-type Double --value 0 --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" VacuumSP --data-type Double --value 0 --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" TargetRuntime --data-type Int32 --value 0 --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" PurgeEnabled --data-type Boolean --value false --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" OperatorNote --data-type String --description "Recipe tag (loaded from the recipe file)"
|
|
ensure_attr "$DR_ID" RecipeValueCount --data-type Int32 --value 0 --description "How many values the last accepted download loaded"
|
|
ensure_attr "$DR_ID" RecipeLoadedFile --data-type String --description "Path of the last accepted recipe file"
|
|
|
|
echo "3/7 Template TestMesMachine (standalone base) — compositions, knobs, alarm tags, alarms..."
|
|
M_ID=$(ensure_template TestMesMachine "In-memory MES/Delmia/alarm-API test machine: Left/Right MES receivers + Delmia receiver, simulated Galaxy side (docker/seed-mes-test-machine.sh)")
|
|
ensure_composition "$M_ID" "$RX_ID" LeftMESReceiver
|
|
ensure_composition "$M_ID" "$RX_ID" RightMESReceiver
|
|
ensure_composition "$M_ID" "$DR_ID" DelmiaReceiver
|
|
ensure_attr "$M_ID" SimNextBatchID --data-type Int32 --value 5000 --description "Next batch id the MoveIn simulator hands out (increments)"
|
|
ensure_attr "$M_ID" SimMoveInReject --data-type Boolean --value false --description "true = MoveIn simulator rejects every move-in"
|
|
ensure_attr "$M_ID" SimMoveOutReject --data-type Boolean --value false --description "true = MoveOut simulator rejects every move-out"
|
|
ensure_attr "$M_ID" SimRecipeReject --data-type Boolean --value false --description "true = Delmia simulator rejects every download"
|
|
ensure_attr "$M_ID" SimNoResponse --data-type Boolean --value false --description "true = simulators never answer (exercises the 25 s handshake timeout)"
|
|
ensure_attr "$M_ID" VacuumPressure --data-type Double --value 0.5 --description "Alarm source: HighVacuumFault (hi 5 / hihi 10)"
|
|
ensure_attr "$M_ID" ArgonPressure --data-type Double --value 50 --description "Alarm source: LowArgonPressure (lo 20 / lolo 10)"
|
|
ensure_attr "$M_ID" FurnaceTemp --data-type Double --value 800 --description "Alarm source: FurnaceOverTemp (hi 1050 / hihi 1100)"
|
|
ensure_attr "$M_ID" DoorOpen --data-type Boolean --value false --description "Alarm source: DoorOpenAlarm (true)"
|
|
ensure_attr "$M_ID" SensorStatus --data-type Int32 --value 0 --description "Alarm source: SensorFault (-1)"
|
|
ensure_alarm "$M_ID" HighVacuumFault --trigger-type HiLo --attribute VacuumPressure --hi 5 --hihi 10 --priority 800 --mes-reportable \
|
|
--description "Vacuum pressure high — MES-reportable"
|
|
ensure_alarm "$M_ID" LowArgonPressure --trigger-type HiLo --attribute ArgonPressure --lo 20 --lolo 10 --priority 600 --mes-reportable \
|
|
--description "Argon supply pressure low — MES-reportable"
|
|
ensure_alarm "$M_ID" FurnaceOverTemp --trigger-type HiLo --attribute FurnaceTemp --hi 1050 --hihi 1100 --priority 900 --mes-reportable \
|
|
--description "Furnace over temperature — MES-reportable"
|
|
ensure_alarm "$M_ID" DoorOpenAlarm --trigger-type ValueMatch --attribute DoorOpen --match-value true --priority 300 \
|
|
--description "Door open — NOT MES-reportable"
|
|
# --match-value=-1 (not a space) so the leading dash is not parsed as another option.
|
|
ensure_alarm "$M_ID" SensorFault --trigger-type ValueMatch --attribute SensorStatus --match-value=-1 --priority 700 \
|
|
--description "Sensor fault — not reportable on the template; SBTEST02 overrides it to reportable"
|
|
|
|
# ---------------------------------------------------------------------------------------------------
|
|
echo "4/7 Scripts on TestMesMachine..."
|
|
|
|
read -r -d '' MOVEIN_CODE <<'CS' || true
|
|
// TestMesMachine MesMoveIn — CvdReactor.MesMoveIn semantics on STATIC attributes.
|
|
// SAPID format: <SAPID>_<A|B>[_LT]; _A => LeftMESReceiver, _B => RightMESReceiver; _LT => MoveInType "LT".
|
|
System.Func<object?,bool> IsTrue = v => { var s=v?.ToString(); return (v is bool b && b)||s=="1"||string.Equals(s,"true",System.StringComparison.OrdinalIgnoreCase); };
|
|
System.Func<string,object?> P = k => Parameters.ContainsKey(k) ? Parameters[k] : null;
|
|
var sapid = P("SAPID")?.ToString() ?? "";
|
|
var isLeakTest = sapid.EndsWith("_LT");
|
|
var core = isLeakTest ? sapid.Substring(0, sapid.Length - 3) : sapid;
|
|
var side = core.EndsWith("_A") ? "Left" : core.EndsWith("_B") ? "Right" : null;
|
|
if (side == null)
|
|
return new { WasSuccessful=false, ErrorText="SAPID missing _A/_B side suffix", BatchID=0 };
|
|
var rx = Children[side + "MESReceiver"];
|
|
if (!IsTrue(await rx.Attributes.GetAsync("MoveInReadyFlag")))
|
|
return new { WasSuccessful=false, ErrorText="Machine move in ready flag not set to true", BatchID=0 };
|
|
try {
|
|
await rx.Attributes.SetAsync("MoveInCompleteFlag", false);
|
|
await rx.Attributes.SetAsync("MoveInType", isLeakTest ? "LT" : "Run");
|
|
await rx.Attributes.SetAsync("MoveInMesContainerNum", P("MoveInMesContainerNum"));
|
|
await rx.Attributes.SetAsync("MoveInJobSequenceNumber", P("MoveInJobSequenceNumber"));
|
|
await rx.Attributes.SetAsync("MoveInNumberWorkOrders", P("MoveInNumberWorkOrders"));
|
|
await rx.Attributes.SetAsync("MoveInWorkOrderNumbers", P("MoveInWorkOrderNumbers"));
|
|
await rx.Attributes.SetAsync("MoveInPartNumbers", P("MoveInPartNumbers"));
|
|
await rx.Attributes.SetAsync("MoveInOperatorName", P("MoveInOperatorName"));
|
|
await rx.Attributes.SetAsync("MoveInFlag", true);
|
|
if (!await rx.Attributes.WaitAsync("MoveInCompleteFlag", true, System.TimeSpan.FromSeconds(25)))
|
|
return new { WasSuccessful=false, ErrorText="Timeout waiting for move in information to be processed", BatchID=0 };
|
|
int batch=0; try { batch=System.Convert.ToInt32(await rx.Attributes.GetAsync("MoveInBatchID") ?? 0); } catch { }
|
|
return new {
|
|
WasSuccessful = IsTrue(await rx.Attributes.GetAsync("MoveInSuccessfulFlag")),
|
|
ErrorText = (await rx.Attributes.GetAsync("MoveInErrorText"))?.ToString() ?? "",
|
|
BatchID = batch
|
|
};
|
|
} catch (Exception ex) {
|
|
return new { WasSuccessful=false, ErrorText="MoveIn failed: " + ex.Message, BatchID=0 };
|
|
}
|
|
CS
|
|
|
|
read -r -d '' MOVEOUT_CODE <<'CS' || true
|
|
// TestMesMachine MesMoveOut — CvdReactor.MesMoveOut semantics on STATIC attributes. The "Data" rows are
|
|
// SIMULATED (CvdReactor reads dbo.MachineCycle / the LeakTest child; the test machine has neither).
|
|
System.Func<object?,bool> IsTrue = v => { var s=v?.ToString(); return (v is bool b && b)||s=="1"||string.Equals(s,"true",System.StringComparison.OrdinalIgnoreCase); };
|
|
System.Func<string,object?> P = k => Parameters.ContainsKey(k) ? Parameters[k] : null;
|
|
var none = new System.Collections.Generic.List<object>();
|
|
var sapid = P("SAPID")?.ToString() ?? "";
|
|
var isLeakTest = sapid.EndsWith("_LT");
|
|
var core = isLeakTest ? sapid.Substring(0, sapid.Length - 3) : sapid;
|
|
var side = core.EndsWith("_A") ? "Left" : core.EndsWith("_B") ? "Right" : null;
|
|
if (side == null)
|
|
return new { WasSuccessful=false, ErrorText="SAPID missing _A/_B side suffix", BatchID=0, Data=none };
|
|
var rx = Children[side + "MESReceiver"];
|
|
if (!IsTrue(await rx.Attributes.GetAsync("MoveOutReadyFlag")))
|
|
return new { WasSuccessful=false, ErrorText="Machine move out ready flag not set to true", BatchID=0, Data=none };
|
|
try {
|
|
await rx.Attributes.SetAsync("MoveOutCompleteFlag", false);
|
|
await rx.Attributes.SetAsync("MoveOutType", isLeakTest ? "LT" : "Run");
|
|
await rx.Attributes.SetAsync("MoveOutMesContainerNum", P("MoveOutMesContainerNum"));
|
|
await rx.Attributes.SetAsync("MoveOutNumberWorkOrders", P("MoveOutNumberWorkOrders"));
|
|
await rx.Attributes.SetAsync("MoveOutWorkOrderNumbers", P("MoveOutWorkOrderNumbers"));
|
|
await rx.Attributes.SetAsync("MoveOutPartNumbers", P("MoveOutPartNumbers"));
|
|
await rx.Attributes.SetAsync("MoveOutOperatorName", P("MoveOutOperatorName"));
|
|
await rx.Attributes.SetAsync("MoveOutFlag", true);
|
|
if (!await rx.Attributes.WaitAsync("MoveOutCompleteFlag", true, System.TimeSpan.FromSeconds(25)))
|
|
return new { WasSuccessful=false, ErrorText="Timeout waiting for move out information to be processed", BatchID=0, Data=none };
|
|
int batch=0; try { batch=System.Convert.ToInt32(await rx.Attributes.GetAsync("MoveOutBatchID") ?? 0); } catch { }
|
|
var ok = IsTrue(await rx.Attributes.GetAsync("MoveOutSuccessfulFlag"));
|
|
var err = (await rx.Attributes.GetAsync("MoveOutErrorText"))?.ToString() ?? "";
|
|
var data = new System.Collections.Generic.List<object>();
|
|
if (ok && batch != 0) {
|
|
var values = isLeakTest
|
|
? new (string, object)[] { ("LTType", "Rate of Rise"), ("LTDuration", 300), ("LTStart", 0.052), ("LTEnd", 0.061), ("LTDeltaVac", 0.009), ("LTResult", "Pass") }
|
|
: new (string, object)[] { ("TargetRuntime", 480), ("ReactorTempSP", 1050), ("PotTempSP", 980), ("AirSP", 0), ("ArgonSP", 40),
|
|
("Cl2SP", 12), ("H2SP", 25), ("VacSP", 0.5), ("ReactorTempAvg", 1047), ("PotTempAvg", 978),
|
|
("AirAvg", 0), ("ArAvg", 39), ("Cl2Avg", 12), ("H2Avg", 24), ("VacAvg", 0.51), ("RunDuration", 482) };
|
|
foreach (var (name, value) in values)
|
|
data.Add(new { BatchId = batch, CycleId = isLeakTest ? 0 : batch * 10 + 1, ValueName = name, Value = System.Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture) });
|
|
}
|
|
return new { WasSuccessful = ok, ErrorText = err, BatchID = batch, Data = data };
|
|
} catch (Exception ex) {
|
|
return new { WasSuccessful=false, ErrorText="MoveOut failed: " + ex.Message, BatchID=0, Data=none };
|
|
}
|
|
CS
|
|
|
|
read -r -d '' RECIPE_CODE <<'CS' || true
|
|
// TestMesMachine ProcessRecipeDownload — CvdReactor.ProcessRecipeDownload semantics on STATIC attributes
|
|
// of the composed DelmiaReceiver (same flags, same 25 s budget, same result mapping).
|
|
System.Func<object?,bool> IsTrue = v => { var s=v?.ToString(); return (v is bool b && b)||s=="1"||string.Equals(s,"true",System.StringComparison.OrdinalIgnoreCase); };
|
|
System.Func<string,object?> P = k => Parameters.ContainsKey(k) ? Parameters[k] : null;
|
|
var recv = Children["DelmiaReceiver"];
|
|
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
|
|
|
|
# The MES alarm-status projection (same as docker/seed-mes-alarm-demo.sh). Computed alarms report
|
|
# Acknowledged=true / AckTime=null by construction, so StatusCode reads "Triggered.Acked" and
|
|
# IncludeAcked=false drops every computed alarm (it only bites on mirrored native conditions).
|
|
read -r -d '' ALARM_CODE <<'CS' || true
|
|
var code = Parameters["MachineCode"]?.ToString() ?? "";
|
|
var flaggedOnly = Parameters.Get<bool?>("FlaggedOnly") ?? false;
|
|
var includeAck = Parameters.Get<bool?>("IncludeAcked") ?? true;
|
|
var minSev = Parameters.Get<int?>("MinSeverity");
|
|
var maxSev = Parameters.Get<int?>("MaxSeverity");
|
|
var nameFilter = Parameters.ContainsKey("NameFilter") ? Parameters["NameFilter"]?.ToString() : null;
|
|
try {
|
|
var alarms = flaggedOnly ? await Alarms.MesReportableAsync() : await Alarms.CurrentAsync();
|
|
var infos = alarms
|
|
.Where(a => a.Active && !a.IsConfiguredPlaceholder)
|
|
.Where(a => minSev == null || a.Severity >= minSev)
|
|
.Where(a => maxSev == null || a.Severity <= maxSev)
|
|
.Where(a => string.IsNullOrEmpty(nameFilter) || a.Name.Contains(nameFilter, StringComparison.OrdinalIgnoreCase))
|
|
.Where(a => includeAck || !a.Acknowledged)
|
|
.Select(a => new {
|
|
Name = a.Name,
|
|
HierarchicalName = code + "." + a.Name,
|
|
Description = !string.IsNullOrEmpty(a.Message) ? a.Message : !string.IsNullOrEmpty(a.AlarmTypeName) ? a.AlarmTypeName : a.Name,
|
|
IsFlaggedForMES = a.IsMesReportable,
|
|
Severity = a.Severity,
|
|
StatusCode = a.Acknowledged ? "Triggered.Acked" : "Triggered",
|
|
TriggeredDT = (a.OriginalRaiseTime ?? a.Timestamp).ToString("o"),
|
|
AckDT = a.AckTime?.ToString("o"),
|
|
AckComment = a.OperatorComment })
|
|
.ToList();
|
|
return new { WasSuccessful = true, ErrorText = (string)null, Alarms = infos };
|
|
} catch (Exception ex) {
|
|
return new { WasSuccessful = false, ErrorText = "AlarmStatus failed: " + ex.Message, Alarms = Array.Empty<object>() };
|
|
}
|
|
CS
|
|
|
|
MOVEIN_PARAMS='{"type":"object","properties":{"SAPID":{"type":"string"},"MoveInMesContainerNum":{"type":"string"},"MoveInJobSequenceNumber":{"type":"string"},"MoveInNumberWorkOrders":{"type":"integer"},"MoveInWorkOrderNumbers":{"type":"array","items":{"type":"string"}},"MoveInPartNumbers":{"type":"array","items":{"type":"string"}},"MoveInOperatorName":{"type":"string"}},"required":[]}'
|
|
MOVEOUT_PARAMS='{"type":"object","properties":{"SAPID":{"type":"string"},"MoveOutMesContainerNum":{"type":"string"},"MoveOutNumberWorkOrders":{"type":"integer"},"MoveOutWorkOrderNumbers":{"type":"array","items":{"type":"string"}},"MoveOutPartNumbers":{"type":"array","items":{"type":"string"}},"MoveOutOperatorName":{"type":"string"}},"required":[]}'
|
|
RECIPE_PARAMS='{"type":"object","properties":{"DownloadPath":{"type":"string"},"WorkOrderNumber":{"type":"string"},"PartNumber":{"type":"string"},"JobStepNumber":{"type":"string"},"Username":{"type":"string"}},"required":[]}'
|
|
ALARM_PARAMS='{"type":"object","properties":{"MachineCode":{"type":"string"},"FlaggedOnly":{"type":"boolean"},"IncludeAcked":{"type":"boolean"},"MinSeverity":{"type":"integer"},"MaxSeverity":{"type":"integer"},"NameFilter":{"type":"string"}},"required":["MachineCode"]}'
|
|
ALARM_RETURN='{"type":"object","properties":{"WasSuccessful":{"type":"boolean"},"ErrorText":{"type":"string"},"Alarms":{"type":"array","items":{"type":"object","properties":{"Name":{"type":"string"},"HierarchicalName":{"type":"string"},"Description":{"type":"string"},"IsFlaggedForMES":{"type":"boolean"},"Severity":{"type":"integer"},"StatusCode":{"type":"string"},"TriggeredDT":{"type":"string"},"AckDT":{"type":"string"},"AckComment":{"type":"string"}}}}},"required":["WasSuccessful","Alarms"]}'
|
|
|
|
upsert_script "$M_ID" MesMoveIn --trigger-type Call --parameters "$MOVEIN_PARAMS" --code "$MOVEIN_CODE"
|
|
upsert_script "$M_ID" MesMoveOut --trigger-type Call --parameters "$MOVEOUT_PARAMS" --code "$MOVEOUT_CODE"
|
|
upsert_script "$M_ID" ProcessRecipeDownload --trigger-type Call --parameters "$RECIPE_PARAMS" \
|
|
--return-def '{"type":"object","properties":{"Result":{"type":"boolean"},"ResultText":{"type":"string"}},"required":["Result","ResultText"]}' \
|
|
--code "$RECIPE_CODE"
|
|
upsert_script "$M_ID" AlarmStatus --trigger-type Call --parameters "$ALARM_PARAMS" --return-def "$ALARM_RETURN" --code "$ALARM_CODE"
|
|
|
|
# --- Galaxy-side simulators (ValueChange on each trigger flag; act on the rising edge only) ----------
|
|
sim_movein_code() { # $1 = Left|Right
|
|
cat <<CS
|
|
// Plays the Galaxy side of the $1 MESReceiver MoveIn: rejects when SimMoveInReject is set or a work
|
|
// order contains "FAIL"; otherwise hands out SimNextBatchID (and increments it). SimNoResponse = never answer.
|
|
System.Func<object?,bool> IsTrue = v => { var s=v?.ToString(); return (v is bool b && b)||s=="1"||string.Equals(s,"true",System.StringComparison.OrdinalIgnoreCase); };
|
|
var rx = Children["$1MESReceiver"];
|
|
if (!IsTrue(await rx.Attributes.GetAsync("MoveInFlag"))) return null;
|
|
if (IsTrue(await Attributes.GetAsync("SimNoResponse"))) return null;
|
|
var wos = (await rx.Attributes.GetAsync("MoveInWorkOrderNumbers")) as System.Collections.IEnumerable;
|
|
var hasFail = wos != null && !(wos is string) && wos.Cast<object?>().Any(w => (w?.ToString() ?? "").Contains("FAIL", StringComparison.OrdinalIgnoreCase));
|
|
var forced = IsTrue(await Attributes.GetAsync("SimMoveInReject"));
|
|
var ok = !forced && !hasFail;
|
|
var batch = 0;
|
|
if (ok) {
|
|
batch = System.Convert.ToInt32(await Attributes.GetAsync("SimNextBatchID") ?? 5000);
|
|
await Attributes.SetAsync("SimNextBatchID", batch + 1);
|
|
}
|
|
await rx.Attributes.SetAsync("MoveInBatchID", batch);
|
|
await rx.Attributes.SetAsync("MoveInSuccessfulFlag", ok);
|
|
await rx.Attributes.SetAsync("MoveInErrorText", ok ? "" : forced ? "Simulated move in reject (SimMoveInReject)" : "Simulated move in reject: work order contains FAIL");
|
|
await rx.Attributes.SetAsync("MoveInFlag", false);
|
|
await rx.Attributes.SetAsync("MoveInCompleteFlag", true);
|
|
return null;
|
|
CS
|
|
}
|
|
sim_moveout_code() { # $1 = Left|Right
|
|
cat <<CS
|
|
// Plays the Galaxy side of the $1 MESReceiver MoveOut: moves out the batch last moved in on this side
|
|
// (MoveInBatchID, cleared on success); rejects when nothing is moved in, SimMoveOutReject is set or a
|
|
// work order contains "FAIL". SimNoResponse = never answer.
|
|
System.Func<object?,bool> IsTrue = v => { var s=v?.ToString(); return (v is bool b && b)||s=="1"||string.Equals(s,"true",System.StringComparison.OrdinalIgnoreCase); };
|
|
var rx = Children["$1MESReceiver"];
|
|
if (!IsTrue(await rx.Attributes.GetAsync("MoveOutFlag"))) return null;
|
|
if (IsTrue(await Attributes.GetAsync("SimNoResponse"))) return null;
|
|
var wos = (await rx.Attributes.GetAsync("MoveOutWorkOrderNumbers")) as System.Collections.IEnumerable;
|
|
var hasFail = wos != null && !(wos is string) && wos.Cast<object?>().Any(w => (w?.ToString() ?? "").Contains("FAIL", StringComparison.OrdinalIgnoreCase));
|
|
var forced = IsTrue(await Attributes.GetAsync("SimMoveOutReject"));
|
|
var batch = 0; try { batch = System.Convert.ToInt32(await rx.Attributes.GetAsync("MoveInBatchID") ?? 0); } catch { }
|
|
var err = forced ? "Simulated move out reject (SimMoveOutReject)"
|
|
: hasFail ? "Simulated move out reject: work order contains FAIL"
|
|
: batch == 0 ? "No batch is moved in on the $1 side"
|
|
: "";
|
|
var ok = err.Length == 0;
|
|
await rx.Attributes.SetAsync("MoveOutBatchID", ok ? batch : 0);
|
|
await rx.Attributes.SetAsync("MoveOutSuccessfulFlag", ok);
|
|
await rx.Attributes.SetAsync("MoveOutErrorText", err);
|
|
if (ok) await rx.Attributes.SetAsync("MoveInBatchID", 0);
|
|
await rx.Attributes.SetAsync("MoveOutFlag", false);
|
|
await rx.Attributes.SetAsync("MoveOutCompleteFlag", true);
|
|
return null;
|
|
CS
|
|
}
|
|
read -r -d '' SIM_RECIPE_CODE <<'CS' || true
|
|
// Plays the Galaxy DelmiaReceiver. On the rising edge of RecipeDownloadFlag it READS the downloaded recipe
|
|
// file through the Files global (script file access, design 2026-09-28 — only beneath the site's configured
|
|
// ScriptFileAccess:AllowedRoots), parses "RecipeValueName,RecipeValue" lines, converts each value to its
|
|
// recipe tag's type and loads them. All-or-nothing: any bad line rejects the download and loads nothing.
|
|
// Blank lines and '#' comments are ignored; everything after the FIRST comma is the value; a value wrapped
|
|
// in double quotes is unwrapped. SimRecipeReject / SimNoResponse knobs still apply.
|
|
System.Func<object?,bool> IsTrue = v => { var s=v?.ToString(); return (v is bool b && b)||s=="1"||string.Equals(s,"true",System.StringComparison.OrdinalIgnoreCase); };
|
|
var recv = Children["DelmiaReceiver"];
|
|
if (!IsTrue(await recv.Attributes.GetAsync("RecipeDownloadFlag"))) return null;
|
|
if (IsTrue(await Attributes.GetAsync("SimNoResponse"))) return null;
|
|
var path = (await recv.Attributes.GetAsync("DownloadPath"))?.ToString()?.Trim() ?? "";
|
|
var wo = (await recv.Attributes.GetAsync("WorkOrderNumber"))?.ToString() ?? "";
|
|
// RECIPE_SCHEMA — recipe tag name -> target type (keep in step with the TestDelmiaReceiver attributes).
|
|
var schema = new System.Collections.Generic.Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {
|
|
["RecipeName"] = "String", ["RecipeRevision"] = "Int32", ["CoatingType"] = "String",
|
|
["ReactorTempSP"] = "Double", ["PotTempSP"] = "Int32", ["ArgonFlowSP"] = "Double",
|
|
["HydrogenFlowSP"] = "Int32", ["ChlorineFlowSP"] = "Double", ["VacuumSP"] = "Double",
|
|
["TargetRuntime"] = "Int32", ["PurgeEnabled"] = "Boolean", ["OperatorNote"] = "String" };
|
|
var inv = System.Globalization.CultureInfo.InvariantCulture;
|
|
var errors = new System.Collections.Generic.List<string>();
|
|
var values = new System.Collections.Generic.List<(string Name, object Value)>();
|
|
if (IsTrue(await Attributes.GetAsync("SimRecipeReject"))) errors.Add("simulated reject (SimRecipeReject)");
|
|
else if (wo.Length == 0) errors.Add("missing work order number");
|
|
else if (path.Length == 0) errors.Add("missing download path");
|
|
else {
|
|
IReadOnlyList<string>? lines = null;
|
|
try { lines = await Files.ReadLinesAsync(path); }
|
|
catch (ScriptFileAccessException ex) { errors.Add($"recipe file not readable ({ex.Failure}): {ex.Message}"); }
|
|
for (var i = 0; lines != null && i < lines.Count; i++) {
|
|
var line = lines[i].Trim();
|
|
if (line.Length == 0 || line.StartsWith("#")) continue;
|
|
var comma = line.IndexOf(',');
|
|
if (comma <= 0) { errors.Add($"line {i + 1}: expected RecipeValueName,RecipeValue"); continue; }
|
|
var name = line.Substring(0, comma).Trim();
|
|
var raw = line.Substring(comma + 1).Trim();
|
|
if (raw.Length >= 2 && raw[0] == '"' && raw[raw.Length - 1] == '"') raw = raw.Substring(1, raw.Length - 2);
|
|
var key = schema.Keys.FirstOrDefault(k => string.Equals(k, name, StringComparison.OrdinalIgnoreCase));
|
|
if (key == null) { errors.Add($"line {i + 1}: unknown recipe value '{name}'"); continue; }
|
|
if (values.Any(v => v.Name == key)) { errors.Add($"line {i + 1}: '{key}' appears more than once"); continue; }
|
|
object? typed = null;
|
|
switch (schema[key]) {
|
|
case "Int32": if (int.TryParse(raw, System.Globalization.NumberStyles.Integer, inv, out var n)) typed = n; break;
|
|
case "Double": if (double.TryParse(raw, System.Globalization.NumberStyles.Float, inv, out var d)) typed = d; break;
|
|
case "Boolean": if (bool.TryParse(raw, out var bv)) typed = bv; break;
|
|
default: typed = raw; break;
|
|
}
|
|
if (typed == null) { errors.Add($"line {i + 1}: {key} expects {schema[key]}, got '{raw}'"); continue; }
|
|
values.Add((key, typed));
|
|
}
|
|
if (lines != null && errors.Count == 0 && values.Count == 0) errors.Add("recipe file contains no values");
|
|
}
|
|
var ok = errors.Count == 0;
|
|
if (ok) {
|
|
foreach (var (name, value) in values) await recv.Attributes.SetAsync(name, value);
|
|
await recv.Attributes.SetAsync("RecipeValueCount", values.Count);
|
|
await recv.Attributes.SetAsync("RecipeLoadedFile", path);
|
|
}
|
|
var file = path.Substring(path.LastIndexOfAny(new[] { '\\', '/' }) + 1);
|
|
var text = ok ? $"Loaded {values.Count} recipe values from {file} for {wo}"
|
|
: "Recipe rejected: " + string.Join("; ", errors.Take(5)) + (errors.Count > 5 ? $" (+{errors.Count - 5} more)" : "");
|
|
if (!ok) Log.Warn(text);
|
|
await recv.Attributes.SetAsync("RecipeProcessResult", ok);
|
|
await recv.Attributes.SetAsync("RecipeProcessResultText", text);
|
|
await recv.Attributes.SetAsync("RecipeDownloadFlag", false);
|
|
await recv.Attributes.SetAsync("RecipeProcessedFlag", true);
|
|
return null;
|
|
CS
|
|
|
|
for side in Left Right; do
|
|
upsert_script "$M_ID" "Sim${side}MoveIn" --trigger-type ValueChange --trigger-config "{\"attributeName\":\"${side}MESReceiver.MoveInFlag\"}" --code "$(sim_movein_code $side)"
|
|
upsert_script "$M_ID" "Sim${side}MoveOut" --trigger-type ValueChange --trigger-config "{\"attributeName\":\"${side}MESReceiver.MoveOutFlag\"}" --code "$(sim_moveout_code $side)"
|
|
done
|
|
upsert_script "$M_ID" SimDelmiaRecipe --trigger-type ValueChange --trigger-config '{"attributeName":"DelmiaReceiver.RecipeDownloadFlag"}' --code "$SIM_RECIPE_CODE"
|
|
|
|
# ---------------------------------------------------------------------------------------------------
|
|
echo "5/7 Instances ${INSTANCES[*]} on site $SITE_ID..."
|
|
INST_IDS=()
|
|
for name in "${INSTANCES[@]}"; do
|
|
id=$("$CLI" instance list "${A[@]}" | find_id_opt uniqueName "$name")
|
|
if [ -z "$id" ]; then
|
|
id=$("$CLI" instance create "${A[@]}" --name "$name" --template-id "$M_ID" --site-id "$SITE_ID" | json_id)
|
|
echo " created $name id=$id"
|
|
else
|
|
echo " $name exists id=$id"
|
|
fi
|
|
INST_IDS+=("$id")
|
|
done
|
|
# INSTANCES[1] = SBTEST02 (bash 3.2 on macOS has no associative arrays)
|
|
"$CLI" instance alarm-override set "${A[@]}" --instance-id "${INST_IDS[1]}" --alarm SensorFault --mes-reportable true > /dev/null
|
|
echo " ${INSTANCES[1]}: SensorFault overridden to MES-reportable"
|
|
|
|
echo "6/7 Deploying..."
|
|
for i in "${!INSTANCES[@]}"; do
|
|
"$CLI" instance deploy "${A[@]}" --id "${INST_IDS[$i]}" > /dev/null
|
|
echo " deployed ${INSTANCES[$i]}"
|
|
done
|
|
|
|
# ---------------------------------------------------------------------------------------------------
|
|
echo "7/7 Inbound alarm methods SimpleAlarmStatusRequest / AlarmStatusRequest (BTDB machine lookup)..."
|
|
|
|
# Shared: legacy-compatible machine resolution. SAPID matches the BTDB value exactly first, then by its
|
|
# leading digits (the MesMoveIn convention, so "990001_A" works too). The instance is the BTDB Code with a
|
|
# trailing A/B side letter stripped (the MesMoveOut convention); HierarchicalName keeps the BTDB Code.
|
|
read -r -d '' SIMPLE_METHOD_CODE <<'CS' || true
|
|
System.Func<string, object?> P = k => Parameters.ContainsKey(k) ? Parameters[k] : null;
|
|
var raw = (P("SAPID")?.ToString() ?? "").Trim();
|
|
var none = Array.Empty<object>();
|
|
try {
|
|
string? code = null;
|
|
if (raw.Length > 0) {
|
|
code = await Database.QuerySingleAsync<string>("BTDB", "SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s", new { s = raw });
|
|
var m = System.Text.RegularExpressions.Regex.Match(raw, @"^(\d+)");
|
|
if (string.IsNullOrEmpty(code) && m.Success && m.Groups[1].Value != raw)
|
|
code = await Database.QuerySingleAsync<string>("BTDB", "SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s", new { s = m.Groups[1].Value });
|
|
}
|
|
if (string.IsNullOrEmpty(code))
|
|
return new { WasSuccessful = false, ErrorText = $"Failed to find machine with SAPID '{raw}'", Alarms = none };
|
|
var instance = (code.EndsWith("A") || code.EndsWith("B")) ? code.Substring(0, code.Length - 1) : code;
|
|
try {
|
|
return await Route.To(instance).Call("AlarmStatus", new {
|
|
MachineCode = code, FlaggedOnly = true, IncludeAcked = true,
|
|
MinSeverity = (int?)null, MaxSeverity = (int?)null, NameFilter = (string)null });
|
|
} catch (Exception ex) when (ex.Message.Contains("not found on instance")) {
|
|
return new { WasSuccessful = false, ErrorText = $"Alarm status is not supported on machine '{code}'", Alarms = none };
|
|
}
|
|
} catch (Exception ex) {
|
|
return new { WasSuccessful = false, ErrorText = "Failed to read machine alarm status: " + ex.Message, Alarms = none };
|
|
}
|
|
CS
|
|
|
|
read -r -d '' FULL_METHOD_CODE <<'CS' || true
|
|
// Legacy POST /mes/alarmstatus: MachineFilter precedence SAPID -> Code -> ZTag -> MachineID (first
|
|
// non-empty wins, each resolved through BTDB.dbo.Machine), then AlarmFilter applied by the site script.
|
|
// AlarmFilter.IncludeTriggered is accepted and ignored, exactly like the legacy service.
|
|
var none = Array.Empty<object>();
|
|
var mf = Parameters.ContainsKey("MachineFilter") ? Parameters["MachineFilter"] as System.Collections.Generic.IDictionary<string, object?> : null;
|
|
var af = Parameters.ContainsKey("AlarmFilter") ? Parameters["AlarmFilter"] as System.Collections.Generic.IDictionary<string, object?> : null;
|
|
object? M(string k) => mf != null && mf.TryGetValue(k, out var v) ? v : null;
|
|
object? F(string k) => af != null && af.TryGetValue(k, out var v) ? v : null;
|
|
string? S(object? o) { var s = o?.ToString()?.Trim(); return string.IsNullOrEmpty(s) ? null : s; }
|
|
try {
|
|
string? code = null, error = null;
|
|
if (S(M("SAPID")) is string sap) {
|
|
code = await Database.QuerySingleAsync<string>("BTDB", "SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s", new { s = sap });
|
|
var m = System.Text.RegularExpressions.Regex.Match(sap, @"^(\d+)");
|
|
if (string.IsNullOrEmpty(code) && m.Success && m.Groups[1].Value != sap)
|
|
code = await Database.QuerySingleAsync<string>("BTDB", "SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s", new { s = m.Groups[1].Value });
|
|
if (string.IsNullOrEmpty(code)) error = $"Failed to find machine with SAPID '{sap}'";
|
|
} else if (S(M("Code")) is string c) {
|
|
code = await Database.QuerySingleAsync<string>("BTDB", "SELECT TOP 1 Code FROM dbo.Machine WHERE Code=@c", new { c });
|
|
if (string.IsNullOrEmpty(code)) error = $"Failed to find machine with Code '{c}'";
|
|
} else if (S(M("ZTag")) is string z) {
|
|
code = await Database.QuerySingleAsync<string>("BTDB", "SELECT TOP 1 Code FROM dbo.Machine WHERE ZTag=@z", new { z });
|
|
if (string.IsNullOrEmpty(code)) error = $"Failed to find machine with ZTag '{z}'";
|
|
} else if (S(M("MachineID")) is string idText && long.TryParse(idText, out var id)) {
|
|
code = await Database.QuerySingleAsync<string>("BTDB", "SELECT TOP 1 Code FROM dbo.Machine WHERE MachineID=@id", new { id });
|
|
if (string.IsNullOrEmpty(code)) error = $"Failed to find machine with MachineID '{id}'";
|
|
} else {
|
|
error = "Failed to find machine with given machine filter (supply SAPID, Code, ZTag or MachineID)";
|
|
}
|
|
if (error != null || code == null)
|
|
return new { WasSuccessful = false, ErrorText = error, Alarms = none };
|
|
var instance = (code.EndsWith("A") || code.EndsWith("B")) ? code.Substring(0, code.Length - 1) : code;
|
|
int? I(object? o) => o == null ? null : int.TryParse(o.ToString(), out var n) ? n : null;
|
|
try {
|
|
return await Route.To(instance).Call("AlarmStatus", new {
|
|
MachineCode = code,
|
|
FlaggedOnly = F("FlaggedOnly") is bool fo && fo,
|
|
IncludeAcked = !(F("IncludeAcked") is bool ia) || ia,
|
|
MinSeverity = I(F("MinSeverity")),
|
|
MaxSeverity = I(F("MaxSeverity")),
|
|
NameFilter = S(F("NameFilter")) });
|
|
} catch (Exception ex) when (ex.Message.Contains("not found on instance")) {
|
|
return new { WasSuccessful = false, ErrorText = $"Alarm status is not supported on machine '{code}'", Alarms = none };
|
|
}
|
|
} catch (Exception ex) {
|
|
return new { WasSuccessful = false, ErrorText = "Failed to read machine alarm status: " + ex.Message, Alarms = none };
|
|
}
|
|
CS
|
|
|
|
SIMPLE_PARAMS='{"type":"object","properties":{"SAPID":{"type":"string"}}}'
|
|
FULL_PARAMS='{"type":"object","properties":{"MachineFilter":{"type":"object","properties":{"MachineID":{"type":"integer"},"SAPID":{"type":"string"},"ZTag":{"type":"string"},"Code":{"type":"string"}}},"AlarmFilter":{"type":"object","properties":{"NameFilter":{"type":"string"},"MinSeverity":{"type":"integer"},"MaxSeverity":{"type":"integer"},"IncludeTriggered":{"type":"boolean"},"IncludeAcked":{"type":"boolean"},"FlaggedOnly":{"type":"boolean"}}}}}'
|
|
|
|
upsert_method() { # $1 name, $2 params, $3 script
|
|
local existing
|
|
existing=$("$CLI" api-method list "${A[@]}" | find_id_opt name "$1")
|
|
if [ -n "$existing" ]; then
|
|
# KNOWN TRAP: a non-compiling UPDATE keeps serving the OLD delegate silently — smoke it afterwards.
|
|
"$CLI" api-method update "${A[@]}" --id "$existing" --timeout 30 --parameters "$2" --return-def "$ALARM_RETURN" --script "$3" > /dev/null
|
|
echo " updated $1 id=$existing"
|
|
else
|
|
existing=$("$CLI" api-method create "${A[@]}" --name "$1" --timeout 30 --parameters "$2" --return-def "$ALARM_RETURN" --script "$3" | json_id)
|
|
echo " created $1 id=$existing"
|
|
fi
|
|
}
|
|
upsert_method SimpleAlarmStatusRequest "$SIMPLE_PARAMS" "$SIMPLE_METHOD_CODE"
|
|
upsert_method AlarmStatusRequest "$FULL_PARAMS" "$FULL_METHOD_CODE"
|
|
|
|
echo ""
|
|
echo "TEST_MES_RECEIVER_ID=$RX_ID TEST_DELMIA_RECEIVER_ID=$DR_ID TEST_MES_MACHINE_ID=$M_ID"
|
|
for i in "${!INSTANCES[@]}"; do echo "INSTANCE ${INSTANCES[$i]} id=${INST_IDS[$i]}"; done
|