#!/bin/bash set -euo pipefail # Live gate for docker/seed-mes-test-machine.sh — drives the REAL inbound methods (MesMoveIn, MesMoveOut, # DelmiaRecipeDownload, SimpleAlarmStatusRequest, AlarmStatusRequest) through `flow-debug run-method` # (the same inbound executor an HTTP call reaches, minus the API-key check — no key needed) against the # SBTEST01 / SBTEST02 test machines, and trips/clears the computed alarms by writing their static source # attributes. Needs the BTDB rows SBTEST01=990001 / SBTEST02=990002 (vd03 only). # # SCADABRIDGE_URL=http://wonder-app-vd03.zmr.zimmer.com:8085 bash docker/verify-mes-test-machine.sh # # Leaves both machines as it found them: alarm sources reset to their defaults, no batch moved in. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" export SB_CLI="$PROJECT_ROOT/src/ZB.MOM.WW.ScadaBridge.CLI/bin/Debug/net10.0/scadabridge" export SB_URL="${SCADABRIDGE_URL:-http://localhost:9000}" export SB_USER="${SCADABRIDGE_USERNAME:-multi-role}" SB_PASS="${SCADABRIDGE_PASSWORD:-password}" python3 - <<'PY' import json, os, subprocess, sys CLI, URL = os.environ["SB_CLI"], os.environ["SB_URL"] AUTH = ["--url", URL, "--format=json", "--username", os.environ["SB_USER"], "--password", os.environ["SB_PASS"]] def cli(*args, timeout=180): r = subprocess.run([CLI, *args, *AUTH], capture_output=True, text=True, timeout=timeout) if r.returncode != 0: raise RuntimeError(f"{' '.join(args[:3])}: {r.stderr.strip() or r.stdout.strip()}") return json.loads(r.stdout) methods = {m["name"]: m["id"] for m in cli("api-method", "list")} instances = {i["uniqueName"]: i["id"] for i in cli("instance", "list")} def call(method, params): """Run an inbound method via the Flow Debugger and return its response body (dict).""" d = cli("flow-debug", "run-method", "--method-id", str(methods[method]), "--params", json.dumps(params), "--timeout-seconds", "90", timeout=150) ev = next(e for e in d["events"] if e["action"] == "ApiInbound.InboundRequest") det = json.loads(ev["detailsJson"]) if det.get("status") != "Delivered": raise RuntimeError(f"{method} not delivered: {det.get('status')} {det.get('errorMessage')}") return json.loads(det["responseSummary"]) def write(inst, attr, value): subprocess.run([CLI, "flow-debug", "run-value-change", "--instance-id", str(instances[inst]), "--attribute", attr, "--value", str(value), "--wait-seconds", "1", *AUTH], capture_output=True, text=True, timeout=90) passed = failed = 0 def check(label, cond, detail=""): global passed, failed if cond: passed += 1; print(f" PASS {label}") else: failed += 1; print(f" FAIL {label} {detail}") WO = [{"WorkOrderNumber": "WO-1001", "PartNumber": "PN-2001"}] def movein(sap, wo=WO): return call("MesMoveIn", {"SAPID": sap, "ContainerNumber": "CN-1", "JobSequenceNumber": "10", "NumberWorkOrders": len(wo), "WorkOrders": wo, "OperatorName": "sb-test"}) def moveout(sap, wo=WO): return call("MesMoveOut", {"SAPID": sap, "ContainerNumber": "CN-1", "NumberWorkOrders": len(wo), "WorkOrders": wo, "OperatorName": "sb-test"}) def names(r): return sorted(a["Name"] for a in r.get("Alarms") or []) print("A. DelmiaRecipeDownload (routes by MachineCode)") r = call("DelmiaRecipeDownload", {"MachineCode": "SBTEST01", "DownloadPath": r"\\share\recipes\r1.xml", "WorkOrderNumber": "WO-1001", "PartNumber": "PN-2001", "JobStepNumber": "20", "Username": "sb-test"}) check("A1 accepted", r.get("Result") is True and "accepted" in r.get("ResultText", ""), r) r = call("DelmiaRecipeDownload", {"MachineCode": "SBTEST01", "DownloadPath": r"\\share\recipes\r1.bad", "WorkOrderNumber": "WO-1001"}) check("A2 .bad path rejected", r.get("Result") is False and "reject" in r.get("ResultText", "").lower(), r) print("B. MesMoveIn / MesMoveOut (SAPID -> BTDB -> SBTEST01)") r = movein("990001_A") batch = r.get("BatchID", 0) check("B1 MoveIn left succeeds with a batch id", r.get("WasSuccessful") is True and batch >= 5000, r) r = moveout("990001_A") check("B2 MoveOut left returns the same batch", r.get("WasSuccessful") is True and r.get("BatchID") == batch, r) check("B3 MoveOut Run returns 16 cycle rows", len(r.get("Data") or []) == 16 and r["Data"][0].get("ValueName") == "TargetRuntime", r.get("Data")) r = moveout("990001_A") check("B4 second MoveOut rejected (nothing moved in)", r.get("WasSuccessful") is False and "No batch" in r.get("ErrorText", ""), r) r = movein("990001_A", [{"WorkOrderNumber": "WO-FAIL-1", "PartNumber": "PN-2001"}]) check("B5 FAIL work order rejected", r.get("WasSuccessful") is False and "FAIL" in r.get("ErrorText", ""), r) r = movein("990001") check("B6 missing side suffix", r.get("WasSuccessful") is False and "suffix" in r.get("ErrorText", ""), r) r = movein("990001_B_LT"); lt = r.get("BatchID", 0) check("B7 leak-test MoveIn right", r.get("WasSuccessful") is True and lt > 0, r) r = moveout("990001_B_LT") check("B8 leak-test MoveOut returns 6 LT rows", r.get("WasSuccessful") is True and len(r.get("Data") or []) == 6 and r["Data"][0]["ValueName"] == "LTType", r) r = movein("999999_A") check("B9 unknown SAPID", r.get("WasSuccessful") is False and "No machine found" in r.get("ErrorText", ""), r) print("C. Alarm APIs") r = call("SimpleAlarmStatusRequest", {"SAPID": "990001"}) check("C1 no alarms active -> success, empty", r.get("WasSuccessful") is True and names(r) == [], r) for inst in ("SBTEST01", "SBTEST02"): write(inst, "VacuumPressure", 12); write(inst, "DoorOpen", "true"); write(inst, "SensorStatus", -1) r = call("SimpleAlarmStatusRequest", {"SAPID": "990001"}) check("C2 SBTEST01 simple = flagged only (HighVacuumFault)", names(r) == ["HighVacuumFault"], names(r)) a = (r.get("Alarms") or [{}])[0] check("C3 row shape", a.get("HierarchicalName") == "SBTEST01.HighVacuumFault" and a.get("IsFlaggedForMES") is True and a.get("Severity") == 800 and a.get("StatusCode") == "Triggered.Acked" and a.get("TriggeredDT"), a) r = call("SimpleAlarmStatusRequest", {"SAPID": "990002"}) check("C4 SBTEST02 simple includes the overridden SensorFault", names(r) == ["HighVacuumFault", "SensorFault"], names(r)) r = call("AlarmStatusRequest", {"MachineFilter": {"Code": "SBTEST01"}}) check("C5 full, no filter = all 3 active", names(r) == ["DoorOpenAlarm", "HighVacuumFault", "SensorFault"], names(r)) flags = {x["Name"]: x["IsFlaggedForMES"] for x in r.get("Alarms") or []} check("C6 IsFlaggedForMES per alarm", flags == {"DoorOpenAlarm": False, "HighVacuumFault": True, "SensorFault": False}, flags) r = call("AlarmStatusRequest", {"MachineFilter": {"SAPID": "990001_A"}, "AlarmFilter": {"MinSeverity": 500}}) check("C7 SAPID w/ suffix + MinSeverity 500", names(r) == ["HighVacuumFault", "SensorFault"], names(r)) r = call("AlarmStatusRequest", {"MachineFilter": {"ZTag": "SBTEST01"}, "AlarmFilter": {"NameFilter": "door"}}) check("C8 ZTag + NameFilter", names(r) == ["DoorOpenAlarm"], names(r)) r = call("AlarmStatusRequest", {"MachineFilter": {"MachineID": 204}, "AlarmFilter": {"FlaggedOnly": True, "MaxSeverity": 750}}) check("C9 MachineID + FlaggedOnly + MaxSeverity", names(r) == ["SensorFault"], names(r)) r = call("AlarmStatusRequest", {"MachineFilter": {"Code": "SBTEST01"}, "AlarmFilter": {"IncludeAcked": False}}) check("C10 IncludeAcked=false drops computed (always-acked) alarms", r.get("WasSuccessful") is True and names(r) == [], names(r)) r = call("AlarmStatusRequest", {"MachineFilter": {"SAPID": "123"}}) check("C11 unknown SAPID error text", r.get("WasSuccessful") is False and "Failed to find machine with SAPID '123'" in (r.get("ErrorText") or ""), r) r = call("AlarmStatusRequest", {}) check("C12 empty filter error", r.get("WasSuccessful") is False and "machine filter" in (r.get("ErrorText") or ""), r) r = call("SimpleAlarmStatusRequest", {"SAPID": "103559"}) check("C13 machine without AlarmStatus script -> not supported", r.get("WasSuccessful") is False and "not supported" in (r.get("ErrorText") or ""), r) print("D. Reset alarm sources") for inst in ("SBTEST01", "SBTEST02"): write(inst, "VacuumPressure", 0.5); write(inst, "DoorOpen", "false"); write(inst, "SensorStatus", 0) r = call("AlarmStatusRequest", {"MachineFilter": {"Code": "SBTEST02"}}) check("D1 cleared", r.get("WasSuccessful") is True and names(r) == [], names(r)) print(f"\n{'PASS' if failed == 0 else 'FAIL'} {passed}/{passed + failed}") sys.exit(1 if failed else 0) PY