Advanced Metal Research
GitHub Contact AMR

Cartesian motion server

On this page
  1. Run it
  2. Command line
  3. Bindings
  4. Network and status
  5. Offline modes
  6. UDP intent packet
  7. Scaling
  8. Frames
  9. What halts the jog
  10. NATS commands
  11. Leader lease
  12. Robot commands
  13. Status document
  14. Resolve-only protocol
  15. Refusal and halt reasons
  16. Related pages

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-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#

FlagRequiredDescription
--backend rt_corenoThe only backend. Default from MOTION_SERVER_BACKEND, else rt_core.
--rt-control-socket PATHyesThe rt-control Unix socket
--rt-jog-socket PATHyesThe jog datagram socket, jog.sock beside control.sock
--rt-pair-id IDyesThe pair binding rt-control was started with
--rt-pair-revision NyesPositive integer
--rt-configuration-sha256 HASHyesThe compiled configuration digest
--rt-axis-ids J1,…,J6yesSix Describe axis IDs, in URDF J1..J6 order. Each must be in rad. Other axes stay unselected.
--rt-urdf PATHyesThe URDF the resolver loads
--rt-urdf-sha256 HASHyesSHA-256 of that file, lowercase hex. If the file changes on disk, jog stops.

Network and status#

FlagDefaultDescription
--udp-listen HOST:PORTnone (required)UDP intent listener. Must name a concrete host.
--udp-ready-file PATHnoneWritten with the bound address once the socket is open
--status-output PATH/run/robot-v4-cartesian/status.jsonThe status document, rewritten atomically
--latency-report-output PATHnoneLatency report output
--nats-url URLnone (required)nats://HOST:PORT, or a daemon-v1://PEER/ROLE/NAME reference resolved through --daemon-state-url
--daemon-state-url URLhttp://127.0.0.1:8787/api/stateOnly used to resolve a daemon-v1:// NATS reference
--robot-command-subject SUBJfrom the manifestThe subject the server subscribes to for commands (required)
--robot-cell NAMEthe manifest nameCommands whose robot differs are ignored
--nats-status-subject SUBJfrom the manifestWhere the status document is published, at most every 250 ms
--nats-plan-subject SUBJfrom the manifestPlan subject for the self-test plan paths
--rtcore-status-subject SUBJenv ROBOT_V4_RTCORE_STATUS_SUBJECT, or the manifestValidated against the manifest if one is given
--manifest PATHenv ROBOT_V4_MANIFESTA deployment manifest that supplies the subjects, the cell name and leader_controller_roles
--trusted-lan-leader-authorityoff (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 N100Control loop rate for the smoother
--diagnostic-echo-sourceoffDiagnostic 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#

InvocationDescription
--resolve-only REPO_ROOT MODELThe resolve-only protocol on stdin and stdout. No sockets, no grant.
--self-test NAMEOffline 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.

OffsetFieldTypeDescription
0magicu160x4A49 ("JI")
2versionu81 or 2
3sample_timestamp_nsu64When the source sampled the input
11sequenceu32Must increase for each packet under one lease
15frameu8See frames
16tool_id_lenu80–31
17axes[6]6 × f32Normalised X, Y, Z, RX, RY, RZ, each clamped to [-1, 1]
41speed_scalef32Clamped to [0, 1]
45deadmanu8Nonzero while the operator holds the enabling control
46tool_tcp[6]6 × f32For an ARM frame, tool_tcp[0] > 0.5 means arm and ≤ 0.5 disarm
70tool_idtool_id_len bytesThe sender identity, <role>:<instance>. A bare value such as steamdeck-1 is read as steamdeck:steamdeck-1.
70 + nleader_fence_epochu64Version 2 only. Nonzero.
78 + nlease_id_lenu8Version 2 only. 1–63.
79 + nlease_idbytesVersion 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#

CodeNameOn the rt_core backend
0BASECartesian jog
1TOOLCartesian jog. The rt_core runtime resolves it exactly like BASE; it does not rotate the twist into the tool frame.
2JOINTRefused: halts with rt_core_command_unavailable
3HOMENative Home on the selected axes
4TELEMETRYRefused
5ARMArm or disarm, from tool_tcp[0]
6GO_HOMERefused
7WELD_IORefused

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
}
FieldTypeRequiredDescription
schemastringyesrobot.v4.robot-command.v1
commandstringyesSee the table below
robotstringyesMust equal --robot-cell
command_idstringyesA resend with the same ID is acknowledged as a duplicate and not run again
sender_idstringyes<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_epochstring, string, integerfor owned commandsThe 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.

