# Programs

> Write motion programs in JSON, load them with upan-cli, and understand how the device checks them before the first step.

A program is a fixed sequence of motion and I/O steps that the device stores and runs on its own: moves, homing, waits, outputs and counted loops. Programs have no conditions and no variables, so the motion MCU can check the whole program — including every loop iteration — before the motor makes its first step. You write a program in JSON with human units, a host tool compiles it into protocol instructions, and the device either accepts it or tells you exactly which instruction is wrong. A running program keeps running if the USB or Wi-Fi connection drops.

> **Note:** This page follows board revision A and firmware in development. The JSON format below is the one accepted by the host compiler in `upan-cli`. The network API carries the same instructions in a lower-level JSON form; see [Network API](/docs/network-api).

## A first program

```json
{"program": [
  {"move_abs": "10mm", "velocity": "8mm/s"},
  {"loop": 3, "do": [
    {"move_rel": "20mm", "velocity": "10mm/s"},
    {"set_output": 1, "level": true},
    {"wait": "300ms"},
    {"set_output": 1, "level": false}
  ]},
  {"move_abs": "0mm"}
]}
```

Save it as `feed.json`. The program uses absolute moves, so home the axis first (or declare the current position with `set-position`). Then load and run it. The device checks the program on load, checks it again on run, and only then starts moving:

```powershell
upan-cli -p COM5 home
```

```powershell
upan-cli -p COM5 program load feed.json --run
```

Try it without a board on the simulator, started already homed at 0:

```powershell
upan-cli -p sim --sim-arg=--homed program load feed.json --run
```

Check the syntax without any device. The listing shows the instruction numbers that error messages refer to:

```powershell
upan-cli program check feed.json
```

```text
#0    move_abs 10000 velocity=8000
#1    loop 3 ×
#2    move_rel 20000 velocity=10000
#3    set OUT1=1
#4    wait 300 ms
#5    set OUT1=0
#6    loop_end
#7    move_abs 0
8 instructions — syntax ok (limits are checked by the device on load and run)
```

## File format

A program file is either a JSON list of steps or an object with a `program` list. Each step is an object with exactly one instruction key plus its options. Unknown keys are rejected.

| Step | Example | Options |
|---|---|---|
| `move_abs` | `{"move_abs": "10mm"}` | `velocity`, `acceleration` |
| `move_rel` | `{"move_rel": "-5mm"}` | `velocity`, `acceleration` |
| `home` | `{"home": true}` | — |
| `wait` | `{"wait": "500ms"}` | — |
| `wait_input` | `{"wait_input": 2, "level": true, "timeout": "5s"}` | `level` (default `true`), `timeout` (default none) |
| `set_output` | `{"set_output": "OUT1", "level": true}` | `level` (required) |
| `loop` | `{"loop": 10, "do": [ … ]}` | `do`: list of steps |
| `move_linear` | `{"move_linear": {"0": "120mm", "1": "40mm"}}` | `by`, `velocity`, `acceleration` |

### Steps in detail

