Advanced Metal Research
GitHub Contact AMR

Offline programming HTTP API

On this page
  1. Quick start
  2. Conventions
  3. Errors
  4. Target fencing
  5. Routes at a glance
  6. Service
  7. Authoring and planning
  8. Plan a program
  9. Cells and targets
  10. Machine control
  11. Observe
  12. Home
  13. Arm and disarm
  14. Heartbeat
  15. Load, Play, Stop
  16. Joint jog
  17. Joint move
  18. Cartesian jog
  19. Cartesian move
  20. Local simulator
  21. Program catalog
  22. Legacy and disabled routes
  23. Error codes
  24. Related pages

The offline programming (OLP) server is the backend of the OLP web app and of the Steam Deck v5 pendant. It serves CAD import, seam authoring and weld planning, a local simulator, and machine control: Home, Arm, joint and Cartesian jog, joint and Cartesian moves, Load, Play and Stop on a selected cell, through rt-control.

All routes are under /api/offline-programming/v1 on 127.0.0.1:8794 by default (serve --listen). The server has no authentication. It is meant to be reached from the same host, by the UI's dev proxy or by a pendant's local process. Don't expose it on a network.

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.

Tip

Machine-readable. This page's routes, request fields and error codes as an OpenAPI 3.1 document, generated from this page.

Quick start#

This selects the local simulated cell that the dev stack provides, homes it, arms it, jogs J1 for a moment, and stops.

OLP=http://127.0.0.1:8794/api/offline-programming/v1

# 1. Pick a cell. The dev stack lists "local-simulation" first.
curl -s $OLP/targets
curl -s -X POST $OLP/targets/select -d '{"cell_id":"local-simulation","model_id":"rosie_1400_v3"}'

# 2. Every mutating dense-execution call carries the selection it was made against.
STATUS=$(curl -s $OLP/dense-execution/status)
GEN=$(echo "$STATUS" | jq -r .target.selection_generation)
CELL=$(echo "$STATUS" | jq -r .target.cell_id)
FENCE=(-H "X-RT-Target-Generation: $GEN" -H "X-RT-Target-Cell: $CELL")

# 3. Home, then arm. Arm acquires the rt-control lease.
curl -s -X POST $OLP/dense-execution/home "${FENCE[@]}"
curl -s -X POST $OLP/dense-execution/arm "${FENCE[@]}" -d '{"armed":true}'

# 4. Jog J1 at 10 % for one hold. A real client repeats "update" every 50 ms.
TARGET=$(curl -s $OLP/dense-execution/capabilities | jq -r .cell)
SESSION=$(curl -s $OLP/dense-execution/status | jq -r .session.id)
REV=$(curl -s $OLP/dense-execution/jog/state | jq -r .revision)
JOG="{\"target_id\":\"$TARGET\",\"session_id\":\"$SESSION\",\"revision\":$REV,\"axis\":0,\"direction\":1,\"fraction\":0.1}"
curl -s -X POST $OLP/dense-execution/jog/begin "${FENCE[@]}" -d "$JOG"
curl -s -X POST $OLP/dense-execution/jog/end -d "$JOG"

# 5. Stop always works and needs no fence. It also releases the lease.
curl -s -X POST $OLP/dense-execution/stop

While OLP holds the lease, send a heartbeat at least every 5 s, or OLP stops the machine with ui_heartbeat_lost.

Conventions#

  • Request and response bodies are JSON unless a route says otherwise. Unknown request fields are ignored; trailing data after the JSON value is refused.
  • Units are in the field names: _rad, _deg, _mm, _m, _s, _ms, _ns. Where a name has no unit, the table says.
  • Request body limits: 64 KiB for dense-execution routes, 12 MiB for /cadquery/topology and /seam, 24 MiB for /weld-plan.
  • Responses are gzip-compressed when the client accepts it.

Errors#

Two error shapes are in use.