CommandFieldsDescription
leader_acquiresender_id, controller_boot_id, request_id, campaign_generation (> 0), ttl_ms (> 0)Request the lease. Must not carry lease_id or fence_epoch.
leader_renewthe above plus lease_id, fence_epochExtend the lease
leader_releasesender_id, controller_boot_id, request_id, campaign_generation, lease_id, fence_epochEnd the lease. No ttl_ms.
leader_cancelsender_id, controller_boot_id, request_id, campaign_generationWithdraw 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#

CommandLeaseEffect
stop, disarmnot neededHalt: end the jog, run Stop, cancel any pending Home or position run
end_run (or end-run)not neededRevoke the current source run and halt
armneeded"armed": true enables and arms. "armed": false halts.
home (alias hm35, hm35_home)neededNative 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)neededNeeds a source run admitted through position; otherwise halts with rt_core_run_not_admitted
positionneededOne 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.

FieldDescription
backendrt_core
staterunning, or inhibited after a halt
refusal, typed_refusalThe last halt or refusal reason, and the structured native refusal
servos_armed_requestedArm was requested and accepted
last_stop_confirmedtrue when the last Stop was acknowledged, false when its outcome is uncertain
latest_input_freshA jog is running on fresh input
latest_applied_qd_rad_sThe joint velocities last sent, rad/s
latest_joint_limit_scale, latest_singularity_scale, latest_singularity_classThe scaling applied to the last jog; the class is clear, warning, hard_stop or unavailable
datagrams_received, rejected_input_count, rtcore_outputs_sent, latest_sequenceInput counters
latest_command_kindrt_core_intent_applied, or the last refusal
latest_processed_cold_command_id, _kind, _accepted, _resultThe last NATS command and whether it was applied
leader_*, latest_leader_*The leader lease, as above
rt_core_statusThe complete rt-control status snapshot
axesThe per-axis logical status from rt-control, including readiness, Home and statusword
fault_tableThe recovery faults from rt-control, or null
v4_fields_availableAlways 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_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: twistJSON
{"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": []}
FieldTypeRequiredDescription
modelstringyesMust equal the MODEL argument
framestringyesbase or tool. Here, unlike the UDP path, a tool twist is rotated into the base frame.
operationstringnoEmpty for a twist, move for a displacement
twist6 numbersyesm/s and rad/s, multiplied by fraction. Required even for move.
delta6 numbersfor moveExactly one nonzero component: up to 1 m linear or π rad angular
pose6 numbers, radyesMeasured J1–J6 positions
lower, upper6 numbers, radyesJoint limits. Intersected with the URDF limits.
fractionnumberyes(0, 1]
input_age_nsinteger, nsyesThe 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.

ReasonMeaning
cartesian_input_invalidMalformed request, wrong model, zero twist, or a move with not exactly one component
joint_limitThe pose is outside the limits, or the result would leave them
jacobian_gateToo close to a singularity
ik_no_solutionNo joint solution, or no motion results
cartesian_reachThe displacement is too long, or the path crosses the singularity gate

Refusal and halt reasons#

ReasonMeaning
input_safety_barrierAn unsafe or untimed packet was in the batch
deadman_released, operator_disarm, operator_stopThe operator ended motion
leader_fence_or_sequence_rejectedA packet without the current lease or with an old sequence
leader_transitionThe leader lease changed hands
invalid_cartesian_inputA packet failed to decode
rt_core_urdf_changedThe URDF on disk no longer matches --rt-urdf-sha256
rt_core_command_unowned, rt_core_command_unavailable, rt_core_command_unavailable_or_unownedSee Robot commands
rt_core_position_input_unresolvedA position command without exactly one resolved joint target
rt_core_run_not_admittedgo_home or a follow-up command without an admitted source run
rt_core_home_abandoned, rt_core_home_failed, rt_core_home_requires_idle_runHome was replaced, failed, or asked for during a run
home_preempted_by_arm, home_replaced, home_replaced_by_udpA new request replaced a pending Home
rt_core_position_timeout, rt_core_admission_timeout, rt_core_source_permit_rejectedA position run expired or its permit was refused

rt-control's own reasons pass through in refusal and typed_refusal. See Error codes.