diff --git a/mes-delmia-integration-api.md b/mes-delmia-integration-api.md new file mode 100644 index 0000000..58acbc8 --- /dev/null +++ b/mes-delmia-integration-api.md @@ -0,0 +1,377 @@ +# MES and Delmia-DNC integrations — API & MXAccess write specification + +Documents two existing Wonderware integrations hosted on the `ZimmerBiomet` Gitea org +(`http://wonder-app-vd03.zmr.zimmer.com:3000`): + +| Integration | Repo | What it does | Who does the MXAccess write | +|---|---|---|---| +| **MES** | [`ZimmerBiomet/MESAPI`](http://wonder-app-vd03.zmr.zimmer.com:3000/ZimmerBiomet/MESAPI) (solution `WWSupport`) | REST API the Camstar MES calls to move-in / move-out work orders against a machine, and to read machine alarm status | **The service itself** (`MesNotifier`, in-repo) | +| **Delmia DNC** | [`ZimmerBiomet/DelmiaIntegration`](http://wonder-app-vd03.zmr.zimmer.com:3000/ZimmerBiomet/DelmiaIntegration) | Pull an NC/recipe document from the DELMIA/Apriso (Intercim) DNC server and push the resulting recipe-download notification into Wonderware | **An external receiver service** at `wonder-app-vd01:9001/notify`; the actual flag handshake is implemented in the Galaxy `$DelmiaReceiver` object (`ProcessRecipe`/`Reset` scripts) — *not* in this repo | + +Both integrations talk to AVEVA System Platform ("Galaxy") through **MXAccess COM** +(`ArchestrA.MxAccess.LMXProxyServerClass`) and use the same general pattern: + +> **Handshake pattern** — read a *ready* flag → write the data tags → set a *trigger* flag +> → wait (bounded by a timeout) for a *complete* flag → read a *success* flag + *error text* +> → return the result and unsubscribe. + +All facts below are taken verbatim from source at the repo `master` branch (cloned +2026-06-17) unless explicitly marked **(inferred)**. + +--- + +## 1. MES integration — `MESAPI` / `WWSupport` + +### 1.1 Topology & hosting + +``` +Camstar MES ──HTTPS/JSON──▶ WWSupport API (ServiceStack self-host) + │ ├─ SQL Server "BT" (machine lookup by SAPID, alarm catalog) + │ └─ MXAccess COM (LMXProxyServerClass, client "MesNotifier") + ▼ + Galaxy object {MachineCode}.MesReceiver.* (move-in / move-out tags) + Galaxy object {MachineCode}.{AlarmName}.* (alarm attributes) +``` + +- **Framework:** ServiceStack, self-hosted via `AppSelfHostBase` (`AppHost : base("APIServer", typeof(MesServices).Assembly)`), .NET Framework, run as a Windows service. +- **Listen URL (per environment, `App.config` → `HttpListener`):** + - DEV `http://*:9501/` · QA `http://*:9500/` · PROD `http://*:9500/` +- **Database (`App.config` → connection `BatchDB`, DB `BT`):** + - DEV `wonder-sql-vd01.zmr.zimmer.com` · QA `wondersqlqa.zmr.zimmer.com` · PROD (same form). User `wonderapp`. +- **Auth:** every operation is decorated `[Authenticate]` + `[RequiredRole("MESAPI")]` (`MesServices.cs`). + `AppHost` registers an `AuthFeature` with two providers: `ApiKeyAuthProvider` and `LdapAuthProvider`. + - Unauthenticated → **401**; authenticated without the `MESAPI` role → **403**. +- **Serialization:** `JsConfig.IncludeNullValues = true` (null fields ARE emitted in JSON). `PostmanFeature` + `OpenApiFeature` (Swagger) are enabled. +- **MES counterpart object:** the live Galaxy attribute listing for the receiver object is in + [`mesrec.md`](mesrec.md) (`$MESReceiver` template). Note the API binds tags under the contained + name `MesReceiver` (i.e. `{MachineCode}.MesReceiver.`). + +### 1.2 Endpoints (inputs / outputs) + +Routes come from `[Route(...)]` on the request DTOs; all are **POST**, JSON in / JSON out, handled by `MesServices.Any(...)` which resolves a per-request `MesNotifier`. + +#### `POST /mes/movein` → `MoveInResponse` + +Request `MoveInRequest`: + +| Field | Type | Notes | +|---|---|---| +| `SAPID` | string | machine key; looked up in `BT.Machine` to get `Machine.Code` | +| `OperatorName` | string | | +| `JobSequenceNumber` | string | | +| `WorkOrders` | `List` | each = `{ WorkOrderNumber: string, PartNumber: string }` | + +Response `MoveInResponse`: `WasSuccessful` (bool), `ErrorText` (string), `BatchID` (int?, only set if machine returns non-zero). + +#### `POST /mes/moveout` → `MoveOutResponse` + +Request `MoveOutRequest`: `SAPID` (string), `OperatorName` (string), `WorkOrders` (`List`). +*(Move-out has no `JobSequenceNumber`.)* +Response `MoveOutResponse`: identical shape to `MoveInResponse` (`WasSuccessful`, `ErrorText`, `BatchID`). + +#### `POST /mes/alarmstatus` → `AlarmStatusResponse` + +Request `AlarmStatusRequest`: +- `MachineFilter` = `{ MachineID: int?, SAPID: string, ZTag: string, Code: string }` (any one identifies the machine) +- `AlarmFilter` = `{ NameFilter: string, MinSeverity: int?, MaxSeverity: int?, IncludeTriggered: bool=true, IncludeAcked: bool=true, FlaggedOnly: bool=false }` + +#### `POST /mes/simplealarmstatus` → `AlarmStatusResponse` + +Request `SimpleAlarmStatusRequest`: `SAPID` (string). Internally loads only alarms with `FlaggedForMES == true` for that machine. + +Response `AlarmStatusResponse` (both alarm endpoints): `WasSuccessful` (bool), `ErrorText` (string), `Alarms` (`List`). + +`AlarmInfo`: `Name` (string), `HierarchicalName` (string, `{Code}.{AlarmName}`), `Description` (string), `IsFlaggedForMES` (bool), `Severity` (int), `StatusCode` (string — `"Triggered"` or `"Triggered.Acked"`), `TriggeredDT` (DateTime), `AckDT` (DateTime?), `AckComment` (string). + +### 1.3 MXAccess connection model + +`MesNotifier` owns one MXAccess proxy for the request: + +```csharp +_lmxProxy = new ArchestrA.MxAccess.LMXProxyServerClass(); +_lmxHandle = _lmxProxy.Register("MesNotifier"); +_lmxProxy.OnDataChange += ...; // value updates resolve pending read/OnValue tasks +_lmxProxy.OnWriteComplete += ...; // write acks resolve pending write tasks +``` + +Tags are added with `AddItem` + `AdviseSupervisory` (subscribe), updated via `OnDataChange`, +and removed with `UnAdvise` + `RemoveItem` on cleanup. Reads/writes are wrapped as `Task` +that complete on the corresponding callback or **fail (`false`) on cancellation/timeout**. +A read is considered valid only if MXAccess **quality == 192** ("good"). + +**Target selection:** request `SAPID` → `db.Single(x => x.SAPID == SAPID)` → `Machine.Code` +becomes the tag prefix. Move tags live under `{Code}.MesReceiver.*`; alarm tags under +`{Code}.{MachineAlarm.Name}.*` (alarm catalog from `db.Select(...)`). + +### 1.4 Tag mappings + +**Move-in** (`MesMoveInTagset`, all `{Code}.MesReceiver.`): + +| Tag | Type | Dir | Role | Source field | +|---|---|---|---|---| +| `MoveInReadyFlag` | bool | read | gate — must be `true` before writing | — | +| `MoveInFlag` | bool | **write** | **trigger** — set `true` last | — | +| `MoveInCompleteFlag` | bool | read | completion — handshake waits on this | — | +| `MoveInSuccessfulFlag` | bool | read | result | → `response.WasSuccessful` | +| `MoveInErrorText` | string | read | result | → `response.ErrorText` | +| `MoveInBatchID` | int | read | result | → `response.BatchID` (if ≠ 0) | +| `MoveInOperatorName` | string | write | data | `request.OperatorName` | +| `MoveInJobSequenceNumber` | string | write | data | `request.JobSequenceNumber` | +| `MoveInNumberWorkOrders` | int | write | data | `request.WorkOrders.Count` | +| `MoveInWorkOrderNumbers[]` | string[] | write | data (fixed length 50) | `WorkOrders.Select(w => w.WorkOrderNumber)` | +| `MoveInPartNumbers[]` | string[] | write | data (fixed length 50) | `WorkOrders.Select(w => w.PartNumber)` | + +**Move-out** (`MesMoveOutTagset`): identical set with `MoveOut` prefix, **minus** `JobSequenceNumber` +(`MoveOutReadyFlag`, `MoveOutFlag`, `MoveOutCompleteFlag`, `MoveOutSuccessfulFlag`, `MoveOutErrorText`, +`MoveOutBatchID`, `MoveOutOperatorName`, `MoveOutNumberWorkOrders`, `MoveOutWorkOrderNumbers[]`, `MoveOutPartNumbers[]`). + +**Alarms** (`AlarmTagset`, all `{Code}.{AlarmName}.`): `Quality` (int), `InAlarm` (bool), +`TimeAlarmOn` (DateTime), `DescAttrName` (string), `Acked` (bool), `TimeAlarmAcked` (DateTime?), `AckMsg` (string). + +### 1.5 The handshake — `MesNotifier.MoveIn` (move-out is identical with `MoveOut*` tags) + +Whole operation is bounded by **`new CancellationTokenSource(30000)` = 30 s**. + +1. **Look up machine** by `SAPID`. Not found → `WasSuccessful=false`, `ErrorText="Failed to find machine with SAPID '{SAPID}'"`, return. +2. **Subscribe** to every move-in tag (`Advise(t, cts)`), `await Task.WhenAll(...)`. Any subscription that fails / quality ≠ 192 → `ErrorText="Failed to connect to machine"`. +3. **Check ready flag:** `if (!MoveInReadyFlag.Value)` → `ErrorText="Machine move in ready flag not set to true"`, stop. +4. **Arm completion watch:** `Task flagTask = MoveInCompleteFlag.OnValue(true, cts);` (completes when the flag goes `true`, or `false` on the 30 s timeout). +5. **Write data + trigger (in parallel, trigger last):** `MoveInOperatorName`, `MoveInJobSequenceNumber`, `MoveInNumberWorkOrders`, `MoveInPartNumbers[]` (padded to 50), `MoveInWorkOrderNumbers[]` (padded to 50), then `MoveInFlag = true`. `await Task.WhenAll(writeTasks)`; any write `!= true` → `ErrorText="Failed to write move in information to machine"`. +6. **Wait for completion:** `await Task.WhenAll(flagTask)`. + - `flagTask.Result == true` → read results: `WasSuccessful = MoveInSuccessfulFlag.Value`, `ErrorText = MoveInErrorText.Value`, `BatchID = MoveInBatchID.Value` (if ≠ 0). + - `flagTask.Result == false` (timed out) → `WasSuccessful=false`, `ErrorText="Timeout waiting for move in information to be processed"`. +7. **Cleanup:** `Tags.ForEach(Unadvise)` and return. + +```csharp +using (var cts = new CancellationTokenSource(30000)) { // 30 s budget + ... + if (!moveInTagset.MoveInReadyFlag.Value) { /* not-ready error */ } + var flagTask = moveInTagset.MoveInCompleteFlag.OnValue(true, cts); // arm completion watch + var writeTasks = new List> { + Write(moveInTagset.MoveInOperatorName.Handle, request.OperatorName, cts), + Write(moveInTagset.MoveInJobSequenceNumber.Handle, request.JobSequenceNumber, cts), + Write(moveInTagset.MoveInNumberWorkOrders.Handle, request.WorkOrders.Count, cts), + Write(moveInTagset.MoveInPartNumbers.Handle, request.WorkOrders.Select(wo => wo.PartNumber).ToFixedLength(50), cts), + Write(moveInTagset.MoveInWorkOrderNumbers.Handle, request.WorkOrders.Select(wo => wo.WorkOrderNumber).ToFixedLength(50), cts), + Write(moveInTagset.MoveInFlag.Handle, true, cts) // TRIGGER — set last + }; + await Task.WhenAll(writeTasks); + await Task.WhenAll(flagTask); + if (flagTask.Result) { + response.WasSuccessful = moveInTagset.MoveInSuccessfulFlag.Value; + response.ErrorText = moveInTagset.MoveInErrorText.Value; + if (moveInTagset.MoveInBatchID.Value != 0) response.BatchID = moveInTagset.MoveInBatchID.Value; + } else { + response.WasSuccessful = false; + response.ErrorText = "Timeout waiting for move in information to be processed"; + } + moveInTagset.Tags.ForEach(Unadvise); +} +``` + +> There is **no busy-poll loop**: completion is event-driven via the MXAccess `OnDataChange` +> callback; the 30 s `CancellationTokenSource` is the only timeout. + +### 1.6 Alarm-status path + +1. Resolve machine from `MachineFilter` (`SAPID` / `Code` / `ZTag` / `MachineID`) — wrong/missing → error. +2. Load `MachineAlarm` rows for the machine; apply filters (`FlaggedOnly`, `MinSeverity`, `MaxSeverity`, case-insensitive `NameFilter.Contains`). *(`IncludeTriggered` is read but not used in the filter.)* +3. Subscribe + read each alarm's `Quality` and `InAlarm` (30 s budget). Bad quality / read failure → `ErrorText="Failed to read machine alarm status"`. +4. For alarms where `InAlarm == true`, additionally read `TimeAlarmOn`, `DescAttrName`, `Acked`, `TimeAlarmAcked`, `AckMsg`. +5. Build `AlarmInfo` per triggered alarm; `StatusCode = "Triggered.Acked"` if acked else `"Triggered"`. If `AlarmFilter.IncludeAcked == false`, acked alarms are skipped. +6. Unsubscribe; on failure `Alarms` is cleared. + +### 1.7 Outputs / error handling (MES) + +- **Transport status is always 200** for handled responses — success/failure is carried by the body's `WasSuccessful` flag + `ErrorText`. (401/403 only from the auth layer.) +- Success: `{ "WasSuccessful": true, "ErrorText": null, "BatchID": }`. +- Failure/timeout: `{ "WasSuccessful": false, "ErrorText": "", "BatchID": null }`. +- Distinct `ErrorText` values: machine-not-found, "Failed to connect to machine", "…ready flag not set to true", "Failed to write … to machine", "Timeout waiting for … to be processed", "Failed to read machine alarm status". + +--- + +## 2. Delmia-DNC integration — `DelmiaIntegration` (+ Galaxy `$DelmiaReceiver`) + +### 2.1 Topology — three hops + +``` +Operator (DelmiaIntegration.exe WinForms) + │ ① DelmiaClient ──HTTP POST (form-url-encoded)──▶ DELMIA/Apriso DNC "Downloader.asmx" + │ (e.g. http://dnc-app-vd01.zmr.zimmer.com/IntercimService/Downloader.asmx) + │ ◀── XML (SearchResults / DownloadResult, ns http://intercim.com/ruleset) ── + │ ② recipe file written to disk; WWNotifier.exe launched with CLI args + ▼ +WWNotifier.exe ──HTTP POST (JSON RecipeDownload)──▶ WW receiver service (http://wonder-app-vd01:9001/notify) + ◀── JSON RecipeDownloadResult ── │ ③ MXAccess COM write + ▼ + Galaxy object {machine}.$DelmiaReceiver.* (recipe tags + flags) + + ArchestrA scripts ProcessRecipe / Reset +``` + +**Assemblies in the repo:** `DelmiaContracts` (XML DTO library), `DelmiaIntegration` (`DelmiaClient` + WinForms UI), +`WWNotifier.exe` (console notifier), plus test harnesses (`AdminTestUtil`, `DownloadTestUtil`, `TestUI`). + +> **Scope note.** Hops ① and ② are fully in this repo. Hop ③ — the service at `:9001/notify` +> that actually performs the MXAccess write — is **not** in the `ZimmerBiomet` Gitea org; only the +> JSON contract (below) and the Galaxy-side `$DelmiaReceiver` object (scripts + attributes, exported +> under `AA_EXPORT/`) are available. `WWNotificationSystem` *also* uses MXAccess but is an unrelated +> tag→email alerting service (port `:9876`, client name `WWNotifierMonitor`) — **not** the recipe receiver. + +### 2.2 DNC server interface (`DelmiaClient`, hop ①) + +- **Transport:** `HttpClient.PostAsync` with `FormUrlEncodedContent`; response is XML deserialized + with `XmlSerializer`. Base URL is `DelmiaClient.URL`; per-call `Timeout` default **30 s**. + Action is appended to the base URL (`URL.TrimEnd('/') + "/"`). +- **Base URL (from `AdminTestUtil` `DefaultURL`):** `http://dnc-app-vd01.zmr.zimmer.com/IntercimService/Downloader.asmx`. +- On any exception the client returns a result object with the error in `ErrorMessage` / + `TransferSuccessful=false` (it does not throw to the caller). + +| Method (sync + `…Async`) | POST action | Form fields | Returns | +|---|---|---|---| +| `Search` | `/Search` | `username, machineID, partNumber, operationNumber` | `SearchResults` | +| `RequestProvenDocument` | `/RequestProvenDocument` | `username, machineID, partNumber, operationNumber, workOrderNumber` | `DownloadResult` | +| `RequestDocument` | `/RequestDocument` | `username, machineID, partNumber, operationNumber, workOrderNumber, documentKey` | `DownloadResult` | + +DTO field lists (`DelmiaContracts`, XML namespace `http://intercim.com/ruleset`): + +- **`SearchResults`**: `Results` (`List`), `ErrorMessage` (string). +- **`SearchResult`**: `ShopOrderKey` (int), `ShopOrderID` (string), `ShopOrderStatus` (string), + `ShopOrderOperKey` (int), `ShopOrderOperID` (string), `ShopOrderOperStatus` (string), + `DocumentKey` (int), `DocumentObjectID` (int, with `…Specified` flag), `DocumentName` (string), + `DocumentRev` (string), `DocumentStatus` (string), `DocumentURL` (string), `PartID` (string), `PartRev` (string). +- **`DownloadResult`**: `UserKey` (int), `UserName` (string), `UserSite` (string), `MachineKey` (int), + `MachineID` (string), `MachineSite` (string), `WorkOrderNumber` (string), `ShopOrderKey` (int), + `ShopOrderID` (string), `ShopOrderStatus` (string), `ShopOrderOperKey` (int), `ShopOrderOperID` (string), + `ShopOrderOperStatus` (string), `DocumentKey` (int), `DocumentName` (string), `DocumentRev` (string), + `DocumentStatus` (string), `PartID` (string), `PartRev` (string), **`TransferSuccessful` (bool)**, + **`ErrorMessage` (string)**. +- **`MachineInfo`** (contract present; not called by current code): `MachineKey` (int), `MachineID` (string), + `MachineName` (string), `DownloadPath` (string), `MachineDescription` (string), `MachineSite` (string), `MachineStatus` (string). +- **`UserInfo`**: `UserKey` (int), `UserName` (string), `UserSite` (string), `IsActive` (bool). + +**Recipe file:** the downloaded document is a key/value recipe file parsed by `DelmiaIntegration/Models/RecipeSet.cs` +(`KEY,VALUE` lines; typed accessors `GetString/GetInt/GetBool/GetFloat/GetDouble/...`). It is written to disk; +its path is what gets handed to `WWNotifier` (`--downloadpath`). + +### 2.3 WWNotifier (hop ②) — invocation & handoff contract + +`WWNotifier.exe` (uses `CommandLineParser`); CLI options (`CommandLineOptions`): + +| Short | Long | Required | Field | +|---|---|---|---| +| `-d` | `--downloadpath` | yes | `DownloadPath` (recipe file path) | +| `-m` | `--machine` | yes | `MachineCode` | +| `-w` | `--workorder` | yes | `WorkOrderNumber` | +| `-p` | `--partnumber` | yes | `PartNumber` | +| `-s` | `--seqop` | no | `JobStepNumber` | +| `-u` | `--username` | no | `Username` | + +Config (`WWNotifier/App.config`): `NotifyURL = http://wonder-app-vd01.zmr.zimmer.com:9001/notify` +(comma-separated list allowed — tried in order until one succeeds), `NotifyTimeout = 30` (seconds, +applied as the global Flurl HTTP timeout). + +Handoff (Flurl): `url.PostJsonAsync(recipeDownload).ReceiveJson()`. + +- **Request body `RecipeDownload`** (JSON): `MachineCode`, `DownloadPath`, `WorkOrderNumber`, `PartNumber`, `JobStepNumber`, `Username` (all string). +- **Response body `RecipeDownloadResult`** (JSON): `Result` (bool), `ResultText` (string). +- **Outputs:** prints `YES` and exit code `0` on success; prints `NO` + a message and sets exit code `-1` + on failure (parse error, missing `NotifyURL`/`NotifyTimeout`, `Result==false`, or HTTP exception). + *(Caveat: on a caught exception it logs `error.InnerException.Message`, which throws a NRE when there is no inner exception — so bare transport errors surface only as a generic failure.)* + +### 2.4 MXAccess write — Galaxy `$DelmiaReceiver` object (hop ③) + +The receiver service maps `RecipeDownload` fields onto the `$DelmiaReceiver` object instance selected by +`MachineCode`. Object attributes (from `AA_EXPORT/.../$DelmiaReceiver` export) and their roles: + +| Attribute | Type | Role | Maps from | +|---|---|---|---| +| `ReadyFlag` | Boolean | gate — receiver expects `true` before writing | — | +| `DownloadPath` | String | data | `RecipeDownload.DownloadPath` | +| `WorkOrderNumber` | String | data | `RecipeDownload.WorkOrderNumber` | +| `PartNumber` | String | data | `RecipeDownload.PartNumber` | +| `JobStepNumber` | String | data | `RecipeDownload.JobStepNumber` | +| `Username` | String | data | `RecipeDownload.Username` | +| `RecipeDownloadFlag` | Boolean | **trigger** — set `true` to start processing | — | +| `RecipeProcessedFlag` | Boolean | completion — handshake waits on this | — | +| `RecipeProcessResult` | Boolean | result | → `RecipeDownloadResult.Result` | +| `RecipeProcessResultText` | String | result | → `RecipeDownloadResult.ResultText` | + +*(`MachineCode` selects which receiver instance; it is not itself a written attribute.)* + +**Galaxy-side handshake (authoritative — ArchestrA scripts on `$DelmiaReceiver`):** + +`ProcessRecipe` (runs when `RecipeDownloadFlag` is set): +``` +Me.RecipeDownloadFlag = false; ' clear trigger +Me.ReadyFlag = false; ' clear ready +try + Me.RecipeProcessResult = true; + Me.RecipeProcessResultText = "Success"; +catch + Me.RecipeProcessResult = false; + Me.RecipeProcessResultText = "Failed to read recipe file"; +endtry; +Me.RecipeProcessedFlag = true; ' signal completion +``` + +`Reset` (clears the slot for the next download): +``` +Me.RecipeDownloadFlag = false; Me.RecipeProcessedFlag = false; +Me.RecipeProcessResult = false; Me.RecipeProcessResultText = ""; +Me.DownloadPath = ""; Me.WorkOrderNumber = ""; Me.PartNumber = ""; +Me.JobStepNumber = ""; Me.Username = ""; +``` + +**Receiver-side sequence (inferred** — mirrors the MES handshake and is driven by the flags above; the C# source is not in the repo**):** +1. resolve the `$DelmiaReceiver` instance from `MachineCode`; +2. (optionally) verify `ReadyFlag == true`; +3. write `DownloadPath`, `WorkOrderNumber`, `PartNumber`, `JobStepNumber`, `Username`; +4. set `RecipeDownloadFlag = true` (trigger) → Galaxy `ProcessRecipe` fires; +5. wait for `RecipeProcessedFlag == true`, bounded by the request timeout; +6. read `RecipeProcessResult` → `Result`, `RecipeProcessResultText` → `ResultText`; return the `RecipeDownloadResult` JSON; +7. `Reset` the object. + +### 2.5 Outputs / error handling (Delmia) + +- **DNC server call:** failures are swallowed into the returned DTO — `SearchResults.ErrorMessage`, or + `DownloadResult.TransferSuccessful=false` + `ErrorMessage="Failed to call Delmia web service at ''."`. +- **Notify handoff:** `RecipeDownloadResult.Result` (bool) + `ResultText` (string); `WWNotifier` exit code + `0` (`YES`) / `-1` (`NO`). +- **Galaxy script:** `RecipeProcessResultText` is `"Success"` or `"Failed to read recipe file"`. + +--- + +## 3. Side-by-side summary + +| | MES (`MESAPI`) | Delmia DNC (`DelmiaIntegration`) | +|---|---|---| +| Caller | Camstar MES (HTTP/JSON) | Operator UI → DELMIA DNC server, then WWNotifier | +| API style | ServiceStack REST, `POST /mes/*` | DNC = form-url-encoded → XML; notify = JSON POST | +| Who writes MXAccess | the service (`MesNotifier`, in-repo) | external `:9001/notify` receiver (source not in repo) | +| MXAccess client | `LMXProxyServerClass`, register `"MesNotifier"` | `LMXProxyServerClass` (receiver), Galaxy `$DelmiaReceiver` scripts | +| Target object | `{MachineCode}.MesReceiver.*` | `{MachineCode}.$DelmiaReceiver` instance | +| Ready / trigger / complete | `MoveInReadyFlag` / `MoveInFlag` / `MoveInCompleteFlag` | `ReadyFlag` / `RecipeDownloadFlag` / `RecipeProcessedFlag` | +| Result / error | `MoveInSuccessfulFlag` / `MoveInErrorText` (+`MoveInBatchID`) | `RecipeProcessResult` / `RecipeProcessResultText` | +| Timeout | 30 s (`CancellationTokenSource(30000)`), event-driven | 30 s HTTP (`NotifyTimeout`); Galaxy wait at receiver | + +--- + +## 4. Sources & open gaps + +**Repos (Gitea `ZimmerBiomet`, `master`):** +- MES: `MESAPI` — `APIServer.ServiceInterface/MesServices.cs`, `MesNotifier.cs`, + `Mes{MoveIn,MoveOut}Tagset.cs`, `AlarmTagset.cs`, `Tag.cs`/`OnValueTask.cs`; + `APIServer.ServiceModel/Types/*`; `APIServer/AppHost.cs`, `App.config`. +- Delmia: `DelmiaIntegration` — `DelmiaIntegration/DelmiaClient.cs`, `Models/RecipeSet.cs`; + `WWNotifier/Program.cs`, `CommandLineOptions.cs`, `Models/RecipeDownload(Result).cs`, `App.config`; + `DelmiaContracts/*`. + +**Galaxy export (`~/Desktop/AA_EXPORT/EXTRACTED/$DelmiaReceiver`):** `scripts/ProcessRecipe.txt`, +`scripts/Reset.txt`, `$DelmiaReceiver.top_level_attributes.csv`. MES receiver object attributes: [`mesrec.md`](mesrec.md). + +**Open gaps / to verify against the box:** +1. The Delmia recipe **`/notify` receiver service** source (the actual MXAccess writer at `wonder-app-vd01:9001`) + was not found in the Gitea org — §2.4 receiver steps are inferred from the contract + Galaxy scripts. +2. MES tag prefix in code is `{Code}.MesReceiver.*`, while the live probe in `mesrec.md` shows a top-level + `MESReceiver_002` instance — confirm the exact contained-name/instance convention on the live Galaxy. +3. PROD `HttpListener`/DB host values should be read from the deployed `App.config`, not assumed. diff --git a/mesrec.md b/mesrec.md new file mode 100644 index 0000000..60be9c5 --- /dev/null +++ b/mesrec.md @@ -0,0 +1,77 @@ +# MESReceiver object — attributes (wonder-app-vd03 Galaxy) + +Source: live AVEVA Galaxy DB **`ZB`** on **wonder-app-vd03** (the MxAccessGateway box), +read via the gateway's own `AttributesSql` (recursive `deployed_package_chain` over +`dynamic_attribute` / `attribute_definition`) run with `sqlcmd` over ssh — `2026-06-16`. + +## Instance probed +- **TagName:** `MESReceiver_002` (GobjectId `5909`, ParentGobjectId `5908`, HostedBy `5049`) +- **TemplateChain:** `$MESDemo.MESReceiver` → `$MESReceiver` → `$gUserDefined` → `$UserDefined` +- **CategoryId:** 10, `IsArea = false` +- There are many MESReceiver instances on this Galaxy (1,253 `MESReceiver` references in the + `galaxy-snapshot.json` hierarchy cache). `MESReceiver_002` is representative of the template. + +> Note: the cached `galaxy-snapshot.json` (gateway Server dir) holds the **object hierarchy only** +> — it carries **no attributes**, so attribute discovery requires the DB query (or gRPC `galaxy-discover`). + +## Container number +There is **no attribute literally named `ContainerNumber`**. The MES container number is carried by: +- **`MoveInMesContainerNum`** — String (move-in interface) +- **`MoveOutMesContainerNum`** — String (move-out interface) + +Full tag references: `MESReceiver_002.MoveInMesContainerNum`, `MESReceiver_002.MoveOutMesContainerNum`. + +## MES interface UDAs (attribute-category 10, security-classification 1 = secured/operate) + +### Move-In +| Attribute | Type | Full tag reference | +|---|---|---| +| **MoveInMesContainerNum** | String | `MESReceiver_002.MoveInMesContainerNum` | +| MoveInBatchID | Integer | `MESReceiver_002.MoveInBatchID` | +| MoveInJobSequenceNumber | String | `MESReceiver_002.MoveInJobSequenceNumber` | +| MoveInNumberWorkOrders | Integer | `MESReceiver_002.MoveInNumberWorkOrders` | +| MoveInWorkOrderNumbers | String[] | `MESReceiver_002.MoveInWorkOrderNumbers[]` | +| MoveInPartNumbers | String[] | `MESReceiver_002.MoveInPartNumbers[]` | +| MoveInOperatorName | String | `MESReceiver_002.MoveInOperatorName` | +| MoveInFlag | Boolean | `MESReceiver_002.MoveInFlag` | +| MoveInReadyFlag | Boolean | `MESReceiver_002.MoveInReadyFlag` | +| MoveInCompleteFlag | Boolean | `MESReceiver_002.MoveInCompleteFlag` | +| MoveInSuccessfulFlag | Boolean | `MESReceiver_002.MoveInSuccessfulFlag` | +| MoveInErrorText | String | `MESReceiver_002.MoveInErrorText` | + +### Move-Out (symmetric; no JobSequenceNumber) +| Attribute | Type | Full tag reference | +|---|---|---| +| **MoveOutMesContainerNum** | String | `MESReceiver_002.MoveOutMesContainerNum` | +| MoveOutBatchID | Integer | `MESReceiver_002.MoveOutBatchID` | +| MoveOutNumberWorkOrders | Integer | `MESReceiver_002.MoveOutNumberWorkOrders` | +| MoveOutWorkOrderNumbers | String[] | `MESReceiver_002.MoveOutWorkOrderNumbers[]` | +| MoveOutPartNumbers | String[] | `MESReceiver_002.MoveOutPartNumbers[]` | +| MoveOutOperatorName | String | `MESReceiver_002.MoveOutOperatorName` | +| MoveOutFlag | Boolean | `MESReceiver_002.MoveOutFlag` | +| MoveOutReadyFlag | Boolean | `MESReceiver_002.MoveOutReadyFlag` | +| MoveOutCompleteFlag | Boolean | `MESReceiver_002.MoveOutCompleteFlag` | +| MoveOutSuccessfulFlag | Boolean | `MESReceiver_002.MoveOutSuccessfulFlag` | +| MoveOutErrorText | String | `MESReceiver_002.MoveOutErrorText` | + +## Standard ArchestrA `$UserDefined` / system attributes (also present) +`AlarmCntsBySeverity[]`, `AlarmCntsBySeverityEnableShelved[]`, `AlarmInhibit`, `AlarmMode`, +`AlarmModeCmd`, `AlarmMostUrgentAcked`, `AlarmMostUrgentInAlarm`, `AlarmMostUrgentMode`, +`AlarmMostUrgentSeverity`, `AlarmMostUrgentShelved`, `AliasName`, `Area`, `CmdData`, `CodeBase`, +`ConfigVersion`, `ContainedName`, `Container`, `Errors[]`, `ExecutionRelatedObject`, +`ExecutionRelativeOrder`, `Extensions`, `HierarchicalName`, `Host`, `InAlarm`, `MinorVersion`, +`PropagatedAlarmInhibit`, `ScanState`, `ScanStateCmd`, `SecurityGroup`, `ShortDesc`, `Tagname`, +`UDAs`, `UserAttrData`, `PropagatedAlarmInhibit`. + +## How to reproduce +1. `MESReceiver` instances are in the gateway hierarchy cache + `E:\ApiInstall\MxGateway\Server\galaxy-snapshot.json` (objects only, no attrs). +2. Attribute list: run the gateway's `AttributesSql` + (`src/.../Server/Galaxy/GalaxyRepository.cs`) scoped to one instance via + `... WHERE g.is_template = 0 AND g.deployed_package_id <> 0 AND g.tag_name = 'MESReceiver_002'` + in the `deployed_package_chain` anchor, then `sqlcmd -S (local) -d ZB -U wwadmin -P ` + on the box (Galaxy creds from `MxGateway.Galaxy.ConnectionString` in the gateway appsettings). +3. Live values would need an MXAccess read through the gateway (gRPC), not the repo SQL. + +In OtOpcUa (Galaxy-as-standard-driver model) these bind as ordinary equipment tags: +`Tag{ DriverInstanceId = GalaxyMxGateway, TagConfig = {"FullName":"MESReceiver_002.MoveInMesContainerNum"} }`. diff --git a/nj.md b/nj.md new file mode 100644 index 0000000..defb03c --- /dev/null +++ b/nj.md @@ -0,0 +1,212 @@ +# New Jersey — DARS reactors Z28061 / Z28062 (wonder Galaxy) + +Source: live AVEVA Galaxy DB **`ZB`** on **wonder-app-vd03** (MxAccessGateway box), via the gateway's `AttributesSql` over `sqlcmd`/ssh — `2026-06-16`. Lists the **meaningful (user-defined, cat-10) attributes**; ArchestrA system attributes (alarm framework, identity, security) and per-field config sub-attributes (`.EngUnits`, `.TrendHi`, `.Dev.*`, history/alarm settings) and scripts are omitted. + +## Hierarchy +``` +NewJersey (area) + └─ CVDAisle_1 ($CVDAisle, gobject 7243) + ├─ Z28061 ($DARSReactor, gobject 7171) + │ ├─ Left (Left_002, $DARSReactor.Left, gobject 7172) + │ └─ Right (Right_002, $DARSReactor.Right, gobject 7173) + └─ Z28062 ($DARSReactor, gobject 7202) + ├─ Left (Left_003, $DARSReactor.Left, gobject 7203) + └─ Right (Right_003, $DARSReactor.Right, gobject 7204) +``` +(`Z28061Sim`, gobject 7146, is a simulator sibling of Z28061.) + +## Objects +| Object | GobjectId | Template | Compound path | UDA attrs | Total attrs | +|---|---|---|---|---|---| +| Left_002 | 7172 | `$DARSReactor.Left` | Z28061.Left | 72 | 1907 | +| Left_003 | 7203 | `$DARSReactor.Left` | Z28062.Left | 72 | 1907 | +| Right_002 | 7173 | `$DARSReactor.Right` | Z28061.Right | 72 | 1907 | +| Right_003 | 7204 | `$DARSReactor.Right` | Z28062.Right | 72 | 1907 | +| Z28061 | 7171 | `$DARSReactor` | Z28061 | 16 | 195 | +| Z28062 | 7202 | `$DARSReactor` | Z28062 | 16 | 195 | + +> Attribute sets are template-defined: `Z28061`≡`Z28062`, `Left_002`≡`Left_003`, `Right_002`≡`Right_003`. Listed once per template. + +## $DARSReactor — Z28061 / Z28062 (reactor body) + +**Instances:** `Z28061` (Z28061, gobject 7171), `Z28062` (Z28062, gobject 7202) +**Template:** `$DARSReactor` · **16 meaningful (UDA) attributes** · 195 total incl. system + config sub-attributes. + +| Attribute | Type | Hist | Alarm | +|---|---|---|---| +| ChlorineFlow | Integer | ✓ | | +| ConfirmTimeoutSec | Integer | | | +| Deadband | Double | | | +| HeartbeatInterval | Integer | | | +| HeartbeatRequest | Boolean | | | +| HeartbeatTimeout | Integer | | | +| HeartbeatTimeoutAlarm | Boolean | | ✓ | +| HydrogenFlow | Integer | ✓ | | +| LeakTestMaxDelta | Integer | | | +| LeakTestMinDuration | Integer | | | +| LeakTestTimeout | Integer | | | +| MachineCode | String | | | +| MachineDescription | String | | | +| MachineID | String | | | +| Runtime_Alert | Boolean | | | +| TrunkPressure | Float | ✓ | | + +## $DARSReactor.Left — Left_002 / Left_003 (left chamber) + +**Instances:** `Left_002` (Z28061.Left, gobject 7172), `Left_003` (Z28062.Left, gobject 7203) +**Template:** `$DARSReactor.Left` · **72 meaningful (UDA) attributes** · 1907 total incl. system + config sub-attributes. + +| Attribute | Type | Hist | Alarm | +|---|---|---|---| +| AIR_SVC | String | | | +| AirAvg | Integer | ✓ | | +| AirFlow | Integer | ✓ | | +| AirFlowDevAlertPcnt | Float | ✓ | | +| AR_SVC | String | | | +| ArAvg | Integer | ✓ | | +| ArgonFlow | Integer | ✓ | | +| ChlorineFlowR | Integer | | | +| CL2_SVC | String | | | +| Cl2Avg | Integer | ✓ | | +| ClFlowDevAlertPcnt | Float | ✓ | | +| CoilDiameter | Boolean | ✓ | | +| CoilHeight | Integer | ✓ | | +| ContainerID | String | ✓ | | +| ContainerLoaded | Boolean | ✓ | | +| CurrentStep | Integer | | | +| CycleEndConfirm | Boolean | | | +| CycleEndNotify | Boolean | | | +| CycleEndTimeoutAlarm | Boolean | | | +| CycleRunning | Boolean | ✓ | | +| CycleStartConfirm | Boolean | | | +| CycleStartNotify | Boolean | | | +| CycleStartTimeoutAlarm | Boolean | | | +| Deadband | Double | | | +| DelmiaJobStep | Integer | | | +| FRR_Reset | Boolean | ✓ | | +| FRR_Runtime | Integer | ✓ | | +| FRR_Warning | Boolean | ✓ | ✓ | +| FurnaceTemp | Integer | ✓ | | +| FurnaceTempOverAlarmDev | Integer | ✓ | | +| FurnaceTempOverAlertDev | Integer | ✓ | | +| FurnaceTempUnderAlarmDev | Integer | ✓ | | +| FurnaceTempUnderAlertDev | Integer | ✓ | | +| H2_SVC | String | | | +| H2Avg | Integer | ✓ | | +| HydrogenFlowR | Integer | | | +| HyFlowDevAlertPcnt | Float | ✓ | | +| MachineBatchID | Integer | | | +| MachineBatchWOID | Integer | | | +| MachineCycleID | Integer | | | +| MantleTemp | Integer | ✓ | | +| MoveInReady | Boolean | | | +| MoveOutReady | Boolean | | | +| MTA_Lockout | Boolean | ✓ | ✓ | +| MTA_Reset | Boolean | ✓ | | +| MTA_Runtime | Integer | ✓ | | +| MTA_Warning | Boolean | ✓ | ✓ | +| NumOfParts | Integer | ✓ | | +| NumOfTurns | Integer | ✓ | | +| OperatorID | String | ✓ | | +| PartNumber | String | ✓ | | +| PID_Alarm | Boolean | | ✓ | +| PID_AlarmString | String | | | +| PID_CV | Integer | ✓ | | +| PotNumber | String | ✓ | | +| PotTempAvg | Integer | ✓ | | +| PreviousStep | Integer | | | +| ReactorTempAvg | Integer | ✓ | | +| RunDuration | Integer | | | +| RunEndTime | Time | | | +| RunStartTime | Time | | | +| SampleCount | Integer | | | +| StartingWeight | Integer | ✓ | | +| TableSide | Boolean | | | +| TableStatus | Integer | | | +| TC_Furnace_ID | String | | | +| TC_Mantle_ID | String | | | +| TC_Spare_ID | String | | | +| Vacuum | Float | ✓ | | +| VacuumAvg | Float | ✓ | | +| VacuumMode | Boolean | ✓ | | +| WorkOrder | String | | | + +## $DARSReactor.Right — Right_002 / Right_003 (right chamber) + +**Instances:** `Right_002` (Z28061.Right, gobject 7173), `Right_003` (Z28062.Right, gobject 7204) +**Template:** `$DARSReactor.Right` · **72 meaningful (UDA) attributes** · 1907 total incl. system + config sub-attributes. + +| Attribute | Type | Hist | Alarm | +|---|---|---|---| +| AIR_SVC | String | | | +| AirAvg | Integer | ✓ | | +| AirFlow | Integer | ✓ | | +| AirFlowDevAlertPcnt | Float | ✓ | | +| AR_SVC | String | | | +| ArAvg | Integer | ✓ | | +| ArgonFlow | Integer | ✓ | | +| ChlorineFlowR | Integer | | | +| CL2_SVC | String | | | +| Cl2Avg | Integer | ✓ | | +| ClFlowDevAlertPcnt | Float | ✓ | | +| CoilDiameter | Boolean | ✓ | | +| CoilHeight | Integer | ✓ | | +| ContainerID | String | ✓ | | +| ContainerLoaded | Boolean | ✓ | | +| CurrentStep | Integer | | | +| CycleEndConfirm | Boolean | | | +| CycleEndNotify | Boolean | | | +| CycleEndTimeoutAlarm | Boolean | | | +| CycleRunning | Boolean | ✓ | | +| CycleStartConfirm | Boolean | | | +| CycleStartNotify | Boolean | | | +| CycleStartTimeoutAlarm | Boolean | | | +| Deadband | Double | | | +| DelmiaJobStep | Integer | | | +| FRR_Reset | Boolean | ✓ | | +| FRR_Runtime | Integer | ✓ | | +| FRR_Warning | Boolean | ✓ | ✓ | +| FurnaceTemp | Integer | ✓ | | +| FurnaceTempOverAlarmDev | Integer | ✓ | | +| FurnaceTempOverAlertDev | Integer | ✓ | | +| FurnaceTempUnderAlarmDev | Integer | ✓ | | +| FurnaceTempUnderAlertDev | Integer | ✓ | | +| H2_SVC | String | | | +| H2Avg | Integer | ✓ | | +| HydrogenFlowR | Integer | | | +| HyFlowDevAlertPcnt | Float | ✓ | | +| MachineBatchID | Integer | | | +| MachineBatchWOID | Integer | | | +| MachineCycleID | Integer | | | +| MantleTemp | Integer | ✓ | | +| MoveInReady | Boolean | | | +| MoveOutReady | Boolean | | | +| MTA_Lockout | Boolean | ✓ | ✓ | +| MTA_Reset | Boolean | ✓ | | +| MTA_Runtime | Integer | ✓ | | +| MTA_Warning | Boolean | ✓ | ✓ | +| NumOfParts | Integer | ✓ | | +| NumOfTurns | Integer | ✓ | | +| OperatorID | String | ✓ | | +| PartNumber | String | ✓ | | +| PID_Alarm | Boolean | | ✓ | +| PID_AlarmString | String | | | +| PID_CV | Integer | ✓ | | +| PotNumber | String | ✓ | | +| PotTempAvg | Integer | ✓ | | +| PreviousStep | Integer | | | +| ReactorTempAvg | Integer | ✓ | | +| RunDuration | Integer | | | +| RunEndTime | Time | | | +| RunStartTime | Time | | | +| SampleCount | Integer | | | +| StartingWeight | Integer | ✓ | | +| TableSide | Boolean | | | +| TableStatus | Integer | | | +| TC_Furnace_ID | String | | | +| TC_Mantle_ID | String | | | +| TC_Spare_ID | String | | | +| Vacuum | Float | ✓ | | +| VacuumAvg | Float | ✓ | | +| VacuumMode | Boolean | ✓ | | +| WorkOrder | String | | |