Machine-control routes (/dense-execution/*, /targets/select):

{"error": "home_required", "detail": "home_required: establish the current joint position reference in Home before loading the program"}

error is a stable code. A refusal from rt-control keeps rt-control's reason as the code. A limit refusal can add limit_violation: {kind, segment, sample, axis, value, limit, unit}.

Authoring and service routes:

{"ok": false, "code": "seam_worker_unavailable", "error": "the seam worker is unavailable, so no plan request can be packed"}

Target fencing#

When the server runs with a cell catalogue (OFFLINE_PROGRAMMING_CELLS), every POST under /dense-execution/ must name the selection it was issued against:

HeaderValue
X-RT-Target-Generationstatus.target.selection_generation, a decimal integer (sent as a string in JSON)
X-RT-Target-Cellstatus.target.cell_id

If another client has selected a different cell since, the request is refused with 409 target_changed and nothing is sent to the robot.

These routes are exempt, so they always work: /stop, /pause, /heartbeat, /target, /cells, /jog/stop, /jog/end, and /arm with {"armed": false}. /cartesian/stop and /cartesian/halt are not exempt.

Without a catalogue (a single target from --rt-core-config), there is no selection and no fencing.

Routes at a glance#

Method and pathPurpose
GET /healthServer status
GET /capabilitiesFeature flags
GET /robots, POST /robots/capture-modelRobot catalogue; compiled URDF for a recorded pose
POST /cadquery/topologySTEP topology and tessellation
POST /seamSeam worker operations
POST /weld-plan, POST /weld-plan/exportPlan a program; export the .weldplan
GET /targets, POST /targets/selectList and select cells
GET /dense-execution/cells, POST /dense-execution/cells, DELETE /dense-execution/cells/{id}, POST /dense-execution/targetCell registry
GET /dense-execution/capabilities, GET /dense-execution/status, GET /dense-execution/cell, GET /dense-execution/telemetry/windowObserve the selected cell
POST /dense-execution/home, POST /dense-execution/armHome and arm
POST /dense-execution/load, play, pause, stop, heartbeatRun a planned program
GET /dense-execution/jog/state, POST /dense-execution/jog/{begin,update,end}Joint jog
POST /dense-execution/moveJoint move
GET /dense-execution/cartesian/state, POST /dense-execution/cartesian/{start,intent,stop,halt}Cartesian jog
POST /dense-execution/cartesian/moveCartesian move
POST /dense-execution/go-homeNot available on rt_core
/local-simulator/*The loopback simulator and its preview jog
/programs*Program catalog proxy

Service#

GET /api/offline-programming/v1/health

Server status. Always 200.

{"ok": true, "schema": "offline-programming.server-status.v1", "mode": "offline_preview",
 "network_required": false, "connected": false, "connected_targets": 0,
 "local_simulator": { … }, "local_ready": true, "execution_enabled": false,
 "target_planning": false, "teleop_enabled": false,
 "program_catalog": {"available": false, "owner": "motion-server"}}

execution_enabled and connected_targets describe the legacy connected-execution path, not dense execution.

GET /api/offline-programming/v1/capabilities

Feature flags, schema offline-programming.capabilities.v1: whether the seam worker is wired (weld_planner.enabled), the local simulator's availability, and the planning flags.

GET /api/offline-programming/v1/robots

The robot descriptions this server can plan against: the workstation's own, plus any fetched from cells, by identity.

{"robots": [{"model_id": "rosie_1400_v3", "robot_description_sha256": "sha256:…",
   "frames": {"base": "world", "tool": "tool0", "work": "positioner_table_a_top",
              "work_world_m": [0, -0.622, 0.1], "arm_base_world_m": [ … ]},
   "axes": {"driven": ["J1", "J2", "J3", "J4", "J5", "J6", "J7"], "held": {"J8": 0, "J9": 0}},
   "reset_pose": { … }}],
 "problems": []}

problems names each description that failed to load. A 500 with robot_store_unreadable means the description directory itself could not be read.

POST /api/offline-programming/v1/robots/capture-model

Compiles the URDF for a model at a given description and cell calibration, for recording a pose.

FieldTypeRequiredDescription
model_idstringyesRobot model
robot_description_sha256stringyesDescription identity, sha256:<64 hex>
machine_planning_calibration_base64stringnoThe cell's machine_planning_calibration.json, base64. Empty uses the empty calibration.

Returns {"urdf": "<xml>", "identity": …}. A failure returns 400 {"error": "…"}. If the description is neither local, cached nor advertised by the selected cell: adopt the matching robot description before recording.

Authoring and planning#

POST /api/offline-programming/v1/cadquery/topology

Forwards the body to the bounded CadQuery worker (one subprocess per request) and returns its JSON.

The worker operations are extract_topology, build_seam, build_sequence and tessellate. Error codes are the same set as the seam worker's, below.

POST /api/offline-programming/v1/seam

Forwards the body to the seam worker, python -m seam_worker.workers --stdin, and returns its JSON.

The body names the operation: {"operation": "detect_joints", …}. Operations: detect_joints, check_torch_fits, plan_torch_path, sample_seam_frames, build_plan_request. The response header X-Offline-Seam-Part identifies the part the answer is about.

CodeHTTPMeaning
invalid_request400The worker refused the request
payload_too_large413Body over 12 MiB
busy429No worker free
canceled408The client went away
timeout504The worker ran out of time
unavailable503The worker's pixi environment is missing
worker_failed, invalid_response502The worker crashed or answered with invalid JSON

See the weld program format for the operations' payloads.

Plan a program#

POST /api/offline-programming/v1/weld-plan

Packs a .weldplan, sends it to the weld planner, and brings back the planner's result and the dense trajectory the robot will play. One plan at a time; up to 30 minutes.

FieldTypeRequiredDescription
programobjectyesA robot.v4.program.v2 document
toolingobjectyesThe fitted torch, packed as tooling.json
step_base64stringyesThe workpiece STEP file, base64. Empty only when workpiece_absent is true.
step_filenamestringnoIts file name
workpiece_absentbooleannoPlan with no workpiece
cell_idstringyesThe robot model whose description to plan against
free_space_backendstringnoPassed to the planner: bspline, curobo or legacy
machine_planning_calibration_base64stringnoOnly read when no cell is connected. While a cell is selected, the server fetches the cell's own calibration.
program_idstringnoStamped into the plan identity
program_digeststringnoEchoed in the response
manifest_revision, plan_revisionintegernoStamped into the plan identity
fixturesobjectnoWorkcell bodies in the cell's world frame, packed as fixtures.json

The server always asks the planner for weld and connecting trajectory optimisation, verification and a dense trajectory.

200 OKJSON
{"result": { … }, "weldplan_sha256": "…", "program_digest": "sha256:…",
 "dense": {"trajectory_digest": "sha256:…", "plan_id": "bracket_fillet:3f1c0a9d2b7e", … },
 "dense_blob_base64": "…"}
FieldDescription
resultThe planner's result document, amr-weld-planner-v1.motion-plan-result.v1, unchanged. See the Weld planner HTTP API.
weldplan_sha256SHA-256 of the packed request
denseThe dense trajectory summary: digest, plan_id, samples, segments, robot_cell
dense_blob_base64The .rdt bytes, base64, so the project keeps a copy
CodeHTTPMeaning
weld_plan_invalid400Invalid JSON, or missing STEP
seam_worker_unavailable, motion_origin_unavailable503The seam worker or OFFLINE_PROGRAMMING_WELD_PLANNER_MOTION_ORIGIN is not configured
motion_plan_seam_refused502One or more welds could not be planned
motion_plan_unjoined502The welds planned but the connecting moves did not
motion_plan_no_trajectory502No dense trajectory was produced; dense_error gives {reason, detail, segment_index}
motion_plan_failed, motion_plan_invalid_response, dense_blob_unreachable502The planner call failed
seam worker codesas abovePacking failed
POST /api/offline-programming/v1/weld-plan/export

Packs the same .weldplan without planning it. Same body as /weld-plan.

Returns the container bytes as an attachment named <program_id>.weldplan, with its hash in X-Weldplan-SHA256.

Cells and targets#

A cell is a commissioned machine in the server's catalogue. Selecting one creates the controller for it and bumps the selection generation used for target fencing. Selection grants no authority; Arm or Load does. See Connect to a cell for the catalogue file.

GET /api/offline-programming/v1/targets

Lists the catalogue's cells with what each one reports about itself.

{"targets": [{"cell_id": "local-simulation", "label": "Local simulation", "role": "LOCAL SIMULATION",
   "models": ["rosie_1400_v3"], "requested_mode": "simulation",
   "backend": "simulation", "simulation": true, "host": "localhost"}],
 "selected": null}

A cell that cannot be used has backend: "unreachable" and a reason, for example cell_configuration_missing, cell_mode_mismatch or cell_unreachable.

POST /api/offline-programming/v1/targets/select

Selects a cell. Stops and releases any session on the previous cell first.

FieldTypeRequiredDescription
cell_idstringyesA catalogue cell
model_idstringyesOne of that cell's models

Returns {"selected": {cell_id, model_id}, "target": {…view…}}. Refusals, all 409: cell_switch_busy, cell_switch_while_playing, cell_model_mismatch, cell_id_unknown, cell_mode_mismatch (the cell's backend is not the requested mode), cell_previous_stop_failed, cell_backend_unavailable, cell_configuration_mismatch, cell_description_invalid, cell_backend_mismatch, cell_unreachable. Without a catalogue: 503 cell_catalogue_unavailable.

POST /api/offline-programming/v1/dense-execution/target

The Cells dialog's selection. Body {"cell": "<id>"}; an empty string clears the selection. Returns the dense-execution capabilities. Not fenced.

GET /api/offline-programming/v1/dense-execution/cells

The registered cells, with reachability: {"cells": [{id, label, address, host, credential_ref, model_id, backend, configuration_digest, source, reachable, error}], "active_cell": "…"}.

POST /dense-execution/cells only accepts a cell that matches a commissioned server-side binding (otherwise cell_not_commissioned or cell_binding_pinned). DELETE /dense-execution/cells/{id} always refuses with cell_commissioning_required, or dense_cell_active for the selected cell: commissioned cells are removed from the catalogue file, not from the UI.

Machine control#

Every route below answers 503 dense_target_unavailable when no cell is selected. Every mutating route except the exempt ones needs the fencing headers.

Observe#

GET /api/offline-programming/v1/dense-execution/capabilities

What the selected controller is and whether it is reachable.

{"backend": "rt_core", "available": true, "cell": "local-dev", "controller_id": "offline-programming:server-3f9a1c",
 "daemon_reachable": true, "rt_core": {"description": { … }, "address_host": ""},
 "active_cell": "local-simulation", "cells": [ … ]}

cell is the cell's pair ID. Joint and Cartesian jog and moves send it as target_id. rt_core.description is rt-control's Describe. reason explains an unreachable controller, for example rt_core_identity_mismatch.

GET /api/offline-programming/v1/dense-execution/status

The machine and session state. Takes no authority. Poll it.

{
  "backend": "rt_core", "available": true,
  "target": {"selection_generation": "4", "cell_id": "local-simulation", "model_id": "rosie_1400_v3",
             "label": "Local simulation", "backend": "simulation", "simulation": true, "host": "localhost"},
  "session": {"id": "ds-9b1e4f07a2c3", "kind": "trajectory", "state": "loaded", "dry_run": true,
              "trajectory_digest": "sha256:…", "plan_id": "bracket_fillet:3f1c0a9d2b7e",
              "lease": {"held": true, "expires_in_ms": 0},
              "heartbeat_age_ms": 820, "heartbeat_deadline_ms": 5000,
              "stop_reason": "", "last_error": ""},
  "armed": true,
  "robot_cell": {"configured": true, "valid": true, "model_id": "rosie_1400_v3",
                 "robot_description_sha256": "sha256:…", "machine_planning_calibration_sha256": "sha256:…",
                 "machine_planning_calibration_present": false, "error": ""},
  "rt_core": {"inhibited": false, "stop_uncertain": false, "recovery_action": "",
              "refusal": "", "refusal_kind": "", "reason": "", "status": { … }, "events": [ … ], "description": { … }},
  "round_trip_ns": 180000, "link_lease_ns": 500000000
}
FieldDescription
targetThe selection this status belongs to. Use it for the fencing headers.
session.idThe session to name in heartbeat, Play, jog and moves
session.kindauthority (after Arm or Home), trajectory (after Load)
session.stateidle, loaded, playing or stopped
session.lease.heldOLP holds the rt-control lease
session.stop_reason, last_errorWhy the session stopped
armedArmed, lease held and no refusal pending
robot_cellWhich robot the machine says it is. See Robot and cell identity.
rt_core.statusrt-control's full status. See status.
rt_core.eventsEvents since the last poll
rt_core.refusal, refusal_kindThe last refused operation and which kind it was
rt_core.inhibited, stop_uncertain, recovery_actionA Stop or Release was not confirmed. recovery_action says what to do: retry POST /dense-execution/stop.
round_trip_ns, link_lease_nsMeasured round trip to rt-control, and the effective lease
GET /api/offline-programming/v1/dense-execution/cell

The selected machine's robot identity, its machine_planning_calibration.json as base64, and its execution limits. This is what a plan request for this cell carries.

GET /api/offline-programming/v1/dense-execution/telemetry/window

The newest seconds of the machine's telemetry ring, thinned.

QueryTypeDefaultDescription
secondsnumber, s3In (0, 30]
strideinteger1Keep every N-th record, 1–100

Returns {daemon_incarnation, configuration_sha256, cycle_period_ns, axis_count, stride, seconds, first_sequence, last_sequence, head_estimate, fetched_records, fetch_ms, records: [...]}. One second of 1 kHz telemetry is about 4.9 MB from the cell before thinning. Errors: 400 telemetry_window_invalid, 503 telemetry_unavailable, 409 telemetry_identity_changed.

Home#

POST /api/offline-programming/v1/dense-execution/home

Runs native Home and waits until Home is valid on the named axes. Acquires the lease if OLP does not hold it. Does not arm.

FieldTypeRequiredDescription
axesinteger arraynoAxis indices, 0–15, a subset of the cell's axis mask. Omit the body, or send [], to home every configured axis.

Returns the status body. If an axis has a latched reference fault whose recovery is a reset, Home resets it first. Other latched faults refuse with rt_core_fault. Errors: 400 home_axes_invalid, 409 capability_unimplemented, and rt-control reasons.

Arm and disarm#

POST /api/offline-programming/v1/dense-execution/arm

{"armed": true} acquires the lease, enables and arms, and waits until every configured axis is ready. {"armed": false} runs Stop and Release.

FieldTypeRequiredDescription
armedbooleanyesArm or disarm
200 OKJSON
{"ok": true, "accepted": true, "status": { … }}

If a latched execution fault can be cleared by a reset (recovery class reset_clears or reset_after_condition_clears), Arm resets it first, then re-acquires and arms, so one Arm recovers the machine. Faults that need a re-Home or a restart are left to their refusal. Errors: 400 dense_arm_invalid, 409 rt_core_arm_refused, 409 rt_core_arm_timeout, and rt-control reasons such as control_already_owned or not_ready.

Heartbeat#

POST /api/offline-programming/v1/dense-execution/heartbeat

Tells OLP a supervising client is still there. Send it at least every 5 s while OLP holds the lease. Not fenced.

requestJSON
{"session_id": "ds-9b1e4f07a2c3"}
200 OKJSON
{"ok": true, "deadline_ms": 5000, "age_ms": 1004}

age_ms is how long the previous beat had stood. If no beat arrives for 5 s, OLP stops the machine with ui_heartbeat_lost, except while an accepted Cartesian move is finishing. Errors: 400 dense_session_required, 409 dense_session_mismatch.

Load, Play, Stop#

POST /api/offline-programming/v1/dense-execution/load

Fetches a verified .rdt from the weld planner by digest, checks it, and prepares it on rt-control.

requestJSON
{
  "trajectory_digest": "sha256:…",
  "plan_id": "bracket_fillet:3f1c0a9d2b7e",
  "program_id": "bracket_fillet",
  "program_digest": "sha256:…",
  "manifest_revision": 1,
  "plan_revision": 3,
  "dry_run": true
}
FieldTypeRequiredDescription
trajectory_digeststringyessha256:<64 hex>. The server fetches GET /api/motion/dense/{digest} from the weld planner.
plan_id, program_id, program_digeststringyesMust equal the .rdt header exactly
manifest_revision, plan_revisionintegeryesMust equal the header. JSON integers.
dry_runbooleannoStrip the torch bits and process markers before upload. The planner sets the torch bit on weld segments unless the program's execution_mode is dry_run, and a program that needs process outputs is refused, so set this for any planned weld program.

Load runs these checks in order, before any byte reaches the robot:

  1. The identity matches the header (dense_identity_mismatch).
  2. The plan's robot_cell matches the machine (robot_cell_missing, robot_cell_mismatch, robot_cell_unavailable).
  3. Every configured axis has valid Home (home_required).
  4. OLP acquires the lease and calls prepare_program. rt-control applies its admission checks.
  5. The prepared identity, axis mask and process requirement match (program_identity_or_process_mismatch).
200 OKJSON
{"ok": true, "session_id": "ds-9b1e4f07a2c3",
 "trajectory": {"trajectory_digest": "sha256:…", "total_sample_count": 4210, "total_duration_s": 42.09, "dt_s": 0.01,
                "segments": [{"index": 0, "kind": "freespace", "source_id": "approach-1", "sample_count": 350, "duration_s": 3.49}]},
 "status": { … },
 "notes": ["the machine's configuration changed since this plan was made (…); the robot description and planning calibration still match, so it loads"]}

Other errors: 400 dense_identity_invalid, 400 dense_trajectory_invalid, 404 dense_blob_not_found, 503 dense_blob_source_unavailable, 409 program_already_executing, and rt-control reasons with an optional limit_violation.

POST /api/offline-programming/v1/dense-execution/play

Starts the loaded program. Body {"session_id": "…"} from the Load answer. Waits until the machine is armed and every axis ready, then sends start_program. Returns the status body.

Refused with control_session_stale unless the session is the loaded one and OLP holds the lease. Arm before Play.

POST /api/offline-programming/v1/dense-execution/stop

Stops the machine now, then releases the lease. Needs no body and no fence. Retry it until it succeeds.

200 OKJSON
{"ok": true, "stopped": true, "attempts": 1, "status": { … }}

note: "nothing_held" means there was nothing to stop. If Stop or Release cannot be confirmed, the answer is an error that still carries stopped, attempts and status, and further motion is blocked (rt_core_inhibited) until a Stop succeeds. A receipt does not prove the robot is standing still; see Software stops.

POST /api/offline-programming/v1/dense-execution/pause

Always refused on rt_core with capability_unimplemented. Use Stop.

POST /api/offline-programming/v1/dense-execution/go-home

Always refused on rt_core with capability_unimplemented. Use Home, or a joint move.

Joint jog#

POST /api/offline-programming/v1/dense-execution/jog/{action}

Holds one axis at a fraction of its described velocity. action is begin, update or end. Needs an armed session. GET /dense-execution/jog/state returns the current state.

FieldTypeRequiredDescription
target_idstringyesThe cell's pair ID, capabilities.cell
session_idstringyesstatus.session.id
revisionintegeryesjog/state.revision. It advances when a jog ends or a move starts, so a late request from an old hold is refused.
axisintegeryesAxis index, within the cell's axis mask
directionintegeryes1 or -1
fractionnumberyes(0, 1]. The axis moves at fraction × max_velocity × 0.75.

Send begin, then update every 50 ms while the operator holds the control, then end. An update may not change axis, direction or fraction; end the hold and begin a new one. Each update lives for the cell's jog input age (250 ms on a LAN cell). If updates stop, the jog ends with jog_input_deadline_expired and the axis ramps to a hold.

jog/stateJSON
{"active": true, "revision": 12, "generation": 3, "sequence": 41, "axis": 0, "reason": "", "cadence_ms": 50,
 "move": {"axis": 0, "state": "done", "reason": "", "revision": 11}}

Errors (409 unless noted): jog_session_stale, jog_mode_conflict (a move, a program or another hold is active), no_grant, jog_not_ready, invalid_axis_mask, 400 jog_invalid_vector, jog_input_deadline_expired, jog_transport_congested, dense_target_unavailable, and native jog reasons.

Joint move#

POST /api/offline-programming/v1/dense-execution/move

Moves one or more joints by a bounded amount and returns when the move is done. Needs an armed session.

request: all arm joints to zeroJSON
{"target_id": "local-dev", "session_id": "ds-9b1e4f07a2c3", "revision": 12, "fraction": 0.2,
 "axis": 0, "degrees": 0,
 "axes": [{"axis": 0, "position_rad": 0}, {"axis": 1, "position_rad": 0}, {"axis": 2, "position_rad": 0},
          {"axis": 3, "position_rad": 0}, {"axis": 4, "position_rad": 0}, {"axis": 5, "position_rad": 0}]}
FieldTypeRequiredDescription
target_id, session_id, revisionyesAs for joint jog
axisintegeryesThe joint, for a single-joint move
degreesnumber, degreesyesSigned relative move for a single joint. Non-zero.
fractionnumberyes(0, 1] of each joint's velocity and acceleration
axes[]arraynoA synchronised move of several joints. Each entry has axis and either degrees (relative, degrees) or position_rad (absolute, radians, from the held position). When present, it replaces axis and degrees.

Each joint follows its own trapezoid at fraction × max_velocity × 0.75 and fraction × its jog acceleration (or its described maximum acceleration, if lower). Shorter profiles are stretched so all joints start and finish together. The points are sampled at the core's cycle and sent with prepare_trajectory and start_trajectory. The target must be inside the joint's limits.

200 OKJSON
{"axis": 0, "state": "done", "reason": "", "revision": 13, "axes": [0, 1, 2, 3, 4, 5]}

Errors: joint_move_invalid, joint_move_limits, joint_move_units, joint_move_description_invalid, joint_move_too_many_points, joint_move_prepare_timeout, joint_move_prepare_busy, joint_move_interrupted, joint_move_completion_timeout, joint_move_feedback_invalid, client_disconnected, plus the joint jog errors. If the HTTP client disconnects, OLP stops the move.

Cartesian jog#

POST /api/offline-programming/v1/dense-execution/cartesian/{action}

Jogs the tool in the base or tool frame. action is start, intent, stop or halt (stop and halt both end the hold). Same session, revision and timing rules as joint jog. GET /dense-execution/cartesian/state returns the jog state.

FieldTypeRequiredDescription
target_id, session_id, revisionyesAs for joint jog
framestringyesbase or tool
twist6 numbersyesX, Y, Z in m/s (each |v| ≤ 1), RX, RY, RZ in rad/s (each |ω| ≤ π). At least one non-zero.
fractionnumberyes(0, 1]

OLP resolves the twist into joint velocities from the measured pose with robot-v4-cartesiand --resolve-only, then scales the whole vector so no joint exceeds fraction × max_velocity × 0.75. The twist sets the direction; the joint ceilings usually set the speed. A step that would leave a joint's limits within one input lifetime is refused with joint_limit.

Errors: 400 cartesian_input_invalid, cartesian_resolver_unavailable, cartesian_resolver_invalid, cartesian_pose_stale, cartesian_axes_invalid, joint_limit, jacobian_gate, ik_no_solution, plus the joint jog errors.

Cartesian move#

POST /api/offline-programming/v1/dense-execution/cartesian/move

Moves the tool a bounded distance along one axis of the base or tool frame, in a straight line, and returns when done.

FieldTypeRequiredDescription
target_id, session_id, revisionyesAs for joint jog
framestringyesbase or tool
axisintegeryes0–2 for X, Y, Z; 3–5 for RX, RY, RZ
distance_mmnumber, mmfor axes 0–2Non-zero for a linear axis, zero otherwise. At most 1,000 mm.
angle_degreesnumber, degreesfor axes 3–5Non-zero for a rotary axis, zero otherwise. At most 180°.
fractionnumberyes(0, 1]

The resolver solves IK along the line at 1 mm or 0.25° spacing and checks each waypoint against joint limits and the singularity gate. OLP then times the path with a smooth (quintic) progress clock, so that no joint exceeds fraction × max_velocity × 0.75, reduced further near a singularity, or fraction × its acceleration limit. It sends the result as one trajectory. An accepted move finishes even if the HTTP client disconnects; Stop still cancels it.

Errors: cartesian_input_invalid, cartesian_reach, ik_no_solution, joint_limit, cartesian_pose_stale, cartesian_resolver_invalid, joint_move_too_many_points, plus the joint move errors.

Local simulator#

The server can own a loopback-only rt-core simulator for preview. It requires a loopback --listen address and is on by default (--enable-local-simulator).

Method and pathDescription
GET /local-simulator/statusSimulator phase and readiness
POST /local-simulator/start, POST /local-simulator/stopStart or stop it. No request body.
POST /local-simulator/plansPlan a saved program against the simulator
POST /local-simulator/jog/start, /jog/stopStart or stop the preview jog runtime
POST /local-simulator/jog/intent{mode: "base"|"tool"|"joint", axes: [6 values in -1..1], selectedJoint, speedScale: 0..1}
GET /local-simulator/jog/stateJoints in rad, axis names, running flag
POST /local-simulator/jog/home, /jog/haltHome or halt the preview
POST /local-simulator/jog/set-zero{axisMask}
POST /local-simulator/jog/step{joint, deltaDeg} (degrees); answers 202

Once a machine is selected, preview jog requests are refused with target_changed, so a preview cannot run beside a selected controller.

Program catalog#

GET/POST /programs, GET/PUT /programs/{program_id} and GET /programs/{program_id}/revisions proxy to an external program catalog set with --program-catalog-origin. Without one they answer catalog_unavailable. The OLP app stores projects in the browser (IndexedDB) and does not need the catalog.

Legacy and disabled routes#

  • connected-targets, execution-gateway, execution, targets/{profile}/plans and plans/{plan_id} belong to the older connected-execution path over NATS. They stay off unless the server is started with --enable-connected-planning or --enable-connected-execution. Don't use them for new work.
  • POST /connect, POST /execute and POST /teleop are retired and answer 403 {"ok": false, "enabled": false}.

Error codes#

Codes specific to machine control, with their usual HTTP status. rt-control reasons such as control_already_owned, not_ready, native_limit_exceeded or mode_conflict pass through with 409; see Error codes.

CodeHTTPMeaning
dense_target_unavailable503, 409No cell selected, or the backend cannot do this
target_changed409The fencing headers name an old selection
ui_heartbeat_lost409OLP stopped the machine: no heartbeat for 5 s
rt_core_inhibited409A previous Stop or Release was not confirmed. Retry Stop.
rt_core_identity_mismatch409Describe no longer matches the cell's pinned identity
rt_core_request_slow409rt-control did not answer within the call budget. The lease is kept; retry.
rt_core_connection_reopened409The request was not sent; the client reconnected. Retry.
rt_core_transport_lost, rt_core_protocol_error, rt_core_closed, rt_core_cancelled, rt_core_failure409Transport or protocol failures
rt_core_incarnation_changed, rt_core_event409rt-control restarted, or an event (a fault, a lost grant, dropped events) ended the session
control_renewal_lost409Lease renewal failed; OLP stopped
control_session_stale409The session was stopped or replaced before this request ran
capability_unimplemented409Pause, go-home, or a missing rt-control capability
dense_session_required, dense_session_mismatch400, 409Missing or unknown session_id
dense_internal500Unexpected server error