0008ca891c
Compact Native-AOT win-x64 console app DELMIA invokes to notify ScadaBridge of a recipe download via POST /api/DelmiaRecipeDownload (X-API-Key). Drop-in CLI/output parity with legacy WWNotifier; appsettings.json + SCADABRIDGE_API_KEY env var; comma-list base URLs with connect-failure-only failover.
159 lines
8.0 KiB
Markdown
159 lines
8.0 KiB
Markdown
# 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`](../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:
|
|
|
|
1. **Arg parser** — hand-rolled, the 6 legacy flags → an in-memory request model.
|
|
2. **Config loader** — reads `appsettings.json` (via `System.Text.Json` source generator) → POCO;
|
|
reads the API key from the environment.
|
|
3. **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.
|
|
4. **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 `YES` on success, or `NO` followed by a reason line on failure. Nothing else is
|
|
written to stdout (Delmia parses it).
|
|
- **exit code:** `0` on success, `-1` on failure (matches the legacy `Environment.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):
|
|
|
|
```json
|
|
{
|
|
"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/DelmiaRecipeDownload` is appended by the app; each entry is a base URL only.
|
|
- `TimeoutSeconds` — per-attempt `HttpClient.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`, header `X-API-Key: <key>`,
|
|
`Content-Type: application/json`, body = the flat `RecipeDownload` JSON.
|
|
- 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 `5xx` from 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 want
|
|
> `5xx`/`503` to also fail over.)
|
|
|
|
## Error handling & logging
|
|
|
|
- **stdout is reserved** for the `YES`/`NO` contract. All diagnostics — per-URL attempt, status code,
|
|
exception detail, which URL answered — go to **stderr** and, if `LogPath` is set, an appended log
|
|
file. Hand-written; no logging dependency.
|
|
- Failure reasons surfaced on the `NO` reason line: missing required arg, missing API key, no
|
|
`BaseUrls` configured, 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 to `ZB.MOM.WW.ScadaBridge.slnx`.
|
|
- **Build note:** the AOT `win-x64` native 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. Managed `dotnet build` / `dotnet test` still 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 → `RecipeDownload` JSON (field-for-field).
|
|
- Result mapping: `Result true/false` and non-2xx → correct stdout + exit code.
|
|
- Failover loop (via the HTTP seam / a fake handler): connect-failure advances; first responding node
|
|
is final; `Result==false` does **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 earlier `curl` verification.
|
|
|
|
## 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.
|