# 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`](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: ```csharp [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 whose `Environment == "test"` is routed to the `TestDb` connection instead of the production DB (`AppHost.GetDbConnection`). **Caveat:** that redirect only applies to OrmLite lookups — the move-out cycle-data read uses a raw `ConnectionStrings["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 `, or HTTP Basic with the key as username); `?apikey=` 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). | ```json { "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 1. **30 s budget** (`CancellationTokenSource(30000)`). 2. **Resolve machine:** `db.Single(x => x.SAPID == request.SAPID)`. If not found → `WasSuccessful = false`, `ErrorText = "Failed to find machine with SAPID ''"` (early return). 3. **Subscribe** to the machine's move-in tagset (`MesMoveInTagset`, all under `{Machine.Code}.MesReceiver.*`) via `AdviseSupervisory`; wait for the first value of each. If any subscription fails → `"Failed to connect to machine"`. 4. **Ready gate:** require `MoveInReadyFlag == true`, else `"Machine move in ready flag not set to true"`. 5. 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"`. 6. **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`. | ```json { "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** `SqlConnection` on `ConnectionStrings["BatchDB"]` and run a parameterized query (`@machineBatchID = MoveOutBatchID`) against `BT.dbo.MachineCycle`, expanding each cycle's `OtherData` JSON (via `OPENJSON … CROSS APPLY (VALUES …)`) into one `MoveOutData` row per key/value pair. Only rows where `ISJSON(mc.OtherData) = 1` are processed. ```sql -- 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: 1. **Advise** — `AddItem(path)` then `AdviseSupervisory`; the first `OnDataChange` per tag resolves that tag's read task (success = quality `192` / OPC-Good). Values are coerced to the tag's CLR type via `Convert.ChangeType`. 2. **Write** — `LMXProxyServerClass.Write`; `OnWriteComplete` resolves the write task with the driver's success flag. 3. **Watch** — `Tag.OnValue(target)` completes when a subsequent `OnDataChange` reports a value equal to the target (used for the `…CompleteFlag → true` step). 4. **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 with `null`); a 51st+ work order is **silently dropped**. `PartNumbers` and `WorkOrderNumbers` are written as two parallel arrays — index *i* of one corresponds to index *i* of the other (positional pairing from the same `WorkOrders` list). `NumberWorkOrders` carries the true count. - **Connect-failure message gets clobbered.** The "connect" check and the "ready flag" check are sequential `if`s with no early return; if the subscribe fails, `MoveInReadyFlag.Value` is its default (`false`), so the ready-flag check also fires and **overwrites** `ErrorText` with `"…ready flag not set to true"`. A connection problem can surface as a ready-flag error. - **`BatchID` is null unless the machine reports a non-zero batch id.** Move-out additionally requires cycle storage to be enabled before it sets `BatchID`. - **Cycle data is opt-in per machine.** `Data` is empty unless `Machine.OtherData` contains `"StoreCycleDataForMES"` *and* `MoveOutBatchID != 0`. - **Cycle-data read bypasses the test-DB redirect.** It uses a raw `ConnectionStrings["BatchDB"]` connection against hard-coded `BT.dbo.MachineCycle`, so a `test`-environment API key (which redirects *OrmLite* to `TestDb`) still reads cycle data from `BatchDB`. - **`WasSuccessful` reflects the machine, not just the transport.** Even with a clean handshake, `WasSuccessful` is whatever the machine wrote to `…SuccessfulFlag`, and `ErrorText` is the machine's `…ErrorText`. - **Synchronous blocking.** `MesServices` calls `.Result` on the async methods; combined with the single shared MXAccess proxy, throughput is effectively serialized per request thread. - **Machine lookup uses `db.Single`** on `SAPID`; returns null when unmatched (handled), and the first match if `SAPID` is non-unique. - The route attributes carry placeholder Swagger text (`Summary = "POST Summary"`, `Notes = "Notes"`); cosmetic only. --- ## Quick reference (curl) ```bash # Move in a container + work order curl -X POST http://:/mes/movein \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -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://:/mes/moveout \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "SAPID":"100012345", "OperatorName":"chamalas", "ContainerNumber":"cont-012", "WorkOrders":[{"WorkOrderNumber":"W111111","PartNumber":"P111111"}] }' ``` > Exact auth header format depends on how `ApiKeyAuthProvider` is 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().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("BTDB", "SELECT … Machine WHERE SAPID=@s")` inside the inbound script (see `docs/plans/2026-06-16-ipsen-mes-movein-design.md`). This file documents the **legacy** WWSupport contract for reference/parity during that migration.