Files
scadaproj/mes-delmia-integration-api.md
T
Joseph Doherty d5b134b117 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.
2026-06-17 06:52:36 -04:00

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.configHttpListener):
    • 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 ($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/moveinMoveInResponse

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/moveoutMoveOutResponse

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/alarmstatusAlarmStatusResponse

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/simplealarmstatusAlarmStatusResponse

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 SAPIDdb.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 != trueErrorText="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.
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 RecipeProcessResultResult, RecipeProcessResultTextResultText; 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: MESAPIAPIServer.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: DelmiaIntegrationDelmiaIntegration/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:

  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.