SAPID(+side) -> BTDB Machine.SAPID -> Code -> instance; inbound script does the lookup via a new scoped read-only DB helper, then routes to a new T1 template script that gates on MoveInReadyFlag and writes the MoveIn to the correct Left/Right MES receiver. -LT deferred.
11 KiB
Design — Ipsen MES MoveIn → reactor MES receiver
Date: 2026-06-16
Status: Approved (design); implementation pending
Scope: ScadaBridge — inbound API POST /api/IpsenMESMoveIn, a small inbound-script DB
capability, and a new template script on the reactor template that writes a MoveIn to the
correct MES-receiver child.
Target environment: production wonder-app-vd03 (Central + Site, ConfigDb ScadaBridge
on wonder-app-vd03,1433; reactors live on Site 2). BTDB = db-connection id 1 (dbo.Machine
on wonder-sql-vd03/BT).
1. Problem
The deployed IpsenMESMoveIn inbound method is a stub (Script = Parameters["SAPID"]). It must
become a real MoveIn: take the MES payload, resolve which reactor instance + side it targets,
and write the values onto that reactor's correct MES-receiver child — but only when that
receiver is ready to accept a MoveIn.
2. Verified facts (current system)
- Inbound
/apiscript context (InboundScriptContext,InboundScriptExecutor.cs) exposes onlyParameters,Route,CancellationToken. No DB access. It returns a JSON object validated against the method'sReturnDefinition; a thrown exception becomes a generic HTTP 500 ("Internal script error"), and a return-shape mismatch becomes a 500 ("…did not match its return definition").Route.To(instanceCode).Call(scriptName, params)routes to the instance's site and returns that script's result object. RouteHelper can only target an instance code (no shared-script entry point). - Template/site scripts (
ScriptGlobals/ScriptRuntimeContext) exposeDatabase.Connection(name)+Database.CachedWrite(...),Attributes[...],Children["<comp>"].Attributes/.SetAttribute/.GetAttribute,Instance.SetAttribute(string value) which routes aSetStaticAttributeCommandto the Instance Actor → for data-connected attributes the DCL performs the authorized Galaxy write, synchronously, throwing on failure. DatabaseGateway/IDatabaseGatewayis registered viaAddExternalSystemGateway()inHost/Program.cs(shared root) andHost/SiteServiceRegistration.cs. So the Central host can also open BTDB.DatabaseConnectionDefinitionhas noSiteId(global).- Reactor template =
IpsenFurnaceTitan(TemplateId 1). InstancesZ28061(id 1) andZ28062(id 2), both Site 2, Enabled. Children (TemplateCompositions on T1):LeftMESReceiver(→T5),RightMESReceiver(→T6),LeftReactorSide(→T8),RightReactorSide(→T9). No leak-test receiver. No existing TemplateScripts/SharedScripts. - MES-receiver attributes live on base template T3
MESReceiver(full set;MoveInPartNumbers+MoveInWorkOrderNumbersareList<String>; alsoMoveInType). The derived T5/T6 still carry staleStringplaceholder rows for those two array attrs — expected to beIsInheritedplaceholders skipped at flatten time (the T3Listdefinition wins); must be verified before relying on array writes. - BTDB
dbo.Machine(141 rows): match columnSAPID(nvarchar 10) → result columnCode(nvarchar 50); alsoZTag,Name. The side suffix is baked intoCode(Z28061A= T5A,Z28061B= T5B);ZTagof the A-row = bareZ28061.SAPIDis null on the present reactor rows and there are noZ28062*rows — i.e. these are partial test rows; production must haveSAPIDpopulated. The ScadaBridge instance UniqueName is the bare reactor (Z28061/Z28062).
3. Decisions
- Lookup: input value =
<SAP#>-<side>(e.g.131453-A). Strip the suffix → SAP#;SELECT Code FROM dbo.Machine WHERE SAPID=@sap; the matchedCodewith a single trailingA/Bremoved = the instance UniqueName; the suffix mapsA→Left,B→Right. - Leak test deferred: only
-A/-Bare handled now. Any other suffix (-A-LT,-B-LT, missing, unknown) →WasSuccessful=falsewith an "unsupported side/target" message. Leak test can be added later once its MES-receiver child + Galaxy reference exist. - Architecture: the inbound script does the lookup via a new scoped, read-only DB helper
on the inbound context (named connections only; no raw
DbConnection; parameterized). Single routed hop to the reactor template script. (Chosen over a config-only resolver-instance + 2-hop approach.) - Write set (Q1): the reactor script writes every provided param that maps to a receiver
attribute, except
MoveInReadyFlag(read-only gate) andSAPID(routing only).MoveInCompleteFlag/MoveInSuccessfulFlag/MoveInErrorTextare treated as PLC-owned outputs and are NOT written (even though the API accepts the first three as params). Written:MoveInMesContainerNum,MoveInBatchID,MoveInJobSequenceNumber,MoveInNumberWorkOrders,MoveInWorkOrderNumbers[],MoveInPartNumbers[],MoveInOperatorName,MoveInFlag. - Return BatchID (Q2): echo the input
MoveInBatchID(no read-back).
4. Components
A. ScadaBridge source change — inbound read-only DB helper
- New
InboundDatabaseHelperbacked byIDatabaseGateway, surface:T? QuerySingle<T>(string conn, string sql, object? args = null)andIReadOnlyList<IReadOnlyDictionary<string, object?>> Query(string conn, string sql, object? args = null). No write methods. - Add
Databaseproperty toInboundScriptContext; construct it inInboundScriptExecutorfrom a service scope (gateway is scoped). Add the helper's assembly to the scriptScriptOptions.WithReferences(...)and confirmForbiddenApiCheckerstill passes (the script references onlyDatabase.QuerySingle, neverSystem.Data). - Unit tests with a fake
IDatabaseGateway.
B. Rewrite inbound /api/IpsenMESMoveIn script (data, via management API)
Pseudocode (always returns the 3-field shape; never throws out):
try {
var raw = (Parameters["SAPID"] as string ?? "").Trim();
var dash = raw.IndexOf('-');
if (dash < 0) return new { WasSuccessful = false, ErrorText = "SAPID missing -A/-B side suffix", BatchID = 0 };
var sap = raw.Substring(0, dash);
var suf = raw.Substring(dash + 1); // "A", "B", "A-LT", ...
var side = suf == "A" ? "Left" : suf == "B" ? "Right" : null;
if (side == null) return new { WasSuccessful = false, ErrorText = $"Unsupported side '{suf}'", BatchID = 0 };
var code = Database.QuerySingle<string>("BTDB",
"SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s", new { s = sap });
if (string.IsNullOrEmpty(code)) return new { WasSuccessful = false, ErrorText = $"No machine for SAP {sap}", BatchID = 0 };
var instance = (code.EndsWith("A") || code.EndsWith("B")) ? code.Substring(0, code.Length - 1) : code;
return await Route.To(instance).Call("IpsenMoveIn", new {
side,
MoveInMesContainerNum = Parameters["MoveInMesContainerNum"],
MoveInBatchID = Parameters["MoveInBatchID"],
MoveInJobSequenceNumber= Parameters["MoveInJobSequenceNumber"],
MoveInNumberWorkOrders = Parameters["MoveInNumberWorkOrders"],
MoveInWorkOrderNumbers = Parameters["MoveInWorkOrderNumbers"],
MoveInPartNumbers = Parameters["MoveInPartNumbers"],
MoveInOperatorName = Parameters["MoveInOperatorName"],
MoveInFlag = Parameters["MoveInFlag"],
});
} catch (Exception ex) {
return new { WasSuccessful = false, ErrorText = "MoveIn failed: " + ex.Message, BatchID = 0 };
}
ParameterDefinitions / ReturnDefinition unchanged from today.
C. New template script IpsenMoveIn on T1 (data, via management API)
- On-demand (no trigger). Parameters:
side+ the written MoveIn fields. Return:{ WasSuccessful, ErrorText, BatchID }. - Logic:
var recv = Children[ (string)Parameters["side"] == "Right" ? "RightMESReceiver" : "LeftMESReceiver" ];
var batchId = Parameters.Get<int>("MoveInBatchID");
var ready = recv.Attributes["MoveInReadyFlag"]; // live Galaxy read
if (!(ready is bool b && b)) // tolerate bool/"True"/1
return new { WasSuccessful = false, ErrorText = "MoveInReadyFlag is not true", BatchID = batchId };
await recv.SetAttribute("MoveInMesContainerNum", ...);
await recv.SetAttribute("MoveInBatchID", ...);
await recv.SetAttribute("MoveInJobSequenceNumber", ...);
await recv.SetAttribute("MoveInNumberWorkOrders", ...);
await recv.SetAttribute("MoveInWorkOrderNumbers", <array-encoded>); // see §5
await recv.SetAttribute("MoveInPartNumbers", <array-encoded>);
await recv.SetAttribute("MoveInOperatorName", ...);
await recv.SetAttribute("MoveInFlag", ...);
return new { WasSuccessful = true, ErrorText = "", BatchID = batchId };
MoveInReadyFlagreadiness check tolerates the value arriving asbool,"True", or1.
5. Open implementation items (not blockers)
- Array write encoding:
SetAttribute(name, string)takes a string. Confirm how the recent multivalue-array support expects aList<String>value to be written from a script (JSON array string vs. anAttributes[name] = listpath). If arrays can't be written from a script today, add a small ScadaBridge item to support it. - Derived placeholders: verify T5/T6's stale
Stringrows for the two array attrs areIsInherited=true(skipped at flatten so the T3Listtypes apply). If they are real overrides, fix them. - SAPID format: match is exact-string; live rows store leading zeros ("002797"). Confirm MES sends the SAP in the stored format, or normalize in the lookup.
- Multiple/ambiguous SAP rows:
SELECT TOP 1; if a SAP maps to >1 row they must resolve to the same bare instance — otherwise surface a clear error.
6. Error handling / return contract
Every path returns the validated {WasSuccessful, ErrorText, BatchID}:
- success →
{true, "", batchId} - not ready →
{false, "MoveInReadyFlag is not true", batchId} - bad/missing side, unknown SAP, route/instance-not-found, write failure →
{false, "<reason>", 0}
7. Testing
- Unit:
InboundDatabaseHelper+ inbound-script DB path (fakeIDatabaseGateway); inbound parse/route logic; reactor script gate + write-set (fake instance/children). - On-box dry run:
POST /api/IpsenMESMoveInwith a known SAP+-A→ resolves toZ28061, gates whenMoveInReadyFlagfalse, writes when true (verify values land on the Galaxy receiver via the gateway, as in the earlier enrichment). Negative: bad suffix, unknown SAP, not-ready.
8. Deployment
- Implement + test Component A; build ScadaBridge.
- Rebuild + redeploy ScadaBridge Central on
wonder-app-vd03(preserveapp\data\; apply any EF migration BEFORE boot — Central is Production, no auto-migrate). - Apply Component B (inbound script update) + Component C (new T1 template script) via the management API; redeploy config so the reactor instances pick up the new script.
- Verify on-box; commit source change; keep this design doc + the implementation plan.
9. Out of scope
- Leak-test (
-LT) routing/receivers. - Writing PLC-output flags (Complete/Successful/ErrorText) from the MoveIn.
Z28062BTDB data completeness (operational data fix, not code).