- **`move_abs`** — move to a position. Needs a homed axis (see [Rules](#rules-checked-by-the-device)).
- **`move_rel`** — move by a signed distance. Works without homing.
- **`home`** — run homing with the method from HardConfig. Refused if the method is `HOME_NONE`. See [Configuration](/docs/configuration#homing).
- **`wait`** — pause for a time.
- **`wait_input`** — wait until an input reaches `level`. With a `timeout`, the program is aborted when it expires (`EVENT_PROGRAM_ABORTED` with error `ERROR_TIMEOUT`). Without one, it waits indefinitely.
- **`set_output`** — switch an output on (`true`) or off (`false`).
- **`loop`** — repeat the steps in `do` a fixed number of times (at least 1).
- **`move_linear`** — move several axes of a module stack together along a straight line (see [Multi-axis steps](#multi-axis-steps)).

`velocity` and `acceleration` are optional. If omitted, the device uses `velocity_default` and `acceleration_default` from SoftConfig. If given, they must be greater than zero.

Inputs and outputs are numbered as printed on the board: `1`, `2`, or `"IN1"`, `"OUT2"`. Board revision A has two inputs and two outputs. The protocol numbers them from 0; the compiler converts.

### Units in strings

| Kind | Linear axis | Rotary axis | Plain number means |
|---|---|---|---|
| Distance, position | `"10mm"`, `"250um"`, `"1.5cm"`, `"0.5in"` | `"90deg"`, `"500mdeg"`, `"1rev"` | mm (linear) or degrees (rotary) |
| Velocity | `"8mm/s"` | `"30deg/s"` | mm/s or deg/s |
| Acceleration | `"40mm/s2"` (also `mm/s^2`, `mm/s²`) | `"100deg/s2"` | mm/s² or deg/s² |
| Time | `"300ms"`, `"2s"`, `"1min"` | same | ms |

The compiler converts with exact decimal arithmetic and rounds to the nearest base unit (µm or millidegree). Inches are exact: 1 in = 25 400 µm. Using a rotary unit on a linear axis, or the other way round, is an error.

## Multi-axis steps

In a stack of UPAN Core and UPAN Axis modules, `move_linear` moves several axes so that they start together, travel proportionally and stop together.

```json
{"program": [
  {"move_linear": {"0": "120mm", "1": "40mm"}, "velocity": "10mm/s"},
  {"wait": "1s"},
  {"move_linear": {"1": "-5mm", "2": "5mm"}, "by": true}
]}
```

- Keys are axis numbers: `0` is the UPAN Core, `1`…`7` are UPAN Axis modules (the `bus_node` setting of each module).
- Values are positions. With `"by": true` they are distances, and no homing is needed.
- `velocity` and `acceleration` apply **along the line**. The line length is the square root of the sum of the squared axis distances.
- Each axis may appear only once in a step.

The host still talks only to the Core; the Core coordinates the other axes over the module bus. Programs with `move_linear` are checked against the limits of **every** axis involved, see below.

## Size and nesting limits

| Limit | Value |
|---|---|
| Instructions per program | 256 |
| Encoded program size | 6 KB |
| Loop nesting depth | 4 |
| Loop count | ≥ 1 |

A `loop` step compiles into two instructions (begin and end), so it counts twice. A program over the size limit is refused with `ERROR_PROGRAM_TOO_LARGE`. The host splits the upload into chunks that fit one protocol frame; you do not need to handle this yourself.

## Rules checked by the device

The host compiler checks only the shape of the file. Limits, travel and loop structure are checked by the motion MCU, which trusts no host. The check happens twice.

### On load (Commit)

When the last chunk is uploaded, the device checks the whole program before making it active:

- loop structure: balanced begin/end, nesting ≤ 4, every count ≥ 1;
- input and output numbers exist on this board;
- `home` is possible with the configured homing method;
- each move's velocity and acceleration against SoftConfig, and each `move_abs` target against the soft travel;
- each `move_linear` step names valid, distinct axes;
- the checksum matches the uploaded instructions.

### On run

Limits may have changed since the upload, and the start position is only known at run time. So `ProgramRun` repeats the checks and adds:

- **the exact extreme positions of the whole program**, starting from the current position and including every loop iteration and every repeat, against the soft travel;
- for `move_linear` steps, the same analysis for every axis in the stack, against that axis's own limits; the line velocity and acceleration must not exceed the smallest maximum of the axes in the step.

Any refusal happens **before the first step** of the motor.

### Homing rules

- `move_abs` and programs need a homed axis (after `Home` or `SetPosition`).
- Exception: a program may run on an axis that is not homed if its first motion step is `home`.
- `move_rel` and relative `move_linear` (`"by": true`) do not need homing.
- An absolute `move_linear` needs every axis in the step to be homed.

Otherwise `ProgramRun` is refused with `ERROR_NOT_HOMED`.

## Errors and the instruction index

Instruction indexes start at 0 and refer to the compiled list shown by `program check`.

| Error | When | Details in the response |
|---|---|---|
| `ERROR_PROGRAM_INVALID` | on load: broken structure, unknown input/output, `home` without a homing method | `program_instruction_index` |
| `ERROR_LIMIT_EXCEEDED` | on load or run: a value outside the limits | `program_instruction_index`, `limit_violation` (limit, requested, allowed) and, for `move_linear`, `axis` |
| `ERROR_NOT_HOMED` | on run: absolute motion without homing | `program_instruction_index` |
| `ERROR_PROGRAM_TOO_LARGE` | on load: more than 256 instructions or 6 KB | — |
| `ERROR_PROGRAM_CHECKSUM` | on load: uploaded data does not match the checksum | — |
| `ERROR_NO_PROGRAM` | run without a loaded program | — |

`upan-cli` prints the reason in human units with the allowed value, for example:

```text
device refused: LIMIT_EXCEEDED: TRAVEL_MAX — requested 120.000 mm, allowed 100.000 mm (program instruction #7)
```

Exit codes of `upan-cli`: 0 — success, 1 — the device refused or an error occurred during motion, 2 — error in the command or the file, 3 — no connection.

See [`Response`](/docs/api#Response) and [`ErrorCode`](/docs/api#ErrorCode) for the full list.

## While a program runs

- Telemetry reports the current instruction (`program_pc`) and repeat (`program_repeat`).
- Moves inside a program do not send `EVENT_MOVE_DONE`; the end of the program sends `EVENT_PROGRAM_DONE`.
- **Pause** stops after the current move; **Resume** continues; **Abort** stops like `Stop` and resets the program counter.
- `Stop`, e-stop, a fault or a `wait_input` timeout abort the program with `EVENT_PROGRAM_ABORTED`. The event carries the error and the instruction index in `detail`; for a stack, `axis` names the module that refused.
- A lost USB or network connection does **not** stop a running program.

```powershell
upan-cli -p COM5 program run --repeat 10
```

```powershell
upan-cli -p COM5 program pause
```

```powershell
upan-cli -p COM5 program abort
```

## Example: feed with a foot pedal

A woodworking feed: home, then 50 times wait for the pedal on IN1, feed 300 mm, pulse OUT1 for a saw, and return.

```json
{"program": [
  {"home": true},
  {"loop": 50, "do": [
    {"wait_input": "IN1", "level": true},
    {"move_rel": "300mm", "velocity": "80mm/s", "acceleration": "400mm/s2"},
    {"set_output": "OUT1", "level": true},
    {"wait": "2s"},
    {"set_output": "OUT1", "level": false},
    {"move_rel": "-300mm"}
  ]}
]}
```

```powershell
upan-cli -p COM5 program load feed-pedal.json --run
```

If the soft limits do not allow 80 mm/s, the device refuses on load and names instruction #3 with the allowed velocity.
