Manual verification (task step 3) skipped: this worktree cannot reach a live environment to fetch the deployed DelmiaRecipeDownload script via `scadabridge api-method get`. Documents only the mandated contract per PLAN-06 Task 23; verifying the deployed script against it is deferred to whoever has live environment access. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
9.6 KiB
Design — Delmia Recipe-Download Notifier (ZB.MOM.WW.ScadaBridge.DelmiaNotifier)
Date: 2026-06-26 · Status: Approved (brainstorming) · Next: implementation plan (writing-plans)
Purpose
A compact Windows console application that DELMIA Apriso shells out to on each recipe/NC-program
download to notify the plant system that a recipe was placed at a path for a machine. It is the
modern replacement for the legacy WWNotifier (see
../former-api-specs/dnc/Delmia-Integration-API.md,
Surface B), repointed from the old Wonderware /notify receiver to the ScadaBridge Inbound API
method DelmiaRecipeDownload.
It is a strict drop-in for Delmia's existing call site: same command-line flags, same YES/NO
stdout contract, same exit-code semantics — so Delmia's invocation and output parsing are unchanged.
Key decisions (from brainstorming)
| Decision | Choice |
|---|---|
| Target | ScadaBridge Inbound API POST {baseUrl}/api/DelmiaRecipeDownload |
| Auth | X-API-Key: <key> header |
| CLI/output | Exact parity with legacy WWNotifier (flags + YES/NO + exit code) |
| Config | appsettings.json next to the exe (URLs, timeout, optional log path) |
| Secret | API key from env var SCADABRIDGE_API_KEY — never in a file |
| Packaging | Self-contained Native AOT, win-x64 (fast startup for per-invocation use) |
| Failover | Comma-list of base URLs; advance only on connect failure |
| Location | New project src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier, in ZB.MOM.WW.ScadaBridge.slnx |
| Implementation | Approach A — zero-dependency single-file; hand-rolled arg parser, System.Text.Json source-gen, raw HttpClient |
| Exe name | AssemblyName = WWNotifier (literal drop-in for Delmia's existing call path) |
Architecture
Single-purpose console app, no DI/generic host. One Program.cs orchestrates four small pieces:
- Arg parser — hand-rolled, the 6 legacy flags → an in-memory request model.
- Config loader — reads
appsettings.json(viaSystem.Text.Jsonsource generator) → POCO; reads the API key from the environment. - Notifier — builds the JSON payload and runs the failover POST loop behind a small seam (interface/delegate) so the loop + result mapping are unit-testable without real HTTP.
- Result reporter — maps the outcome to the
YES/NO+ exit-code contract (stdout) and writes diagnostics (stderr + optional log file).
Zero third-party NuGet dependencies (BCL only) to keep the Native-AOT surface minimal and trim-clean.
CLI contract (drop-in parity)
| Short | Long | Required | → payload field |
|---|---|---|---|
-d |
--downloadpath |
yes | DownloadPath |
-m |
--machine |
yes | MachineCode |
-w |
--workorder |
yes | WorkOrderNumber |
-p |
--partnumber |
yes | PartNumber |
-s |
--seqop |
no | JobStepNumber |
-u |
--username |
no | Username |
- stdout: exactly
YESon success, orNOfollowed by a reason line on failure. Nothing else is written to stdout (Delmia parses it). - exit code:
0on success,-1on failure (matches the legacyEnvironment.ExitCode). - A missing required flag →
NO+ reason, exit-1, no HTTP attempt.
Configuration & secret
appsettings.json placed next to the exe, loaded directly into a small POCO with a
JsonSerializerContext source generator (no reflection-based binder):
{
"ScadaBridge": {
"BaseUrls": "http://host-a:8085,http://host-b:8085",
"TimeoutSeconds": 30,
"LogPath": "logs/delmia-notifier.log"
}
}
BaseUrls— comma-separated failover list (legacy-style). The method path/api/DelmiaRecipeDownloadis appended by the app; each entry is a base URL only.TimeoutSeconds— per-attemptHttpClient.Timeout(default 30).LogPath— optional diagnostic log file (relative to the exe). Omit to log to stderr only.- API key comes from
SCADABRIDGE_API_KEY. If unset/empty →NO+ "API key not configured", exit-1, no attempt. The key is never read from or written to a file.
Request / response
- Per attempt:
POST {baseUrl}/api/DelmiaRecipeDownload, headerX-API-Key: <key>,Content-Type: application/json, body = the flatRecipeDownloadJSON. - DTOs (local, source-gen serializable):
RecipeDownload { MachineCode, DownloadPath, WorkOrderNumber, PartNumber, JobStepNumber, Username }RecipeDownloadResult { bool Result, string ResultText }
Failover (connect-failure only)
Try each base URL in order; advance to the next only when the attempt fails to connect (no HTTP response came back). A node that responds at all is authoritative — its answer is final.
| Attempt outcome | Failover? | Final result |
|---|---|---|
| No response — connection refused/reset, DNS failure, TLS error, or timeout | Yes → next URL | only if all URLs fail to connect → NO + last connection error, exit -1 |
HTTP 2xx + Result == true |
No — stop | YES, exit 0 |
HTTP 2xx + Result == false |
No — stop | NO + ResultText, exit -1 |
| HTTP non-2xx (401/403/4xx/5xx) | No — stop | NO + status/error, exit -1 |
Deliberate consequence: a
5xxfrom the first node is reported as a failure, not rolled over to the next node — failover is strictly for unreachable nodes. (Revisit only if operations want5xx/503to also fail over.)
Delivery semantics: at-least-once
The connect-failure-only failover loop above is by design (no code change here) — but its
consequence is at-least-once delivery of the DelmiaRecipeDownload POST, not exactly-once. Two
cases both count as "no response" from the client's point of view, trigger failover to the next
URL, and produce a duplicate POST even though the first node may have already processed the
request:
- Connection reset after the server processed the POST — the request reaches the site and is
applied, but the response never makes it back to the client (dropped connection, reset). The
client sees a connect failure (row 1 of the table above), rolls over, and re-POSTs to the next
BaseUrlsentry. - Slow response exceeding
TimeoutSeconds— the server is still processing (or has already finished) when the client'sHttpClient.Timeoutfires. The client treats the timeout the same as an unreachable node, rolls over, and re-POSTs.
Requirement: the deployed DelmiaRecipeDownload inbound method MUST be idempotent on the
natural key (MachineCode, DownloadPath, WorkOrderNumber). A duplicate POST for the same key must
return the same Result/ResultText as the original call, not double-apply side effects (e.g. not
queue the recipe download twice). This is a contract on the inbound method's script, not on the
DelmiaNotifier client — the client cannot distinguish a "duplicate delivered" retry from a
"first delivery" attempt, so idempotency must live on the receiving side. See the timeout row in
the failover table above for the triggering condition.
Error handling & logging
- stdout is reserved for the
YES/NOcontract. All diagnostics — per-URL attempt, status code, exception detail, which URL answered — go to stderr and, ifLogPathis set, an appended log file. Hand-written; no logging dependency. - Failure reasons surfaced on the
NOreason line: missing required arg, missing API key, noBaseUrlsconfigured, or "all URLs unreachable: <last error>". - The HTTP call sits behind a tiny seam so the failover/result logic is unit-tested without network.
Project layout & packaging
src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/
ZB.MOM.WW.ScadaBridge.DelmiaNotifier.csproj
Program.cs # entry, arg parse, orchestration, result reporting
Config.cs # POCO + loader
RecipeDownload.cs # request DTO
RecipeDownloadResult.cs # response DTO
NotifierJsonContext.cs # JsonSerializerContext (source gen)
appsettings.json # copied to output
.csproj:net10.0,OutputType=Exe,AssemblyName=WWNotifier,PublishAot=true,RuntimeIdentifier=win-x64,InvariantGlobalization=true, AOT/trim analyzer warnings treated as errors. Added toZB.MOM.WW.ScadaBridge.slnx.- Build note: the AOT
win-x64native exe must be published on Windows (dotnet publish -c Release -r win-x64) with the MSVC build tools present — Native AOT does not cross-compile from macOS/Linux. Manageddotnet build/dotnet teststill run cross-platform for development and CI of the logic.
Testing
tests/ZB.MOM.WW.ScadaBridge.DelmiaNotifier.Tests (xUnit), all logic-level (no AOT needed, runs
cross-platform):
- Arg parsing: all-flags, required-missing → failure, optional omitted.
- Config: comma-split of
BaseUrls, defaults (TimeoutSeconds), missing file/section. - Payload mapping: flags →
RecipeDownloadJSON (field-for-field). - Result mapping:
Result true/falseand non-2xx → correct stdout + exit code. - Failover loop (via the HTTP seam / a fake handler): connect-failure advances; first responding node
is final;
Result==falsedoes not advance; all-unreachable →NO+ last error. - Missing/empty
SCADABRIDGE_API_KEY→ fail-fast, no attempt. - Manual live smoke against
wonder-app-vd03(/api/DelmiaRecipeDownload) from the Windows build, mirroring the earliercurlverification.
Out of scope (YAGNI)
- No retry/backoff beyond the connect-failover loop; no Polly.
- No DI/generic host, no
Microsoft.Extensions.*. - No support for other inbound methods — recipe download only.
- No DPAPI/secret-store integration (env var is the agreed mechanism); revisit if required.