# Cartesian motion server

> robot-v4-cartesiand, the Cartesian jog server, with its command-line bindings, the UDP intent packet, the NATS leader lease and robot commands, the status document and the resolve-only JSON protocol OLP uses.

URL: https://advancedmetalresearch.com/docs/apis/cartesian-motion-server
Section: RosieOS docs / APIs
Last updated: 2026-10-10

`robot-v4-cartesiand` turns a stream of UDP intent packets into Cartesian jog on the robot. It resolves each Cartesian twist into joint velocities with a damped Jacobian, applies joint-limit and singularity scaling, and streams the result on the `rt-control` jog lane. NATS carries its leader lease and its lifecycle commands: arm, disarm, Home and stop.

The same binary has a second, motion-free mode, `--resolve-only`, which OLP runs as a subprocess to turn a twist or a displacement into joint velocities or waypoints.

`rt_core` is the only backend. The retired selections exit with `backend_retired`.

> [!WARNING] **Energised motion.** This moves the robot. RosieOS has no software E-stop: keep the hardware E-stop within reach and clear the cell before you arm. Jog, moves and Home are checked against joint limits only, not against collisions. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model).

## Run it

Every binding is required. The server refuses to start without the control and jog sockets, the pair binding, six axis IDs, a URDF with its SHA-256, a UDP listen address, a NATS URL, a robot command subject and `--trusted-lan-leader-authority`.

```bash
robot-v4-cartesiand --backend rt_core \
  --rt-control-socket /run/rosie-rt-core/control.sock \
  --rt-jog-socket /run/rosie-rt-core/jog.sock \
  --rt-pair-id "$ROSIE_RT_PAIR_ID" --rt-pair-revision "$ROSIE_RT_PAIR_REVISION" \
  --rt-configuration-sha256 "$ROSIE_RT_CONFIGURATION_SHA256" \
  --rt-axis-ids J1,J2,J3,J4,J5,J6 \
  --rt-urdf robot_description/robots/rosie_1400_v3/robot.urdf \
  --rt-urdf-sha256 "$URDF_SHA256" \
  --udp-listen 127.0.0.1:9000 \
  --nats-url nats://127.0.0.1:14222 \
  --robot-cell cell-a \
  --robot-command-subject robot/v4/robot.cell-a.command \
  --nats-status-subject robot/v4/motion-server.cell-a.status \
  --trusted-lan-leader-authority
```

Build it with `make -C motion-server/v1 all`. The binary goes to `$(ROSIE_HOME)/motion-server/v1/bin`; set `BIN_DIR` to change it. On an installed host, `motion-server/v1/start-motion-server.sh` supplies the `--rt-*` bindings from the `ROSIE_RT_*` environment variables (`ROSIE_RT_CONTROL_SOCKET`, `ROSIE_RT_JOG_SOCKET`, `ROSIE_RT_PAIR_ID`, `ROSIE_RT_PAIR_REVISION`, `ROSIE_RT_CONFIGURATION_SHA256`, `ROSIE_RT_AXIS_IDS`, `ROSIE_RT_URDF`, `ROSIE_RT_URDF_SHA256`), and `MOTION_SERVER_BINARY` names the binary.

On startup the server acquires `rt-control` with its pair binding and renews the grant every 100 ms. It enables and arms only when a controller asks. Home never arms. On shutdown it ends the jog, stops and releases.

## Command line

### Bindings

| Flag | Required | Description |
|---|---|---|
| `--backend rt_core` | no | The only backend. Default from `MOTION_SERVER_BACKEND`, else `rt_core`. |
| `--rt-control-socket PATH` | yes | The `rt-control` Unix socket |
| `--rt-jog-socket PATH` | yes | The jog datagram socket, `jog.sock` beside `control.sock` |
| `--rt-pair-id ID` | yes | The pair binding `rt-control` was started with |
| `--rt-pair-revision N` | yes | Positive integer |
| `--rt-configuration-sha256 HASH` | yes | The compiled configuration digest |
| `--rt-axis-ids J1,…,J6` | yes | Six Describe axis IDs, in URDF J1..J6 order. Each must be in rad. Other axes stay unselected. |
| `--rt-urdf PATH` | yes | The URDF the resolver loads |
| `--rt-urdf-sha256 HASH` | yes | SHA-256 of that file, lowercase hex. If the file changes on disk, jog stops. |

### Network and status

