Weld planner HTTP API
On this page
The weld planner's motion server is a small FastAPI app. You POST a .weldplan container and get back a plan result, and, if every segment passed the verifier, the digest of a dense trajectory (.rdt) you can then fetch. The offline programming (OLP) server is its usual client. See Weld planning and verification for what the planner does.
The server listens on 0.0.0.0:8796 by default. It has no authentication, and there is no OpenAPI page (/docs is disabled).
Warning
Bind the weld planner to localhost, or firewall it. By default it listens on every network interface, and anyone who can reach port 8796 can queue GPU plans and download every stored trajectory. Start it with --host 127.0.0.1 when OLP runs on the same machine. When a Steam Deck or another host must reach it, allow only those hosts through a firewall. See Run the weld planner.
Tip
Machine-readable. The four routes, their query parameters and errors as an OpenAPI 3.1 document, generated from this page.
Quick start#
Check the GPU, plan a committed example part, and fetch its trajectory:
PLANNER=http://localhost:8796
curl -s $PLANNER/api/motion/health
# {"cuda": true, "device": "NVIDIA RTX A4000"}
# Plan with verification and a dense trajectory. This takes minutes.
curl -s -X POST --data-binary @weld_planner/v1/data/motion/bracket_a_2x.weldplan \
-H "Content-Type: application/octet-stream" \
"$PLANNER/api/motion/plan?run_weld_trajopt=true&run_connecting_trajopt=true&verify=true&dense=true&program_id=bracket_a_2x" \
-o result.json
jq '.dense.trajectory_digest // .dense_error' result.json
# Fetch the verified trajectory by its digest.
DIGEST=$(jq -r .dense.trajectory_digest result.json)
curl -s -o plan.rdt "$PLANNER/api/motion/dense/$DIGEST"The device name in the health answer is whatever your GPU reports. To make your own .weldplan, use Export .weldplan… in OLP, or POST /api/offline-programming/v1/weld-plan/export on the OLP server.
Routes#
| Method and path | Purpose |
|---|---|
GET /api/motion/health | Whether CUDA is available |
GET /api/motion/progress | The running plan's current stage |
POST /api/motion/plan | Plan a .weldplan |
GET /api/motion/dense/{trajectory_digest} | Fetch a stored .rdt |
Health#
/api/motion/health Reports whether the planner's Torch build can see a CUDA device.
{"cuda": true, "device": "NVIDIA RTX A4000"}| Field | Type | Description |
|---|---|---|
cuda | boolean | true if CUDA is available |
device | string or null | The name of device 0, or null without CUDA |
Progress#
/api/motion/progress The stage of the plan that is running now, for a client watching a long request.
{"stage": "verify"}stage is null when no plan is running. While one is, it is the last stage the planner reported, for example sweep, weldseam, freespace seed, optimize or verify. There is one slot, because only one plan runs at a time.
Plan#
/api/motion/plan Plans every seam and move of the posted .weldplan, verifies them, and optionally writes the dense trajectory.
The body is the raw .weldplan bytes. Options go in the query string.
Query parameters#
| Name | Type | Default | Range | Description |
|---|---|---|---|---|
k_best | integer | 5 | 1–16 | Candidate paths to keep per seam from the M4 search |
screen_collisions | boolean | true | Screen search poses against the collision model, and include collision avoidance in M5 and M6. false means unchecked, not safe. | |
samples_per_seam | integer or null | null | 2–512 | Space the M4 lattice by sample count. Null spaces it by time, from the weld's travel speed. |
run_weld_trajopt | boolean | false | Run M5, the continuous weld trajectories. Minutes rather than seconds. | |
run_connecting_trajopt | boolean | true | Run M6, the approach, transits, retract and taught moves | |
free_space_backend | string | bspline | bspline, curobo, legacy | The M6 solver. curobo uses cuRobo for comparison and is optional at runtime. legacy is deprecated and kept to reproduce old results. Any other value returns 422. |
verify | boolean | true | Run the verifier on M5 and M6 output | |
verify_margin_mm | number, mm | 2.0 | 0–50 | The verifier's clearance margin |
order_seams | boolean | false | Reorder welds to shorten the transits. Off, the program's own weld order is kept. | |
dense | boolean | false | Build, check and store the .rdt. Needs run_weld_trajopt and run_connecting_trajopt. | |
program_id | string | "" | up to 120 characters | Stamped into the plan identity. A dense trajectory needs a plain identifier (letters, digits, ., _, :, -). |
manifest_revision | integer | 1 | ≥ 1 | Stamped into the plan identity |
plan_revision | integer | 1 | ≥ 1 | Stamped into the plan identity |
The OLP server always sends run_weld_trajopt=true, run_connecting_trajopt=true, verify=true and dense=true, plus the free-space backend and the program identity.
How a request runs#
- One plan at a time. A second request waits for the first. It can still be cancelled while it waits.
- One process per plan. Each admitted request runs in a fresh Python process that owns the GPU. This costs interpreter, model and CUDA start-up on every plan.
- Cancellation. If the client disconnects, the server sends SIGTERM to the planning process group, then SIGKILL after 2 s, and answers 499. A cancelled plan never stores a
.rdt. - Atomic publication. The
.rdtis written to the store only after the planning process succeeded and the client is still connected.
Response#
200 OK with the result document, schema amr-weld-planner-v1.motion-plan-result.v1. On the wire, angles are in degrees and lengths in mm; times are in s. A trimmed example:
{
"schema": "amr-weld-planner-v1.motion-plan-result.v1",
"cell_id": "rosie_1400_v3",
"screened": true,
"k_best": 5,
"request": {"request_sha256": "3f1c0a9d2b7e…", "program_id": "bracket_a_2x", "cell_id": "rosie_1400_v3", "…": "…"},
"producer": {"module": "amr-weld-planner/v1", "source_revision": "0123456789abcdef0123456789abcdef01234567", "source_revision_state": "bound"},
"coverage": {"candidate_collision_screening": "sampled_lattice_nodes",
"weld_trajectory_continuous_verification": "evaluated_for_all_emitted_trajectories",
"connecting_trajectory_continuous_verification": "evaluated_for_all_emitted_trajectories",
"canonical_motion_server": "not_evaluated", "physical_motion": "not_evaluated",
"execution_authority": "none", "candidate_limit_checks": "reported_per_candidate"},
"seams": [{
"id": "seam_0001", "length_mm": 118.0, "crossed": true,
"axis_names": ["J1", "J2", "J3", "J4", "J5", "J6", "J7"], "held": {"J8": 0.0, "J9": 0.0},
"candidates": [{"cost_deg": 41.8, "joints_deg": [[…]], "times_s": […]}],
"trajectory": {
"knots_deg": [[…]], "knot_velocity_deg_s": [[…]], "times_s": […],
"sampled_tracking_mm": {"knots": 0.02, "midpoints": 0.07},
"tracking": {"verdict": "PASS", "tolerance_mm": 0.5, "bound_mm": 0.21, "worst_seen_mm": 0.08, "segments_out": 0, "undecided": 0, "unverifiable": []},
"verdict": {"verdict": "PASS", "collisions": 0, "penetrating": 0, "limit_violations": 0, "unverifiable": [], "worst_clearance_mm": 6.412}
}
}],
"connecting_trajectories": [{
"kind": "approach", "from": null, "to": "seam_0001", "duration_s": 4.2,
"knots_deg": [[…]], "knot_velocity_deg_s": [[…]], "times_s": […],
"clearance_mm": {"env": 38.5, "self": 61.2},
"verdict": {"verdict": "PASS", "collisions": 0, "penetrating": 0, "limit_violations": 0, "unverifiable": []}
}],
"dense": {
"trajectory_digest": "sha256:9b1e4f07a2c3…", "plan_id": "bracket_a_2x:3f1c0a9d2b7e",
"program_digest": "sha256:3f1c0a9d2b7e…", "dt_s": 0.01, "total_sample_count": 5230,
"total_duration_s": 52.29, "robot_cell": {"model_id": "rosie_1400_v3", "…": "…"},
"segments": [{"index": 0, "kind": "freespace", "source_id": "home->seam_0001", "sample_count": 421, "duration_s": 4.2}]
},
"options": {"k_best": 5, "verify": true, "verify_margin_mm": 2.0, "…": "…"},
"timings_ms": {"…": 0}
}The values above are illustrative. The fields that matter most:
| Field | Description |
|---|---|
seams[].crossed | Whether the M4 search found a path across the seam. When false, frontier and frontier_causes say where and why it stopped. |
seams[].trajectory | The M5 curve: knots_deg, knot_velocity_deg_s and times_s form a cubic Hermite, with its verdict and tracking reports |
connecting_trajectories[] | The M6 moves, in execution order: kind (approach, transit or retract), from and to seam ids (null at the home end), and a node_id for moves tied to a program node |
connecting_trajectories_error | Present when M6 failed. The welds may still be good. |
program_order | Present when the program has taught moves: the enabled nodes in order |
speed_scale | Present when the program's plan speed is below 1. times_s are already stretched and knot_velocity_deg_s already scaled. |
dense | With dense=true: the .rdt summary, including trajectory_digest, plan_id (<program_id>:<first 12 hex of the request digest>), program_digest (sha256: plus the request digest), dt_s, total_sample_count, total_duration_s, max_abs_qd_rad_s (rad/s), robot_cell, bytes and segments[] |
dense_error | With dense=true, when no .rdt was written: {reason, detail, segment_index} |
See The plan result for every block, and Verdicts for what PASS, REFUSED and FAIL mean.
A plan without a trajectory#
A plan can answer 200 OK and still have no .rdt. Then dense_error says why, and the server keeps the request and result under <dense store>/refused/ (newest 5) for diagnosis. The reasons are admission's and the encoder's:
reason | Meaning |
|---|---|
result_empty | dense=true without both trajectory stages, or nothing playable in the result |
identity_invalid | program_id is empty or not a plain identifier |
seam_not_planned | A seam was not crossed by the search |
motion_not_verified | A seam or move has no PASS, a non-zero collision, penetration or limit count, an unverifiable check, or a tracking certificate out of tolerance |
motion_join_failed | The connecting moves could not be planned |
motion_result_invalid | The result is malformed, for example duplicate seam ids or an unknown move kind |
motion_not_planned | A program node has no planned motion |
qd_limit_exceeded | A sample is faster than the cell's per-axis velocity ceiling |
q_step_exceeded, boundary_qd_nonzero, boundary_q_discontinuity, time_grid_invalid, sample_count_overflow, … | The resampled trajectory broke an .rdt format rule; see the .rdt format |
segment_index names the failing segment when there is one.
Errors#
| Status | Body | When |
|---|---|---|
| 400 | {"error": "an empty body is not a .weldplan"} | Empty body |
| 422 | {"detail": "unknown free_space_backend …"} | Unknown free_space_backend |
| 422 | {"detail": [ … ]} | A query parameter is out of range or the wrong type |
| 422 | {"error": "<type>: <message>"} | Planning failed, including a .weldplan the planner refused to open (bad zip, digest mismatch, wrong program schema) or missing cell meshes |
| 499 | {"error": "Planning cancelled"} | The client disconnected |
Fetch a trajectory#
/api/motion/dense/{trajectory_digest} Returns the stored .rdt bytes for a digest.
curl -s -o plan.rdt http://localhost:8796/api/motion/dense/sha256:9b1e4f07a2c3…| Name | In | Type | Description |
|---|---|---|---|
trajectory_digest | path | string | sha256: and 64 lowercase hex characters, from dense.trajectory_digest |
The response is application/octet-stream with Cache-Control: no-store. A malformed digest returns 422 (expected sha256:<64 hex>). An unknown digest returns 404.
The store only holds trajectories that passed admission. It lives in --dense-store-dir, else WELD_PLANNER_DENSE_STORE_DIR, else ~/.cache/rosieos-olp/weld-planner-dense, as sha256-<hex>.rdt. It is a cache: if a file is gone, plan again. OLP's Load fetches from this route and answers dense_blob_not_found when the planner no longer has the file. See the .rdt format for the file layout.