# 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.
