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.
24 KiB
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 (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 |
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/· QAhttp://*:9500/· PRODhttp://*:9500/
- DEV
- Database (
App.config→ connectionBatchDB, DBBT):- DEV
wonder-sql-vd01.zmr.zimmer.com· QAwondersqlqa.zmr.zimmer.com· PROD (same form). Userwonderapp.
- DEV
- Auth: every operation is decorated
[Authenticate]+[RequiredRole("MESAPI")](MesServices.cs).AppHostregisters anAuthFeaturewith two providers:ApiKeyAuthProviderandLdapAuthProvider.- Unauthenticated → 401; authenticated without the
MESAPIrole → 403.
- Unauthenticated → 401; authenticated without the
- 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($MESReceivertemplate). Note the API binds tags under the contained nameMesReceiver(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:
_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.
- Look up machine by
SAPID. Not found →WasSuccessful=false,ErrorText="Failed to find machine with SAPID '{SAPID}'", return. - Subscribe to every move-in tag (
Advise(t, cts)),await Task.WhenAll(...). Any subscription that fails / quality ≠ 192 →ErrorText="Failed to connect to machine". - Check ready flag:
if (!MoveInReadyFlag.Value)→ErrorText="Machine move in ready flag not set to true", stop. - Arm completion watch:
Task<bool> flagTask = MoveInCompleteFlag.OnValue(true, cts);(completes when the flag goestrue, orfalseon the 30 s timeout). - Write data + trigger (in parallel, trigger last):
MoveInOperatorName,MoveInJobSequenceNumber,MoveInNumberWorkOrders,MoveInPartNumbers[](padded to 50),MoveInWorkOrderNumbers[](padded to 50), thenMoveInFlag = true.await Task.WhenAll(writeTasks); any write!= true→ErrorText="Failed to write move in information to machine". - 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".
- Cleanup:
Tags.ForEach(Unadvise)and return.
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
OnDataChangecallback; the 30 sCancellationTokenSourceis the only timeout.
1.6 Alarm-status path
- Resolve machine from
MachineFilter(SAPID/Code/ZTag/MachineID) — wrong/missing → error. - Load
MachineAlarmrows for the machine; apply filters (FlaggedOnly,MinSeverity,MaxSeverity, case-insensitiveNameFilter.Contains). (IncludeTriggeredis read but not used in the filter.) - Subscribe + read each alarm's
QualityandInAlarm(30 s budget). Bad quality / read failure →ErrorText="Failed to read machine alarm status". - For alarms where
InAlarm == true, additionally readTimeAlarmOn,DescAttrName,Acked,TimeAlarmAcked,AckMsg. - Build
AlarmInfoper triggered alarm;StatusCode = "Triggered.Acked"if acked else"Triggered". IfAlarmFilter.IncludeAcked == false, acked alarms are skipped. - Unsubscribe; on failure
Alarmsis cleared.
1.7 Outputs / error handling (MES)
- Transport status is always 200 for handled responses — success/failure is carried by the body's
WasSuccessfulflag +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
ErrorTextvalues: 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/notifythat actually performs the MXAccess write — is not in theZimmerBiometGitea org; only the JSON contract (below) and the Galaxy-side$DelmiaReceiverobject (scripts + attributes, exported underAA_EXPORT/) are available.WWNotificationSystemalso uses MXAccess but is an unrelated tag→email alerting service (port:9876, client nameWWNotifierMonitor) — not the recipe receiver.
2.2 DNC server interface (DelmiaClient, hop ①)
- Transport:
HttpClient.PostAsyncwithFormUrlEncodedContent; response is XML deserialized withXmlSerializer. Base URL isDelmiaClient.URL; per-callTimeoutdefault 30 s. Action is appended to the base URL (URL.TrimEnd('/') + "/<Action>"). - Base URL (from
AdminTestUtilDefaultURL):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…Specifiedflag),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
YESand exit code0on success; printsNO+ a message and sets exit code-1on failure (parse error, missingNotifyURL/NotifyTimeout,Result==false, or HTTP exception). (Caveat: on a caught exception it logserror.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**):**
- resolve the
$DelmiaReceiverinstance fromMachineCode; - (optionally) verify
ReadyFlag == true; - write
DownloadPath,WorkOrderNumber,PartNumber,JobStepNumber,Username; - set
RecipeDownloadFlag = true(trigger) → GalaxyProcessRecipefires; - wait for
RecipeProcessedFlag == true, bounded by the request timeout; - read
RecipeProcessResult→Result,RecipeProcessResultText→ResultText; return theRecipeDownloadResultJSON; Resetthe object.
2.5 Outputs / error handling (Delmia)
- DNC server call: failures are swallowed into the returned DTO —
SearchResults.ErrorMessage, orDownloadResult.TransferSuccessful=false+ErrorMessage="Failed to call Delmia web service at '<URL>'.". - Notify handoff:
RecipeDownloadResult.Result(bool) +ResultText(string);WWNotifierexit code0(YES) /-1(NO). - Galaxy script:
RecipeProcessResultTextis"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.
Open gaps / to verify against the box:
- The Delmia recipe
/notifyreceiver service source (the actual MXAccess writer atwonder-app-vd01:9001) was not found in the Gitea org — §2.4 receiver steps are inferred from the contract + Galaxy scripts. - MES tag prefix in code is
{Code}.MesReceiver.*, while the live probe inmesrec.mdshows a top-levelMESReceiver_002instance — confirm the exact contained-name/instance convention on the live Galaxy. - PROD
HttpListener/DB host values should be read from the deployedApp.config, not assumed.