- former-api-specs/mes: Alarm-API, MoveIn-MoveOut-API, API-key authgaps (from ~/Desktop/mesapi) - former-api-specs/dnc: Delmia-Integration-API — Delmia document service + WW recipe-download notify (from ~/Desktop/delmiaintegration) - known-issues: inbound API compile error not client-visible; no api-method validate
16 KiB
WWSupport MES API — Move In / Move Out API
Reference for the batch move-in / move-out endpoints exposed by the WWSupport / APIServer
ServiceStack service. These endpoints hand a container/work-order payload to a machine's
MesReceiver object in AVEVA Wonderware (System Platform / Galaxy) over MXAccess, using a
flag-based request/response handshake, and read back the resulting batch id (and, for move-out,
recorded cycle data from SQL).
- Service host:
AppHost(AppSelfHostBase, name"APIServer") —APIServer/APIServer/AppHost.cs - Service implementation:
MesServices—APIServer/APIServer.ServiceInterface/MesServices.cs - Business logic:
MesNotifier—APIServer/APIServer.ServiceInterface/MesNotifier.cs - Framework: ServiceStack 6.0.2, self-hosted; SQL Server (OrmLite,
SqlServer2016Dialect)
Serialization note:
JsConfig.IncludeNullValues = true, so null fields are emitted in JSON responses (e.g."BatchID": null).
This document covers /mes/movein and /mes/moveout. The two alarm endpoints
(/mes/alarmstatus, /mes/simplealarmstatus) are documented in Alarm-API.md.
Endpoints
| Verb | Route | Request DTO | Response DTO |
|---|---|---|---|
POST |
/mes/movein |
MoveInRequest |
MoveInResponse |
POST |
/mes/moveout |
MoveOutRequest |
MoveOutResponse |
Both are dispatched through ServiceStack Any(...) handlers in MesServices, which resolve the
singleton MesNotifier and call MoveIn(...) / MoveOut(...).
The handlers call the async business methods with a blocking
.Result(mesNotifier.MoveIn(request).Result), so each request occupies its thread until the handshake completes or the 30 s timeout fires.
The service also enables PostmanFeature and OpenApiFeature (Swagger), so a running instance
exposes a browsable contract and a Postman collection.
Authentication & authorization
Identical to the alarm endpoints — all MES services are decorated with:
[Authenticate]
[RequiredRole("MESAPI")]
public class MesServices : Service { ... }
The caller must be authenticated and hold the MESAPI role. Configured in AppHost.Configure:
- API key —
ApiKeyAuthProvider(SessionCacheDuration = 30 min,RequireSecureConnection = false). An API key whoseEnvironment == "test"is routed to theTestDbconnection instead of the production DB (AppHost.GetDbConnection). Caveat: that redirect only applies to OrmLite lookups — the move-out cycle-data read uses a rawConnectionStrings["BatchDB"]connection and is not affected by the test-key redirect (see gotchas). - LDAP —
LdapAuthProvider. AllowGetAuthenticateRequests = true.
Default API-key transport is the standard Authorization header (Bearer <key>, or HTTP Basic
with the key as username); ?apikey=<key> also works since AllowInHttpParams defaults on.
POST /mes/movein
Hands a container + its work orders to a machine's MesReceiver to start/stage a batch.
Request — MoveInRequest
| Field | Type | Notes |
|---|---|---|
SAPID |
string | SAP identifier of the target machine. Used to resolve the Machine row. Required. |
OperatorName |
string | Operator initiating the move-in. |
JobSequenceNumber |
string | Job sequence number. |
ContainerNumber |
string | MES container number. |
WorkOrders |
WorkOrderInfo[] |
Work orders being moved in (default empty). Each item: WorkOrderNumber (string), PartNumber (string). |
{
"SAPID": "100012345",
"OperatorName": "chamalas",
"JobSequenceNumber": "50",
"ContainerNumber": "cont-012",
"WorkOrders": [
{ "WorkOrderNumber": "W111111", "PartNumber": "P111111" }
]
}
Response — MoveInResponse
| Field | Type | Notes |
|---|---|---|
WasSuccessful |
bool | true only if the machine reported MoveInSuccessfulFlag = true. |
ErrorText |
string | Failure reason, or the machine's MoveInErrorText on a completed-but-failed move. |
BatchID |
int? | The machine's MoveInBatchID, only when non-zero; otherwise null. |
Behavior
- 30 s budget (
CancellationTokenSource(30000)). - Resolve machine:
db.Single<Machine>(x => x.SAPID == request.SAPID). If not found →WasSuccessful = false,ErrorText = "Failed to find machine with SAPID '<id>'"(early return). - Subscribe to the machine's move-in tagset (
MesMoveInTagset, all under{Machine.Code}.MesReceiver.*) viaAdviseSupervisory; wait for the first value of each. If any subscription fails →"Failed to connect to machine". - Ready gate: require
MoveInReadyFlag == true, else"Machine move in ready flag not set to true". - If still successful, perform the handshake:
- Arm a watcher for
MoveInCompleteFlag → true(OnValue). - Write the payload tags (see table) and set
MoveInFlag = true. If any write is not acknowledged →"Failed to write move in information to machine". - Wait for
MoveInCompleteFlag:- Completed:
WasSuccessful = MoveInSuccessfulFlag;ErrorText = MoveInErrorText;BatchID = MoveInBatchID(only if ≠ 0). - Timed out:
"Timeout waiting for move in information to be processed".
- Completed:
- Arm a watcher for
- Unsubscribe all tags.
Tags written / read ({Machine.Code}.MesReceiver.*)
| Property | MXAccess tag suffix | Type | Direction | Source / meaning |
|---|---|---|---|---|
MoveInReadyFlag |
MoveInReadyFlag |
bool | read (gate) | Machine must be ready to accept a move-in. |
MoveInMesContainerNumber |
MoveInMesContainerNum |
string | write | request.ContainerNumber. |
MoveInOperatorName |
MoveInOperatorName |
string | write | request.OperatorName. |
MoveInJobSequenceNumber |
MoveInJobSequenceNumber |
string | write | request.JobSequenceNumber. |
MoveInNumberWorkOrders |
MoveInNumberWorkOrders |
int | write | request.WorkOrders.Count. |
MoveInPartNumbers |
MoveInPartNumbers[] |
string[] | write | WorkOrders[*].PartNumber, fixed length 50. |
MoveInWorkOrderNumbers |
MoveInWorkOrderNumbers[] |
string[] | write | WorkOrders[*].WorkOrderNumber, fixed length 50. |
MoveInFlag |
MoveInFlag |
bool | write (trigger) | Set true to start processing. |
MoveInCompleteFlag |
MoveInCompleteFlag |
bool | watch | Machine sets true when done. |
MoveInSuccessfulFlag |
MoveInSuccessfulFlag |
bool | read (result) | Machine's success verdict. |
MoveInErrorText |
MoveInErrorText |
string | read (result) | Machine's error message. |
MoveInBatchID |
MoveInBatchID |
int | read (result) | Created batch id (0 = none). |
POST /mes/moveout
Closes out a container's work orders on a machine's MesReceiver, and (when the machine is
configured for it) returns the recorded cycle data for the resulting batch.
Request — MoveOutRequest
Same as MoveInRequest minus JobSequenceNumber:
| Field | Type | Notes |
|---|---|---|
SAPID |
string | SAP identifier of the target machine. Required. |
OperatorName |
string | Operator initiating the move-out. |
ContainerNumber |
string | MES container number. |
WorkOrders |
WorkOrderInfo[] |
Work orders being moved out. Each: WorkOrderNumber, PartNumber. |
{
"SAPID": "100012345",
"OperatorName": "chamalas",
"ContainerNumber": "cont-012",
"WorkOrders": [
{ "WorkOrderNumber": "W111111", "PartNumber": "P111111" }
]
}
Response — MoveOutResponse
| Field | Type | Notes |
|---|---|---|
WasSuccessful |
bool | true only if the machine reported MoveOutSuccessfulFlag = true. |
ErrorText |
string | Failure reason, or the machine's MoveOutErrorText. |
BatchID |
int? | The machine's MoveOutBatchID (non-zero), and only when cycle storage is enabled (below). |
Data |
MoveOutData[] |
Recorded cycle values (empty unless cycle storage is enabled). Defaults to []. |
MoveOutData
| Field | Type | Source |
|---|---|---|
BatchId |
int | MachineCycle.MachineBatchId. |
CycleId |
int | MachineCycle.MachineCycleId. |
ValueName |
string | One of the cycle-data keys (below). |
Value |
object | The value for that key (null becomes empty string in SQL via COALESCE). |
Behavior
Steps 1–5 mirror move-in (resolve Machine by SAPID; subscribe MesMoveOutTagset; require
MoveOutReadyFlag == true; write payload + MoveOutFlag = true; wait for MoveOutCompleteFlag).
On completion: WasSuccessful = MoveOutSuccessfulFlag, ErrorText = MoveOutErrorText.
Cycle-data read (move-out only). If MoveOutBatchID != 0 and the machine's OtherData
contains the literal "StoreCycleDataForMES":
BatchID = MoveOutBatchID.- Open a raw
SqlConnectiononConnectionStrings["BatchDB"]and run a parameterized query (@machineBatchID = MoveOutBatchID) againstBT.dbo.MachineCycle, expanding each cycle'sOtherDataJSON (viaOPENJSON … CROSS APPLY (VALUES …)) into oneMoveOutDatarow per key/value pair. Only rows whereISJSON(mc.OtherData) = 1are processed.
-- Shape of the cycle-data query (BT.dbo.MachineCycle, WHERE MachineBatchId = @machineBatchID)
SELECT mc.MachineBatchId, mc.MachineCycleId, v.ValueName, COALESCE(v.Value, N'') AS Value
FROM BT.dbo.MachineCycle mc
CROSS APPLY OPENJSON(mc.OtherData) WITH ( /* one column per key below */ ) od
CROSS APPLY (VALUES (N'ProgramNum', od.ProgramNum), /* … one row per key … */ ) AS v(ValueName, Value)
WHERE mc.MachineBatchId = @machineBatchID AND ISJSON(mc.OtherData) = 1;
Cycle-data keys extracted (19 per cycle): ProgramNum, DewPointStart, SegmentStart2,
HighVacEndSeg1, SegmentStart3, SegmentStart4, SoakStartTime, SegmentStart5,
SoakEndTime, DurationFinalSoak, MaxSoakTemp, MinSoakTemp, MaxSoakPressure,
MinSoakPressure, SegmentStart6, QuenchTemp, DewPointMax, StartTimestamp, EndTimestamp.
Tags written / read ({Machine.Code}.MesReceiver.*)
Same set as move-in with the MoveOut prefix, minus JobSequenceNumber:
MoveOutReadyFlag (gate), MoveOutMesContainerNum / MoveOutOperatorName /
MoveOutNumberWorkOrders / MoveOutPartNumbers[] / MoveOutWorkOrderNumbers[] (write),
MoveOutFlag (trigger), MoveOutCompleteFlag (watch), MoveOutSuccessfulFlag /
MoveOutErrorText / MoveOutBatchID (result).
How the handshake works (MXAccess)
MesNotifier holds a single process-wide LMXProxyServerClass (handle registered as
"MesNotifier", wired to OnDataChange + OnWriteComplete). For each request it:
- Advise —
AddItem(path)thenAdviseSupervisory; the firstOnDataChangeper tag resolves that tag's read task (success = quality192/ OPC-Good). Values are coerced to the tag's CLR type viaConvert.ChangeType. - Write —
LMXProxyServerClass.Write;OnWriteCompleteresolves the write task with the driver's success flag. - Watch —
Tag.OnValue(target)completes when a subsequentOnDataChangereports a value equal to the target (used for the…CompleteFlag → truestep). - Unadvise — every tag is unsubscribed and removed at the end of the request.
Everything is bounded by the per-request 30 s CancellationTokenSource; on expiry, all
pending reads/writes/watches resolve as false.
The move-in/move-out contract is therefore a classic flag protocol on the machine's MesReceiver:
read ready → write payload + set request flag → wait for complete flag → read success/error/batch.
Underlying data model — Machine
SQL table backing machine lookup (APIServer.ServiceModel/DTO/Machine.cs); Code is also the
MXAccess tag prefix and SAPID is the move-in/move-out selector:
| Column | Type | Constraints |
|---|---|---|
MachineID |
int | PK, auto-increment |
Code |
string | Required, ≤ 50 — MXAccess tag prefix ({Code}.MesReceiver.*) |
Name |
string | Required, ≤ 50 |
ZTag |
string | ≤ 10 |
SAPID |
string | ≤ 10 — move-in/out selector |
Description |
string | ≤ 256 |
TimeZone |
string | Required, ≤ 128 |
MultipleBatch / MultipleCycle / Active |
bool | |
LastUpdate |
DateTime | |
LastUpdateBy |
string | Required, ≤ 128 |
OtherData |
string | Max-length text — gates cycle storage when it contains "StoreCycleDataForMES" |
Behavior notes & gotchas
- Work-order arrays are fixed length 50.
ToFixedLength(50)always writes exactly 50-element string arrays (padded withnull); a 51st+ work order is silently dropped.PartNumbersandWorkOrderNumbersare written as two parallel arrays — index i of one corresponds to index i of the other (positional pairing from the sameWorkOrderslist).NumberWorkOrderscarries the true count. - Connect-failure message gets clobbered. The "connect" check and the "ready flag" check are
sequential
ifs with no early return; if the subscribe fails,MoveInReadyFlag.Valueis its default (false), so the ready-flag check also fires and overwritesErrorTextwith"…ready flag not set to true". A connection problem can surface as a ready-flag error. BatchIDis null unless the machine reports a non-zero batch id. Move-out additionally requires cycle storage to be enabled before it setsBatchID.- Cycle data is opt-in per machine.
Datais empty unlessMachine.OtherDatacontains"StoreCycleDataForMES"andMoveOutBatchID != 0. - Cycle-data read bypasses the test-DB redirect. It uses a raw
ConnectionStrings["BatchDB"]connection against hard-codedBT.dbo.MachineCycle, so atest-environment API key (which redirects OrmLite toTestDb) still reads cycle data fromBatchDB. WasSuccessfulreflects the machine, not just the transport. Even with a clean handshake,WasSuccessfulis whatever the machine wrote to…SuccessfulFlag, andErrorTextis the machine's…ErrorText.- Synchronous blocking.
MesServicescalls.Resulton the async methods; combined with the single shared MXAccess proxy, throughput is effectively serialized per request thread. - Machine lookup uses
db.SingleonSAPID; returns null when unmatched (handled), and the first match ifSAPIDis non-unique. - The route attributes carry placeholder Swagger text (
Summary = "POST Summary",Notes = "Notes"); cosmetic only.
Quick reference (curl)
# Move in a container + work order
curl -X POST http://<host>:<port>/mes/movein \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <api-key>" \
-d '{
"SAPID":"100012345",
"OperatorName":"chamalas",
"JobSequenceNumber":"50",
"ContainerNumber":"cont-012",
"WorkOrders":[{"WorkOrderNumber":"W111111","PartNumber":"P111111"}]
}'
# Move out the same container (returns cycle Data when the machine stores it for MES)
curl -X POST http://<host>:<port>/mes/moveout \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <api-key>" \
-d '{
"SAPID":"100012345",
"OperatorName":"chamalas",
"ContainerNumber":"cont-012",
"WorkOrders":[{"WorkOrderNumber":"W111111","PartNumber":"P111111"}]
}'
Exact auth header format depends on how
ApiKeyAuthProvideris configured for the deployment (bearer token vs. HTTP Basic with the key as username). Confirm against the live Swagger/Postman metadata for the target server.
ScadaBridge equivalent (porting note)
ScadaBridge re-implements these flows as Inbound API methods (POST /api/{method},
X-API-Key header — not the ServiceStack Authorization/apikey scheme) that route to a
site's MesReceiver instance script via Route.To(<instanceCode>).Call("MesMoveIn"/"MesMoveOut", …):
IpsenMESMoveIn/MesMoveIn↔/mes/movein;MesMoveOut↔/mes/moveout.- The ready/trigger/complete flag handshake moves into the site instance script (Site Runtime), rather than the central API driving MXAccess tags directly.
- Machine resolution by SAP id is a
Database.QuerySingleAsync<string>("BTDB", "SELECT … Machine WHERE SAPID=@s")inside the inbound script (seedocs/plans/2026-06-16-ipsen-mes-movein-design.md).
This file documents the legacy WWSupport contract for reference/parity during that migration.