| Flag | Default | Description |
|---|---|---|
| `--udp-listen HOST:PORT` | none (required) | UDP intent listener. Must name a concrete host. |
| `--udp-ready-file PATH` | none | Written with the bound address once the socket is open |
| `--status-output PATH` | `/run/robot-v4-cartesian/status.json` | The [status document](https://advancedmetalresearch.com/docs/apis/cartesian-motion-server#status), rewritten atomically |
| `--latency-report-output PATH` | none | Latency report output |
| `--nats-url URL` | none (required) | `nats://HOST:PORT`, or a `daemon-v1://PEER/ROLE/NAME` reference resolved through `--daemon-state-url` |
| `--daemon-state-url URL` | `http://127.0.0.1:8787/api/state` | Only used to resolve a `daemon-v1://` NATS reference |
| `--robot-command-subject SUBJ` | from the manifest | The subject the server subscribes to for commands (required) |
| `--robot-cell NAME` | the manifest `name` | Commands whose `robot` differs are ignored |
| `--nats-status-subject SUBJ` | from the manifest | Where the status document is published, at most every 250 ms |
| `--nats-plan-subject SUBJ` | from the manifest | Plan subject for the self-test plan paths |
| `--rtcore-status-subject SUBJ` | env `ROBOT_V4_RTCORE_STATUS_SUBJECT`, or the manifest | Validated against the manifest if one is given |
| `--manifest PATH` | env `ROBOT_V4_MANIFEST` | A deployment manifest that supplies the subjects, the cell name and `leader_controller_roles` |
| `--trusted-lan-leader-authority` | off (required) | Enables the NATS leader lease. This is cooperative fencing on a trusted network, not authentication: anyone who can publish on the subject can send commands. |
| `--control-frequency-hz N` | `100` | Control loop rate for the smoother |
| `--diagnostic-echo-source` | off | Diagnostic latency echo |

A subject must name the concrete motion-server peer: `robot/v4/motion-server.<peer>.…` or `robot/v4/robot.<peer>.…`. `--nats-arm-subject`, `--nats-go-home-subject`, `--nats-io-subject` and `--rtcore-target` are retired and exit with `backend_retired`.

### Offline modes

| Invocation | Description |
|---|---|
| `--resolve-only REPO_ROOT MODEL` | The [resolve-only protocol](https://advancedmetalresearch.com/docs/apis/cartesian-motion-server#resolve-only) on stdin and stdout. No sockets, no grant. |
| `--self-test NAME` | Offline solver and contract tests: `cartesian-io`, `spreadsheet-tesseract-plan`, `spreadsheet-tesseract-plan-server`, `accepted-plan-contract`, `solver-speed`, `kinematics-authority`, `table-calibration-fit`, `table-calibration-apply`. They take `--input`, `--output`, `--manifest`, `--samples` (5), `--period-ms` (10), `--iterations` (1000) and `--oneshot` as each test needs. |

## UDP intent packet

One datagram per intent, big-endian throughout. Version 2 adds the leader lease, and motion on the `rt_core` backend needs it.

| Offset | Field | Type | Description |
|---|---|---|---|
| 0 | `magic` | u16 | `0x4A49` ("JI") |
| 2 | `version` | u8 | `1` or `2` |
| 3 | `sample_timestamp_ns` | u64 | When the source sampled the input |
| 11 | `sequence` | u32 | Must increase for each packet under one lease |
| 15 | `frame` | u8 | See [frames](https://advancedmetalresearch.com/docs/apis/cartesian-motion-server#frames) |
| 16 | `tool_id_len` | u8 | 0–31 |
| 17 | `axes[6]` | 6 × f32 | Normalised X, Y, Z, RX, RY, RZ, each clamped to [-1, 1] |
| 41 | `speed_scale` | f32 | Clamped to [0, 1] |
| 45 | `deadman` | u8 | Nonzero while the operator holds the enabling control |
| 46 | `tool_tcp[6]` | 6 × f32 | For an ARM frame, `tool_tcp[0] > 0.5` means arm and `≤ 0.5` disarm |
| 70 | `tool_id` | `tool_id_len` bytes | The sender identity, `<role>:<instance>`. A bare value such as `steamdeck-1` is read as `steamdeck:steamdeck-1`. |
| 70 + n | `leader_fence_epoch` | u64 | Version 2 only. Nonzero. |
| 78 + n | `lease_id_len` | u8 | Version 2 only. 1–63. |
| 79 + n | `lease_id` | bytes | Version 2 only |

A version 1 packet is exactly `70 + tool_id_len` bytes. A version 2 packet is exactly `79 + tool_id_len + lease_id_len` bytes. The maximum is 173 bytes. Anything else, or any non-finite float, is rejected.

### Scaling

Each linear axis maps to `axes[i] × speed_scale × 0.2 m/s` and each angular axis to `axes[i] × speed_scale × π rad/s`. The command is slew-limited at 0.75 m/s² linear and 540°/s² angular, resolved into six joint velocities, and scaled as one vector so no joint exceeds 100 rpm and the Jacobian's singularity gate. The `rt_core` backend then scales the whole vector again to Describe's per-axis velocity caps. One common scale is applied, so the direction of a Cartesian jog never bends.

Each jog output's deadline is at most 250 ms after the packet arrived, shortened by the time it waited in the socket queue.

### Frames

| Code | Name | On the `rt_core` backend |
|---|---|---|
| 0 | BASE | Cartesian jog |
| 1 | TOOL | Cartesian jog. The `rt_core` runtime resolves it exactly like BASE; it does not rotate the twist into the tool frame. |
| 2 | JOINT | Refused: halts with `rt_core_command_unavailable` |
| 3 | HOME | Native Home on the selected axes |
| 4 | TELEMETRY | Refused |
| 5 | ARM | Arm or disarm, from `tool_tcp[0]` |
| 6 | GO_HOME | Refused |
| 7 | WELD_IO | Refused |

### What halts the jog

The server reads every waiting datagram (up to 64 per loop) and acts only on the newest. It halts, which ends the jog and runs Stop, when any packet in the batch:

- fails to decode, or has the deadman released
- is a neutral hold (a TOOL or JOINT packet with every axis within 0.02 of zero)
- is a disarm
- arrived without a kernel receive timestamp, or waited in the socket queue for 250 ms or more

It also halts when a packet's lease ID, fence epoch or sender does not match the current leader, or its sequence does not increase (`leader_fence_or_sequence_rejected`). After a halt, fresh input cannot resume motion. The controller must arm again.

## NATS commands

Commands arrive on `--robot-command-subject` as `robot.v4.robot-command.v1` JSON. The server ignores messages for another `robot` and commands it does not own.

```json
{
  "schema": "robot.v4.robot-command.v1",
  "command": "arm",
  "robot": "cell-a",
  "command_id": "c-17",
  "sender_id": "steamdeck:deck-1",
  "controller_boot_id": "boot-5c1e",
  "leader_lease_id": "…",
  "leader_fence_epoch": 3,
  "armed": true
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `schema` | string | yes | `robot.v4.robot-command.v1` |
| `command` | string | yes | See the table below |
| `robot` | string | yes | Must equal `--robot-cell` |
| `command_id` | string | yes | A resend with the same ID is acknowledged as a duplicate and not run again |
| `sender_id` | string | yes | `<role>:<instance>`. The role must be in the manifest's `leader_controller_roles`, which defaults to `["steamdeck"]`. |
| `controller_boot_id`, `leader_lease_id`, `leader_fence_epoch` | string, string, integer | for owned commands | The sender's current leader lease |

Replies use `robot.v4.command-reply.v1`:

```json
{"schema": "robot.v4.command-reply.v1", "component": "motion-server", "command": "arm", "command_id": "c-17", "accepted": true, "duplicate": false, "state": "completed"}
```

A reply that the command was queued is not proof it was applied. Read `latest_processed_cold_command_id`, `latest_processed_cold_accepted` and `latest_processed_cold_result` in the status document.

### Leader lease

The server grants one leader lease at a time. It is process-local and empty after every restart. A new grant is revoke-first: motion stops before the new fence epoch exists.

| Command | Fields | Description |
|---|---|---|
| `leader_acquire` | `sender_id`, `controller_boot_id`, `request_id`, `campaign_generation` (> 0), `ttl_ms` (> 0) | Request the lease. Must not carry `lease_id` or `fence_epoch`. |
| `leader_renew` | the above plus `lease_id`, `fence_epoch` | Extend the lease |
| `leader_release` | `sender_id`, `controller_boot_id`, `request_id`, `campaign_generation`, `lease_id`, `fence_epoch` | End the lease. No `ttl_ms`. |
| `leader_cancel` | `sender_id`, `controller_boot_id`, `request_id`, `campaign_generation` | Withdraw a pending acquire. No `ttl_ms`, `lease_id` or `fence_epoch`. |

`ttl_ms` is clamped to 500–2000 ms. The grant shows in the status document: `leader_id`, `leader_lease_id`, `leader_fence_epoch`, `leader_expires_in_ms` and `leader_lease_fresh`. `latest_leader_request_id` and `latest_leader_result` report the outcome of your request, for example `acquired`, `renewed`, `released`, `rejected` or `expired`.

Lease commands spell the fence `fence_epoch`. Motion commands and UDP packets spell it `leader_fence_epoch`.

### Robot commands

| Command | Lease | Effect |
|---|---|---|
| `stop`, `disarm` | not needed | Halt: end the jog, run Stop, cancel any pending Home or position run |
| `end_run` (or `end-run`) | not needed | Revoke the current source run and halt |
| `arm` | needed | `"armed": true` enables and arms. `"armed": false` halts. |
| `home` (alias `hm35`, `hm35_home`) | needed | Native Home on the selected axes. Completes when a fresh status shows a new Home epoch with Home valid on every selected axis. |
| `go_home` (alias `go-home`) | needed | Needs a source run admitted through `position`; otherwise halts with `rt_core_run_not_admitted` |
| `position` | needed | One joint to a target: `axis` and exactly one of `target_rad`, `target_deg` or `relative_jog_rad`, with optional `min_rad`/`max_rad`, `max_speed_rad_s` and `timeout_ms`. Anything else is refused with `rt_core_position_input_unresolved`. |

> [!NOTE] A `position` command first needs a source-run admission: the server sends `position_execution_admit` on the command subject and waits for a `robot.v4.bridge-position-permit.v1` reply from the run's source bridge. No component in this repository sends that reply outside its tests, so position and go-home runs need an external bridge.

A command that needs the lease and arrives without a matching one halts the server with `rt_core_command_unowned`, which stops any motion in progress. Any other command halts with `rt_core_command_unavailable_or_unowned`.

## Status document

Written to `--status-output` and published on `--nats-status-subject`. The schema name is `robot_v4_motion_server_cartesian_live_status_v1`.

| Field | Description |
|---|---|
| `backend` | `rt_core` |
| `state` | `running`, or `inhibited` after a halt |
| `refusal`, `typed_refusal` | The last halt or refusal reason, and the structured native refusal |
| `servos_armed_requested` | Arm was requested and accepted |
| `last_stop_confirmed` | `true` when the last Stop was acknowledged, `false` when its outcome is uncertain |
| `latest_input_fresh` | A jog is running on fresh input |
| `latest_applied_qd_rad_s` | The joint velocities last sent, rad/s |
| `latest_joint_limit_scale`, `latest_singularity_scale`, `latest_singularity_class` | The scaling applied to the last jog; the class is `clear`, `warning`, `hard_stop` or `unavailable` |
| `datagrams_received`, `rejected_input_count`, `rtcore_outputs_sent`, `latest_sequence` | Input counters |
| `latest_command_kind` | `rt_core_intent_applied`, or the last refusal |
| `latest_processed_cold_command_id`, `_kind`, `_accepted`, `_result` | The last NATS command and whether it was applied |
| `leader_*`, `latest_leader_*` | The leader lease, as above |
| `rt_core_status` | The complete `rt-control` status snapshot |
| `axes` | The per-axis logical status from `rt-control`, including readiness, Home and statusword |
| `fault_table` | The recovery faults from `rt-control`, or `null` |
| `v4_fields_available` | Always `false` on this backend. The earlier backend's mode and activation fields are present and `null`. |

## Resolve-only protocol

```bash
robot-v4-cartesiand --resolve-only /path/to/RosieOS rosie_1400_v3
```

The server loads `REPO_ROOT/robot_description/robots/<MODEL>/robot.urdf` once, then answers one JSON line on stdout for each JSON line on stdin. `MODEL` is `rosie_1400_v3` or `rosie_1420_v1`. No sockets are opened and no grant is taken. Exit code 2 means a bad invocation, an unavailable model or a request line over 16,384 bytes; end of input exits 0.

request: twist:

```json
{"model": "rosie_1400_v3", "frame": "base",
 "twist": [0.05, 0, 0, 0, 0, 0], "fraction": 0.5, "input_age_ns": 250000000,
 "pose": [0, -0.4, 0.8, 0, 0.6, 0], "lower": [-3.14, -1.9, -1.57, -3.14, -3.37, -2.09],
 "upper": [3.14, 1.9, 1.53, 3.14, 1.3, 3.14]}
```

response (values illustrative):

```json
{"accepted": true, "reason": "", "velocities": [0.0, 0.07, -0.05, 0.0, -0.02, 0.0],
 "joint_limit_scale": 1, "singularity_scale": 1, "waypoints": []}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `model` | string | yes | Must equal the `MODEL` argument |
| `frame` | string | yes | `base` or `tool`. Here, unlike the UDP path, a `tool` twist is rotated into the base frame. |
| `operation` | string | no | Empty for a twist, `move` for a displacement |
| `twist` | 6 numbers | yes | m/s and rad/s, multiplied by `fraction`. Required even for `move`. |
| `delta` | 6 numbers | for `move` | Exactly one nonzero component: up to 1 m linear or π rad angular |
| `pose` | 6 numbers, rad | yes | Measured J1–J6 positions |
| `lower`, `upper` | 6 numbers, rad | yes | Joint limits. Intersected with the URDF limits. |
| `fraction` | number | yes | (0, 1] |
| `input_age_ns` | integer, ns | yes | The input lifetime. A twist is refused if `pose + velocity × lifetime` would leave the limits. |

For a twist, `velocities` are J1–J6 in rad/s. For a `move`, `waypoints` are J1–J6 positions in rad at 1 mm or 0.25° spacing along the straight line.

| Reason | Meaning |
|---|---|
| `cartesian_input_invalid` | Malformed request, wrong model, zero twist, or a `move` with not exactly one component |
| `joint_limit` | The pose is outside the limits, or the result would leave them |
| `jacobian_gate` | Too close to a singularity |
| `ik_no_solution` | No joint solution, or no motion results |
| `cartesian_reach` | The displacement is too long, or the path crosses the singularity gate |

## Refusal and halt reasons

| Reason | Meaning |
|---|---|
| `input_safety_barrier` | An unsafe or untimed packet was in the batch |
| `deadman_released`, `operator_disarm`, `operator_stop` | The operator ended motion |
| `leader_fence_or_sequence_rejected` | A packet without the current lease or with an old sequence |
| `leader_transition` | The leader lease changed hands |
| `invalid_cartesian_input` | A packet failed to decode |
| `rt_core_urdf_changed` | The URDF on disk no longer matches `--rt-urdf-sha256` |
| `rt_core_command_unowned`, `rt_core_command_unavailable`, `rt_core_command_unavailable_or_unowned` | See [Robot commands](https://advancedmetalresearch.com/docs/apis/cartesian-motion-server#robot-commands) |
| `rt_core_position_input_unresolved` | A `position` command without exactly one resolved joint target |
| `rt_core_run_not_admitted` | `go_home` or a follow-up command without an admitted source run |
| `rt_core_home_abandoned`, `rt_core_home_failed`, `rt_core_home_requires_idle_run` | Home was replaced, failed, or asked for during a run |
| `home_preempted_by_arm`, `home_replaced`, `home_replaced_by_udp` | A new request replaced a pending Home |
| `rt_core_position_timeout`, `rt_core_admission_timeout`, `rt_core_source_permit_rejected` | A position run expired or its permit was refused |

`rt-control`'s own reasons pass through in `refusal` and `typed_refusal`. See [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes).

## Related pages

- [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning#cartesian)
- [NATS subjects and streams](https://advancedmetalresearch.com/docs/reference/nats-subjects)
- [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority)
- [Ports, sockets and environment](https://advancedmetalresearch.com/docs/reference/ports-and-environment)

## Sources

Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS):

- `motion-server/v1/src/robot_v4_cartesian_cli.hpp:1-414`
- `motion-server/v1/src/robot_v4_cartesian_daemon.cpp:53-103,1155-1199,1384-1389,3508-3579`
- `motion-server/v1/src/rt_core_cartesian_runtime.hpp:1-544`
- `motion-server/v1/src/robot_v4_cartesian_nats_protocol.hpp:816-834,1008-1073,1152-1170,1225-1333,1370-1406,2403-2519,2882-2894`
- `motion-server/v1/src/robot_v4_cartesian_command_safety.hpp:114-190`
- `motion-server/v1/src/robot_v4_cartesian_leader_authority.hpp:17-60,495-530,750-753`
- `motion-server/v1/src/robot_v4_position_protocol.hpp:75-125`
- `motion-server/v1/src/cartesian_resolver.hpp:1-250`
- `motion-server/v1/src/rt_core_jog_backend.hpp:20`
- `motion-server/v1/start-motion-server.sh:5-30`
- `motion-server/v1/Makefile`
