# Network API

> Draft of the device's local REST and WebSocket API — discovery, login, control ownership, JSON messages and units.

The network module of UPAN serves a local API over Wi-Fi: a WebSocket for commands, telemetry and events, and REST endpoints for scripts. It works in your local network without internet or a cloud account. The network module only translates: it checks login, control ownership and message format, and passes commands to the motion MCU. Motion limits are checked only by the motion MCU, and its refusals come back to you unchanged, with details.

> **Warning:** This API is a **draft**. Endpoints, message names and status codes may change before release. The network module firmware is not written yet. Use this page to plan integrations, not to ship them.

## Principles

- **JSON mirrors the protocol 1:1.** Field names are the same as in the protocol schema (snake_case). Enum values are strings with the schema name, for example `"STATE_MOVING"` or `"ERROR_LIMIT_EXCEEDED"`. One schema describes both the USB protocol and this API; see the [API reference](/docs/api).
- **Base units.** Numbers are integers in base units: µm, µm/s, µm/s² on a linear axis, millidegrees on a rotary axis. There are no millimetres or inches in the JSON; conversion happens in `upan.js` and `upan-mcp`. See [Configuration](/docs/configuration#base-units).
- **One origin.** The built-in page, apps on the device, `/lib/upan.js` and the API are served from the same address. No CORS, no mixed content, no internet needed.

## Address map

| Path | Content | Login |
|---|---|---|
| `/` | built-in page: login, state, list of apps, Wi-Fi settings | — |
| `/app/<name>/…` | apps stored on the device (static files) | the app logs in itself |
| `/lib/upan.js` | browser library for apps; its version follows the network module firmware | — |
| `/api/v1/info` | name, serial number, versions, `axis_kind`, capabilities | not required |
| `/api/v1/auth/*` | login | — |
| `/api/v1/ws` | WebSocket: commands, telemetry, events | required |
| `/api/v1/…` | REST | required |

Port 80, plain HTTP in the local network. HTTPS is not part of the first version.

## Finding the device

- **mDNS:** host name `upan-xxxx.local` (`xxxx` is the end of the serial number) and service `_upan._tcp`. Works on PCs and iPhones; unreliable on Android.
- **IP address:** the main path for phones. The built-in page shows a QR code with its own address — open it on a PC and scan it with the phone. `upan-mcp` prints the device address too.
- **DHCP name** `upan-xxxx`, for routers that list client names.

> **Note:** An app served by the device must not hard-code the device address. It is loaded from the device, so the API is always at `location.origin`.

## Authentication

Each device has its own password, printed on a label or card in the box. You can change it on the built-in page. The password is never sent over the network; login is a challenge-response:

1. `GET /api/v1/auth/challenge`

   ```json
   { "nonce": "<32 bytes, hex>", "salt": "<hex>", "iterations": 10000 }
   ```

2. The client computes:

   ```text
   key   = PBKDF2-SHA256(password, salt, iterations)
   proof = HMAC-SHA256(key, nonce)
   ```

3. `POST /api/v1/auth/login`

   ```json
   { "nonce": "…", "proof": "…" }
   ```

   Response: `{ "token": "…", "expires_s": 86400 }` and a cookie `upan_session` (HttpOnly, SameSite=Strict).

Rules:

- Browser apps use the cookie; the browser sends it with the WebSocket automatically.
- `upan-mcp` and scripts send `Authorization: Bearer <token>`.
- A nonce is single-use and valid for 30 s. After 5 failed attempts the device waits, with a growing delay.
- `crypto.subtle` is not available on plain `http://` pages. `upan.js` therefore includes its own SHA-256, HMAC and PBKDF2. Apps should log in through `upan.js` and not call Web Crypto themselves.

> **Warning:** On a trusted local network the password cannot be sniffed, but an active attacker on the same network could take over a session. Such an attacker still cannot change the machine limits (those need USB and a button press on the device) and cannot bypass the hardware e-stop. Full channel encryption is not part of the first version.

## Control ownership

Several clients can be connected at the same time — a phone, an agent, the built-in page. Only one may move the axis.

- **Read** (telemetry, events, configuration): every logged-in client.
- **Move or change settings:** only the **controller**. A client takes control with `{"cmd": "acquire_control"}` over the WebSocket.
- Control is released by `release_control`, by closing the WebSocket, or after 10 s without any message from the controller.
- If another client holds control, `acquire_control` fails with `ERROR_CONTROL_HELD` and the name of the current controller. With `"force": true` the client takes over; the previous controller receives `control_lost`.
- **`stop` and `estop` are accepted from anyone**, without control.
- If the controller's WebSocket closes during a jog, the network module sends `Stop` to the motion MCU immediately, without waiting for the jog timeout.

## WebSocket

`/api/v1/ws` — one socket per client, JSON text frames.

### Client to device

Each command has an `id` (incremented by the client) and `cmd`; the other fields are the fields of that command in the protocol.

```json
{ "id": 7, "cmd": "move_rel", "distance": 150000, "velocity": 50000 }
```

```json
{ "id": 8, "cmd": "jog", "velocity": -20000, "timeout_ms": 300 }
```

```json
{ "id": 9, "cmd": "subscribe", "telemetry_hz": 20, "events": true }
```

`cmd` is the name of a command in the protocol: `move_abs`, `move_rel`, `jog`, `stop`, `estop`, `home`, `set_position`, `program_run`, `program_pause`, `program_resume`, `program_abort`, `soft_config_get`, `soft_config_set`, `hard_config_get`, `output_set`, `info_get`, `fault_clear`. Network-only commands: `subscribe`, `acquire_control`, `release_control`, `ping`.

`hard_config_set` and `bootloader_enter` do **not** exist over the network. They are USB-only.

### Device to client

```json
{ "type": "response", "id": 7, "error": "ERROR_NONE" }
```

```json
{ "type": "response", "id": 8, "error": "ERROR_LIMIT_EXCEEDED",
  "limit_violation": { "limit": "LIMIT_VELOCITY", "requested": -20000, "allowed": -15000 } }
```

```json
{ "type": "telemetry", "state": "STATE_JOGGING", "homed": true, "position": 81234, "velocity": -15000 }
```

```json
{ "type": "event", "kind": "EVENT_LIMIT_HIT", "position": 0, "detail": 0 }
```

```json
{ "type": "control_lost", "by": "upan-mcp@workshop-pc" }
```

- Each command gets exactly one `response` with the same `id`.
- Telemetry over the network is limited to 50 Hz.
- A `jog` must be repeated more often than its `timeout_ms`. It works like a throttle pedal: each repeat can carry a new `velocity`, and the device changes speed smoothly with its acceleration limit.

The message bodies are [`Response`](/docs/api#Response), [`Telemetry`](/docs/api#Telemetry) and [`Event`](/docs/api#Event).

## REST

For scripts and `curl`, where a socket is unnecessary. Request bodies are the same JSON as over the WebSocket, without `id`.

| Method and path | Purpose |
|---|---|
| `GET /api/v1/status` | latest telemetry |
| `POST /api/v1/cmd/<cmd>` | one command (`move_rel`, `home`, `stop`, …); needs control — REST takes it implicitly for the duration of the command if it is free |
| `GET` / `PUT /api/v1/config/soft` | soft limits |
| `GET /api/v1/config/hard` | machine limits, **read only** |
| `PUT /api/v1/program` | a whole program as JSON; the network module splits it into chunks, the motion MCU validates it |
| `GET /api/v1/apps` | list of apps on the device |
| `PUT /api/v1/apps/<name>/<path>` | upload an app file |
| `DELETE /api/v1/apps/<name>` | delete an app |

| HTTP status | Meaning |
|---|---|
| `200` | accepted |
| `401` | not logged in |
| `409` | refused because of state: `ERROR_BUSY`, `ERROR_NOT_HOMED`, `ERROR_CONTROL_HELD`, … |
| `422` | refused because of values: `ERROR_LIMIT_EXCEEDED`, `ERROR_PROGRAM_INVALID`, `ERROR_INVALID_VALUE` |

The response body is always a `Response` in JSON, so the client can see what to correct.

```powershell
curl.exe -X POST http://upan-1a2b.local/api/v1/cmd/move_rel -H "Authorization: Bearer $env:UPAN_TOKEN" -H "Content-Type: application/json" -d '{\"distance\": 150000}'
```

### Programs over REST

The program body uses protocol instructions directly, in base units and with inputs and outputs numbered from 0. Loops are written as `loop_begin` / `loop_end`.

```json
{ "repeat": 1, "instructions": [
  { "home": {} },
  { "loop_begin": { "count": 10 } },
  { "move_rel":   { "distance": 150000 } },
  { "set_output": { "output": 0, "level": true } },
  { "wait_time":  { "ms": 500 } },
  { "set_output": { "output": 0, "level": false } },
  { "wait_input": { "input": 0, "level": true, "timeout_ms": 60000 } },
  { "loop_end": {} }
] }
```

The response arrives after the program is committed: either accepted, or `ERROR_PROGRAM_INVALID` with `program_instruction_index`. Start it separately with `program_run`. The checks are the same as over USB; see [Programs](/docs/programs#rules-checked-by-the-device).

## Not in the first version

- **HTTPS in the local network**, with a certificate that phones accept without warnings.
- **Remote access** from outside the local network through a relay. If it comes, it will be optional; the device keeps working locally without it.
- **Bluetooth LE** as a fallback channel without Wi-Fi.
- **Retargeting `move_abs` while moving** — currently refused with `ERROR_BUSY`.

See also [Web apps on the device](/docs/web-apps), [upan.js](/docs/upan-js) and [upan-mcp](/docs/upan-mcp).
