8a78e759c0
- 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
340 lines
16 KiB
Markdown
340 lines
16 KiB
Markdown
# 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 <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). |
|
||
|
||
```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<Machine>(x => x.SAPID == request.SAPID)`. If not found →
|
||
`WasSuccessful = false`, `ErrorText = "Failed to find machine with SAPID '<id>'"` (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://<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 `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(<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 (see `docs/plans/2026-06-16-ipsen-mes-movein-design.md`).
|
||
|
||
This file documents the **legacy** WWSupport contract for reference/parity during that migration.
|