Cartesian motion server
On this page
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.
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.
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-authorityBuild 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, 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 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 |
| 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.
{
"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:
{"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#
robot-v4-cartesiand --resolve-only /path/to/RosieOS rosie_1400_v3The 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.
{"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]}{"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 |
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.