diff --git a/docs/api-specs/Delmia-Recipe-Download-API.md b/docs/api-specs/Delmia-Recipe-Download-API.md new file mode 100644 index 00000000..7b3ea914 --- /dev/null +++ b/docs/api-specs/Delmia-Recipe-Download-API.md @@ -0,0 +1,199 @@ +# DelmiaRecipeDownload — ScadaBridge Inbound API spec (as deployed) + +**Captured:** 2026-09-02 from the live **prod** management API on `wonder-app-vd03` (method id 4, +API keys, the `CvdReactor` template's `ProcessRecipeDownload` script, the `DelmiaReceiver` template, +and the inbound audit log). **QA (`vq03`/`vq04`) was NOT read** — its management API requires real +LDAP credentials (prod runs with `DisableLogin`); re-capture QA with `scadabridge api-method get --id …` +once credentials are to hand. The definition is config-as-data in the central ConfigDb (not in git), so +this file is the durable record; the legacy predecessor is documented in +[`../former-api-specs/dnc/Delmia-Integration-API.md`](../former-api-specs/dnc/Delmia-Integration-API.md) +(Surface B), and the client that calls it is the `WWNotifier.exe` drop-in +([`DelmiaNotifier`](../../src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/README.md)). + +## 1. Endpoint + +| Item | Value | +|---|---| +| Method / path | `POST /api/DelmiaRecipeDownload` | +| Auth | `X-API-Key: sbk__` **or** `Authorization: Bearer sbk__` (Authorization wins if both) | +| Content type | `application/json`, flat PascalCase object | +| Server timeout | **45 s** (`TimeoutSeconds` on the method); the site-side handshake wait inside it is **25 s** | +| Prod base URL | `http://wonder-app-vd03.zmr.zimmer.com:8085` (single central node) | +| QA base URLs | `http://10.220.158.2:8085` (vq03), `http://10.220.158.3:8085` (vq04) — list **both** in the notifier's `BaseUrls`; a standby central relays to the active peer | + +Unauthenticated / bad-key calls return **401** `{"error":"Invalid or missing API key"}` (audit kind +`InboundAuthFailure`). General Inbound API status semantics (413 body cap, 503 `STANDBY_NODE` / +`ACTIVE_NODE_UNREACHABLE`, unknown method) are in +[`Component-InboundAPI.md`](../requirements/Component-InboundAPI.md). + +## 2. Request + +Deployed `ParameterDefinitions` (JSON Schema, verbatim — note `required` is **empty**; the +script itself rejects a missing/blank `MachineCode`): + +```json +{"type":"object","properties":{ + "MachineCode":{"type":"string"}, + "DownloadPath":{"type":"string"}, + "WorkOrderNumber":{"type":"string"}, + "PartNumber":{"type":"string"}, + "JobStepNumber":{"type":"string"}, + "Username":{"type":"string"}}, + "required":[]} +``` + +| Field | Notifier flag | Meaning | Where it lands on the site | +|---|---|---|---| +| `MachineCode` | `-m` | **Routing key** — must equal an instance `UniqueName` exactly (trimmed). Prod today: `Z28061`, `Z28062`, `Z28061Sim`. | `Route.To(MachineCode)` | +| `DownloadPath` | `-d` | Path where the recipe/NC file was placed | `DelmiaReceiver.DownloadPath` | +| `WorkOrderNumber` | `-w` | Work order | `DelmiaReceiver.WorkOrderNumber` | +| `PartNumber` | `-p` | Part / item number | `DelmiaReceiver.PartNumber` | +| `JobStepNumber` | `-s` | Job step / sequence op | `DelmiaReceiver.JobStepNumber` | +| `Username` | `-u` | Operator (audit only) | `DelmiaReceiver.Username` | + +Example (a real call taken from the prod audit log): + +```bash +curl -s -X POST http://wonder-app-vd03.zmr.zimmer.com:8085/api/DelmiaRecipeDownload \ + -H "X-API-Key: $SCADABRIDGE_API_KEY" -H "Content-Type: application/json" \ + -d '{"MachineCode":"Z28061Sim","DownloadPath":"C:\\recipes\\test.nc","WorkOrderNumber":"WO-TEST-001","PartNumber":"PN-TEST","JobStepNumber":"0100","Username":"delmia-sim-test"}' +``` + +## 3. Response + +Deployed `ReturnDefinition`: `{"type":"object","properties":{"Result":{"type":"boolean"},"ResultText":{"type":"string"}},"required":["Result","ResultText"]}` + +**HTTP 200 is returned for every business outcome** — success is `Result == true`, never the status +code. `ResultText` values the deployed scripts can produce: + +| `Result` | `ResultText` | Produced by | +|---|---|---| +| `true` | echo of `DelmiaReceiver.RecipeProcessResultText` (often `""`) | Galaxy object acknowledged the download | +| `false` | `DelmiaReceiver.RecipeProcessResultText` | Galaxy object rejected the download (`RecipeProcessResult` false) | +| `false` | `MachineCode is required` | central script, blank `MachineCode` | +| `false` | `Receiver ready flag not set to true` | site script, `ReadyFlag` not true (**or unreadable — see §7**) | +| `false` | `Timeout waiting for recipe to be processed` | site script, `RecipeProcessedFlag` not true within 25 s | +| `false` | `RecipeDownload failed: ` | either script — e.g. unknown instance, site unreachable, `Script 'ProcessRecipeDownload' on instance 'X' failed: …` | + +## 4. Processing flow + +``` +Delmia ─► WWNotifier.exe ─► POST /api/DelmiaRecipeDownload (central, active node) + │ inbound method script (id 4): validate MachineCode + ▼ + Route.To(MachineCode).Call("ProcessRecipeDownload", {5 fields}) + │ gRPC central→site, lands on the instance actor + ▼ + CvdReactor.ProcessRecipeDownload (site Call script) on the composed DelmiaReceiver child: + 1. read ReadyFlag → must be true, else "Receiver ready flag not set to true" + 2. write RecipeProcessedFlag = false + 3. WriteBatchAndWaitAsync({DownloadPath, WorkOrderNumber, PartNumber, JobStepNumber, Username}, + trigger RecipeDownloadFlag = true, + wait RecipeProcessedFlag == true, timeout 25 s) + 4. read RecipeProcessResult / RecipeProcessResultText → {Result, ResultText} +``` + +Both scripts as deployed on prod (verbatim): + +
Central inbound method script (id 4, timeout 45 s) + +```csharp +try { + System.Func P = k => Parameters.ContainsKey(k) ? Parameters[k] : null; + var code = (P("MachineCode") as string ?? "").Trim(); + if (string.IsNullOrEmpty(code)) return new { Result = false, ResultText = "MachineCode is required" }; + var suf = code.Substring(code.Length - 1); + + return await Route.To(code).Call("ProcessRecipeDownload", new { + DownloadPath = P("DownloadPath"), + WorkOrderNumber = P("WorkOrderNumber"), + PartNumber = P("PartNumber"), + JobStepNumber = P("JobStepNumber"), + Username = P("Username"), + }); +} catch (Exception ex) { + return new { Result = false, ResultText = "RecipeDownload failed: " + ex.Message }; +} +``` +
+ +
Site script CvdReactor.ProcessRecipeDownload (template 1, script id 4, trigger call) + +```csharp +// Generic DELMIA recipe-download — performed DIRECTLY on the composed DelmiaReceiver child +// (mirrors how MesMoveIn writes the MESReceiver child) instead of delegating via CallScript. +// No side logic; the receiver's own DelmiaReceiver.Download method holds the same handshake. +var recv = Children["DelmiaRecv"]; +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(recv.Attributes["ReadyFlag"])) + return new { Result=false, ResultText="Receiver ready flag not set to true" }; +try { + recv.Attributes["RecipeProcessedFlag"] = false; + var data = new System.Collections.Generic.Dictionary { + { "DownloadPath", P("DownloadPath") }, + { "WorkOrderNumber", P("WorkOrderNumber") }, + { "PartNumber", P("PartNumber") }, + { "JobStepNumber", P("JobStepNumber") }, + { "Username", P("Username") }, + }; + if (!await recv.Attributes.WriteBatchAndWaitAsync(data, "RecipeDownloadFlag", true, "RecipeProcessedFlag", true, System.TimeSpan.FromSeconds(25))) + return new { Result=false, ResultText="Timeout waiting for recipe to be processed" }; + return new { + Result = IsTrue(recv.Attributes["RecipeProcessResult"]), + ResultText = recv.Attributes["RecipeProcessResultText"]?.ToString() ?? "" + }; +} catch (Exception ex) { + return new { Result=false, ResultText="RecipeDownload failed: " + ex.Message }; +} +``` +
+ +## 5. Galaxy-side contract — the `DelmiaReceiver` template (id 12; composed clone id 26) + +All ten attributes are **data-sourced over the site `MxGateway` connection**, bound per instance to the +reactor-level Galaxy object (prod: `DelmiaReceiver_038` on Z28061, `_039` on Z28062, `_037` on +Z28061Sim; `Left_00x.DelmiaReceiver` / `Right_00x.DelmiaReceiver` back the unused side compositions). + +| Attribute | Type | Direction | Role in the handshake | +|---|---|---|---| +| `ReadyFlag` | Boolean | Galaxy → SB | Precondition; must be true or the call is refused | +| `RecipeDownloadFlag` | Boolean | SB → Galaxy | **Trigger** — set true after the five data fields are written | +| `RecipeProcessedFlag` | Boolean | both | SB clears to false first; Galaxy sets true when done (waited ≤ 25 s) | +| `RecipeProcessResult` | Boolean | Galaxy → SB | Outcome → `Result` | +| `RecipeProcessResultText` | String | Galaxy → SB | Outcome text → `ResultText` | +| `DownloadPath`, `WorkOrderNumber`, `PartNumber`, `JobStepNumber`, `Username` | String | SB → Galaxy | Payload, written as one batch before the trigger | + +## 6. API keys scoped to this method (prod, names only — secrets are shown once at creation) + +`mes`, `mes api key`, `delmia-mes-receivers`, `TestKey`, `JOE_TEST_KEY` — all enabled, all also +scoped to the MES methods (`MesMoveIn`, `MesMoveOut`, `MesSampleData`, `SimpleAlarmStatusRequest`, +most also `IpsenMESMoveIn`). `delmia-mes-receivers` is the one whose name says it is the Delmia +caller's key; the audit `actor` column shows which key served each call. + +## 7. Observed behaviour and open issues (from the prod audit log, 2026-06-17 → 2026-06-26) + +Nine calls total, all test traffic (actors `delmia-mes-receivers`, `delmia-sim-test`), none since +2026-06-26 — **no production Delmia traffic has hit this method yet.** Every 200 answered +`Result=false`: mostly `Receiver ready flag not set to true`, one +`Script 'ProcessRecipeDownload' … The given key 'side' was not present` from an earlier script +revision, and one `MachineCode must end with A (Left) or B (Right)` from a retired left/right variant +(the current script routes on the whole `MachineCode`; the leftover `suf` variable is unused). + +1. **Likely broken since the 2026-06-27 rename — verify before go-live.** The site script still + addresses the child as `Children["DelmiaRecv"]`, but the `CvdReactor` composition was renamed + `DelmiaRecv` → `DelmiaReceiver` that day (`../deployment/deployment-records/2026-06-27-wonder-app-vd03.md` + §2b), so the flattened paths are `DelmiaReceiver.*`. `ChildrenAccessor` just concatenates the + name into a path and `GetAttribute` on an unknown path logs a warning and yields null, so + `IsTrue(null)` is false and **every call will answer `Receiver ready flag not set to true` + regardless of the real flag**. No call has been made since the rename, so this is inferred from + the code, not observed. Fix = change that one line to `Children["DelmiaReceiver"]` on the + template (`scadabridge template script update`) and redeploy the three CvdReactor instances; + check QA's copy of the script for the same string. +2. **Not idempotent.** The design requires idempotency on `(MachineCode, DownloadPath, WorkOrderNumber)` + because the notifier retries on timeout/connect failure (at-least-once). The deployed script + re-runs the full handshake on a duplicate, so a retry after a slow first attempt will trigger a + second download on the Galaxy object. +3. `required` is empty in the parameter schema; only `MachineCode` is enforced, in script. Missing + payload fields are written to the receiver as null/empty. +4. QA definitions unread (credentials) — assumed identical (same build lineage), not verified. diff --git a/docs/api-specs/README.md b/docs/api-specs/README.md new file mode 100644 index 00000000..2261dc0d --- /dev/null +++ b/docs/api-specs/README.md @@ -0,0 +1,9 @@ +# API Specs (as deployed) + +Point-in-time captures of ScadaBridge's **own** externally-called contracts — Inbound API methods and +the site-side handshakes behind them — pulled from a live environment's management API. Method +definitions are config-as-data in the central ConfigDb (not source-controlled), so these files are the +durable record; each states which environment it was captured from and when. Legacy/predecessor +system specs live in [`../former-api-specs/`](../former-api-specs). + +- [`Delmia-Recipe-Download-API.md`](Delmia-Recipe-Download-API.md) — `POST /api/DelmiaRecipeDownload` (DELMIA Apriso recipe-download notification), prod `wonder-app-vd03`, 2026-09-02. diff --git a/docs/former-api-specs/README.md b/docs/former-api-specs/README.md index 6a27fea0..848e9099 100644 --- a/docs/former-api-specs/README.md +++ b/docs/former-api-specs/README.md @@ -16,3 +16,5 @@ this folder holds only the **outgoing/legacy** systems' specs. Wonderware recipe-download notification (`~/Desktop/delmiaintegration`). Drop the relevant spec documents into the matching subfolder. + +ScadaBridge's **own** externally-called contracts, captured as deployed, live in [`../api-specs/`](../api-specs). diff --git a/docs/former-api-specs/dnc/Delmia-Integration-API.md b/docs/former-api-specs/dnc/Delmia-Integration-API.md index 5aad37e3..763f3386 100644 --- a/docs/former-api-specs/dnc/Delmia-Integration-API.md +++ b/docs/former-api-specs/dnc/Delmia-Integration-API.md @@ -203,4 +203,5 @@ never implemented. Shapes, for reference: `MachineInfo` = `MachineKey`/`MachineI call if pulled into ScadaBridge). This file documents the **legacy** `delmiaintegration` contracts for reference/parity during that -migration. +migration. The **as-deployed ScadaBridge contract** (method definition, site handshake, keys, observed +calls) is captured in [`../../api-specs/Delmia-Recipe-Download-API.md`](../../api-specs/Delmia-Recipe-Download-API.md).