docs: add MES + Delmia-DNC integration API/MXAccess specs

mes-delmia-integration-api.md: endpoints, request/response DTOs, and the MXAccess flag handshake for MESAPI (in-repo MesNotifier) and DelmiaIntegration (DNC Downloader.asmx -> WWNotifier /notify -> Galaxy $DelmiaReceiver). mesrec.md / nj.md: live Galaxy receiver + reactor attribute references.
This commit is contained in:
Joseph Doherty
2026-06-17 06:52:36 -04:00
parent eb8b44c29d
commit d5b134b117
3 changed files with 666 additions and 0 deletions
+377
View File
@@ -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.<tag>`).
### 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<WorkOrderInfo>` | 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<WorkOrderInfo>`).
*(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>`).
`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<bool>`
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<Machine>(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<MachineAlarm>(...)`).
### 1.4 Tag mappings
**Move-in** (`MesMoveInTagset`, all `{Code}.MesReceiver.<tag>`):
| 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}.<attr>`): `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<bool> 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<Task<bool>> {
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": <int|null> }`.
- Failure/timeout: `{ "WasSuccessful": false, "ErrorText": "<message>", "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('/') + "/<Action>"`).
- **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<SearchResult>`), `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<RecipeDownloadResult>()`.
- **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 '<URL>'."`.
- **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.
+77
View File
@@ -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 <pwd>`
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"} }`.
+212
View File
@@ -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 | | |