# Advanced Metal Research (full text) > Advanced Metal Research makes open-source, AI-native industrial robots, machined and assembled in the USA. Every function unlocked, programmable from the pendant, in code or with any AI, with skills on top, starting with welding. Every page of https://advancedmetalresearch.com/ and the RosieOS documentation, generated from the site sources. Index: https://advancedmetalresearch.com/llms.txt. --- # Open-source, AI-native industrial robots. > Advanced Metal Research makes open-source, AI-native industrial robots, machined and assembled in the USA. Every function unlocked, programmable from the pendant, in code or with any AI, with skills on top, starting with welding. URL: https://advancedmetalresearch.com/ Hawthorne, California · Founded 2025 Six-axis arms, built-in sensing Machined and assembled in the USA. The controller, RosieOS, is open source and fully unlocked. Program the robot any way you like: from the pendant, in code or with any AI. Add skills when you need them, starting with welding. [Contact AMR](https://advancedmetalresearch.com/#contact) [See the platform](https://advancedmetalresearch.com/#platform) [Read the RosieOS docs](https://advancedmetalresearch.com/docs/) *Figure: Animated line drawing of the War Machine cell, close up, running its load-and-weld cycle, shown with the optional robot loader: Rosie 1400 MIG-welds a T-joint on the robot-side bay while a second Rosie outside the open load end swaps the finished part for a blank; the positioner then turns 180 degrees to swap the bays. Operator loading is standard.* Robot · Rosie 1400, six axes · 1,400 mm reach Controller · RosieOS, Apache 2.0 · Every function unlocked Made in USA · From US and imported parts Rosie 1400 · Hawthorne R&D lab Video (https://advancedmetalresearch.com/assets/video/rosie-lab.webm) *Rosie 1400 running in the Hawthorne R&D lab.* The problem · Buying Programming Service ## Industrial robots are reliable. Everything around them holds factories back. The established robot makers build machines that run for years. Buying, programming and servicing them is the hard part. ### Features sold back to you Motion functions, process packages and programming seats are licensed one by one, on a robot you already paid for. ### Closed languages Each maker has its own language and tools. Programs do not move between brands, and outside software, AI included, gets limited access. ### Service you cannot see into Service is slow, opaque or expensive. When a line is down, the wait is the cost. ### No real alternative Buyers stay because the robots are reliable. Some makers publish open drivers, but those still need extra software options unlocked on the controller. Platform · Open stack Agent workflow ## One open stack, from arm to application. The platform - Skills: paid and optional, welding first. - Open sensing: raw data in the robot's frames. - RosieOS: the controller, open source under Apache 2.0. - Rosie 1400: the hardware. How an AI agent can do the integration 1. Learn: docs any agent can read. 2. Simulate: the cell and the weld physics, in a weld physics simulator with an AI world model. 3. Check: the program is verified before it loads. 4. Drive: one open control API. 5. See: open sensors. 6. Verify: the metallurgy lab sections the weld and tests hardness and microstructure, and the results go back to the agent. Sensors show what happened. **We cut our welds open and test them in our lab to understand the metallurgical processes and record the data for training.** From that data, the simulator learns why. Like any industrial equipment, the machine has physical E-stops. [Read the docs](https://advancedmetalresearch.com/docs/) [Docs llms.txt](https://advancedmetalresearch.com/docs/llms.txt) [Control API](https://advancedmetalresearch.com/docs/apis/rt-control-http) [Run the quickstart](https://advancedmetalresearch.com/docs/get-started/quickstart) Machine · Designed, machined, and assembled by AMR ## Rosie 1400, inside War Machine. Every weld cell, machine tool and new line needs an arm. Rosie 1400 is a six-axis industrial arm for the production floor. War Machine is the production cell around it: the operator loads one bay while Rosie works the other, so the arm is not left waiting on part handling. As an option, a second Rosie loads the cell instead. Every Rosie 1400 ships with the arm, controller, drive cabinet, cabling, pendant, and commissioning at your site. *Figure: Animated line drawing of the War Machine cell running its load-and-weld cycle, shown with the optional robot loader: Rosie 1400 MIG-welds a T-joint on the robot-side bay while a second Rosie outside the open load end swaps the finished part for a blank through the muted light curtain; the positioner then turns 180 degrees to swap the bays. Operator loading is standard.* ### Rosie 1400 - **Axes:** 6 - **Reach:** 1,400 mm - **Payload:** 15 kg - **Repeatability:** ±0.05 mm - **Supply:** 200 to 240 VAC, 3-phase - **Controller:** RosieOS, Apache 2.0 - **Made in:** USA, from US and imported parts ### War Machine - **Processes:** TIG, MIG, laser - **Laser source:** 2–10 kW fiber laser - **Positioner:** H-frame, turntable and A/B rotisserie - **Format:** 10 ft or 20 ft container - **System power:** 20 kW [Rosie 1400 specification](https://advancedmetalresearch.com/rosie#rosie-1400)[War Machine specification](https://advancedmetalresearch.com/war-machine) Welding intelligence · First expert skill Seam tracking within 0.20 mm Video (https://advancedmetalresearch.com/assets/video/seam-tracking.webm) - **Track:** Lock - **Arc:** On - **Travel:** 000.0 mm - **Velocity:** 3.4 mm/s ## Welding first: measured before, during, and after every weld. Welding is our lead application. The US needs 320,500 new welding professionals by 2029, according to the [American Welding Society](https://weldingworkforcedata.com/). The welding intelligence package is a paid, optional skill on the open platform. It measures the part, tracks the seam while welding, and adjusts as the seam changes. The raw sensor data underneath stays open. - **Seam tracking:** Holds the torch on the joint while the arc is on; Within 0.20 mm - **Weld-pool analysis:** Images the molten pool during the weld; Every weld - **Arc-sound fault detection:** Detects arc start and process faults from arc sound; In process - **Adaptive control:** Adjusts the weld as the seam and pool change; Path, torch, heat The cell also inspects fit-up and finished geometry against the CAD model with micron-level measurement, and every procedure we develop is sectioned and inspected in our metallurgical lab to ISO 13919-1. [Welding automation](https://advancedmetalresearch.com/welding-automation)[The metallurgical lab](https://advancedmetalresearch.com/metallurgy) Contact ## Talk to AMR. Tell us about your application. [Contact AMR](mailto:generalcontact@advancedmetalresearch.com?subject=AMR%20inquiry) Send us a part. Drawing · material · volume We'll tell you what Rosie and War Machine can do with it. --- # Rosie > Rosie is a family of six-axis industrial robots from Advanced Metal Research in three reaches: Rosie 600, Rosie 1000 and Rosie 1400. Open RosieOS controller with everything switched on, configured to order. URL: https://advancedmetalresearch.com/rosie Six-axis industrial robots · Three reaches Configured to order Hawthorne, California Advanced Metal Research Precise six-axis industrial arms you can program any way you like, in three reaches: 600, 1,000 and 1,400 mm. One design and one open controller, from bench cells to long welds. $19,900 launch price · fully refundable deposit [Pre-order Rosie 1000 · $500 deposit](https://buy.stripe.com/bJeaER3oocI39HBdls9k402) [Compare the three](https://advancedmetalresearch.com/rosie#compare) Rosie 600: $14,900 and Rosie 1000: $19,900 per robot, launch prices for pre-orders placed before 31 December 2026. Rosie 1400: $29,000 per robot, deliveries begin Q1 2027. Every deposit is fully refundable. The balance is invoiced before shipment. *Figure: Isometric line drawing of the Rosie 1000 six-axis robot from the front right; after it is drawn, the arm runs a motion demonstration: straight-line moves, a circle, a tool reorientation about a fixed point and joint moves.* Models · Rosie 600 · 1000 · 1400 · 600 to 1,400 mm reach Control · RosieOS · Built by AMR, Apache 2.0 Key figures · Rosie 1000 - **Axes:** 6 - **Reach:** 1,000 mm - **Payload:** 7.4 kg - **Supply:** 220–240 V - **Controller:** RosieOS - **Price:** $19,900 Reach to the wrist centre, 1,077 mm to the tool flange. Rated payload with its centre of mass 100 mm out and 50 mm off the tool axis, in every pose. Single-phase supply on a 20 A circuit. Launch price for pre-orders placed before 31 December 2026. Three reaches · Elevation, one scale Reach to the wrist centre ## Three reaches, one design. The same six-axis layout, wrist and RosieOS controller in three sizes. Pick the reach your cell needs. Rosie 600 and Rosie 1000 share the base, the wrist and the tool flange. The 1000 has a 470 mm upper arm and a longer forearm, with larger drives at the shoulder and elbow. Both have harmonic gearboxes and absolute encoders on all six joints. Rosie 1400 has the longest reach and the highest payload. *Figure: Side elevations of Rosie 600, Rosie 1000 and Rosie 1400 drawn to one scale on one floor line, each with its arm stretched out level; a dimension from the base axis to the wrist centre reads 600, 1,000 and 1,400 mm.* *Rosie 600, 1000 and 1400 compared* | Figure | Rosie 600 | Rosie 1000 | Rosie 1400 | | --- | --- | --- | --- | | Reach | 600 mm | 1,000 mm | 1,400 mm | | Payload, rated | 9 kg | 7.4 kg | 15 kg nominal | | Payload, peak | 29.7 kg | 29.7 kg | 23.4 kg | | Repeatability | ±0.03 mm | ±0.04 mm | ±0.05 mm | | Axes | 6 | 6 | 6 | | Supply | 220–240 VAC, single-phase, 16 A circuit | 220–240 VAC, single-phase, 20 A circuit | 200 to 240 VAC, 3-phase, 35 A | | Line current, peak / average | 10.8 A / 2.6 A | 16.4 A / 3.1 A | 17.4 A / 3.1 A per phase | | Base | Ø230 floor plate, 6 × M10 anchors | Ø230 floor plate, 6 × M10 anchors | 300 × 300 mm plate, 4 × Ø26 anchors | | Tool flange | ISO 9409-1-50-4-M6 | ISO 9409-1-50-4-M6 | Ø38 face, 6 × M5 on PCD 27, ISO 50 adapter | | Controller | RosieOS | RosieOS | RosieOS | | Price | $14,900 launch | $19,900 launch | $29,000 | [Pre-order Rosie 600 · $500 deposit](https://buy.stripe.com/7sYeV70ccazVaLF8189k401) $14,900 launch price [Pre-order Rosie 1000 · $500 deposit](https://buy.stripe.com/bJeaER3oocI39HBdls9k402) $19,900 launch price [Pre-order Rosie 1400 · $500 deposit](https://buy.stripe.com/eVqbIV1ggfUf4nha9g9k400) $29,000 · deliveries begin Q1 2027 Launch prices are for pre-orders placed before 31 December 2026. Full figures for each model are in the [specification](https://advancedmetalresearch.com/rosie#specs). Line current is at 230 V, power factor 0.6, over a standard pick-and-place cycle at rated payload. In motion · Hawthorne R&D lab Video (https://advancedmetalresearch.com/assets/video/rosie-lab.webm) *Rosie 1400 articulating its wrist in the Hawthorne R&D lab.* Processes · Illustrative cycles Planned on the Rosie 1400 model ## One arm, many processes. Welding comes first. With a different end-effector and program, the same arm can handle, assemble, dispense, deburr, and cut. Each cycle below is planned on the kinematic model of Rosie 1400 and timed to its rated joint speeds, with a conservative estimate of its acceleration. Every target is within the joint limits, and the tooling is drawn to scale. Rosie 600 and Rosie 1000 take the same kinds of tooling for parts within their reach and payload. Tell us the part and the process, and we will scope the tooling and the right Rosie with the proposal. 1. *Figure: Line drawing of Rosie 1400 with a MIG torch, laying a fillet weld along a T-joint clamped to a fixture table.* ### Welding: MIG fillet A MIG torch lays a fillet weld along a T-joint. 2. *Figure: Line drawing of Rosie 1400 tending a CNC machine: it loads a blank into the vise through the open door and unloads the finished part.* ### Machine tending Rosie loads a blank through the machine door and unloads the finished part. 3. *Figure: Line drawing of Rosie 1400 with a vacuum gripper, stacking cartons from a conveyor onto a pallet in interlocked layers.* ### Palletizing A vacuum gripper stacks cartons on a pallet in interlocked layers. 4. *Figure: Line drawing of Rosie 1400 with a nutrunner, running down and torquing the six cover bolts of a gearbox housing in a star pattern on a two-station index table.* ### Assembly A nutrunner torques a gearbox cover's six bolts in a star pattern. 5. *Figure: Line drawing of Rosie 1400 with a dispensing valve, laying a continuous sealant bead around the flange of a panel.* ### Dispensing A dispensing valve lays a continuous sealant bead around a flange. 6. *Figure: Line drawing of Rosie 1400 with a spray gun, coating a vertical panel in overlapping horizontal passes.* ### Coating A spray gun covers a panel in overlapping passes at a constant standoff. 7. *Figure: Line drawing of Rosie 1400 with a compliant deburring spindle, following the edges of a machined plate held in a fixture.* ### Deburring A compliant spindle follows the part's edges at a set tool angle. 8. *Figure: Line drawing of Rosie 1400 with a plasma torch, cutting a profile with holes from a plate on a slat table.* ### Plasma cutting A plasma torch cuts a profile from plate on a slat table. Illustrative cycles drawn from the robot's kinematic model. Process tooling is quoted per application. Control · Programs checked before load ## RosieOS control and a handheld pendant. Every Rosie runs on RosieOS, AMR's real-time controller software, open source under Apache 2.0. Everything is switched on from day one: you never pay to unlock a feature the robot already has. The teach pendant is a handheld Steam Deck. Weld programs are checked for collisions and joint limits before they can be loaded, and can be replayed in simulation before the real joints move. Operators program, jog, and run weld paths from the pendant. Programs, offsets, and procedures are stored on the controller and travel with the machine. [Read the RosieOS docs](https://advancedmetalresearch.com/docs/)[RosieOS on GitHub](https://github.com/advanced-metal-research/RosieOS) *Figure: The RosieOS teach pendant running on a Steam Deck, Jog page: Rosie in a 3D view beside the Cartesian move and rotate pads, connected to a cell and armed.* *Hold R2 and move the tool with the sticks. Every pad shows the Steam Deck control that drives it, and B stops the robot from every page.* *Figure: RosieOS teach pendant, Program page: a weld program as a spreadsheet of approach, weld and transit rows with their destinations, speeds and status.* *The weld program as a spreadsheet. Record, reteach and edit rows from the pendant, then plan, preview and run them.* *Figure: RosieOS teach pendant, Telemetry page: live velocity plots for every joint over the last ten seconds.* *Live position, velocity, acceleration, following error and torque from every drive, with a 1 kHz capture.* **Offline programming** ### Program the next job on your PC while Rosie runs this one. Offline programming runs in the browser on your workstation. Teach waypoints and build any program from MoveJ, MoveL and MoveC moves, dwells, I/O and Home, or import the STEP part and pick welds from its edges. Every plan is checked for collisions and joint limits, and you replay the exact trajectory in simulation before the robot moves. The pendant opens the same programs, and both are open source under Apache 2.0 with the rest of RosieOS. [See offline programming](https://advancedmetalresearch.com/offline-programming)[Teach waypoints and moves](https://advancedmetalresearch.com/docs/guides/teach-waypoints)[Program a weld from CAD](https://advancedmetalresearch.com/docs/guides/offline-programming) *Figure: The RosieOS offline programming app: the Rosie 1400 cell in 3D with a welded hitch on the positioner table, its welds listed, the J6 and TCP camera views, and a server-validated plan replaying in simulation.* Video (https://advancedmetalresearch.com/assets/video/handheld-control.mp4) *Starting a weld program from the handheld pendant in the AMR R&D lab.* Specification · Rosie 600 Rosie 1000 Rosie 1400 ## Specification The figures needed to specify, install, and integrate each Rosie. ![The Rosie arm with a MIG torch fitted, photographed in the AMR R&D lab](https://advancedmetalresearch.com/assets/img/arm-6dof.webp) *Rosie 1400 with the MIG torch package.* ### Rosie 600 - **Axes:** 6 - **Reach, wrist centre:** 600 mm - **Reach, tool flange:** 677 mm - **Payload, rated:** 9.0 kg - **Payload, peak torque:** 29.7 kg - **Cycle, 25 / 305 / 25 mm, 1 kg:** 0.621 s - **Cycle at rated payload:** 0.814 s - **Tool speed, peak:** 4.3 m/s - **Repeatability:** ±0.03 mm - **Mass:** 29.3 kg - **Height at home, width:** 734 mm, 213 mm - **Supply:** 220–240 VAC, single-phase, 16 A circuit - **Power, peak / average:** 1.50 kW / 358 W - **Line current, peak / average:** 10.8 A / 2.6 A - **Base:** Ø230 floor plate, 6 × M10 anchors - **Tool flange:** ISO 9409-1-50-4-M6 [Pre-order Rosie 600 · $500 deposit](https://buy.stripe.com/7sYeV70ccazVaLF8189k401) $14,900 launch price ### Rosie 1000 - **Axes:** 6 - **Reach, wrist centre:** 1,000 mm - **Reach, tool flange:** 1,077 mm - **Payload, rated:** 7.4 kg - **Payload, peak torque:** 29.7 kg - **Cycle, 25 / 305 / 25 mm, 1 kg:** 0.665 s - **Cycle at rated payload:** 0.801 s - **Tool speed, peak:** 6.6 m/s - **Repeatability:** ±0.04 mm - **Mass:** 38.6 kg - **Height at home, width:** 934 mm, 267 mm - **Supply:** 220–240 VAC, single-phase, 20 A circuit - **Power, peak / average:** 2.27 kW / 422 W - **Line current, peak / average:** 16.4 A / 3.1 A - **Base:** Ø230 floor plate, 6 × M10 anchors - **Tool flange:** ISO 9409-1-50-4-M6 [Pre-order Rosie 1000 · $500 deposit](https://buy.stripe.com/bJeaER3oocI39HBdls9k402) $19,900 launch price ### Rosie 1400 - **Axes:** 6 - **Reach, wrist centre:** 1,400 mm - **Reach, tool flange:** 1,499 mm - **Payload, rated:** 15 kg - **Payload, peak torque:** 23.4 kg - **Cycle, 25 / 305 / 25 mm, 1 kg:** 0.803 s - **Cycle at rated payload:** 0.845 s - **Tool speed, peak:** 6.6 m/s - **Repeatability:** ±0.05 mm - **Mass:** 77.0 kg - **Height at home, width:** 949 mm, 299 mm - **Supply:** 200 to 240 VAC, 3-phase, 35 A - **Power, peak / average:** 4.15 kW / 745 W - **Line current, peak / average:** 17.4 A / 3.1 A per phase - **Base:** 300 × 300 mm plate, 4 × Ø26 on 250 mm square - **Tool flange:** Ø38 face, 6 × M5 on PCD 27, ISO 50 adapter [Pre-order Rosie 1400 · $500 deposit](https://buy.stripe.com/eVqbIV1ggfUf4nha9g9k400) $29,000 · deliveries begin Q1 2027 Rosie 1400 is made in the USA, from US and imported parts. ### Every Rosie - **Controller:** RosieOS, built by AMR, open source under Apache 2.0 - **Motion control:** Real-time, all six axes - **Teach pendant:** Handheld - **Program check:** Collision and limits before load ### Drives - **Motors:** EtherCAT servos, IP67, 24 VDC holding brakes - **Encoders:** 17-bit absolute - **Installed motor power:** 1,850 W (600), 2,450 W (1000), 3,900 W (1400) - **Gearboxes:** Harmonic, all six joints, IP65 (600, 1000) - **Structure:** Machined 6061-T6 aluminum (600, 1000) *Rosie 600, 1000 and 1400 axes* | Joint | Range, Rosie 600 and 1000 | Range, Rosie 1400 | Speed, Rosie 600 | Speed, Rosie 1000 | Speed, Rosie 1400 | | --- | --- | --- | --- | --- | --- | | J1 base | ±170° | ±177.6° | 336 °/s | 336 °/s | 240 °/s | | J2 shoulder | ±110° | ±108.9° | 288 °/s | 240 °/s | 240 °/s | | J3 elbow | ±150° | ±149.0° | 336 °/s | 288 °/s | 240 °/s | | J4 forearm roll | ±180° | ±177.6° | 360 °/s | 360 °/s | 336 °/s | | J5 wrist bend | ±125° | ±126.1° | 360 °/s | 360 °/s | 360 °/s | | J6 flange roll | ±180° | ±177.6° | 360 °/s | 360 °/s | 360 °/s | ![End-view drawing detail of the Rosie 1400 wrist: the tool flange face with its bolt circles, set in the square wrist housing](https://advancedmetalresearch.com/assets/drawings/rosie/detail/flange-white.webp) *Rosie 1400 wrist flange and ISO 50 tool adapter* ![Underside drawing detail of the Rosie 1400 base plate: a square plate with four corner holes, a ring of twelve counterbored bolt holes, and eight holes on the central disc](https://advancedmetalresearch.com/assets/drawings/rosie/detail/baseplate-white.webp) *Rosie 1400 base plate, 300 × 300 mm, 250 mm anchor square* CAD and simulation · STEP URDF MuJoCo Free download ## CAD and simulation STEP, URDF and MuJoCo models of all three Rosie robots, with full kinematics, joint limits, speeds, torques and mass properties. The models carry the robot's real outside surfaces: each link is one solid, filled inside, with the structural parts' own CAD faces and the motors and gearboxes as plain envelopes of the same size, so you can dimension from them and design a pedestal or tool against them. Every number is also in `robot.json`, including joint accelerations and the exact conditions of the published cycle times, and each kit has tested quickstart code for PyBullet, MuJoCo and ROS 2. Each kit also carries the benchmark cycle as a timestamped joint trajectory, IK reference cases solved by RosieOS's own solver, and a GLB that loads straight into a browser. Joint names and the home pose match RosieOS. Drawings and STEP files of the Rosie 1400 base and tool flange are in the docs under [Mechanical interfaces](https://advancedmetalresearch.com/docs/reference/mechanical-interfaces). ### Rosie 600 600 mm reach 29.3 kg [Download the kit · ZIP, 700 KB](https://advancedmetalresearch.com/assets/sim/rosie-600-sim-kit.zip) - [Robot model · **STEP** AP214, mm, 574 KB](https://advancedmetalresearch.com/assets/sim/600/step/rosie_600.step) - [Robot description · **URDF** meshes in the kit, 17 KB](https://advancedmetalresearch.com/assets/sim/600/urdf/rosie_600.urdf) - [MuJoCo model · **MJCF** actuators and keyframes, 13 KB](https://advancedmetalresearch.com/assets/sim/600/mjcf/rosie_600.xml) - [Kinematics and ratings · **JSON** robot.json, 25 KB](https://advancedmetalresearch.com/assets/sim/600/robot.json) - [Web model · **GLB** joint hierarchy, 223 KB](https://advancedmetalresearch.com/assets/sim/600/glb/rosie_600.glb) - [IK reference cases · **JSON** solved by RosieOS, 31 KB](https://advancedmetalresearch.com/assets/sim/600/ik_cases.json) ### Rosie 1000 1,000 mm reach 38.6 kg [Download the kit · ZIP, 742 KB](https://advancedmetalresearch.com/assets/sim/rosie-1000-sim-kit.zip) - [Robot model · **STEP** AP214, mm, 618 KB](https://advancedmetalresearch.com/assets/sim/1000/step/rosie_1000.step) - [Robot description · **URDF** meshes in the kit, 17 KB](https://advancedmetalresearch.com/assets/sim/1000/urdf/rosie_1000.urdf) - [MuJoCo model · **MJCF** actuators and keyframes, 12 KB](https://advancedmetalresearch.com/assets/sim/1000/mjcf/rosie_1000.xml) - [Kinematics and ratings · **JSON** robot.json, 25 KB](https://advancedmetalresearch.com/assets/sim/1000/robot.json) - [Web model · **GLB** joint hierarchy, 234 KB](https://advancedmetalresearch.com/assets/sim/1000/glb/rosie_1000.glb) - [IK reference cases · **JSON** solved by RosieOS, 26 KB](https://advancedmetalresearch.com/assets/sim/1000/ik_cases.json) ### Rosie 1400 1,400 mm reach 77.0 kg [Download the kit · ZIP, 3.7 MB](https://advancedmetalresearch.com/assets/sim/rosie-1400-sim-kit.zip) - [Robot model · **STEP** AP214, mm, 7.5 MB](https://advancedmetalresearch.com/assets/sim/1400/step/rosie_1400.step) - [Robot description · **URDF** meshes in the kit, 16 KB](https://advancedmetalresearch.com/assets/sim/1400/urdf/rosie_1400.urdf) - [MuJoCo model · **MJCF** actuators and keyframes, 12 KB](https://advancedmetalresearch.com/assets/sim/1400/mjcf/rosie_1400.xml) - [Kinematics and ratings · **JSON** robot.json, 27 KB](https://advancedmetalresearch.com/assets/sim/1400/robot.json) - [Web model · **GLB** joint hierarchy, 1.1 MB](https://advancedmetalresearch.com/assets/sim/1400/glb/rosie_1400.glb) - [IK reference cases · **JSON** solved by RosieOS, 26 KB](https://advancedmetalresearch.com/assets/sim/1400/ik_cases.json) ### Run it in simulation Shown for the Rosie 1000. For the other robots, change 1000 to 600 or 1400. Terminal · Bash ```bash curl -LO https://advancedmetalresearch.com/assets/sim/rosie-1000-sim-kit.zip unzip -q rosie-1000-sim-kit.zip && cd rosie_1000_description pip install mujoco pybullet numpy python examples/mujoco_demo.py # also: pybullet_demo.py, fk_ik.py ``` mujoco_hold.py, in rosie_1000_description · Python ```python import mujoco import numpy as np model = mujoco.MjModel.from_xml_path("mjcf/rosie_1000.xml") data = mujoco.MjData(model) data.ctrl[:] = np.radians([30, 45, -10, 0, -35, 0]) # position servos on J1..J6 for _ in range(1500): # 3 s at 2 ms steps mujoco.mj_step(model, data) print(np.degrees(data.qpos).round(2)) # joint angles (deg) print(data.actuator_force.round(1)) # joint torques (N m) print(data.site("tool0").xpos.round(4)) # flange face (m) ``` pybullet_move.py, in rosie_1000_description · Python ```python import math import pybullet as p p.connect(p.DIRECT) # p.GUI for a window p.setGravity(0, 0, -9.81) robot = p.loadURDF("urdf/rosie_1000.urdf", useFixedBase=True, flags=p.URDF_USE_INERTIA_FROM_FILE) joints = [p.getJointInfo(robot, i) for i in range(p.getNumJoints(robot))] arm = [j for j in joints if j[2] == p.JOINT_REVOLUTE] # J1..J6 tool0 = next(j[0] for j in joints if j[12] == b"tool0") target = [math.radians(a) for a in (30, 45, -10, 0, -35, 0)] for j, q in zip(arm, target): # j[10] peak torque, j[11] max speed p.setJointMotorControl2(robot, j[0], p.POSITION_CONTROL, targetPosition=q, force=j[10], maxVelocity=j[11]) for _ in range(3 * 240): p.stepSimulation() print(p.getLinkState(robot, tool0, computeForwardKinematics=True)[4]) ``` Terminal, in a ROS 2 workspace · Bash ```bash cp -r rosie_1000_description ~/ros2_ws/src/ cd ~/ros2_ws && colcon build --packages-select rosie_1000_description source install/setup.bash ros2 launch rosie_1000_description display.launch.py # RViz with joint sliders ``` fk.py, numpy only · Python ```python import json import urllib.request import numpy as np URL = "https://advancedmetalresearch.com/assets/sim/1000/robot.json" req = urllib.request.Request(URL, headers={"User-Agent": "rosie-sim-kit/1.0"}) robot = json.load(urllib.request.urlopen(req)) def rot(axis, q): x, y, z = axis c, s, t = np.cos(q), np.sin(q), 1 - np.cos(q) return np.array([[t * x * x + c, t * x * y - s * z, t * x * z + s * y], [t * x * y + s * z, t * y * y + c, t * y * z - s * x], [t * x * z - s * y, t * y * z + s * x, t * z * z + c]]) def fk(q): """Pose of tool0 (4x4) in base_link for joint angles q (rad).""" T = np.eye(4) for j, qi in zip(robot["joints"], q): A = np.eye(4) A[:3, 3], A[:3, :3] = j["origin_xyz"], rot(j["axis"], qi) T = T @ A return T @ np.diag([1, -1, -1, 1]) # tool0: z out of the flange print(fk(robot["poses"]["stretched"])[:3, 3]) # (m) ``` Units are metres, kilograms and radians; the STEP is in millimetres. At home every joint is zero, the forearm points along +x and the tool flange faces down. `tool0` is the flange face, z out of the flange. Every file has a stable URL under [/assets/sim/](https://advancedmetalresearch.com/assets/sim/index.json), and the guide [Simulate a Rosie robot](https://advancedmetalresearch.com/docs/guides/simulate-a-rosie-robot) covers frames, ratings and the RosieOS simulator. Options ## Configured to order. Tell us what Rosie needs to move, carry, or position, and we will recommend the reach and prepare a proposal. Every robot ships with the arm, controller, drives, cabling, pendant, and commissioning at your site. - **Arm and controller:** Rosie 600, Rosie 1000 or Rosie 1400, RosieOS controller, drives, handheld pendant, base hardware. Included with every order. - **Welding package:** MIG, TIG, or fiber laser torch with wire feed, gas, and torch-mount tooling on the ISO 50 flange. Laser packages include a 2–10 kW fiber laser source and interlocks. - **War Machine:** Rosie 1400 is also available as the robot inside War Machine, our production welding cell. [See War Machine](https://advancedmetalresearch.com/war-machine). Order ## Order Rosie. Rosie 600 is $14,900 and Rosie 1000 is $19,900, launch prices for pre-orders placed before 31 December 2026. Pre-order Rosie 600 or Rosie 1000 with a fully refundable $500 deposit per robot. We confirm your production slot by email within one business day and send your ship date as production is scheduled. The balance, $14,400 for Rosie 600 and $19,400 for Rosie 1000, is invoiced before shipment. [Pre-order Rosie 600 with a $500 deposit](https://buy.stripe.com/7sYeV70ccazVaLF8189k401) [Pre-order Rosie 1000 with a $500 deposit](https://buy.stripe.com/bJeaER3oocI39HBdls9k402) Rosie 1400 is $29,000. The arm, RosieOS controller, drive cabinet, cabling, pendant, and commissioning at your site are included. Welding packages are quoted per application. Pre-order Rosie 1400 with a fully refundable $500 deposit per robot. Deliveries begin Q1 2027. We confirm your production slot by email within one business day and send your ship date as production is scheduled. The $28,500 balance is invoiced before shipment. [Pre-order Rosie 1400 with a $500 deposit](https://buy.stripe.com/eVqbIV1ggfUf4nha9g9k400) Prefer a written proposal first? Complete the fields that apply. We reply with a proposal, lead time, and a date to see Rosie run in Hawthorne. Not sure which reach? Pick "Help me choose" and describe the part. Sending opens a pre-filled email in your own mail client. This site stores nothing. Or write directly to [generalcontact@advancedmetalresearch.com](mailto:generalcontact@advancedmetalresearch.com?subject=Order%20Rosie) **Form** (fields) - Name - Company - Email - Quantity - Model: - Rosie 600 - Rosie 1000 - Rosie 1400 - Help me choose - Packages: - MIG - TIG - Laser - War Machine cell - What does Rosie need to move, carry, or position? --- # Program Rosie from your PC. > Open source offline programming for Rosie robots, part of RosieOS under Apache 2.0: teach waypoints and build any program from MoveJ, MoveL and MoveC moves, dwells, I/O and Home, plan and verify it against the cell, simulate the exact trajectory and run it on the robot. For welding, pick the welds from the part's STEP file. Runs in your browser. URL: https://advancedmetalresearch.com/offline-programming Offline programming Part of RosieOS · Open source Apache 2.0 Open source · Apache 2.0 · RosieOS Open source offline programming for Rosie, part of RosieOS under Apache 2.0. Teach waypoints and build any program you like from MoveJ, MoveL and MoveC moves, dwells, I/O and Home, row by row in one spreadsheet. Simulate it, then run it on the cell or the pendant. For welding, pick the welds straight from the part's CAD. [Get started](https://advancedmetalresearch.com/docs/get-started/quickstart) [Source on GitHub](https://github.com/advanced-metal-research/RosieOS) *Figure: The RosieOS offline programming workstation: the Rosie 1400 cell in 3D with a welded hitch (tube, upright and gussets on a base plate) on the positioner table, its eight welds listed, the J6 and TCP camera views, and a server-validated plan replaying in simulation.* Licence · Apache 2.0 · Source on GitHub Runs in · Your browser · Linux or WSL2 server Any task · Teach MoveJ MoveL MoveC ## Any task, not just welding. A program is a line of moves and events. Teach the waypoints, choose how the robot gets to each one, and plan it against the cell. Welding from CAD is one application; handling, tending, palletizing, assembly, dispensing and coating are programs built from the same moves. ### Taught moves, no CAD needed. - Record a waypoint from the jogged robot or the preview robot. Reteach replaces it, and Record via adds the pose a circular move passes through. - Make each waypoint a MoveJ, MoveL or MoveC, with its own joint speed and acceleration limits and, for MoveL and MoveC, a constant TCP speed. - Add dwell, I/O and Home nodes between moves, and mix taught moves with welds in one program. - Name every row, and edit its pose in millimetres and degrees or its joint angles. [Teach waypoints and moves](https://advancedmetalresearch.com/docs/guides/teach-waypoints) *Figure: The offline programming spreadsheet with a taught pick and place program: a MoveJ to the infeed approach, MoveL pick and retract, a dwell, a MoveC transfer to the outfeed, MoveL place and retract, a dwell and Home, each row planned, with the preview paused at the pick.* ### Planned against the cell. Plan times every move to the robot's joint limits and checks it against the cell, your stands and fixtures, with the same verifier the welds pass. Simulate replays the planned trajectory with the tool path drawn in the cell. On the cell, Go to plan start, Load and Play. *Figure: The offline programming workstation with the taught pick and place program planned: Rosie 1400 in the preview at the pick over the infeed stand, the transfer arc to the outfeed stand drawn in the cell.* 1. *Figure: Line drawing of Rosie 1400 picking a part from one stand and placing it on another.* ### Pick and place A MoveJ to the approach, MoveL down and back up, a MoveC across to the next station. 2. *Figure: Line drawing of Rosie 1400 tending a CNC machine: it loads a blank into the vise through the open door and unloads the finished part.* ### Machine tending Taught moves through the machine door to the vise and back out, then Home. 3. *Figure: Line drawing of Rosie 1400 with a vacuum gripper, stacking cartons from a conveyor onto a pallet in interlocked layers.* ### Palletizing A taught place for every position in the layer, each a MoveL down from its approach. 4. *Figure: Line drawing of Rosie 1400 with a nutrunner, running down the six cover bolts of a gearbox housing in a star pattern.* ### Assembly A MoveL to each bolt in a star pattern, a dwell, and a MoveL back out. 5. *Figure: Line drawing of Rosie 1400 with a dispensing valve, laying a continuous sealant bead around the flange of a panel.* ### Dispensing MoveL along the straights and MoveC round the corners, at a constant TCP speed. 6. *Figure: Line drawing of Rosie 1400 with a spray gun, coating a vertical panel in overlapping horizontal passes.* ### Coating Overlapping MoveL passes across the panel at a constant TCP speed. Process tooling for each job is quoted per application. [Processes on Rosie](https://advancedmetalresearch.com/rosie#processes) Welding from CAD · STEP Welds Plan Simulate ## From a STEP file to a verified weld program. The whole job happens at your desk. The cell keeps welding while you program the next part. Nothing reaches the robot until the plan has passed its checks, and you can watch it run in simulation first. 1. **01** ### Import and place Import one or more STEP parts, each with its own placement and welds. Add fixtures and stands from STEP files, then drag the part into place on the cell. 2. **02** ### Pick the welds Click an edge of the part. OLP matches it to a detected seam and searches the torch angles in both directions. Accept the weld, or trim it to the stretch the torch can reach. 3. **03** ### Plan and verify The weld planner plans every weld and the moves between them. The verifier checks each segment against the cell, the part, your fixtures and the joint limits before a trajectory is written. 4. **04** ### Simulate and run Replay the exact trajectory the cell would play, from 1× to 16×. Then home, arm, load and play it on the cell, with a dry run first if you want one. The program · Welds Moves Dwells I/O ## Every move is a row you can read and edit. The program is a spreadsheet of nodes: welds with their approach and retract, transits, taught moves, dwells, I/O events and Home. Open any row to edit its pose, speed and process in millimetres and degrees, with undo and redo over every change. - Teach waypoints from the live robot: MoveJ, MoveL and MoveC through a recorded via point. - Weld presets for process, speeds, electrical settings, torch angles and weave. - Validation checks in plain words above the table, before you plan. *Figure: The offline programming spreadsheet: approach, weld and transit rows of the hitch weld program with their geometry, process and status, and the simulation transport running.* ### Weld presets New welds start from a preset: process, travel, approach and retract speeds, electrical settings, work, travel and spin angles, and weave. Edit one weld or the preset every new weld uses. *Figure: The Weld presets card: GMAW process, travel, approach and retract speeds, and torch work, travel and spin angles.* Simulate and deploy · Plans Evidence Load and play ## Watch the exact trajectory before the robot moves. Simulate replays the planner's own trajectory, the same bytes the cell will play, with the TCP path drawn on the part and the J6 and TCP cameras beside it. Every plan is kept with its evidence, and a refusal names the joint, the limit or the bodies in contact. Deploy takes the same plan to the cell: Home, Arm, Go to plan start, Load and Play, with Stop on screen the whole time. Load refuses a trajectory made for a different robot description or cell calibration. *Figure: The Simulate and deploy panel: the Plans list with a server-validated plan of 534 samples, and the simulation running on the Rosie 1400 cell.* Connected to the cell · Jog Live pose 1 kHz telemetry ## Jog it, watch it, measure it. Choose a cell from the Cells menu. Connecting verifies the cell's identity and never arms it. Jog joints or the tool in base or tool frame, and the 3D view follows the live robot. - Joint and Cartesian jog, with Home and move by angle. - Live position, velocity, acceleration, following error and torque for every joint. - Capture the last seconds of the controller's 1 kHz ring, and the executed TCP path for analysis. - Cells that other OLP servers advertise on your network, listed for you to add. *Figure: Offline programming connected to a Rosie 1400 cell in simulation, armed: the joint jog table with live positions beside live velocity plots for every joint.* On the pendant · Steam Deck Same programs ## The same program, in your hands. The Steam Deck teach pendant runs offline programming's own program logic, so it edits the same programs: record and reteach rows at the robot, plan, preview and run them. [The pendant on Rosie](https://advancedmetalresearch.com/rosie#control) *Figure: The RosieOS teach pendant on a Steam Deck, Program page: the hitch weld program as a spreadsheet of approach, weld and transit rows.* Open source · RosieOS Apache 2.0 ## Open source, all of it. Offline programming is part of RosieOS, under Apache 2.0. Read it, change it, run it on your own machines, and build your own tools on the same API it uses. ### The software - **Licence:** Apache 2.0 - **Source:** RosieOS on GitHub - **App:** Your browser - **Server:** Linux x86-64 or WSL2 - **Planning:** Weld planner, NVIDIA GPU ### Open formats - **Programs:** Documented JSON, with a schema - **Plans:** .weldplan, exportable - **API:** Documented HTTP API - **Same program in:** OLP, the pendant, the weld planner - **Unlock fees:** None [Program a weld from CAD](https://advancedmetalresearch.com/docs/guides/offline-programming) [Program format](https://advancedmetalresearch.com/docs/reference/program-format) [OLP HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http) [Licence](https://advancedmetalresearch.com/docs/contributing/license) Get started ## Try it on a simulated cell. The quickstart starts a simulated Rosie cell on your own machine and jogs it from offline programming. No robot needed. [Read the quickstart](https://advancedmetalresearch.com/docs/get-started/quickstart) [Contact AMR](mailto:generalcontact@advancedmetalresearch.com?subject=Offline%20programming) Send us a part. STEP file · material · volume We'll program it in OLP and show you the plan. --- # Welding that knows the weld is good. > Welding intelligence from AMR: seam tracking within 0.20 mm, weld-pool monitoring and analysis, arc-sound fault detection and adaptive control of path, torch and heat. A paid, optional skill on the open RosieOS controller, proven in the AMR metallurgical lab. URL: https://advancedmetalresearch.com/welding-automation Welding intelligence First expert skill · Runs on RosieOS, Apache 2.0 Welding automation The paid, optional welding skill for AMR robots, running on open RosieOS. [Contact AMR](https://advancedmetalresearch.com/welding-automation#contact) [Where it runs](https://advancedmetalresearch.com/welding-automation#runs) *Figure: Hidden-line drawing of the Rosie 1400 arm with a MIG torch, welding a fillet seam on a T-joint.* Seam tracking · Within 0.20 mm · During the weld Runs on · Rosie 1400 · War Machine Before, during, after · Mast scanners End-effector sensors Inspection ## Automated weld inspection, with no added cycle time. Every weld is inspected automatically, in real time, while it is welded. Inspection adds nothing to the cycle time. [The War Machine sensor suite](https://advancedmetalresearch.com/war-machine#sensing) ### Before: measure the part Mast scanners check fit-up and geometry against the CAD model, so a poor fit-up is found before the arc starts. ### During: track, watch, adapt Holds the torch on the joint within 0.20 mm, monitors the weld pool and detects faults from arc sound. Adjusts path, torch and heat in real time. ### After: inspect The finished part is checked against the CAD model with micron-level measurement. Every measurement goes into that part's record. The loop · Sensors Lab Simulator ## Every weld teaches the next. Each finding informs the next weld, fixture and procedure. *Figure: Loop drawing: robot sensors show what happened, the metallurgical lab cuts the welds open and tests them, and the simulator and world model learn why, so every weld teaches the next.* - **Robot sensors:** Show what happened: seam, pool, arc and geometry, on every weld. - **Metallurgical lab:** Cuts the welds open and tests them. Every procedure is sectioned and inspected, with macro inspection to ISO 13919-1 and dye-penetrant testing to ASTM E165. - **Simulator and world model:** Learn why: an AI world model and a weld physics simulator, calibrated against real welds. > Sensors show what happened. We cut our welds open and test them in our lab to understand the metallurgical processes and record the data for training. From that data, the simulator learns why. [The metallurgical lab](https://advancedmetalresearch.com/metallurgy) Open underneath · RosieOS Apache 2.0 ## Paid on top. Open underneath. The package uses the same open APIs and raw sensor access anyone gets. Buying it locks nothing. ### The open platform - **Controller:** RosieOS, Apache 2.0 - **Robot functions:** Every function unlocked - **Control:** One public API - **Sensor data:** Raw, through a documented API - **Built on by:** You, an integrator or an AI agent ### Welding intelligence - **Type:** Paid, optional expert skill - **Runs on:** RosieOS and its public APIs - **Sensor access:** The same raw data as yours - **Robots:** Rosie 1400, War Machine - **Locks underneath:** Nothing [Read the RosieOS docs](https://advancedmetalresearch.com/docs/) [RosieOS licence](https://advancedmetalresearch.com/docs/contributing/license) [Control API](https://advancedmetalresearch.com/docs/apis/rt-control-http) Why welding first ## A big job, short of welders, that needs a lab. ### A top job for robot arms Welding is one of the largest uses of industrial robot arms. ### Too few welders The US needs 320,500 new welding professionals by 2029, according to the [American Welding Society](https://weldingworkforcedata.com/). ### It takes metallurgy Doing it properly needs metallurgy. AMR runs its own metallurgical lab. Where it runs · Rosie 1400 War Machine ## On the arm, and in the cell. ![Line drawing of the Rosie 1400 six-axis robot](https://advancedmetalresearch.com/assets/drawings/rosie/iso-front-ink.webp) ### Rosie 1400 The six-axis arm. The package runs on its RosieOS controller. [Rosie 1400](https://advancedmetalresearch.com/rosie#rosie-1400) Reach · 1,400 mm ![Line drawing of the War Machine cell in its enclosure, from the load side](https://advancedmetalresearch.com/assets/drawings/war-machine-enclosure-iso.webp) ### War Machine The A/B cell. Rosie welds one bay while the next part loads on the other. [War Machine](https://advancedmetalresearch.com/war-machine) Processes · TIG, MIG, laser Contact ## Talk to AMR. Tell us about your welding application. [Contact AMR](mailto:generalcontact@advancedmetalresearch.com?subject=Welding%20automation) Send us a part. Drawing · material · volume We'll tell you what Rosie and War Machine can do with it. --- # War Machine > War Machine is the AMR production cell for welding and other high-throughput robotic processes: the Rosie 1400 six-axis robot, an H-frame A/B positioner, an integrated sensor suite, and TIG, MIG, and laser welding in a 10 ft or 20 ft container format. URL: https://advancedmetalresearch.com/war-machine A/B production cell · Rosie 1400 inside Hawthorne, California Welding and high-throughput processes War Machine is the production cell around Rosie 1400 for welding and other high-throughput processes: an A/B positioner, an integrated sensor suite, control, and a safety envelope, in a 10 ft or 20 ft container format. The operator unloads and loads one side while Rosie works the other, so the arm is not left waiting on part handling. The sensors measure every part before, during, and after the process, and the cell records the evidence. [Contact AMR](mailto:generalcontact@advancedmetalresearch.com?subject=War%20Machine) [Read the specification](https://advancedmetalresearch.com/war-machine#specs) *Figure: Drawing of the War Machine cell: Rosie 1400 welds a part on one bay of the positioner while the other bay is unloaded and reloaded, then the positioner turns to swap the bays.* Weld processes · TIG, MIG, laser · 2–10 kW fiber laser Deployment · 10 ft or 20 ft container · 20 kW system power The cycle · Scan Weld Index Live drawing ## Scan, weld, index, repeat. One production cycle on the War Machine cell, drawn live from its CAD model. The mast scanners check fit-up, Rosie 1400 welds both seams, and the turntable turns 180° to swap the bays. Switch the camera to follow the torch or the loader, circle the cell, or see it from the load end, the rear, or above. Every view runs on the same cycle. *Figure: Animated line drawing of the War Machine cell running its load-and-weld cycle, shown with the optional robot loader: the mast scanners check the part, Rosie 1400 MIG-welds a T-joint on the robot-side bay while a second Rosie outside the open load end swaps the finished part for a blank through the muted light curtain; the positioner then turns 180 degrees to swap the bays. Operator loading is standard. Camera views show the same cycle wide, close on the weld, close on the loader, circling the cell, in iso, from the load end, from the rear, and from above.* The cell · Robot Positioner Sensing Evidence ## Two bays, one arm, and a sensor suite. War Machine is the cell around Rosie 1400: the A/B positioner, fixturing, sensor suite, control, safety envelope, and production format. The robot, the positioner, and the sensors share one machined base and one coordinate system, so a scan taken at one end of the cell is valid for the tool at the other. Research and production run on the same cell and share one data record. ### Robot Rosie 1400, AMR's six-axis robot, with a 1,400 mm reach and the RosieOS controller. Designed, machined, and assembled in the USA. [Rosie 1400](https://advancedmetalresearch.com/rosie#rosie-1400) ### Positioner An H-frame with two fixture bays on a turntable: the operator loads one bay while Rosie works the other. A rotisserie on each bay turns the part to meet the tool, so weld seams present downhand. ### Sensor suite Scanners on the masts measure each part against the CAD model as it comes round to the robot. Sensors on the end-effector measure during the process and inspect the result after it. ### Evidence loop Each part produces a data record. Weld coupons are sectioned, etched, and inspected in the AMR metallurgical lab, and the results inform the next weld, fixture, and procedure. *Figure: Plan view of the cell at 1:10: Rosie on its pedestal at the left of the common base, the H-frame positioner with two fixture plates and a tilt shaft to its right, process equipment on the end wall, controls cabinet, sensing masts, and the operator station by the interlocked door.* Cell plan ### Six zones on one base The robot, the positioner, and the sensors share one base and one coordinate system, so a scan taken at one end of the cell is valid for the tool at the other. Hover over or focus a zone for details. 20 ft container · base 2,593 × 1,374 mm · 6,000 lb · 20 kW Positioner · H-frame Turntable and A/B rotisserie ## Load one side while Rosie works the other. The H-frame carries two fixture bays on a turntable. While the robot works the part on one bay, the operator unloads the finished part from the other and loads the next. The turntable then turns and the bays swap. Unloading, loading, and fixturing happen outside the robot's cycle. While the process time on one bay covers the handling time on the other, the arm goes from one part to the next with only the turntable swap between them. Its utilisation is set by process time, not handling time. On each bay, a rotisserie, the cell's seventh axis, turns the part under the torch. Every seam comes round to a reachable, downhand position, and the arm works in its strongest posture. - **A/B turntable:** Two bays, one robot. One bay faces Rosie 1400 while the other faces the operator, so loading and unloading happen beside the process, not instead of it. - **Rotisserie:** Rolls the part as the torch works, so a circumferential seam welds in the flat position and a fillet opens upward to the torch. - **Moves with the robot:** The positioner and the arm move together, so the part turns to meet the torch through the whole weld. - **Fixture plates:** Two perforated 1,200 mm fixture plates with fixture holes on a 100 mm grid. - **Loading:** The operator unloads and loads the outer bay as standard. As an option, a second Rosie does it from outside the light curtain, as in the cycle drawing at the top of the page. Processes · Welding first Suited to A/B loading ## Welding first, and other processes that suit two bays. The A/B layout pays off where each part holds the arm for at least as long as it takes to change the part on the other bay, and where the part gains from being turned to meet the tool. War Machine is built and equipped for welding. With a different end-effector and program, Rosie 1400 can be tooled for other processes on the same positioner and sensor suite. Tooling for these is specified and integrated per application. [One arm, many processes](https://advancedmetalresearch.com/rosie#processes) - **Welding: TIG, MIG, laser:** The lead process, and the one the cell is equipped for. The next part is fitted up on the other bay while this one is welded, and the rotisserie brings each seam round to the flat position. - **Plasma cutting:** Profiles and holes cut in a fixtured part, with the next part loaded while the current one is cut. - **Deburring and finishing:** A compliant spindle follows the part's edges at a set tool angle, and the positioner turns the part to present each edge to the spindle. - **Inspection scanning:** A laser line profiler sweeps each part and builds a point cloud of its surface, which becomes that part's record while the next part is loaded. - **Dispensing and sealing:** A continuous bead laid around a flange or seam, with the positioner turning the part so the valve can follow the path. Palletizing and machine tending spend most of their cycle on handling, so they gain little from A/B loading. Rosie 1400 is tooled for them in other layouts. Sensor suite · Masts End-effector Seam tracking within 0.20 mm ## Measure every part before, during, and after the process. Stationary scanners on the masts measure each part as it comes round to the robot. Sensors on the end-effector measure as Rosie works and inspect the result. Every measurement goes into that part's record. Parts, fit-up, and fixturing vary, so the cell works from the measured part rather than the nominal one. The mast scan runs on the robot's side of the positioner while the operator loads the other bay. Inspection results feed back into the process. Each record informs the next weld, the next fixture, and the next assembly. The sensing hardware is designed and built by AMR. 1. **Before the process.** The mast scanners measure the part and check fit-up and 3D geometry against the CAD model, so the cell works from where the part actually is, and a poor fit-up is found before the arc starts. 2. **During the process.** The sensors on the end-effector measure as Rosie works. When welding, the welding intelligence package, a paid and optional skill, tracks the seam within 0.20 mm and monitors the weld pool, adjusting path, torch, and heat in real time, and detects process faults from arc sound. [Welding intelligence](https://advancedmetalresearch.com/welding-automation) 3. **After the process.** Inspect the finished part and verify its geometry against the CAD model, with micron-level measurement. 4. **Into the record.** Keep an evidence trail for each part, for qualification, quality review, procedure development, and machine learning. ### Locate the real part Measuring each part in the cell puts the tool where the part is, not where the drawing says it should be, whether the feature is a weld seam, a cut line, an edge, or a bead path. ### Adapt the path The program follows the measured part. When welding, the welding intelligence package's adaptive control also adjusts torch and heat as the seam changes. ### Verify the result The finished part is measured against the CAD model before it leaves the fixture, so a nonconforming part is found at the cell, not downstream. ### Trace every part Each part leaves with its measurements from before, during, and after the process, so qualification and quality review work from that part's own record. Specification · Rosie 1400 inside ## Specification The figures needed to specify and site War Machine. Robot figures are on the Rosie 1400 page. ### War Machine - **Weld processes:** TIG, MIG, laser - **Laser source:** 2–10 kW fiber laser - **Other processes:** Tooled per application - **Positioner:** H-frame, turntable and A/B rotisserie - **Loading:** Operator (standard), robot loader (option) - **Format:** 10 ft or 20 ft container - **System power:** 20 kW ### Cell - **Robot:** Rosie 1400, 6 axes, 1,400 mm reach - **Controller:** RosieOS, handheld pendant - **Sensor suite:** Mast scanners, end-effector sensors - **Cell mass:** 6,000 lb, excluding container - **Common base:** 2,593 × 1,374 mm - **Control cabinet:** Professionally built [Rosie 1400 specification](https://advancedmetalresearch.com/rosie#rosie-1400) Deployment · 10 ft 20 ft Factory floor ## Install it beside the work, or ship it to the site. War Machine goes beside a depot, range, shipyard, supplier, or field-forward team, or onto a factory floor. The same core system serves defense research, supplier development, and production readiness. We prove the weld or process in our Hawthorne R&D lab before the machine ships. - **10 ft container:** A deployable automated lab that sits beside an existing depot, range, or research facility. The compact format suits constrained sites and forward teams. - **20 ft container:** The full cell with welding or process equipment, sensor suite, control, safety envelope, fume handling, and work envelope packaged for transport, with optional onboard diesel power. - **Integrated production node:** A factory-floor version that connects AMR sensing, robot motion, and inspection data to a larger production line. War Machine ## Bring AMR a part family, a weld or process problem, or a site that needs production capacity. We reply with what the cell can do for it, what we would need to prove first in Hawthorne, and how the deployment would look on your site. [Contact AMR](mailto:generalcontact@advancedmetalresearch.com?subject=War%20Machine) Send us a part. Drawing · material · volume We'll tell you what Rosie and War Machine can do with it. --- # The lab that proves the weld. > The AMR metallurgical lab in Hawthorne, California: sectioning, metallography, macro inspection to ISO 13919-1, dye-penetrant NDT to ASTM E165, and traceable weld records organized to ISO 15614-11, next to the cell that made the weld. URL: https://advancedmetalresearch.com/metallurgy Metallurgical lab · Hawthorne R&D lab Hawthorne, California Sectioning, inspection, and evidence Every procedure we develop is sectioned, etched, and inspected in our metallurgical lab, in the same building as the cell that welded it. You receive macro sections, inspection data, and a written procedure. [Contact AMR](https://advancedmetalresearch.com/metallurgy#contact) [Qualification services](https://advancedmetalresearch.com/metallurgy#services) *Figure: Etched weld cross-section with the fusion zone segmented by the inspection model.* Standards · ISO 13919-1 ASTM E165 · Organized to ISO 15614-11 Evidence · Macro sections, NDT · Traveler, data file, report From coupon to record · Section Etch Inspect Record ## Every sample becomes traceable evidence. Outcome: **faster weld qualification.** Procedures are developed and tested at AMR, so the qualification evidence comes from the same lab that ran the weld. 1. **Section.** The weld coupon is cut, mounted, ground, and polished into a traceable cross-section. 2. **Etch and inspect.** Penetration and defects on the etched cross-section are evaluated against the acceptance standard. 3. **Test the surface.** Dye-penetrant testing finds surface-breaking cracks without cutting the part. 4. **Record.** Each sample gets a traveler, a machine-readable analysis file, and a report. Methods · ISO 13919-1 ASTM E165 ISO 15614-11 ## Destructive, nondestructive, and predictive. ![A mounted test coupon held up in front of the grinding and polishing station](https://advancedmetalresearch.com/assets/img/coupon-mount.webp) ### Sectioning and metallography Prepares a weld coupon as a traceable cross-section: cut, mounted, ground, and polished. Mounting press · 0 to 50 kN ![Etched weld cross-section with the fusion zone segmented by the inspection model](https://advancedmetalresearch.com/assets/img/weld-macro-ml.webp) ### Macro inspection Evaluates penetration and defects on the etched cross-section against the acceptance standard. Acceptance · ISO 13919-1 level D ![Dye-penetrant NDT station under the fume hood](https://advancedmetalresearch.com/assets/img/ndt-penetrant.webp) ### Dye-penetrant NDT Detects surface-breaking cracks nondestructively with aerosol dye penetrant. Standard · ASTM E165 Video: Traceable weld records (https://www.youtube.com/watch?v=lrGu7eVj75g) ### Traceable weld records Produces a record for each sample: a traveler, a machine-readable analysis file, and a report. Organized to · ISO 15614-11 Video: Weld-pool simulation (https://www.youtube.com/watch?v=u8M3GVMcg2I) ### Thermal weld-pool simulation A weld physics simulator, calibrated against real welds, predicts melt-pool temperature and shape so procedures are tuned before the first weld. Predicts · pool temperature and shape Services · WPS PQR WPQ ## Name the code you need to meet. We write, test, and qualify the procedure. Welding procedure development, qualification-support evidence, and metallurgical testing, proven in our lab with macro inspection to ISO 13919-1 and dye-penetrant testing to ASTM E165. - **Procedure qualification:** Welding procedure development and qualification-support evidence for WPS, PQR, and WPQ. - **Metallurgical testing:** Sectioning, mounting, polishing, etching, macro inspection, and dye-penetrant NDT. - **Metallurgy research:** Process development, destructive test planning, material characterization, and weld-quality iteration, with automated experiment records. - **Modeling:** Applied mathematical modeling and weld-pool simulation, so a procedure is tuned before the first coupon is welded. [Request qualification](mailto:generalcontact@advancedmetalresearch.com?subject=Welding%20procedure%20qualification) In the loop · Lab Cell One record ## Findings become production rules. The lab sits next to the cell. Welds from every procedure we develop are sectioned and inspected here, and the result informs the next weld, the next fixture, and the next procedure. War Machine and the lab share one data record: the cell sees the part, measures the setup, welds the assembly, and inspects the result, and the lab closes the loop with the cross-section. The same evidence trail supports qualification, quality review, and procedure development. [War Machine](https://advancedmetalresearch.com/war-machine) ![The AMR R&D lab floor: cell, benches, and CNC mill](https://advancedmetalresearch.com/assets/img/shop.webp) *The Hawthorne R&D lab. The cell, the metallurgical lab, and the mill under one roof.* Metallurgical lab ## Send the material, the joint, and the code you need to meet. We reply with what we would weld, section, and test to prove it, and what evidence you would receive. [Contact AMR](mailto:generalcontact@advancedmetalresearch.com?subject=Metallurgical%20lab) Send us a part. Drawing · material · volume We'll tell you what Rosie and War Machine can do with it. --- # Capabilities > AMR capabilities in robotic welding, real-time weld sensing, metallurgical inspection, and technician training, each shown running in the Hawthorne R&D lab with its measured result or governing standard. URL: https://advancedmetalresearch.com/capabilities Designed and built by AMR Hawthorne, California · All video recorded in the AMR R&D lab Machine, sensing, lab, training What each capability does, shown running in the Hawthorne R&D lab, with its measured result or governing standard. [Contact AMR](https://advancedmetalresearch.com/#contact) [View sensing capabilities](https://advancedmetalresearch.com/capabilities#sensing) *Figure: Hidden-line drawing of the Rosie 1400 arm with a MIG torch, welding a fillet seam on a T-joint.* Sections · 01 Machine 02 Sensing · 03 Lab 04 Training Seam tracking · Within 0.20 mm · During the weld Machine · TIG, MIG, laser in one cell ## Robotic welding cell. Outcome: **welding capacity that does not depend on hiring welders.** CAD-to-path planning, programmable weld control, and TIG, MIG, and laser in one cell, designed and built by AMR across mechanical, electrical, controls, and simulation. Video: Robotic welding cell in operation (https://www.youtube.com/watch?v=_0GO0TIaLac) ### Robotic welding cell Plans the weld path from CAD, then welds with TIG, MIG, or laser. Processes · TIG, MIG, laser ![Rosie 1400 six-axis arm with a MIG torch beside a US flag](https://advancedmetalresearch.com/assets/img/arm-6dof.webp) ### Rosie 1400 six-axis robot Positions the torch anywhere within a 1,400 mm reach. Designed, machined, and assembled by AMR. [Specification](https://advancedmetalresearch.com/rosie#rosie-1400). Reach · 1,400 mm Video: Deployable welding cell (https://www.youtube.com/watch?v=N6tYfz4CXyc) ### Containerized welding cell Packages the complete cell in a shipping container for deployment to the work site. [War Machine](https://advancedmetalresearch.com/war-machine). Formats · 10 ft and 20 ft Video: CNC-machined industrial robot parts (https://www.youtube.com/watch?v=cPKctCZcdps) ### Mechanical engineering and machining Designs and machines the arm's structural and drive parts in-house, from CAD to finished metal. Machined · In-house, in the USA Video: Laser welding in progress (https://www.youtube.com/watch?v=2bkXRWLFlSk) ### Laser welding Welds with a 2–10 kW fiber laser inside an enclosure with door and vision safety interlocks. Source · 2–10 kW fiber laser Video: Electrical cabinet build (https://www.youtube.com/watch?v=rQYIrCLFCC8) ### Electrical engineering and controls A professionally built control cabinet: three-phase power distribution, servo drives, and a hardware safety chain. The robot moves only when the safety chain reports ready. Power · 208 VAC 3-phase Video: H-frame positioner turning a part (https://www.youtube.com/watch?v=h2NjvkjooK8) ### H-frame positioner Rotates and tilts the workpiece so every weld sits in a reachable, downhand position. Axes · turntable, A/B rotisserie Video: Actuation and real-time control data flow (https://www.youtube.com/watch?v=SkhpJ_i4vZM) ### Real-time motion control Coordinates all six joints in real time from RosieOS, AMR's controller software, open source under Apache 2.0. Weld programs are collision-checked before they load and can be previewed in simulation first. Repeatability±0.05 mm Sensing ## Real-time weld sensing. Outcome: **measured evidence for every weld.** Sensing hardware designed and built by AMR. With the welding intelligence package, a paid and optional skill, adaptive control adjusts path, torch, and heat during the weld, and the cell inspects every weld it makes. Video: Seam tracking in the cell (https://advancedmetalresearch.com/assets/video/seam-tracking.webm) ### Seam tracking Finds and follows the joint in 3D during the weld, so the torch stays on the seam through part and fit-up variation. Accuracy · within 0.20 mm Video: 3D geometry inspection against CAD and GD&T (https://www.youtube.com/watch?v=yFdHaV-XF1o) ### 3D geometry inspection against CAD Measures the part in 3D and verifies it against the CAD model and its GD&T, with micron-level measurement. Z repeatability · 5 µm Video: In-process weld-pool monitoring (https://www.youtube.com/watch?v=osbh-bzn5wY) ### Weld-pool monitoring Images the molten pool during every weld, so defects are caught while the part is still in the cell. Scope · every weld, in process Video: Real-time weld sensing and adaptive control (https://www.youtube.com/watch?v=2sKyRsxlNlc) ### Adaptive weld control Measures the seam and pool during the weld and adjusts path, torch, and heat in real time, on AMR-built hardware. Adjusts · path, torch, heat Video: Acoustic process monitoring from arc sound (https://www.youtube.com/watch?v=nVElN9h45q4) ### Acoustic process monitoring Detects process faults from arc sound and monitors process stability through the weld. Arc start is clearly distinguishable from ambient noise. Detects · arc start, process faults ![Structured-light scan of a weld coupon](https://advancedmetalresearch.com/assets/img/structured-light.webp) ### Structured-light inspection Checks joint fit-up before welding and weld geometry after it, so a poor fit-up is found before the arc starts. Checks · fit-up before, geometry after Metallurgical lab · ISO 13919-1 ASTM E165 ISO 15614-11 ## Metallurgical laboratory. Outcome: **faster weld qualification.** Procedures are developed and tested at AMR, so the qualification evidence comes from the same lab that ran the weld. [The metallurgical lab](https://advancedmetalresearch.com/metallurgy) ![Mounted metallurgical coupon at the mounting press](https://advancedmetalresearch.com/assets/img/lab-mount.webp) ### Sectioning and metallography Prepares a weld coupon as a traceable cross-section: cut, mounted, ground, and polished. Mounting press · 0 to 50 kN Video: Macro inspection of a weld cross-section (https://www.youtube.com/watch?v=hhglIhZGt3M) ### Macro inspection Evaluates penetration and defects on the etched cross-section against the acceptance standard. Acceptance · ISO 13919-1 level D Video: Weld-pool simulation (https://www.youtube.com/watch?v=u8M3GVMcg2I) ### Thermal weld-pool simulation A weld physics simulator, calibrated against real welds, predicts melt-pool temperature and shape so procedures are tuned before the first weld. Predicts · pool temperature and shape ![Dye-penetrant NDT station under the fume hood](https://advancedmetalresearch.com/assets/img/ndt-penetrant.webp) ### Dye-penetrant NDT Detects surface-breaking cracks nondestructively with aerosol dye penetrant. Standard · ASTM E165 Video: Traceable weld records (https://www.youtube.com/watch?v=lrGu7eVj75g) ### Traceable weld records Produces a record for each sample: a traveler, a machine-readable analysis file, and a report. Organized to · ISO 15614-11 Video: Experiment and test qualification queue (https://www.youtube.com/watch?v=Xpa00qyidQ8) ### Software test and qualification Tests every robot software change and records pass/fail evidence before it is released to a cell. Evidence · per-change test results Training · 17 modules 38 skill nodes ## Technician training. Outcome: **trained operators for advanced manufacturing cells.** We train the technicians who set up, program, and inspect the cell, on the production hardware and software. Video: Weld-sequence authoring (https://www.youtube.com/watch?v=Su1wu-gvPew) ### Weld-sequence authoring Operators select seams on the 3D CAD model, and the software generates the robot sequence. Output · robot weld sequence Video: CAD-to-path planner workflow (https://www.youtube.com/watch?v=pCPsLhI3ZH8) ### CAD-to-path programming Turns a CAD model into a collision-free robot path, which can be previewed in simulation before any live motion. Program check · collisions and limits before load Video: Steam Deck teach pendant (https://advancedmetalresearch.com/assets/video/pendant.mp4) ### Handheld teach pendant Programs, jogs, and runs weld paths from a handheld Steam Deck, outside the interlocked door. Operator safety · hardware e-stop, interlock Video: Technician curriculum graph (https://advancedmetalresearch.com/assets/video/curriculum.mp4) ### Technician curriculum Maps every skill a technician needs as a navigable dependency graph. Curriculum · 38 skill nodes, 4 levels Video: Advanced-manufacturing workforce training (https://www.youtube.com/watch?v=cenjZ60fyDM) ### Hands-on cell training Technicians learn setup, programming, sensing, and inspection on the production cell. Modules · 17 Video: Guided tour of the robotic welding cell (https://www.youtube.com/watch?v=2FWOpnBPS-A) ### Operator orientation Introduces new operators to the arm, positioner, sensing, and controls before they run the cell. Covers · arm, positioner, sensing, lab Contact ## Send AMR a weld problem, an assembly, or a program requirement. Everything on this page runs in the Hawthorne R&D lab today. Schedule a visit or send the part. [Contact AMR](mailto:generalcontact@advancedmetalresearch.com?subject=Capabilities) [Schedule a lab visit](mailto:generalcontact@advancedmetalresearch.com?subject=Visit%20the%20Hawthorne%20R%26D%20lab) Send us a part. Drawing · material · volume We'll tell you what Rosie and War Machine can do with it. --- # RosieOS > Documentation for RosieOS, the open-source software that runs a Rosie welding cell, from the real-time controller to offline programming and the teach pendant. URL: https://advancedmetalresearch.com/docs/ Section: RosieOS docs / Overview Last updated: 2026-10-10 RosieOS runs one Rosie welding cell. Every client drives the robot through one public control API, `rt-control`, which admits one controller at a time. That includes the teach pendant, the offline programming app, the motion servers and your own code. Behind it, a 1 kHz real-time core owns the EtherCAT drives. The same core also runs against a simulated bus, so you can run a whole cell on a laptop. ## What is in RosieOS | Part | What it does | |---|---| | **Real-time core** (`rosie-rt-core`) | 1 kHz C++ controller. It runs the CiA402 drives over EtherCAT, and decides every cycle whether motion is permitted. | | **Control API** (`rt-control`) | The only public interface: HTTP/JSON on a Unix socket, or mutual TLS for remote clients, with a lease, a fence and a dedicated jog lane. Go, C++ and TypeScript clients, plus the `rtctl` CLI. | | **Motion servers** | A Cartesian jog and position server, and a daemon that plays dense precomputed trajectories. | | **Weld planner** | GPU planner that turns CAD and a weld program into joint trajectories. It verifies each one against the cell model before it will emit it. | | **Offline programming** | Go server and browser app to import STEP parts, define welds, plan, simulate, and run on a cell. | | **Teach pendant** | Native Steam Deck app, v5, plus a browser copy of the earlier pendant for simulation. | | **Robot descriptions** | URDF, meshes, planning limits and frames for the Rosie 1400 and Rosie 1420, each with a hashed identity. | Production code covers the core, the control API and its clients, the motion servers, the weld planner, offline programming and the robot descriptions. The v5 pendant is current but not yet qualified on physical input or real motion. See [Architecture](https://advancedmetalresearch.com/docs/concepts/architecture#maturity). ## Safety first RosieOS has no software E-stop. The cell's hardware E-stop is the only emergency stop, and every software stop, interlock and check sits on top of it. Planned weld programs are verified against the cell model, collision and joint limits before they can be loaded, and you can replay them in simulation first. Jog, moves and Home are checked against joint limits only. The [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model) sets out exactly what is enforced, and where. ## For AI agents These docs are also published for machines. [`/docs/llms.txt`](/docs/llms.txt) indexes every page, and [`/docs/llms-full.txt`](/docs/llms-full.txt) holds them all in one file. Any page is available as Markdown at its URL plus `.md`. The APIs come as [OpenAPI 3.1](https://advancedmetalresearch.com/docs/openapi/rt-control.json) documents, and the search index is at [`/assets/docs/search.json`](/assets/docs/search.json). [For AI agents](https://advancedmetalresearch.com/docs/get-started/for-ai-agents) lists them all, with the rules an agent must follow. ## Where to start - **Try it.** [Install the toolchain](https://advancedmetalresearch.com/docs/get-started/installation), then run the [Quickstart](https://advancedmetalresearch.com/docs/get-started/quickstart) to bring up a simulated cell and jog it from your browser. It takes no hardware. - **Program a cell.** Read the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model) before you move hardware. Then read [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation) and the Guides section. - **Integrate RosieOS.** Start with [Your first motion](https://advancedmetalresearch.com/docs/get-started/first-motion), which drives the simulated core from a short Go program. Then read [Architecture](https://advancedmetalresearch.com/docs/concepts/architecture) and the [APIs](https://advancedmetalresearch.com/docs/apis/rt-control-http). - **Contribute.** The [Contributing](https://advancedmetalresearch.com/docs/contributing/repo-layout) section covers the repository layout, building and testing. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/cmd/rt-control/main.go:24-66` - `rt-core/engine/cycle_machine.hpp:3168,3514` - `rt-core/include/motion_readiness.hpp:97-130` - `rt-core/protocol/application-v1.schema.json (rules.external_enable)` - `weld_planner/v1/python/weldplan/native_admission.py:204-250` - `offline-programming/v1/main.go:35,80-96,257-300` - `motion-server/v1/src/robot_v4_cartesian_cli.hpp:232-414` - `motion-server/joint-trajectory/v1/src/joint_trajectory_daemon.cpp:99-128` - `steamdeck/real/v5/src/main.cpp:18-24` - `steamdeck/virtual/bridge/cli.go:20-45` - `robot_description/robots/rosie_1400_v3/config.json` - `robot_description/robots/rosie_1420_v1/config.json` - `dev-stack.sh:1-20` --- # Install the toolchain > The host, compilers and package managers RosieOS needs, which component needs which tool, and how to clone the repository. URL: https://advancedmetalresearch.com/docs/get-started/installation Section: RosieOS docs / Get started Last updated: 2026-10-10 RosieOS is a monorepo of C++, Go, TypeScript and Python components. There is no single installer: you install a small set of standard tools, then each component builds with its own `make`, `go`, `npm` or `pixi` command. For a simulated cell you need the tools marked **Simulation**. The rest depends on what you are working on. ## Host Use **Linux x86-64**. Continuous integration (CI) runs on Ubuntu 22.04, which is the reference. **WSL2** on Windows works too. - The dev stack reads `/proc` and uses `setsid`, so it runs on Linux only. - The OLP launcher builds its server for `linux/amd64`. - The Tesseract environment needs glibc 2.35 or later. - In WSL, keep the working sockets on a Linux filesystem. Unix sockets cannot bind under `/mnt/c`. The dev stack uses `$XDG_RUNTIME_DIR` or `$TMPDIR`, so point `TMPDIR` somewhere like `/dev/shm/rosie` if yours is on the Windows drive. A real cell host also needs a PREEMPT_RT kernel and the IgH EtherCAT master. That is covered in [Install rt-core on a cell host](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host), not here. ## Tools | Tool | Version | Needed for | Used by | |---|---|---|---| | C++17 compiler, `make`, `pkg-config`, pthread | GCC from Ubuntu 22.04 `build-essential` | **Simulation** | rt-core, both motion servers, pendants | | Go | 1.26.2 (`go.mod` declares `go 1.24` with `toolchain go1.26.2`) | **Simulation** | `rt-control`, `rtctl`, the SDK, OLP server, virtual pendant bridge | | Python 3 and `curl` | Any current | **Simulation** | `dev-stack.sh` and `local-rt-core.sh` helpers | | Node.js and npm | 22 (CI pins 22.13.1) | **Simulation** (OLP app) | OLP UI, virtual pendant UI, TypeScript checks | | [pixi](https://pixi.sh) | Current; each project pins its own lock file | **Simulation** (OLP app) | `tesseract/v1` (required by OLP), `weld_planner/v1`, `cadquery/v1` | | NVIDIA driver and CUDA 12 or later | — | Weld planning only | `weld_planner/v1` `motion` environment | | `nats-server` | 2.14 or later | Optional | dev-stack `nats` service | | Qt 5 (Widgets, Gui, Xml, Network, Qml) and Assimp, found through `pkg-config` | — | Pendant v5 only | `steamdeck/real/v5` | | IgH EtherCAT master | 1.6.9 by default | Real hardware only | `make live` in rt-core | On Ubuntu 22.04, this covers the native build dependencies CI installs: ```bash sudo apt-get update sudo apt-get install -y build-essential pkg-config python3 curl git git-lfs ``` Install Go 1.26.2, Node 22 and pixi from their upstream installers, and put them on your `PATH`. `dev-stack.sh` puts `~/.rosie/bin`, `~/.rosie/go-1.26.2/bin` and `~/.rosie/node-22.13.1/bin` in front of your `PATH`. You don't need those directories. If they don't exist, it uses whatever your `PATH` already finds. > [!NOTE] Some READMEs in the repository show commands such as `./rosie.sh …`, `.\rosie.ps1 …` or `rosie … v1 …`. Those belong to an internal tool that is not part of RosieOS, so they will not work from a clone. Use the `make`, `go`, `npm`, `pixi` and `dev-stack.sh` commands in these docs instead. The operator CLI in this repository is [`rtctl`](/docs/reference/rtctl). ## Clone Install Git LFS before you clone. Sample STEP parts and some mesh sources are stored in LFS. ```bash git lfs install git clone https://github.com/advanced-metal-research/RosieOS.git cd RosieOS ``` If you cloned without LFS, run `git lfs pull` in the checkout. ## Check the install Build the real-time core and its tools. This needs only the compiler and Go: ```bash make -C rt-core control sim ls rt-core/build ``` You should see `rt-control`, `rtctl`, `benchdrive`, `rt-natspublisher` and `rosie-rt-core-sim`. For the OLP app, install the Tesseract environment and the UI packages once: ```bash (cd tesseract/v1 && pixi install --locked) (cd offline-programming/v1/ui && npm ci) ``` To plan welds, install the weld planner's environments. The `default` environment (seam detection) runs on the CPU. The `motion` environment needs an NVIDIA GPU with CUDA 12 or later: ```bash cd weld_planner/v1 pixi install -e default pixi install -e motion ``` ## Which component needs what | Component | Directory | Build | Needs | |---|---|---|---| | Real-time core and control API | `rt-core` | `make control sim` (simulation), `make live` (hardware) | C++17, Go; IgH for `live` | | Cartesian motion server | `motion-server/v1` | `make all` | C++17, a built `rt-core` | | Dense trajectory daemon | `motion-server/joint-trajectory/v1` | `make all` | C++17 | | Offline programming server | `offline-programming/v1` | `start-offline-programming.sh` builds it | Go, Tesseract env | | Offline programming UI | `offline-programming/v1/ui` | `npm ci`, then `npm run dev` | Node 22 | | Weld planner | `weld_planner/v1` | `pixi install -e default` / `-e motion` | pixi; CUDA for `motion` | | Virtual pendant | `steamdeck/virtual` | `go build ./cmd/local`; `v1/ui`: `npm ci` | Go, Node 22 | | Pendant v5 | `steamdeck/real/v5` | `make all` | Qt 5, Assimp, Node | ## Next Start the simulated cell in the [Quickstart](https://advancedmetalresearch.com/docs/get-started/quickstart). ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/go.mod:1-13` - `rt-core/Makefile:31-50,295-301` - `.github/workflows/rt-core.yml:17-40` - `.github/workflows/rt-core-consumers.yml:17-113` - `dev-stack.sh:23-27,350-395` - `motion-server/v1/local-rt-core.sh:6-12` - `offline-programming/v1/start-offline-programming.sh:285-340` - `offline-programming/v1/ui/package.json:6-26` - `tesseract/v1/pixi.toml:1-25` - `weld_planner/v1/pixi.toml:1-6,60-77,202-206` - `weld_planner/.gitattributes:1-5` - `steamdeck/virtual/v1/ui/package.json:6-14` - `steamdeck/real/v5/Makefile:10-19` - `dev-stack.sh:384` --- # Quickstart: simulated cell > Build the real-time core, start a simulated Rosie cell with dev-stack.sh, and jog a joint from the offline programming app, with no hardware. URL: https://advancedmetalresearch.com/docs/get-started/quickstart Section: RosieOS docs / Get started Last updated: 2026-10-10 This page takes you from a fresh clone to a simulated robot moving. It takes two steps. First you start the simulated real-time core and its control API, and check them from the command line. Then you start the offline programming (OLP) app, and home, arm and jog the simulated robot from your browser. Everything here runs on your own machine. The simulator (`rosie-rt-core-sim`) runs the same state machine as the real core over a simulated bus, and no drive is involved. You need Linux or WSL2 with the toolchain from [Install the toolchain](https://advancedmetalresearch.com/docs/get-started/installation). The simulator also needs at least two CPUs that the process is allowed to use. ## 1. Clone and build the core ```bash git clone https://github.com/advanced-metal-research/RosieOS.git cd RosieOS make -C rt-core control sim ``` `make control sim` builds five programs into `rt-core/build/`: - `rt-control`, the public control API - `rtctl`, the operator CLI - `benchdrive` - `rt-natspublisher` - `rosie-rt-core-sim`, the simulated core ## 2. Start the simulated core Run everything from the repository root. Keep the UI on loopback, and turn off the launcher's firewall helper, which otherwise tries to add a `ufw` rule for remote access: ```bash export DEV_STACK_UI_HOST=127.0.0.1 DEV_STACK_FIREWALL=0 ./dev-stack.sh start rt-sim rt-control ./dev-stack.sh status rt-sim rt-control ``` `start` compiles a simulation machine config, starts the simulator and `rt-control`, and waits until both are healthy. Status then lists the sockets and reports both services as `healthy`. It looks like this, with your own paths and pids: ```text backend: rt_core rt-core sockets: native=/run/user/1000/rosie-dev-rt-1000/ipc.sock control=/run/user/1000/rosie-dev-rt-1000/control.sock jog=/run/user/1000/rosie-dev-rt-1000/jog.sock rt-sim pid 41210 healthy /run/user/1000/rosie-dev-rt-1000/ipc.sock rt-control pid 41288 healthy /run/user/1000/rosie-dev-rt-1000/control.sock (jog /run/user/1000/rosie-dev-rt-1000/jog.sock) olp target: rt_core control=/run/user/1000/rosie-dev-rt-1000/control.sock app: http://127.0.0.1:5189/offline-programming/v1/ui/ (ui mode: dev, live reload) remote: off (UI bound to 127.0.0.1; unset DEV_STACK_UI_HOST or set it to 0.0.0.0) ``` The directory comes from `$XDG_RUNTIME_DIR`, or from `$TMPDIR` if that is unset, so your paths will differ. The pids differ too. On WSL, the sockets must live on a Linux filesystem such as `/run/user/…` or `/dev/shm`, not under `/mnt/c`. ## 3. Ask the core what it is `rtctl` talks to `rt-control` over its Unix socket. Point it at the control socket from the status output: ```bash sock="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/rosie-dev-rt-$UID/control.sock" rt-core/build/rtctl describe --socket "$sock" | head -20 rt-core/build/rtctl status --socket "$sock" --json | head -c 400; echo ``` `describe` returns: - the backend, `simulation` - the configuration, machine and deployment digests - the cycle time, 1,000,000 ns - the lease and jog-age ceilings, `max_grant_lease_ns` and `max_jog_input_age_ns` - the nine simulated axes J1 to J9, with their limits in radians None of these calls needs control of the robot. The stack binds its clients to the pair `local-dev`, revision 1. ## 4. Start the offline programming app The OLP app needs three more things. Install each once: - the Tesseract collision environment, which the OLP launcher requires - the OLP UI's npm packages - the Cartesian motion server, which the launcher builds itself if it is missing ```bash (cd tesseract/v1 && pixi install --locked) (cd offline-programming/v1/ui && npm ci) ./dev-stack.sh start olp ui ``` The `olp` service runs `offline-programming/v1/start-offline-programming.sh`. The first start builds the OLP server and the motion server, so it takes a few minutes. Follow its log with `./dev-stack.sh logs olp`. When it is ready, the log shows the lines below, and status reports `olp` and `ui` as healthy. ```text olp-start: rails: seam worker UNAVAILABLE -- provision the weld planner environment: cd weld_planner/v1 && pixi install -e default. 'Define welds from edges' will fail until then. olp-start: motion: http://127.0.0.1:8796 ``` `rails: … UNAVAILABLE` is expected until you install the weld planner. You do not need the planner to jog. Open the app at **http://127.0.0.1:5189/offline-programming/v1/ui/**. Use port 5189 (the UI), not 8794 (the API). ## 5. Home, arm and jog > [!WARNING] This step moves only the simulated robot. The same controls drive a real cell when a real one is selected. Before you ever do that, read the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). The Machine panel walks you through five steps: Choose, Connect, Home, Arm, Drive. 1. **Choose a machine:** select **Local simulation**. This is the simulated core you started in step 2. 2. **Connect:** OLP describes the machine and checks its identity. This does not take control. 3. **Home:** press **Home**. The dev-stack simulation requires Home on every axis, just like a real cell. 4. **Arm:** press **ARM**, then **Confirm: energise motors**. OLP acquires the lease, enables the axes and arms them. 5. **Drive:** in the joint table, hold **+** or **−** under **Hold to jog** for J1. The position column and the 3D view follow the simulated joint. Let go and the joint stops. Press **STOP** or **DISARM** when you are done. STOP inhibits outputs at once and releases the lease. ## 6. Stop the stack ```bash ./dev-stack.sh stop ``` `stop` shuts down only what `start` launched, in reverse order. Logs and pid files stay in the stack directory, `${XDG_RUNTIME_DIR:-$TMPDIR}/rosie-stack-$UID`. ## If something is off | Symptom | Cause and fix | |---|---| | `local simulation requires two allowed CPUs` | The simulator pins its cycle thread. Give the VM or container two or more CPUs. | | `olp requires healthy rt-control` | Start `rt-sim rt-control` first, or start all of them in one command in that order. | | `Tesseract pixi env missing` | Install pixi, then run `cd tesseract/v1 && pixi install --locked`. | | `nats: … is missing; skipping` | Harmless. NATS is optional for this quickstart. | | `catalog: … is missing; skipping` | Harmless. The program catalog service is not part of this repository. | | Programs you saved are gone | The project store lives in the browser, per origin. `127.0.0.1` and `localhost` are separate stores. Always use the same address. | ## Next steps - [Your first motion](https://advancedmetalresearch.com/docs/get-started/first-motion) moves a joint from your own Go program. - [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation) covers every dev-stack service, including the weld planner and the virtual pendant. - [Architecture](https://advancedmetalresearch.com/docs/concepts/architecture) explains what you just started. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `dev-stack.sh:12-80,172-183,248-257,284-297,332-395,442-470,500-552,555-613` - `motion-server/v1/local-rt-core.sh:6-12,14-66,67-92,93-130` - `rt-core/Makefile:31-50,295-301` - `rt-core/tools/rtctl/control.go:22-26,48-110` - `offline-programming/v1/start-offline-programming.sh:16-18,70-110,285-310,490-515,620-668` - `offline-programming/v1/ui/vite.config.ts:3-14` - `offline-programming/v1/ui/package.json:6-11` - `offline-programming/v1/ui/src/execution/densePanel.ts:495-534` - `offline-programming/v1/ui/src/execution/jointJog.ts:526-600` --- # Your first motion (simulation) > Start the simulated core directly, then acquire control, enable, arm, jog one joint, stop and release from a short Go program using the rt-core SDK. URL: https://advancedmetalresearch.com/docs/get-started/first-motion Section: RosieOS docs / Get started Last updated: 2026-10-10 In this tutorial you drive the simulated core from your own code. You start `rosie-rt-core-sim` and `rt-control` by hand, then run a Go program that goes through the full control sequence: 1. Acquire control. 2. Enable and arm the axes. 3. Jog J1 at 0.01 rad/s for one second. 4. Stop and release. > [!WARNING] **Energised motion.** This example refuses to run against anything but the simulator it starts. The same calls move a real robot when they are pointed at a real cell's socket. 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). You need the [toolchain](https://advancedmetalresearch.com/docs/get-started/installation) and a clone of the repository. Run every command from `rt-core/`. ## 1. Build and set up a scratch directory ```bash cd rt-core export GOFLAGS=-mod=mod export TMPDIR=/dev/shm/rt-core/first-motion mkdir -p "$TMPDIR" build/first-motion-runtime make control sim ``` `TMPDIR` holds the sockets, so it must be on a Linux filesystem. `/dev/shm` works on Linux and in WSL. ## 2. Start the simulated core Compile the nine-axis simulation machine. `rtctl compile` prints the directory it wrote, and the directory name is the configuration digest: ```bash compiled=$(build/rtctl compile --config config/machines/simulation/simulation-program.json --out build/first-motion-config) export SHA=${compiled##*/} echo "$SHA" ``` The compiler also prints warnings about missing collision watchdogs and legacy flat axes. They are expected for this simulation fixture, which is not a hardware recipe. Start the core. `rtctl run` recompiles, then replaces itself with `rosie-rt-core-sim`: ```bash (cd build/first-motion-runtime && exec ../rtctl run --backend simulation \ --config ../../config/machines/simulation/simulation-program.json \ --out ../first-motion-config --socket "$TMPDIR/ipc.sock") >build/first-motion-core.log 2>&1 & core_pid=$! until test -S "$TMPDIR/ipc.sock"; do sleep 0.05; done ``` Start the public control API on the core's socket, with a pair binding of your choosing: ```bash build/rt-control --core-socket "$TMPDIR/ipc.sock" --socket "$TMPDIR/control.sock" \ --configuration-sha256 "$SHA" --backend simulation \ --pair-id first-motion --pair-revision 1 >build/first-motion-control.log 2>&1 & control_pid=$! until build/rtctl describe --socket "$TMPDIR/control.sock" --json >/dev/null 2>&1; do sleep 0.05; done ``` `rt-control` also creates the jog socket, `$TMPDIR/jog.sock`, next to `control.sock`. ## 3. Write the program Save this as `build/first-motion.go`. It is inside the `rosieos/rt-core` module, so the SDK imports resolve without extra setup. rt-core/build/first-motion.go: ```go package main import ( "context" "errors" "fmt" "os" "time" "rosieos/rt-core/ipcclient" "rosieos/rt-core/sdk/control" ) // Simulation-only inputs: all nine axes, J1 at 0.01 rad/s for one second, // a 10 ms update cadence and a 100 ms deadline on each jog update. const ( mask = 0x1ff // J1..J9 velocity = 0.01 // rad/s duration = time.Second cadence = 10 * time.Millisecond inputAge = 100 * time.Millisecond ) func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "first-motion:", err) os.Exit(1) } } func run() error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() c, err := control.Dial(control.UnixPath(os.Getenv("TMPDIR") + "/control.sock")) if err != nil { return err } defer c.Close() // Refuse anything but the simulator started above. d, err := c.Describe(ctx) if err != nil { return err } if d.Backend != "simulation" || d.ConfigurationSHA256 != os.Getenv("SHA") { return errors.New("not the simulator this example started") } // 1. Acquire: one controller at a time, bound to this pair and configuration. g, err := c.Acquire(ctx, "first-motion", control.Binding{ PairID: "first-motion", Revision: 1, ConfigurationSHA256: d.ConfigurationSHA256, }) if err != nil { return err } released := false defer func() { // on any failure, inhibit and give up control if !released { c.Stop(context.Background()) c.Release(context.Background()) } }() fmt.Printf("acquire generation=%d\n", g.Generation) // Keep the lease alive: interval 0 renews every third of the lease. renewals, err := c.StartRenewal(ctx, 0) if err != nil { return err } // 2. Enable and arm, then wait for every axis to report Operation Enabled. e, err := c.Enable(ctx, mask) if err != nil { return err } fmt.Printf("enable sequence=%d\n", e.Sequence) a, err := c.Arm(ctx) if err != nil { return err } fmt.Printf("arm sequence=%d\n", a.Sequence) tick := time.NewTicker(cadence) defer tick.Stop() for { s, err := c.Status(ctx) if err != nil { return err } if ipcclient.AllOperationEnabled(s.Core) { break } select { case <-tick.C: case <-ctx.Done(): return ctx.Err() } } s, err := c.Status(ctx) if err != nil { return err } before := s.Core.Axes[0].PositionCounts // 3. Jog J1. Each datagram carries its own deadline; if updates stop, // the core ramps the axis to a hold. jog, err := c.PrepareJogSession(ctx, mask) if err != nil { return err } velocities := make([]float64, len(d.Axes)) velocities[0] = velocity for end := time.Now().Add(duration); time.Now().Before(end); { origin := ipcclient.HostMonotonicNS() if err := jog.UpdateAt(velocities, origin, origin+uint64(inputAge)); err != nil { return err } select { case renewal, ok := <-renewals: if !ok { return errors.New("lease renewal ended") } if renewal.Err != nil { return renewal.Err } case <-tick.C: case <-ctx.Done(): return ctx.Err() } } if err := jog.End(ctx); err != nil { return err } s, err = c.Status(ctx) if err != nil { return err } fmt.Printf("jog requested_ns=%d delta_counts=%d\n", duration, s.Core.Axes[0].PositionCounts-before) // 4. Stop inhibits outputs and bumps the fence; Release gives up the session. stopped, err := c.Stop(ctx) if err != nil { return err } fmt.Printf("stop generation=%d\n", stopped.Generation) r, err := c.Release(ctx) if err != nil { return err } released = true fmt.Printf("release session_empty=%t\n", r.Session == "") return nil } ``` ## 4. Run it ```bash go run build/first-motion.go ``` The output looks like this. The count delta depends on timing: ```text acquire generation=1 enable sequence=1 arm sequence=2 jog requested_ns=1000000000 delta_counts=207 stop generation=2 release session_empty=true ``` What happened, step by step: | Step | Call | Effect | |---|---|---| | Acquire | `Acquire(ctx, controller, Binding)` | Returns a fence (session and generation 1) under a lease of at most 500 ms. | | Renew | `StartRenewal(ctx, 0)` | Renews at a third of the lease. If renewal fails, the channel reports it and the lease lapses. | | Enable, Arm | `Enable(ctx, mask)`, `Arm(ctx)` | Requests CiA402 enable on J1 to J9, then arms. Motion is permitted only while armed. | | Jog | `PrepareJogSession`, `UpdateAt`, `End` | Opens a jog generation, sends 224-byte datagrams on `jog.sock`, then ends the jog. | | Stop | `Stop(ctx)` | Inhibits outputs, retires handles and raises the generation to 2. | | Release | `Release(ctx)` | Gives up the session. | This simulation machine has no Home requirement. A real cell, and the dev-stack simulation, require Home on every axis before the motion start gate opens. Call `c.Home(ctx, mask)` after enabling and before arming, and wait for Home to become valid in status. ## 5. Look at what the core recorded Observations don't need control: ```bash build/rtctl status --socket "$TMPDIR/control.sock" --json | head -c 600; echo build/rtctl events --socket "$TMPDIR/control.sock" --after 0 ``` The event log shows the grant and handle transitions of your run. Stop the processes when you are done: ```bash kill -TERM "$control_pid" "$core_pid" wait "$control_pid" "$core_pid" ``` ## Why not rtctl? `rtctl` has `acquire`, `enable`, `arm`, `stop` and `release` commands, but each one is a single request, and it never renews the lease. On the `lan` profile the lease lasts at most 500 ms, so it lapses between one shell command and the next, and the core inhibits outputs. `rtctl` has no `jog` command either. Use `rtctl` to observe and to recover, and use an SDK client (Go, C++) for anything that moves. ## Next steps - Do the same from a browser: [Quickstart](https://advancedmetalresearch.com/docs/get-started/quickstart), step 5. - Learn the lease, fence and jog-lane rules: [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority). - The full SDK: [Go SDK](https://advancedmetalresearch.com/docs/apis/go-sdk). Every operation: [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http). ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/tools/rtctl/command.go:16-60` - `rt-core/tools/rtctl/run.go:13-40` - `rt-core/tools/rtctl/control.go:22-26,48-110` - `rt-core/cmd/rt-control/main.go:24-66` - `rt-core/config/machines/simulation/simulation-program.json` - `rt-core/sdk/control/client.go:78-90` - `rt-core/sdk/control/operations.go:14,82,94-113,126` - `rt-core/sdk/control/renewal.go:25-50` - `rt-core/sdk/control/jog.go:23,87-130,138,175` - `rt-core/ipcclient/client_linux.go:1166` - `rt-core/ipcclient/application_grant_linux.go:69` - `rt-core/adapters/rosie/control/api_generated.go:381-390,462,512` - `rt-core/adapters/rosie/control/http.go:190-243` - `rt-core/protocol/control.json (records.local_jog_update)` - `rt-core/include/motion_readiness.hpp:97-130; recorded output: rt-core/README.md:269-276 (README cited for the sample run only)` --- # Safety model > What RosieOS software does and does not do to keep a cell safe, how the hardware E-stop, software stops, the motion start gate and program verification fit together, and the claims you may make about them. URL: https://advancedmetalresearch.com/docs/get-started/safety-model Section: RosieOS docs / Get started Last updated: 2026-10-10 This page is the reference for how RosieOS protects people and hardware. Every other page that moves the robot links here. Read it before you arm a real cell. The short version: **RosieOS contains no safety-rated function.** The cell's hardware E-stop and safety chain are the only emergency stop. The software adds stops, interlocks and checks that make mistakes less likely, but none of them is a substitute for the hardware chain. > [!DANGER] There is no software E-stop. The STOP buttons, Stop and Halt operations, lease expiry and the pendant's hold-to-enable trigger are software functions. They can fail with the software that runs them. Keep the hardware E-stop within reach whenever the drives are powered. ## At a glance | Layer | What it does | Enforced by | What it is not | |---|---|---|---| | [Hardware E-stop](https://advancedmetalresearch.com/docs/get-started/safety-model#hardware-e-stop) | Removes drive power through the cell's safety chain | Cell wiring, independent of software | Part of RosieOS | | [Software stops](https://advancedmetalresearch.com/docs/get-started/safety-model#software-stops) | Stop, Halt, Release, lease expiry and jog input expiry | rt-control and the 1 kHz core | A safety-rated stop | | [Motion start gate](https://advancedmetalresearch.com/docs/get-started/safety-model#motion-start-gate) | Refuses to start motion unless authority, readiness and limits all hold | The 1 kHz core, every start and every cycle | Collision avoidance | | [Program verification](https://advancedmetalresearch.com/docs/get-started/safety-model#what-is-verified) | Admits a planned weld program only if its collision, limit and tracking certificate passes | The weld planner and OLP Load | A check on jog, moves or Home | | [One controller](https://advancedmetalresearch.com/docs/get-started/safety-model#one-controller) | Only one client can command the robot at a time | rt-control lease and fence | Authentication of people | | [Process outputs](https://advancedmetalresearch.com/docs/get-started/safety-model#process-outputs) | Torch outputs are refused everywhere | rt-core, the dense daemon and OLP | Weld process control | ## The hardware E-stop is the only emergency stop No RosieOS component implements a safety-rated emergency stop, safety gate or enabling device. Wire the cell so that the hardware E-stop and safety chain remove drive power without any help from software. - **rt-core only observes.** You can wire an auxiliary contact of the stop chain to a declared drive digital input so that rt-core reports its state. The public API contract defines these observations as "diagnostics and never safety certification or a control decision". rt-core never uses them to decide whether to move. - **The OLP STOP button is software.** Its tooltip says so: it stops playback, drops the torch output and releases the lease, and "is not the hardware emergency stop". - **The pendant trigger is a software deadman.** On the Steam Deck pendant, jogging needs the right trigger held, and releasing it stops the jog. This is a convenience interlock in application code, not a safety-rated enabling device. ## Software stops These are the software ways motion ends. Each one is useful. None proves that the robot has stopped. | Mechanism | Effect | Default timing | |---|---|---| | **Stop** (`stop`) | Fences the session (the fence generation goes up by one), retires every trajectory, program and jog handle, and inhibits native outputs at once. Cell I/O outputs go OFF in the same cycle. It never waits for a deceleration ramp. | Immediate | | **Halt** (`halt`) | Decelerates to an enabled hold, using the active trajectory's acceleration or the jog acceleration. Keeps the lease and Arm. Refused with `inhibited` unless the machine is armed and enabled. | Ramp length depends on speed | | **Release** (`release`) | Runs Stop, then gives up the session. | Immediate | | **Lease expiry** | If the controlling client stops renewing, outputs are inhibited, jog is cancelled and handles are retired. The core checks lease validity every cycle and clears the drive controlword when it lapses, so this still works if rt-control itself stops responding. | 500 ms on the `lan` link profile | | **Jog input expiry** | Every jog update carries a deadline. When updates stop arriving, the axis ramps to a hold. A deadline is never extended. | Input age at most 250 ms on `lan`; ramp set by the drive config (`arrest_ns` 200 ms, `quick_stop_ns` 300 ms by default) | | **OLP heartbeat loss** | If the browser stops sending heartbeats while OLP holds the lease, OLP stops the machine with `ui_heartbeat_lost`. An accepted Cartesian step finishes its bounded move first. | 5 s | A client may send Stop with a session it knows even after that session's lease has expired: an expired grant cannot move the robot, but it can still inhibit. The lease and jog-age ceilings are per cell, in the machine config's `control` block. The `internet` link profile uses 3 s and 750 ms, and those values are marked unverified in the contract. Longer values delay the unattended stop after a link loss, which the contract states explicitly. > [!WARNING] A Stop or Halt receipt means rt-control accepted the request. It does not prove the axes are standing still. Read status and events, and watch the robot. ## The motion start gate The 1 kHz core decides whether any motion may start: a trajectory, a dense program or a jog. The request is refused unless **all** of these hold: 1. No other motion source is active. A running trajectory, jog or commissioning step refuses a new start with `mode_conflict`. 2. The request carries the current generation, and the lease is valid. 3. The machine is armed, the EtherCAT bus is ready and no safety fault is set. 4. The configuration epoch is verified and every requested axis is configured and verified. 5. Every requested axis that requires Home has a valid Home. 6. Every requested axis has valid limits, is in cyclic synchronous position (CSP) mode, is enabled, and reports Operation Enabled with fresh feedback. 7. No axis is outside its limits and moving further out (`outside_limits_outward`). 8. The first point continues from the held position. A stationary start that is off by no more than the completion tolerance gets a short, bounded alignment ramp. Any other discontinuity is refused. After a start, the core applies a final output permit every cycle. It clears the drive controlword whenever the lease is invalid or a safety fault is set, and it only reports motion as permitted while the permit and Arm both hold and no safety fault is set. Cell I/O outputs are ANDed with the same permit. Per axis, status reports the first gate that fails, in this order: drive alarm, `mode_mismatch`, `home_required`, `coordinate_invalid`, `not_enabled`, `not_operation_enabled`, `brake_wait`, then `ready`. The core also runs a collision watchdog, configured per drive, that faults an axis when torque or following-error bounds are breached. It detects an impact after it happens. It does not avoid collisions, and a machine config can compile without it. The compiler then prints a `missing-collision-watchdog` warning. For the lease, fence and readiness details see [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority) and [The real-time core](https://advancedmetalresearch.com/docs/concepts/real-time-core). ## What is verified before motion ### Planned weld programs are verified A weld program planned in offline programming (OLP) goes through the weld planner's verifier before it can reach the robot. The planner writes a dense trajectory (`.rdt`) only when every seam and every connecting move has a verifier PASS: - collisions, penetrations and limit violations are all zero - no check is left unverifiable - the seam tracking certificate is within tolerance Otherwise no `.rdt` exists, and there is nothing to load. OLP's Load fetches the program by digest from the planner's store only. Before it sends a byte to the robot it also checks: - that the program's identity matches the header - that the robot description and cell calibration the plan was made against match this machine - that every axis has a valid Home rt-core then admits the program against its own position, velocity, step and digest checks. The verifier is a conservative geometric certificate against the cell meshes and the arm's collision spheres. It fails closed: it refuses to judge a trajectory against a cell it cannot see. It is not a physics simulation. You can then replay the exact bytes before Load, in the 3D viewer or on the local rt-core simulator (**Simulate** in OLP). That replay is an operator step. The software does not require it. ### Jog, moves and Home are not verified These motions do **not** pass through the verifier or a simulation first: - joint jog and Cartesian jog, from OLP, the pendants or the Cartesian motion server - joint moves (including "all joints to 0") and Cartesian moves - Home and go-home - anything sent directly through `rtctl`, the Go SDK, the C++ client or the dense trajectory daemon They rely on the [motion start gate](https://advancedmetalresearch.com/docs/get-started/safety-model#motion-start-gate) and on rt-core's position, velocity and continuity admission. rt-core checks joint limits, not collisions with the cell or the part. OLP's own joint and Cartesian moves also plan at 75% of the configured velocity. ### What you may claim Use this wording, which matches the code: > Every planned weld program is verified against the cell model, collision and joint limits, and can be replayed in simulation, before it can be loaded onto the robot. Do not say "every motion runs in simulation before the real joints move". That is only true of planned weld programs. ## One controller at a time rt-control admits one controller at a time. `acquire` returns a fence (a session token and a generation) under an expiring lease, and every command must carry that exact fence. A second client gets `control_already_owned`. Stop and a new acquire bump the generation, so a delayed command from an old session is refused. Jog has its own generation inside the session. `acquire` must also present the deployment binding (pair id, pair revision and configuration digest), so a client configured for one cell cannot take control of another. OLP, the pendants and the motion servers all go through this lease, so they lock each other out. The lease identifies a client process. It does not authenticate a person. See [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority). ## Process outputs are refused RosieOS does not switch a welding torch. Torch-class outputs are refused end to end: - rt-core refuses a torch output intent with `io_torch_unqualified`, and `torch_qualified` must be `false` in every machine config - the dense trajectory daemon refuses a program that contains torch samples with `native_torch_unsupported` - OLP refuses to load a program that still requires process outputs. Choose the **Dry run · process outputs off** run mode, which removes them before Load. There is no seam tracking or sensing. See [Process I/O and sensing](https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing). ## Simulation does not qualify hardware A clean run in simulation says nothing about powered motion. The code and its tests make this point themselves: software success does not qualify powered motion. In particular: - The Steam Deck v5 pendant is not yet qualified on physical input or real motion. - `reset_fault` is an interim capability. - Hardware bring-up and qualification of a cell are done by the cell owner, by hand. ## Before you move hardware 1. Confirm the hardware E-stop removes drive power, and test it before each session. 2. Clear the cell. Stay outside the robot's reach whenever the drives are armed. 3. Check that the client is bound to the cell you expect. Describe shows the backend (`simulation` on the simulator) and the configuration digest. 4. Home, then arm, at low speed. 5. For programs, plan, verify and replay in simulation first, and use dry run until process outputs are qualified. 6. After any Stop, read status before you assume the robot has stopped. ## Standard warning for motion pages Pages that move hardware carry this warning: > [!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). ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/protocol/application-v1.schema.json (rules.external_enable` - `rules.control_timing` - `rules.cell_io, capabilities halt/io_arm, reasons inhibited/io_torch_unqualified/outside_limits_outward/not_ready)` - `rt-core/adapters/rosie/control/http.go:190-243` - `rt-core/adapters/rosie/control/controller.go:546-600,1211-1270` - `rt-core/adapters/rosie/control/halt_linux.go:23-78` - `rt-core/include/motion_readiness.hpp:14-31,97-130` - `rt-core/include/start_alignment.hpp:24-55` - `rt-core/engine/cycle_machine.hpp:811-816,3168,3514-3543` - `rt-core/include/cell_io.hpp:139-200` - `rt-core/config/templates/drive.json:7-10` - `rt-core/config/templates/machine.json:228-233` - `rt-core/sdk/control/renewal.go:25-50` - `weld_planner/v1/python/weldplan/native_admission.py:137-250` - `weld_planner/v1/python/weldplan/dense_joint_trajectory.py:700-705` - `offline-programming/v1/internal/denseexec/rt_core.go:735-820,1020-1028` - `offline-programming/v1/internal/denseexec/session.go:22,236-256` - `offline-programming/v1/internal/denseexec/joint_move.go:17-19` - `offline-programming/v1/ui/src/execution/densePanel.ts:79-81,541-544` - `motion-server/joint-trajectory/v1/src/rt_control_executor.hpp:148-156` - `steamdeck/real/v5/src/window.cpp:400,835` --- # For AI agents > A quick orientation for coding agents and language models building against RosieOS, covering where the machine-readable docs and specs are, the interfaces to use and the safety rules to respect. URL: https://advancedmetalresearch.com/docs/get-started/for-ai-agents Section: RosieOS docs / Get started Last updated: 2026-10-10 This page is for coding agents, assistants and retrieval pipelines that read these docs or write code against RosieOS. Everything here is a summary of other pages. When a detail matters, follow the link and read that page. ## Robot data and downloads The URDF, MuJoCo model, SRDF, STEP, meshes and `robot.json` of the Rosie 600, 1000 and 1400 are static files with stable URLs. [`/agents.md`](/agents.md) is the step-by-step guide to fetching them, with copy-paste curl, Python and JavaScript and a link to every file. [`/agents.json`](/agents.json) is the same list as JSON, and [`/assets/sim/index.json`](/assets/sim/index.json) lists every kit file. - Plain HTTPS GET or HEAD: no login, no API key, no cookies. - Any User-Agent works, including the defaults of Python `urllib`, `requests`, Node `fetch`, Go, curl and wget. - CORS is open (`Access-Control-Allow-Origin: *`) on the data files, so a web page can load the JSON, URDF, STL and GLB files directly. - Byte ranges and ETag revalidation work. - The URDF and MJCF use relative mesh paths, so they load from their URL or from the unzipped kit. If your environment cannot reach advancedmetalresearch.com, use the mirror on GitHub, [advanced-metal-research/rosie-sim-kits](https://github.com/advanced-metal-research/rosie-sim-kits). The same files are at `https://raw.githubusercontent.com/advanced-metal-research/rosie-sim-kits/main/sim/...`, and its `sim/index.json` lists them with mirror URLs. Failing that, ask your user to download the kit ZIP and attach it. Every file is inside it. ## Read the docs as data | What | Where | Contents | |---|---|---| | Site index | [`/llms.txt`](/llms.txt) | Every page with a one-line description, in the llmstxt.org format | | RosieOS docs index | [`/docs/llms.txt`](/docs/llms.txt) | The docs section on its own | | Full text | [`/docs/llms-full.txt`](/docs/llms-full.txt), [`/llms-full.txt`](/llms-full.txt) | Every docs page (or the whole site) as one Markdown file | | One page as Markdown | Add `.md` to the page URL: `/docs/concepts/architecture.md`, and `/docs/index.md` for the docs home. A request with `Accept: text/markdown` gets the same file. | The page source, with links made absolute and its source files listed. Each page also has *View as Markdown* and *Copy page* controls. | | Agent guide | [`/agents.md`](/agents.md), [`/agents.json`](/agents.json) | How to fetch the robot data, and every data file, API spec and guide with its URL | | Search index | [`/assets/docs/search.json`](/assets/docs/search.json) | Title, section, URL, Markdown URL, description, headings and text of every page | | rt-control API | [OpenAPI 3.1](https://advancedmetalresearch.com/docs/openapi/rt-control.json), [original contract](https://advancedmetalresearch.com/docs/openapi/rt-control.contract.json) | Generated from `rt-core/protocol/application-v1.schema.json` | | API catalog | [`/.well-known/api-catalog`](/.well-known/api-catalog) | The rt-control, offline programming and weld planner HTTP APIs and their OpenAPI files, as an RFC 9727 linkset | | Other HTTP APIs | [Offline programming](https://advancedmetalresearch.com/docs/openapi/offline-programming.json), [weld planner](https://advancedmetalresearch.com/docs/openapi/weld-planner.json) | OpenAPI 3.1, generated from their reference pages | | Error codes | [`/docs/data/error-codes.json`](/docs/data/error-codes.json) | All 153 rt-control reasons, with meaning, group, HTTP status and callers | | NATS | [`/docs/data/nats-subjects.json`](/docs/data/nats-subjects.json) | Subjects, streams and the command tables | | Robot models | [`/assets/sim/index.json`](/assets/sim/index.json) | URDF, MuJoCo, STEP and `robot.json` of the Rosie 600, 1000 and 1400, with every file's URL. See [Simulate a Rosie robot](https://advancedmetalresearch.com/docs/guides/simulate-a-rosie-robot). | ## The system in brief - **One public control API.** Every client drives the robot through [`rt-control`](/docs/apis/rt-control-http). That includes the pendant, offline programming (OLP), the motion servers and your code. It serves HTTP/JSON on the Unix socket `/run/rosie-rt-core/control.sock`, and optionally over mutual TLS on `127.0.0.1:8443` for [remote clients](https://advancedmetalresearch.com/docs/apis/remote-access-mtls). Behind it, the 1 kHz [real-time core](https://advancedmetalresearch.com/docs/concepts/real-time-core) owns the EtherCAT drives. See [Architecture](https://advancedmetalresearch.com/docs/concepts/architecture). - **One controller at a time.** `acquire` returns a fence: a session token and a generation, under an expiring lease. Every later command carries that exact fence. Renew before the lease runs out. A second client gets `control_already_owned`. See [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority). - **Three kinds of motion request.** Jog (`begin_jog`, datagrams on `jog.sock`, `end_jog`), trajectories (`prepare_trajectory`, `start_trajectory`) and programs (`prepare_program` with a [`.rdt` file](/docs/reference/rdt-format), then `start_program`). The [motion paths](https://advancedmetalresearch.com/docs/concepts/motion-and-planning) page says which process owns each path. - **Clients.** The [Go SDK](https://advancedmetalresearch.com/docs/apis/go-sdk), the header-only [C++ client](https://advancedmetalresearch.com/docs/apis/cpp-client), [TypeScript contracts](https://advancedmetalresearch.com/docs/apis/typescript-types) (types only, no HTTP client) and the [`rtctl`](/docs/reference/rtctl) CLI. - **Other services.** The [OLP server](https://advancedmetalresearch.com/docs/apis/olp-http) on `127.0.0.1:8794` and the [weld planner](https://advancedmetalresearch.com/docs/apis/weld-planner-http) on port 8796. The planner listens on all interfaces by default. Neither has authentication. Every port and socket is listed in [Ports and environment](https://advancedmetalresearch.com/docs/reference/ports-and-environment). - **Formats.** [`robot.v4.program.v2`](/docs/reference/program-format) programs, the [`.weldplan`](/docs/reference/weld-program-format) plan request, [`.rdt`](/docs/reference/rdt-format) dense trajectories, and [robot description](https://advancedmetalresearch.com/docs/reference/robot-description) and [configuration](https://advancedmetalresearch.com/docs/reference/configuration) files. ## Safety rules you must respect Read the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model) before you write code that moves hardware. In short: - **There is no software E-stop.** RosieOS contains no safety-rated function. The cell's hardware E-stop and safety chain are the only emergency stop. Stop, Halt, lease expiry and the pendant's hold-to-enable trigger are software functions, and they can fail with the software that runs them. - **Only planned weld programs are verified.** A weld program is admitted at OLP Load only if the weld planner's collision, limit and tracking certificate passes. Jog, moves and Home are checked against joint limits only, not against collisions. - **Torch outputs are refused everywhere.** RosieOS does not switch a welding torch. No seam tracking or sensing is implemented; see [Process I/O and sensing](https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing). - **Simulation does not qualify hardware.** - **Claim only what the safety model allows.** It gives the exact sentence you may use about program verification. ## Rules of thumb for code - The server decodes strictly. Unknown fields, duplicate fields and trailing data are refused. - Put a `request_id` on JSON commands so that you can retry them safely, and use a fresh one for every logical attempt. `/v1/program` uploads have no deduplication: never replay an uncertain upload. See [idempotent retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). - Refusals are HTTP 409 with a reason in `error` (413 for an oversized body). Match on the leading label. Treat an unknown label, a malformed reply or a transport failure as an unknown outcome. Stop producing motion, send an authenticated `stop` if you can, and reconcile Status before you acquire again. See [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes). - A 200 reply for motion acknowledges admission, not physical completion. Follow [events and telemetry](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry) to see what happened. - Keep uint64 values exact. In TypeScript they exceed the safe integer range. ## Where to find things | Task | Page | |---|---| | Run a whole cell on one machine | [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation) | | Load a Rosie robot into PyBullet, MuJoCo, ROS 2 or CAD | [Simulate a Rosie robot](https://advancedmetalresearch.com/docs/guides/simulate-a-rosie-robot) | | First program against the core | [Your first motion](https://advancedmetalresearch.com/docs/get-started/first-motion) | | Every rt-control operation, type and reason | [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http) | | Plan and run a weld from CAD | [Program a weld from CAD](https://advancedmetalresearch.com/docs/guides/offline-programming), [Connect to a cell](https://advancedmetalresearch.com/docs/guides/connect-a-cell) | | Mount a tool or the robot | [Mechanical interfaces](https://advancedmetalresearch.com/docs/reference/mechanical-interfaces) | | Repository layout and generated files | [Repository layout](https://advancedmetalresearch.com/docs/contributing/repo-layout) | ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `src/docs/get-started/safety-model.md` - `src/docs/concepts/architecture.md` - `src/docs/concepts/control-authority.md` - `src/docs/concepts/motion-and-planning.md` - `src/docs/concepts/process-io-and-sensing.md` - `src/docs/apis/rt-control-http.md` - `src/docs/apis/typescript-types.md` - `src/docs/reference/error-codes.md` - `src/docs/reference/ports-and-environment.md` --- # Architecture > The RosieOS processes, the hosts they run on, the sockets and ports between them, and how jog, moves and weld programs travel from a client to the drives. URL: https://advancedmetalresearch.com/docs/concepts/architecture Section: RosieOS docs / Concepts Last updated: 2026-10-10 A RosieOS cell is a small set of processes around one controller. The real-time core, `rosie-rt-core`, owns the drives. The Go adapter `rt-control` is the only public way to command it. Every client, whether the offline programming app, a pendant, a motion server or your own code, goes through `rt-control` and needs its lease to move anything. ![RosieOS processes by host: the operator PC runs the OLP UI, OLP server and weld planner; the cell host runs the motion servers, rt-control, rosie-rt-core and the NATS publisher; the Steam Deck runs the pendant and a headless OLP server; the drives and a hardware E-stop sit below.](https://advancedmetalresearch.com/assets/docs/architecture-hosts.svg) *Figure: Processes by host. Solid arrows carry commands, dashed ones observation only. The hardware E-stop removes drive power without any software.* ## Processes | Process | Runs on | Role | Listens on | |---|---|---|---| | `rosie-rt-core` | Cell host | 1 kHz cyclic controller: EtherCAT master, CiA402 drive state, the motion start gate and the final output permit | Private native socket, `/run/rosie-rt-core/native/ipc.sock` when installed | | `rosie-rt-core-sim` | Any Linux host | The same state machine over a simulated bus | Private native socket | | `rt-control` | Cell host | The public control API: lease and fence, jog lane, trajectories and programs, events, telemetry | `control.sock` and `jog.sock` (Unix), plus an optional mutual-TLS listener | | `rt-natspublisher` | Cell host | Republishes status and telemetry to NATS. It has no command authority. | None (reads `control.sock`) | | `robot-v4-cartesiand` | Cell host, and inside OLP | Cartesian motion server: Cartesian jog and position moves on the rt-core jog lane. OLP also runs it in `--resolve-only` mode to turn Cartesian twists into joint velocities. | UDP intent listener (`--udp-listen`) | | `joint_trajectory_daemon` | Cell host | Stores dense `.rdt` programs and plays them through `rt-control` | TCP `127.0.0.1:8797` | | OLP server | Operator PC, or headless on the Deck | Offline programming backend: programs, planning broker, local simulator, machine session | HTTP `127.0.0.1:8794` | | OLP UI | Browser | The programming and operating app | Vite on `:5189` | | Weld planner | Operator PC with an NVIDIA GPU | Turns a `.weldplan` into verified joint trajectories and dense `.rdt` files | HTTP `:8796` | | Pendant v5 | Steam Deck | Native Qt teach pendant. It runs OLP's TypeScript program logic and talks only to a headless OLP server on the Deck. | None | | Pendant v4 | Steam Deck | Earlier native pendant, kept as a rollback. It talks to `rt-control` directly. | None | | Virtual pendant | Operator PC | Browser skin of the v4 pendant plus a loopback bridge, for simulation only | UI `:51711`, bridge `127.0.0.1:51712` | Ports, socket paths and environment variables are all listed in [Ports, sockets and environment](https://advancedmetalresearch.com/docs/reference/ports-and-environment). ## Public and private interfaces - **Public: `rt-control`.** This is HTTP/JSON over the Unix socket `control.sock`, with jog datagrams on `jog.sock` in the same directory. With `--remote-listen` it also serves the same API over mutual TLS on TCP, plus a WebSocket jog lane. Deployed cells pin that listener to `127.0.0.1:8443`. The contract is `rt-core/protocol/application-v1.schema.json`, and the Go, C++ and TypeScript clients are generated from it. See [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http). - **Private: native IPC.** `rt-control` talks to the core over a Unix socket plus shared-memory rings and a fast-control region. The layout is specified in `rt-core/protocol/control.json` and can change between releases. Never program against it. - **Service APIs.** The OLP server, the weld planner and the dense daemon each have their own interface. They are clients of `rt-control`, not alternatives to it. When installed with the host units, `rosie-rt-core` runs as user `rosie-rt` and `rt-control` as `rosie-ctl`. The public sockets live in `/run/rosie-rt-core/public/`, with symlinks at `/run/rosie-rt-core/control.sock` and `/run/rosie-rt-core/jog.sock`. ## How motion reaches the drives There are three ways to move the robot. Each ends at the same motion start gate in the core. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model#motion-start-gate). ### Jog A jog is a stream of velocity updates, each with its own deadline. The client opens a jog generation with `begin_jog`, then sends 224-byte datagrams on `jog.sock`, or WebSocket frames on a remote cell. When the updates stop, the core ramps the axis to a hold. | Client | Path | |---|---| | OLP joint jog | OLP server → Go SDK jog session → `jog.sock` | | OLP Cartesian jog | OLP server → `robot-v4-cartesiand --resolve-only` (twist to joint velocities) → the same jog session | | Cartesian motion server | UDP intent packet → `robot-v4-cartesiand` → C++ client jog producer → `jog.sock` | | Pendant v4 | mutual TLS → WebSocket jog on `/v1/jog` | | Virtual pendant (simulation) | Browser → bridge on `:51712` → Go SDK jog session → `jog.sock` | ### Point-list moves A client can upload a short list of timed points with `prepare_trajectory`. Positions are in rad or m and times in ns from the plan start. The client then starts it with `start_trajectory`. OLP uses this for its joint and Cartesian moves. `rtctl prepare` and `rtctl start` send the same operations from a file. ### Dense weld programs A planned weld program travels as an immutable dense trajectory, a `.rdt` file. 1. The OLP server sends a `.weldplan` to the weld planner. 2. The planner plans each seam and each connecting move, runs the verifier, and writes a `.rdt` only if every segment passes. 3. OLP's **Load** fetches the `.rdt` by digest from the planner and checks it against the robot and the cell. It then uploads it to `rt-control` with `POST /v1/program` (`prepare_program`). 4. **Play** sends `start_program`. rt-core interpolates the samples in the cycle. The dense daemon offers the same upload-then-play path for other clients: TCP ingest on `:8797`, with play and stop over NATS under a leader lease. Programs that arrive through it are not re-verified. See [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning) and [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification). ## Identity ties it together Several digests keep every process agreeing on which robot it is talking to: - The **robot description** (`robot_description/robots//`) has a SHA-256 identity over the files its manifest registers. - A **machine config** pins that description, and `rtctl compile` turns it into an immutable `configuration_sha256`. - `rt-control` is started with that digest and a **pair binding** (pair id and revision). Every `acquire` must present the same binding. - The planner stamps the robot and cell identity into each `.rdt` header. OLP refuses to load a plan made against a different robot or cell. See [Cells, machines and positioners](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners) and [Robot description and coordinate frames](https://advancedmetalresearch.com/docs/concepts/robot-description-and-frames). ## Observation Anyone with access to the socket can read state without taking control: - `GET /v1/describe`, `GET /v1/status` - events from `/v1/events`, or as Server-Sent Events from `/v1/events/stream` - binary telemetry batches from `/v1/telemetry` On a remote listener, a client certificate is still required. `rt-natspublisher` republishes status and telemetry to NATS subjects `robot/v4/rtcore..status` and `robot/v4/rtcore..telemetry.batch`, for displays and archiving. Those subjects carry no command authority. ## Maturity | Status | Components | |---|---| | Production source | rt-core (core, `rt-control`, SDKs, `rtctl`), both motion servers, OLP dense execution and the local simulator, the weld planner, robot descriptions | | Current, not qualified | Pendant v5 (not qualified on physical input or real motion). `reset_fault` is interim. | | Simulation only | Virtual pendant | | Experimental | `mujoco-sim/v1`, an independent MuJoCo model. It is not a controller stand-in. | | Legacy, off by default | OLP connected execution over NATS (`--enable-connected-execution`), and older versioned trees | ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/cmd/rt-control/main.go:24-66` - `rt-core/host/rosie-rt-core.service:12-33` - `rt-core/host/rosie-rt-control.service:12-27` - `rt-core/host/rosie-rt-natspublisher.service:15-27` - `rt-core/adapters/rosie/control/http.go:52-178` - `rt-core/protocol/control.json (wire_contract)` - `rt-core/protocol/application-v1.schema.json (capabilities` - `rules.remote_jog_upgrade)` - `rt-core/sdk/control/jog.go:23,87-130` - `rt-core/tools/natspublisher/publisher/publisher.go:385` - `rt-core/tools/rtctl/control.go:22-26,147-176` - `motion-server/v1/src/robot_v4_cartesian_cli.hpp:236-339` - `motion-server/joint-trajectory/v1/src/joint_trajectory_daemon.cpp:99-128,209-211` - `motion-server/joint-trajectory/v1/config/joint_trajectory_daemon.config.json` - `offline-programming/v1/main.go:257-300` - `offline-programming/v1/internal/denseexec/joint_jog.go:81-83` - `offline-programming/v1/internal/denseexec/joint_move.go:437-491` - `offline-programming/v1/internal/denseexec/cartesian_process.go:49` - `offline-programming/v1/internal/denseexec/rt_core.go:735-840` - `offline-programming/v1/internal/denseexec/session.go:236-256` - `offline-programming/v1/ui/vite.config.ts:3-14` - `weld_planner/v1/python/weld_motion_planner/server/motion_planner_server.py:57,538-565` - `weld_planner/v1/python/weldplan/native_admission.py:204-250` - `steamdeck/real/v5/src/main.cpp:18-24` - `steamdeck/virtual/bridge/cli.go:20-45` - `steamdeck/virtual/bridge/rt_core.go:165-330` - `steamdeck/virtual/v1/ui/vite.config.ts:108-120` - `rt-core/tools/rtctl/command.go:25-45` - `offline-programming/v1/internal/denseexec/cartesian_jog.go:15-120` --- # Robot description and coordinate frames > What a RosieOS robot description contains, how its hashed identity is pinned, served and checked at Load, how a cell's calibration narrows it, and the frames, joints and units of the Rosie 1400 and Rosie 1420. URL: https://advancedmetalresearch.com/docs/concepts/robot-description-and-frames Section: RosieOS docs / Concepts Last updated: 2026-10-10 A **robot description** is one directory that says everything intrinsic to a robot model: its kinematics and meshes, its limits, the planner's policy, its collision spheres and what rt-core needs to drive it. Every cell that mounts that model shares the directory. How one particular cell differs from the model lives on that cell, in a separate calibration file. The description is hashed. A plan records the hash it was made against, and the cell refuses a plan whose hash does not match its own. This page explains what the files are, how that identity moves through the system, and the frames and joints you program against. For the exact file schemas see [Robot description files](https://advancedmetalresearch.com/docs/reference/robot-description). To create a new model see [Add a robot model](https://advancedmetalresearch.com/docs/guides/add-a-robot-model). ## What is in a description A description lives at `robot_description/robots//`. The directory name is the model id, and every file in it must state the same id. | File | Read by | Holds | |---|---|---| | `robot_description_manifest.json` | everyone | Which files the robot is made of. Schema `rosie.robot-manifest.v1`. | | `robot.urdf` | planner, OLP, rt-core compiler | Links, joints, meshes, joint limits (rad, rad/s), the torch link. The URDF `robot name` must equal the model id. | | `robot.srdf` | Tesseract planning | The `manipulator` planning group and its named `home` state. | | `config.json` | planner, OLP, rt-core compiler | What URDF cannot say: frames, planning rates and accelerations, driven and held axes, reset pose, torch convention, calibration caps. Schema `rosie.robot-config.v1`. | | `spheres.json` | weld planner | The arm's collision spheres, in each link's own frame, with the hashes of the URDF and meshes they were fitted to. | | `rtcore_definition.json` | rt-core only | How each joint maps to a drive: drive profile, gearing, encoder scale, direction, Home policy. Schema `rosie.robot-definition.v1`. | | `meshes/` | planner, viewers | The link meshes the URDF names, plus any fixture bodies the planner uses. | | `provenance/`, `README.md` | people | How the files were made. Not registered, so not part of the identity. | The planner never reads `rtcore_definition.json`, and rt-core never reads `spheres.json`. The two halves meet only through the shared URDF, `config.json` and the identity. > [!NOTE] `rtcore_definition.json` holds the drive gearing and encoder scale for each joint. These docs describe its fields, not its values. ### Bench descriptions `bench_one_motor_1to1` and `bench_nine_motors_1to1` describe bare motors on a table. They have only a manifest and `rtcore_definition.json`, with no URDF. rt-core can run them. OLP does not list them and the planner cannot plan for them. ## Identity The description's identity is a SHA-256 over the manifest and every file it registers, written `sha256:<64 hex>`. Files that are not registered, such as a CAD export or a measurement report, are outside the identity: changing them never makes a plan stale. Registering a file, or changing a registered one, changes the identity. That is deliberate: the robot changed. Print the identity of any description: ```bash cd robot_description/go go run ./cmd/identity ../robots/rosie_1400_v3 # sha256:<64 hex> ../robots/rosie_1400_v3 ``` The exact byte layout is in [the reference](https://advancedmetalresearch.com/docs/reference/robot-description#identity-algorithm). ### How the identity travels ![A machine config pins a robot description by path and identity. rtctl compile snapshots the registered files and the cell's calibration into the compiled configuration, which rt-control serves through Describe and the resources endpoint. OLP reads the description and the cell's calibration, the planner receives settled numbers, and the .rdt header records the identities. At Load, OLP describes the cell again and refuses a program whose identities differ.](https://advancedmetalresearch.com/assets/docs/robot-description-identity.svg) 1. **A machine config pins it.** `robot.robot_description` names the directory and its identity. `rtctl compile` refuses a pin that does not match the files on disk. 2. **The compiled configuration snapshots it.** Compiling copies every registered file into the compiled directory as a content-addressed resource. The cell no longer needs the checkout. 3. **rt-control serves it.** Describe lists the model id, the identity and each resource's SHA-256. Clients fetch the bytes with `GET /v1/resources/`, or with [`rtctl resources fetch`](/docs/reference/rtctl#resources-fetch). 4. **OLP plans against it.** OLP's robot configuration is the one place that opens a description and a cell's calibration. The seam worker and the weld planner receive the resulting numbers, never the files. 5. **The program records it.** Every dense trajectory carries a `robot_cell` block in its header with `model_id`, `robot_description_sha256` and `machine_planning_calibration_sha256`. See [Dense trajectory format](https://advancedmetalresearch.com/docs/reference/rdt-format). 6. **Load checks it.** Before sending a program, OLP describes the cell again (it never trusts a cached answer) and compares those three fields. ### Load refusals | Reason | When | |---|---| | `robot_cell_unavailable` | The cell binds no robot description, or its robot identity is not valid. | | `robot_cell_missing` | The program's header names no robot or cell. It was planned before this check existed. Plan it again. | | `robot_cell_mismatch` | The model, description identity or calibration identity differs. The message lists each field, with the plan's value and the cell's. | A machine with flat axes and no robot binding has nothing to compare a plan against, so Load does not run this check on it. The header can also record the machine's whole configuration digest, `machine_configuration_sha256`. That is a record, not a check: the digest also changes for things no plan depends on, such as a CPU number or a bus timeout. When it differs, Load adds a note and continues. ## Cell calibration Two cells built from the same model are never quite identical. A cell states its own deviation in `machine_planning_calibration.json` (schema `rosie.machine-planning-calibration.v1`). It is per cell and is not stored in the repository. When a cell has one, its machine config pins it by path and SHA-256, and the compiled configuration serves it beside the description. The calibration can: - **narrow** a joint's lower and upper limit and its velocity, never widen them - **hold** a joint at a value inside its limits - set a **speed ceiling** no higher than the URDF's fastest joint - apply a small **kinematic correction** to a joint origin, `xyz_m` and `rpy_rad`, each component capped by the description's `kinematic_correction_caps` Every key is checked. An unknown key, a joint the URDF does not have, a widened limit or an over-cap correction is an error, not something quietly ignored. A cell with no calibration serves a fixed empty document for its model, and a plan made against that empty document matches it exactly. The calibration's identity is the SHA-256 of its exact bytes. Reformatting the file changes the identity, so plans made against the old bytes are refused. ## Coordinate frames `config.json` names the frames the URDF cannot: | Frame | Key | Meaning | |---|---|---| | Base | `frames.base` | What everything is measured in. `world` on both Rosie models. | | Tool | `frames.tool` and `torch.tool_frame` | The tool centre point: `tool0`. | | Work | `frames.work` | What a workpiece is fixtured to. On a positioner this is a surface on a table link, not the link origin. | | Work surface | `frames.work_surface` | Optional. Adds the work frame as a fixed child of `parent`, offset by `xyz_m`. | The torch's electrode axis is `torch.electrode_axis`, a unit vector in the tool frame. On both Rosie models it is `[1, 0, 0]`: the wire points along +x of `tool0`. OLP's robot catalogue also reports two positions it computes from the URDF at the zero pose: `work_world_m`, the work frame's origin in world, and `arm_base_world_m`, the child link of the first driven joint. The Cartesian motion server expresses TCP poses relative to the arm base, while viewers use world, so clients convert with that offset. ### Units - Revolute positions are in **rad**, prismatic positions in **m**. - Velocities are in **rad/s**, accelerations in **rad/s²**. - Offsets are `xyz_m` in metres and `rpy_rad` in radians. URDF rotations compose as Rz(yaw)·Ry(pitch)·Rx(roll). - A calibration correction is applied as T' = T_origin · Trans(xyz) · R(rpy). ## Rosie 1400 Model `rosie_1400_v3`: a six-axis arm and an H-frame two-table positioner, nine axes in all. This is the layout of the War Machine cell. ```text world ─ floor ─┬─ J1 ─ link_1 ─ J2 ─ link_2 ─ J3 ─ link_3 ─ J4 ─ link_4 ─ J5 ─ link_5 ─ J6 ─ link_6 ─ end_effector ─ tool0 └─ J9 ─ h_frame ─┬─ J7 ─ positioner_table_a ─ positioner_table_a_top (work) └─ J8 ─ positioner_table_b ``` | Joint | Parent → child | Axis | Limits (rad) | URDF velocity (rad/s) | Planning velocity (rad/s) | Planning acceleration (rad/s²) | |---|---|---|---|---|---|---| | J1 | `floor` → `link_1` | +z | −π to π | 3.14 | 0.785 | 5.0 | | J2 | `link_1` → `link_2` | +y | −1.9 to 1.9 | 3.14 | 0.785 | 5.0 | | J3 | `link_2` → `link_3` | +y | −1.57 to 1.53 | 3.14 | 0.785 | 5.0 | | J4 | `link_3` → `link_4` | +x | −π to π | 17.45 | 1.571 | 10.0 | | J5 | `link_4` → `link_5` | +y | −3.37 to 1.3 | 23.27 | 1.571 | 10.0 | | J6 | `link_5` → `link_6` | +z | −2.094 to 3.14 | 31.42 | 1.571 | 10.0 | | J7 | `h_frame` → `positioner_table_a` | +y | −π to π | 3.14 | 0.785 | 2.5 | | J8 | `h_frame` → `positioner_table_b` | −y | −π to π | 3.14 | 0.785 | 2.5 | | J9 | `floor` → `h_frame` | +z | −π to π | 3.14 | 0.785 | 2.5 | - **Frames:** base `world`, tool `tool0`, work `positioner_table_a_top`, a surface on `positioner_table_a` at `xyz_m` [0, −0.622, 0.1]. - **Driven axes:** J1 to J7. **Held axes:** J8 = 0 and J9 = 0. The planner moves the arm and table A together and keeps table B and the H-frame turn fixed. - **Planning speed ceiling:** 1.571 rad/s. - **Reset pose:** all driven joints at 0. The SRDF `home` state is all nine joints at 0. How the positioner is built and why the planner holds J8 and J9 is covered in [Cells, machines and positioners](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners#the-h-frame-positioner). ## Rosie 1420 Model `rosie_1420_v1`: a six-axis arm on a pedestal. ```text world ─ pedestal ─ base ─ J1 ─ link_1 ─ J2 ─ link_2 ─ J3 ─ link_3 ─ J4 ─ link_4 ─ J5 ─ link_5 ─ J6 ─ link_6 ─ end_effector ─ tool0 ``` | Joint | Axis | Limits (rad) | URDF velocity (rad/s) | Planning velocity (rad/s) | Planning acceleration (rad/s²) | |---|---|---|---|---|---| | J1 | +z | −6.266 to 6.266 | 3.14 | 3.0 | 2.094 | | J2 | −y | −1.9 to 1.9 | 3.14 | 3.0 | 2.094 | | J3 | +y | −4.2 to 1.53 | 3.14 | 3.0 | 2.094 | | J4 | +x | −6.266 to 6.266 | 17.45 | 10.0 | 4.189 | | J5 | +y | −6.266 to 6.266 | 3.14 | 3.0 | 4.189 | | J6 | −z | −10 to 10 | 31.42 | 10.0 | 4.189 | - **Frames:** base `world`, tool `tool0`, work `world`. - **Driven axes:** J1 to J6. No held axes. - **Planning speed ceiling:** 10.0 rad/s. ## Which limit applies where The same description feeds two consumers, and they use different numbers: - **The planner** uses `config.json`'s planning velocity and acceleration, narrowed further by the cell calibration. A description may lower a planning rate below the URDF rating, never raise it: a planning velocity above the URDF's `velocity` is refused when the description loads. - **rt-core** takes each robot-bound axis's travel and maximum velocity from the URDF, and its trajectory and jog acceleration from `config.json`. A machine config cannot override them. rt-core's limit check is therefore the URDF rating, which is higher than the planning rate on most joints. > [!WARNING] rt-core checks joint position, velocity and continuity. It does not check collisions. Only planned weld programs are verified against the cell model before they can be loaded. See [What is verified before motion](https://advancedmetalresearch.com/docs/get-started/safety-model#what-is-verified). ## Related pages - [Robot description files](https://advancedmetalresearch.com/docs/reference/robot-description): every field and the identity algorithm - [Add a robot model](https://advancedmetalresearch.com/docs/guides/add-a-robot-model) - [Cells, machines and positioners](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners) - [Configuration files](https://advancedmetalresearch.com/docs/reference/configuration): how a machine config pins a description - [Dense trajectory format](https://advancedmetalresearch.com/docs/reference/rdt-format): the `robot_cell` header block ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `robot_description/go/identity.go:32-167` - `robot_description/go/cell.go:26-300` - `robot_description/go/description.go:20-285,292-330` - `robot_description/go/cmd/identity/main.go:1-32` - `robot_description/tools/manifest.py:33-63` - `robot_description/robots/rosie_1400_v3/config.json` - `robot_description/robots/rosie_1400_v3/robot.urdf:133-218` - `robot_description/robots/rosie_1400_v3/robot.srdf` - `robot_description/robots/rosie_1400_v3/robot_description_manifest.json` - `robot_description/robots/rosie_1400_v3/spheres.json:1-8` - `robot_description/robots/rosie_1420_v1/config.json` - `robot_description/robots/rosie_1420_v1/robot.urdf:36-157` - `robot_description/robots/bench_one_motor_1to1/robot_description_manifest.json` - `rt-core/tools/rtctl/compiled_resources.go:130-154` - `rt-core/tools/rtctl/compiler.go:180-200` - `rt-core/tools/rtctl/robot_definition.go:250-290` - `rt-core/adapters/rosie/control/resources.go:334-380` - `rt-core/config/templates/machine.json (robot.robot_description` - `robot.machine_planning_calibration)` - `offline-programming/v1/internal/denseexec/robot.go:12-110` - `offline-programming/v1/internal/denseexec/rt_core_robot.go:236-260` - `offline-programming/v1/internal/denseexec/rt_core.go:766` --- # Control authority: leases, fences and the jog lane > How rt-control admits one controller at a time, using an expiring lease, a session and generation fence, idempotent retries and a separate jog lane with its own deadlines. URL: https://advancedmetalresearch.com/docs/concepts/control-authority Section: RosieOS docs / Concepts Last updated: 2026-10-10 A RosieOS cell has exactly one controller at a time. The pendant, the offline programming server, the motion servers, `rtctl` and your own code all ask rt-control for authority the same way, and they exclude each other. Authority is a **grant**: a session token plus a generation number (together, the **fence**), held under a lease that expires unless you renew it. This page explains the model. The exact calls are in the [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http). > [!WARNING] A grant lets you energise the drives and move the robot. The lease and the software Stop are not safety functions: the hardware E-stop is the only emergency stop, and RosieOS has no software E-stop. Keep the E-stop within reach, and read the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model) before you arm a real cell. ## The lifecycle in one example This Go program acquires a grant, keeps it alive in the background, enables and arms the cell, and then gives authority back. Every mutating call carries the fence automatically. authority.go: ```go package main import ( "context" "log" "time" "rosieos/rt-core/sdk/control" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() c, err := control.Dial("/run/rosie-rt-core/control.sock") if err != nil { log.Fatal(err) } defer c.Close() d, err := c.Describe(ctx) // also measures the round trip used to size the lease if err != nil { log.Fatal(err) } g, err := c.Acquire(ctx, "my-app", control.Binding{ PairID: "cell-a", Revision: 1, // the pair rt-control was started with ConfigurationSHA256: d.ConfigurationSHA256, MachineSHA256: d.MachineSHA256, }) if err != nil { log.Fatal(err) // e.g. control_already_owned } log.Printf("session generation=%d lease=%dms", g.Generation, g.LeaseMS) renewals, err := c.StartRenewal(ctx, 0) // 0 = every third of the lease if err != nil { log.Fatal(err) } go func() { for r := range renewals { if r.Err != nil { log.Printf("renewal stopped: %v", r.Err) // authority is gone; stop producing motion } } }() if _, err := c.Enable(ctx, uint32(1)< [!NOTE] Longer is not safer. A longer lease means the robot keeps its last authority for longer after the operator's link drops. A longer input age lets a stale jog velocity apply for longer before it expires. Both are safety trade-offs, not qualified stop times. ## What ends authority | Event | What happens | What you do next | |---|---|---| | `stop` | Outputs are inhibited immediately, execution, uploads, handles and jog are cancelled, and the generation increments. The session survives if it was still valid. | Enable and Arm again, then prepare new motion. | | `halt` | A controlled deceleration to an enabled hold. The grant, Enable and Arm are kept; the active trajectory handle and jog generation are retired. | Start new motion from the held position, or `begin_jog` again. | | `release` | Stop, then the session is cleared. | Acquire again when you need authority. | | Lease expiry | rt-control notices within about 20 ms, emits `grant_expired`, retires every handle, cancels jog and inhibits outputs through the same Stop path. | Acquire again, Enable, Arm and upload anew. | | `reset_fault` | A submitted reset retires the session, even when the core refuses the reset. | Acquire again after recovery. | | Core or adapter restart | Sessions from before the restart are refused. | Describe again, then acquire. | Neither a Stop receipt nor a Halt receipt proves the robot is at standstill. Confirm in Status. ## Idempotent retries Mutating JSON calls can carry a `request_id`. Within a live session, rt-control keeps the last 256 outcomes: a retry with the same ID and the same payload joins the original request or replays its final reply, so a lost reply never causes a second Start. The same ID with a different payload is `request_id_conflict`. Release, expiry and restarts end the guarantee, and binary `.rdt` uploads are never deduplicated. The details are under [Idempotent retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). ## The jog lane Jogging runs on its own lane, separate from the JSON command path, so a stalled HTTP connection cannot hold a velocity in place. 1. `GET /v1/jog/clock` returns the host clock's incarnation. 2. `begin_jog` reserves jog mode for an axis mask and returns a **jog generation**. It carries the capture time and an absolute deadline of the first input. 3. Each velocity update is a 224-byte binary frame: a Unix datagram on `jog.sock`, or a WebSocket frame on the remote listener. It carries the session, grant generation, jog generation, a strictly increasing sequence, its own capture time and deadline, and the velocities in each axis's unit per second. 4. `end_jog` revokes input and the core ramps the jog to a hold. Four rules keep jog input from outliving its operator: - **Deadlines are never extended.** Renewing the grant does not freshen an input, and a refused frame never refreshes the previous one. - **Input ages out.** A frame's deadline may be at most `max_jog_input_age_ns` after its capture (250 ms on LAN). When input stops arriving, the core ramps the jog down within each axis's jog acceleration, holds, and publishes `jog_expired`. If the ramp cannot stay within its bounds, the outputs are inhibited instead. - **Old sessions cannot come back.** After Stop, expiry, a source loss or a Home or configuration epoch change, frames for the old session are refused as `wrong_identity`, even with fresh timestamps. - **Only the newest input counts.** In a burst of datagrams only the newest valid frame is applied. Producers keep only their latest unsent sample and never replay a backlog. Remote jog adds a clock calibration exchange, because the pendant's clock is not the cell's. That is covered in [Remote access](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#remote-jog-over-websocket). ## One controller, many clients Because authority is exclusive, clients on one cell lock each other out while they hold it: a desktop OLP session holding the grant keeps the pendant out, and the other way round. Observation is not exclusive. Anyone who can reach the socket can read Describe, Status, events and telemetry without a lease. ## Related pages - [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http): `acquire`, `renew`, `release`, `stop`, `halt` and the jog calls. - [The real-time core](https://advancedmetalresearch.com/docs/concepts/real-time-core): what the core checks before any motion starts. - [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes#reasons-authority): the authority reasons. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/adapters/rosie/control/controller.go:169-191 (restart fence)` - `rt-core/adapters/rosie/control/controller.go:536-644 (acquire, binding match)` - `rt-core/adapters/rosie/control/controller.go:686-796 (authorize, renew)` - `rt-core/adapters/rosie/control/controller.go:1216-1384 (stop, finishStop)` - `rt-core/adapters/rosie/control/controller.go:1385-1438 (expiry watcher, 20 ms)` - `rt-core/adapters/rosie/control/reset_linux.go:22-88` - `rt-core/adapters/rosie/control/halt_linux.go:25-79` - `rt-core/adapters/rosie/control/handles.go:5-19` - `rt-core/adapters/rosie/control/idempotence.go:12-111` - `rt-core/adapters/rosie/control/remote_listener.go:14-86` - `rt-core/adapters/rosie/control/jog_linux.go:26-135,259-396` - `rt-core/protocol/application-v1.schema.json:1100-1104 (rules.control_timing)` - `rt-core/protocol/control.json (fast_control.max_grant_lease_ns, max_jog_input_age_ns)` - `rt-core/engine/jog.hpp:16-55` - `rt-core/config/templates/machine.json:228-229` - `rt-core/sdk/control/renewal.go:25-93` - `rt-core/sdk/control/timing.go:33-80` - `rt-core/sdk/control/client.go:78-112` --- # The real-time core > How rosie-rt-core runs the cyclic control loop, drives CiA402 servo drives over EtherCAT, gates every motion start, applies a final output permit each cycle, latches faults and records telemetry. URL: https://advancedmetalresearch.com/docs/concepts/real-time-core Section: RosieOS docs / Concepts Last updated: 2026-10-10 `rosie-rt-core` is the cyclic controller at the bottom of RosieOS. It is a C++17 process that talks EtherCAT to the servo drives, runs a fixed-period loop, and owns two things nothing else in the system can override: the drive state and the final output permit. Applications never talk to it directly. They go through [rt-control](https://advancedmetalresearch.com/docs/apis/rt-control-http), which reaches the core over a private IPC channel (a Unix socket plus shared-memory rings). That channel is internal and not a public API. ![Clients call rt-control, which reaches rosie-rt-core over private IPC. The core reads feedback, checks readiness, computes a trajectory or jog target and applies a final output permit each cycle, then writes to the drives over EtherCAT. The hardware E-stop chain acts on drive power directly and is only observed by the core.](https://advancedmetalresearch.com/assets/docs/rt-core-cycle.svg) *Figure: Every command reaches the core through rt-control. The hardware E-stop chain sits outside the software.* > [!WARNING] The core enforces limits and readiness, but it is not a safety system. The hardware E-stop is the only emergency stop; RosieOS has no software E-stop. The core reads the drives' digital inputs and external-enable state for diagnostics only, never as a safety decision. Only planned weld programs pass the planner's collision and limit check before loading; jog, Home and point-list moves rely on the checks on this page. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). ## Three builds of one controller | Binary | Build | Use | |---|---|---| | `rosie-rt-core` | `make live`, with IgH EtherCAT `libethercat` | Real drives. Refuses to start unless the kernel is PREEMPT_RT and an RT CPU and FIFO priority are configured. It locks its memory at startup. | | `rosie-rt-core-sim` | `make sim` | The same state machine and loop against a simulated bus. This is what the quickstart and the offline programming "Simulate" path run. | | `rosie-rt-core-ipc` | `make ipc-only` | IPC-only validation, with no EtherCAT. | All three read one compiled configuration (from `rtctl compile`). The cycle period is `cycle_ns` in the machine configuration: 250 µs to 10 ms. The shipped templates use 1 ms (1 kHz). The [configuration reference](https://advancedmetalresearch.com/docs/reference/configuration) lists every field. ## Drives: CiA402 in cyclic synchronous position Each axis is a CiA402 (DS402) servo drive running in **cyclic synchronous position** (CSP, mode 8). Every cycle the core reads each drive's statusword, position and error code, and writes a controlword and a target position (plus a target velocity where one is mapped). Status reports the decoded state per axis: | Code | DS402 state | |---|---| | 0 | Unknown | | 1 | Not ready to switch on | | 2 | Switch on disabled | | 3 | Ready to switch on | | 4 | Switched on | | 5 | Operation enabled | | 6 | Quick stop active | | 7 | Fault reaction active | | 8 | Fault | Motion is only possible in *Operation enabled*, with fresh feedback, CSP mode and no drive error. ## Bringing an axis up The order is always the same. Each step is an rt-control call made under a grant (see [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority)). 1. **Home**, if the axis requires it (`require_home` in Describe) and has no valid Home. `home` runs the drive's native homing, then leaves the axis disabled. Alternatively, `restore_anchor` re-establishes Home from a saved absolute-encoder anchor without moving. 2. **Enable** the axes with `enable` and an axis mask. The drives move through the DS402 states to *Operation enabled*. 3. **Arm** the core with `arm`. 4. **Wait for readiness.** Poll Status until every axis you will move reports `readiness: "ready"`. 5. **Start** motion: a trajectory, a program or a jog session. `readiness` names the first gate an axis fails, in this order: `faulted`, `mode_mismatch`, `home_required`, `coordinate_invalid`, `not_enabled`, `not_operation_enabled`, `brake_wait`, then `ready`. The table in [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes#axis-readiness) says what to do about each. ## The motion start gate Every Start (trajectory, program or jog) is decided inside the core, not in rt-control. The core refuses to start unless **all** of these hold: - no trajectory, jog or commissioning is already active (otherwise `mode_conflict`) - the request carries the current generation and has not been cancelled - the lease is valid and the core is armed - the bus is ready and no safety fault is latched (`safety_fault_mask == 0`) - the verified configuration epoch is current, and every requested axis is configured and configuration-verified - every requested axis that requires Home has valid Home evidence - limits are valid, every requested axis is in CSP mode and enabled - every requested axis is in *Operation enabled* with fresh feedback - the first point is continuous with the held position - no axis that is outside its limits would move further outward (`outside_limits_outward`) A first point that does not match the held position within one count is refused as a start discontinuity. There is one exception: when Home, the configuration and the encoders are all qualified, a small offset (within the axis's completion tolerance) is closed with a bounded alignment move before the plan's own time starts. The plan's samples and identity do not change. ## The final output permit Passing the start gate is not the last check. On **every cycle**, before it writes to the drives, the core applies a final output permit: - The controlword is forced to zero whenever the lease is not valid or any safety fault is latched. - Motion is permitted only while the final permit holds, the core is armed and no safety fault is set. - Cell I/O outputs are also ANDed with the permit, so they fall back to their safe state together with motion. This is why lease expiry and Stop take effect within a cycle, whatever the client is doing. ## Stop, Halt and expiry | Trigger | Core behaviour | |---|---| | `stop`, lease expiry, authority or connection loss | Outputs are inhibited immediately. Active motion and handles are retired. | | `halt` | Decelerates to an enabled hold, using the trajectory's declared acceleration or the jog acceleration. Each ramp step is checked against the target-lead bound. | | Jog input expires or `end_jog` | The jog ramps down within the jog acceleration and holds; a ramp that cannot stay within its bounds inhibits instead. | | A fault latches | Outputs are inhibited, the core disarms and motion is cancelled. | None of these is an emergency stop, and no receipt proves standstill. ## Limits and interpolation The core admits trajectories only inside each axis's limits, in logical units (rad or m). For robot axes, the travel and velocity limits come from the robot's URDF and the acceleration limits from its planning configuration. Machine configuration cannot override them. Each axis in Describe says what is checked: - **Position** and **velocity** are always checked (`declared`), including the peaks of each interpolated segment. - **Acceleration** is checked on samples when the axis declares `max_acceleration`, otherwise it is `not_declared`. - **Jerk** is never checked (`unsupported`). Between points the core interpolates with cubic Hermite when both points carry a velocity, and linearly otherwise (`hermite_position_with_velocity`, `linear_without`). After the last sample it holds the final position with zero velocity and waits for feedback to settle within `completion_tolerance` by `completion_timeout_ns` (`hold_last_sample_then_settle`). No curve is ever clipped or retimed: a violation is refused. An axis that is outside its limits (after a configuration change, for example) may still be enabled and armed to hold. It may move back inward, but any motion that increases the excursion is refused with `outside_limits_outward`. ## Faults and recovery The core latches execution faults as bits in `core.execution_fault_reasons`, with the affected axes, and sets `safety_fault_mask`. Each bit has a recovery class: | Recovery class | Meaning | |---|---| | `reset_clears` | `reset_fault` clears it. | | `reset_after_condition_clears` | Remove the cause first, then `reset_fault`. | | `rehome_required` | After `reset_fault`, Home (or a qualified anchor restore) is required on the affected axes. | | `restart_required` | Reset cannot clear it; the core must be restarted after investigation. | The full bit table (start discontinuity, position rate, cycle deadline, target lead, following error, coordinate reference, drive readiness, bus transport, brake hold, PDO mapping, collision watchdog, cell I/O and more) is in [Execution fault bits](https://advancedmetalresearch.com/docs/reference/error-codes#fault-bits). The code marks this recovery policy as unverified on hardware. To recover, read `recovery_status` (it changes nothing), remove the cause, then call `reset_fault` on an inhibited, idle machine. `reset_fault` is labelled `interim`: it decides the whole reset at once (any persisting condition refuses it with `fault_persists`), it never starts motion or grants Home, and a submitted reset ends your session. Acquire again afterwards. ## Home and anchors Home is per axis. `home_valid_mask` in Status shows which axes have valid Home evidence, and `home_epoch` increments whenever that evidence changes. Some faults (`rehome_required`) and some trust losses invalidate Home. When rt-control's environment sets `ROSIE_RT_ANCHOR_DIR`, a successful Home also saves an anchor per axis: the relation between the absolute encoder and the command frame, bound to the pair, configuration and drive identity. After a restart, `restore_anchor` can re-establish Home from those anchors without moving, but only if every identity still matches and the core independently agrees with the saved evidence. Otherwise it refuses (`anchor_missing`, `anchor_identity_mismatch`, `anchor_source_invalid`, `anchor_disagrees`) and you Home again. ## Brakes Holding brakes are drive-managed: the core does not command a brake output. It applies the configured release and hold delays, and the axis reports `brake_wait` until the release delay has passed. Unless a "brake released" signal is mapped, the reported brake state (`BRAKE_APPLIED`, `BRAKE_RELEASED`, …) is a timer estimate, not confirmation of the physical brake. If an axis moves while it should be held, the core latches `brake_hold`, which needs a restart. ## Collision watchdog Each axis can arm a watchdog on drive torque and following error. It trips when either quantity strictly exceeds its bound for a configured number of consecutive cycles (2..1000); a single sample never trips it. A trip latches fault bit 12 (`collision_watchdog`), and Stop inhibits the whole group. Recovery is explicit and needs fresh feedback below both bounds. The watchdog is off unless the axis or its drive profile declares thresholds, and compiling a configuration without it prints a warning per axis. The thresholds in the shipped examples are marked unverified starting values. This is a torque and tracking trip, not collision avoidance: collision checking against the cell model happens only in the weld planner. ## Telemetry Every cycle the core captures one record per axis into a shared-memory ring: positions, targets, statuswords, error codes, torque, following error, validity flags, fault masks and the authority generations. Retention is `telemetry.retention_ms` in the machine configuration (default 100 s, up to one hour, within a 2 GiB mapping). rt-control publishes the ring through `GET /v1/telemetry`; see [Events and telemetry](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry#telemetry). ## What the core does not do - **No Cartesian motion.** The core works in joint space only. Cartesian jog and moves are resolved to joint velocities or joint trajectories by the motion servers and the offline programming server. - **No simulation gate.** The core admits any trajectory or `.rdt` program that passes its own limit, continuity and identity checks. The collision and limit verification of planned weld programs happens earlier, in the weld planner: every planned weld program is verified against the cell model, collision and joint limits, and can be replayed in simulation, before it can be loaded onto the robot. - **No torch output.** Torch-class cell outputs are refused at every level. - **No software E-stop.** The hardware E-stop chain acts on drive power directly. ## Related pages - [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority): leases, fences and the jog lane. - [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http): the calls that drive the core. - [Error codes and fault states](https://advancedmetalresearch.com/docs/reference/error-codes). - [Architecture](https://advancedmetalresearch.com/docs/concepts/architecture): where the core sits among the other processes. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/src/main.cpp:21` - `rt-core/engine/cycle_machine.hpp:3155-3175 (controlword zeroed)` - `rt-core/engine/cycle_machine.hpp:3514-3515 (motion_permitted_mask)` - `rt-core/engine/cycle_machine.hpp:3539-3543 (cell I/O permit)` - `rt-core/engine/cycle_machine.hpp:4121-4141 (PREEMPT_RT and FIFO admission)` - `rt-core/include/motion_readiness.hpp:14-21 (readiness order)` - `rt-core/include/motion_readiness.hpp:26-31 (axis ready: mode 8, OperationEnabled)` - `rt-core/include/motion_readiness.hpp:97-137 (start gate)` - `rt-core/include/fault_recovery.hpp:9-64` - `rt-core/adapters/rosie/control/reset_linux.go:22-154` - `rt-core/adapters/rosie/control/controller.go:830-1053 (home, anchors, restore)` - `rt-core/adapters/rosie/control/anchors/store.go:44-49` - `rt-core/engine/brake.hpp:8-40` - `rt-core/include/axis_profiles.hpp:28-42 (collision watchdog bounds)` - `rt-core/engine/cycle_capture.hpp:42 (retention default)` - `rt-core/engine/jog.hpp:16-55` - `rt-core/protocol/control.json (native_enums DS402_*, BRAKE_*)` - `rt-core/protocol/application-v1.schema.json (AxisDescription.interpolation, checks, endpoint; rules.external_enable)` - `rt-core/config/templates/machine.json:6-7,156-158` - `rt-core/Makefile:39-56` --- # Motion paths and planning > The four ways a RosieOS client moves the robot, who holds authority on each path, which rt-control lane each one uses, and what is verified before and during the motion. URL: https://advancedmetalresearch.com/docs/concepts/motion-and-planning Section: RosieOS docs / Concepts Last updated: 2026-10-10 Every motion in RosieOS reaches the drives the same way: a client holds the `rt-control` lease, sends one of three kinds of motion request, and the 1 kHz core decides at its motion start gate whether the motion may begin. What differs between the paths is who the client is, how it gets its authority, and how much checking happens before the request reaches `rt-control`. > [!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). ![Four motion paths. The OLP server, the Cartesian motion server, the dense trajectory daemon and your own code all send requests to rt-control, which forwards them to the motion start gate in rosie-rt-core.](https://advancedmetalresearch.com/assets/docs/motion-paths.svg) *Figure: Four motion owners, one public API, one start gate. The red edge is the core's motion start gate, which every motion passes.* ## The three kinds of motion request `rt-control` accepts motion in three forms. Each one ends at the same [motion start gate](https://advancedmetalresearch.com/docs/get-started/safety-model#motion-start-gate). | Lane | Operations | What the client sends | Used by | |---|---|---|---| | **Jog** | `begin_jog`, datagrams on `jog.sock` (or WebSocket frames on a remote cell), `end_jog` | Per-axis velocities in rad/s or m/s, each update with its own deadline | OLP joint and Cartesian jog, the Cartesian motion server | | **Trajectory** | `prepare_trajectory`, `start_trajectory` | A short list of timed points: `time_ns`, positions in rad or m | OLP joint and Cartesian moves, the Cartesian motion server's position moves | | **Program** | `prepare_program` (`POST /v1/program`), `start_program` | An immutable dense trajectory in the [`.rdt` format](/docs/reference/rdt-format) | OLP Load and Play, the dense trajectory daemon | Home is a fourth, separate operation (`home`). It runs the drive's native homing on the named axes. The operations themselves are documented in the [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http). Their lease and fence rules are in [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority). ## The four motion owners | Path | Owner process | How a client reaches it | Extra authority layer | rt-control lanes | |---|---|---|---|---| | [Offline programming](https://advancedmetalresearch.com/docs/concepts/motion-and-planning#olp) | OLP server | HTTP on `127.0.0.1:8794` | Selected-target generation headers | Jog, trajectory, program, Home | | [Cartesian motion server](https://advancedmetalresearch.com/docs/concepts/motion-and-planning#cartesian) | `robot-v4-cartesiand` | UDP intent packets and NATS commands | NATS leader lease | Jog, trajectory, Home | | [Dense trajectory daemon](https://advancedmetalresearch.com/docs/concepts/motion-and-planning#dense) | `joint_trajectory_daemon` | TCP on `127.0.0.1:8797` for bytes, NATS for play | NATS leader lease | Program, Home | | [Your own client](https://advancedmetalresearch.com/docs/concepts/motion-and-planning#direct) | Your process | The Go or C++ client | None beyond the rt-control lease | Any | The owners share one `rt-control`. Whichever holds the lease is the only one that can move the robot; the others are refused with `control_already_owned` until it releases. See [One controller at a time](https://advancedmetalresearch.com/docs/get-started/safety-model#one-controller). ### Offline programming The OLP server is the operator path, and the only one with plan-time verification. It holds the `rt-control` lease on behalf of a browser or the v5 pendant and exposes the machine-control routes under `/api/offline-programming/v1/dense-execution/`. - **Programs.** Load fetches a `.rdt` by digest from the weld planner. The planner writes one only when every seam and connecting move passes its verifier, so a program that failed verification has nothing to load. Load then checks the plan's identity and robot, requires Home on every configured axis, acquires the lease and calls `prepare_program`. Play calls `start_program`. See [What is verified before motion](https://advancedmetalresearch.com/docs/get-started/safety-model#what-is-verified). - **Joint jog.** One axis at a time, at a fraction of that axis's described maximum velocity. The browser sends a hold every 50 ms. Each hold lives for the cell's jog input age (250 ms on a LAN cell), then the axis ramps to a hold. - **Cartesian jog.** A base- or tool-frame twist. OLP asks `robot-v4-cartesiand --resolve-only` for joint velocities from the measured pose, scales them to the joint ceilings, and sends them on the same jog lane. - **Joint and Cartesian moves.** A bounded displacement, planned by OLP as a per-joint trapezoid (joint moves) or as IK waypoints at 1 mm or 0.25° spacing (Cartesian moves), then sent with `prepare_trajectory` and `start_trajectory`. - **Home.** Native Home on all configured axes or on a subset. Joint jog, moves and Cartesian jog all plan at 75% of each axis's described maximum velocity, times the operator's speed fraction. The 75% is a measured reserve for drive overshoot. A mutating request must carry the selected target's generation and cell ID in `X-RT-Target-Generation` and `X-RT-Target-Cell`, so a request that a browser issued against one cell cannot land on another. OLP also requires a browser heartbeat: if none arrives for 5 s while OLP holds the lease, it stops the machine with `ui_heartbeat_lost`. See the [Offline programming HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http) and the guide [Connect to a cell and run a program](https://advancedmetalresearch.com/docs/guides/connect-a-cell). ### Cartesian motion server `robot-v4-cartesiand` turns a stream of UDP intent packets into Cartesian jog. Each packet carries six normalised axis values in [-1, 1] and a speed scale. The server maps them to at most 0.2 m/s linear and π rad/s angular, slews them at 0.75 m/s² and 540°/s², resolves joint velocities through a damped Jacobian with joint-limit and singularity scaling, and streams them on the `rt-control` jog lane. Each output's deadline is at most 250 ms after the packet arrived. Its authority has two layers. It holds the `rt-control` lease itself, and it grants its own NATS leader lease to one controller at a time. Every motion packet and NATS command must carry the current leader lease ID and fence epoch. Releasing the deadman, a neutral packet, a stale packet or a fence mismatch halts the jog. Joint jog, go-home without an admitted run, and weld I/O are refused on the `rt_core` backend. See the [Cartesian motion server](https://advancedmetalresearch.com/docs/apis/cartesian-motion-server). ### Dense trajectory daemon `joint_trajectory_daemon` is a store-and-play path for `.rdt` programs that come from somewhere other than OLP. The data plane is TCP: `upload` stores and validates a blob and never moves anything. The control plane is NATS: a controller acquires the daemon's leader lease, then sends `play` with the exact six-field plan identity. On `leader_acquire` the daemon takes the `rt-control` lease. On `preload` it calls `prepare_program`. On `play` it runs native Home on any axis whose Home is not valid (or restores the saved anchor, if configured), enables and arms the axes, waits for readiness, and calls `start_program`. > [!IMPORTANT] Programs played through the dense daemon are checked against the `.rdt` format rules and rt-core's native limits only. The daemon does not ask the weld planner or check the plan's robot and cell identity. Treat it as a direct path. See the [Dense trajectory daemon](https://advancedmetalresearch.com/docs/apis/dense-trajectory-daemon). ### Your own client Your code can call `rt-control` directly through the [Go SDK](https://advancedmetalresearch.com/docs/apis/go-sdk) or the [C++ client](https://advancedmetalresearch.com/docs/apis/cpp-client). Use one of these for anything that moves. The SDKs renew the lease in the background. `rtctl` cannot drive motion in practice. It never renews the 500 ms lease, so the lease expires between commands and outputs are inhibited. Use `rtctl` for inspection and one-off setup, not motion. [Your first motion](https://advancedmetalresearch.com/docs/get-started/first-motion) walks through a complete SDK program against the simulator. ## What is checked, and where | Check | Weld program through OLP | Program through the dense daemon | Jog | OLP moves | Home | |---|---|---|---|---|---| | Weld planner verifier: collisions, penetration, limits, tracking | Yes | No | No | No | No | | Plan identity and robot/cell identity | Yes, at Load | Plan identity only, at preload and play | n/a | n/a | n/a | | Planned at 75% of described velocity | Planner's own limits | Producer's own limits | Yes (OLP); 0.2 m/s and π rad/s caps (motion server) | Yes | n/a | | `.rdt` format validation | Yes | Yes | n/a | n/a | n/a | | rt-core native admission: position limits, velocity, continuity | Yes | Yes | Per update | Yes | n/a | | Motion start gate and per-cycle output permit | Yes | Yes | Yes | Yes | Yes | rt-core checks joint limits and rates. It does not check collisions with the cell, the positioner or the part. Only the weld planner's verifier does that, and only for planned weld programs. ## Starting from rest A trajectory or program must continue from the held position. If the robot is at rest and the first point is within the axis's completion tolerance of the held position, the core inserts a short, bounded alignment ramp to the first point. Any larger gap is refused. So the first point of a motion may be slightly offset from the reported position, but never by more than that tolerance. Inside a dense program, segments hand over in place: each segment starts and ends at rest, and the next one starts where the last one ended. The core never interpolates across a segment boundary. See [Segments](https://advancedmetalresearch.com/docs/reference/rdt-format#segments). ## Stopping Every path ends in the same software stops: Stop, Halt, Release and lease expiry. Release runs Stop and then gives up the session. The owners add their own triggers: | Path | Also stops on | |---|---| | OLP | Browser heartbeat lost for 5 s, lease renewal lost, a refused or failed operation (OLP runs Stop and Release before it reports the refusal; a slow or unsent request is the exception), a new target selection | | Cartesian motion server | Deadman released, neutral or invalid packet, leader lease lost or changed, NATS `stop` or `disarm` | | Dense daemon | NATS `stop` (always accepted, never fenced), leader lease released or replaced, any native refusal | None of these is an emergency stop. See [Software stops](https://advancedmetalresearch.com/docs/get-started/safety-model#software-stops). ## Related pages - [Safety model](https://advancedmetalresearch.com/docs/get-started/safety-model) - [Architecture](https://advancedmetalresearch.com/docs/concepts/architecture) - [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority) - [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification) - [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): - `rt-core/adapters/rosie/control/http.go:85-168` - `rt-core/include/motion_readiness.hpp:97-130` - `rt-core/include/start_alignment.hpp:24-55` - `offline-programming/v1/server.go:190-206` - `offline-programming/v1/dense_execution.go:24-63` - `offline-programming/v1/internal/denseexec/rt_core.go:556-616,735-851,980-1031` - `offline-programming/v1/internal/denseexec/session.go:22` - `offline-programming/v1/internal/denseexec/joint_jog.go:17-31,306-361` - `offline-programming/v1/internal/denseexec/joint_move.go:17-19,56-97,437-491` - `offline-programming/v1/internal/denseexec/cartesian_jog.go:52-62,118-208` - `offline-programming/v1/internal/denseexec/cartesian_move.go:17-51` - `motion-server/v1/src/cartesian_resolver.hpp:38-42,99-160` - `motion-server/v1/src/robot_v4_cartesian_daemon.cpp:58-103` - `motion-server/v1/src/robot_v4_cartesian_nats_protocol.hpp:2487-2519` - `motion-server/v1/src/rt_core_cartesian_runtime.hpp:184-544` - `motion-server/joint-trajectory/v1/src/joint_trajectory_daemon.cpp:1-9` - `motion-server/joint-trajectory/v1/src/command_dispatch.hpp:66-207` - `motion-server/joint-trajectory/v1/src/rt_control_executor.hpp:48-318` - `weld_planner/v1/python/weldplan/dense_joint_trajectory.py:700-705` - `weld_planner/v1/python/weldplan/native_admission.py:204-250` - `rt-core/tools/rtctl/control.go:73-83` --- # Weld planning, verification and evidence > How a CAD part becomes a verified joint trajectory, what the weld planner's verifier and admission check prove and do not prove, and what the plan result records. URL: https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification Section: RosieOS docs / Concepts Last updated: 2026-10-10 The weld planner turns a CAD part and a program into joint trajectories that the robot can play. It then proves, against the cell model, that those trajectories are clear of collisions and inside the joint limits, and it only writes the playable file if that proof holds for every segment. This is the only verification path in RosieOS. Nothing else in the system checks a motion against the cell geometry. Read [What is verified before motion](https://advancedmetalresearch.com/docs/get-started/safety-model#what-is-verified) in the safety model first. This page explains what that check covers. ![Three rows. Author: a STEP part goes to the seam worker, which finds weld joints and seams and searches torch angles; the program and the part are packed into a .weldplan. Plan: the weld planner runs the M4 seam search, then M5 and M6 trajectory optimisation, then the verifier, and admission writes a .rdt only if every segment passes. Load and run: Simulate optionally replays the bytes, OLP Load fetches the file by digest and checks identity and Home, rt-control admits it, and Play starts it in rosie-rt-core. Jog, moves, Home and Go to plan start are not on this path and are not verified.](https://advancedmetalresearch.com/assets/docs/weld-planning-pipeline.svg) *Figure: The red box is the verifier: the only place RosieOS checks motion against the cell geometry. No `.rdt` exists unless every segment passes it.* ## The pipeline 1. **Author.** You import a STEP part in offline programming (OLP) and place it on the cell. The seam worker finds the weld joints, proposes seams and searches the torch work and travel angles along each one. You accept or trim each weld. Taught moves, dwells and Home can go in the same program. See [Program a weld from CAD](https://advancedmetalresearch.com/docs/guides/offline-programming). 2. **Pack.** OLP packs the program, the cell descriptor, the torch, the STEP file and any fixtures into one `.weldplan` container, with a SHA-256 digest for every member. See the [weld program format](https://advancedmetalresearch.com/docs/reference/weld-program-format). 3. **Plan.** The weld planner runs three stages on an NVIDIA GPU: - **M4, seam search.** A dynamic program over a sampled lattice of robot poses, including positioner angles, finds the K best ways across each seam. It screens each sampled pose against a sphere model of the arm. The result is exact within the sampled lattice. It is not a continuous check. - **M5, weld trajectory optimisation.** Each seam's best candidates become a continuous curve (cubic Hermite knots and velocities) that keeps the tool on the seam. - **M6, connecting moves.** The approach from the cell's reset pose, the transits between welds, the retract, and any taught moves. 4. **Verify.** Every M5 curve and every M6 move goes to the verifier, which judges it against the exact cell meshes. 5. **Admit and encode.** Admission reads the verifier's reports. If every segment passed, the planner resamples the plan into a dense trajectory (`.rdt`) and files it by digest. If not, no `.rdt` exists. 6. **Load and run.** OLP's Load fetches the `.rdt` by digest from the planner's store. It checks the plan's identity, the robot description and cell calibration and Home. rt-control then admits it against the live machine. See [Connect to a cell and run a program](https://advancedmetalresearch.com/docs/guides/connect-a-cell). The search stages use a fast, approximate collision model: the arm as spheres, and everything it must not hit as signed-distance fields. The verifier deliberately uses a different model, the real triangles, so that the approximation cannot certify itself. ## What the verifier checks The verifier works on the real triangles, in 64-bit floating point on the CPU. It has its own forward kinematics and shares no code with the planner it judges. ### Collisions The verifier does not sample waypoints. For each segment it bounds how fast any point of each link can move. It then asks whether each pair of bodies is further apart, at the middle of an interval, than that bound allows the gap to shrink across the interval. If so, no contact exists anywhere in the interval. If not, it halves the interval and asks again. It stops after 10 halvings, and reaching that depth is a refusal, never a pass. | Bodies | Checked against | |---|---| | Every robot and positioner link that has a mesh in the robot description | Every other link, except links joined by a joint, which touch by construction | | The workpiece, tessellated from the request's STEP file and posed by the placement | The links | | Fixtures from the request, in the cell's world frame | The moving arm links and the workpiece only | A pair counts as a collision when it comes closer than the verification margin: 2 mm by default, set by `verify_margin_mm` (0 to 50 mm). The margin exists because the cell meshes are the visual meshes and fixtures are measured by hand. The report counts `collisions`, and separately `penetrating` for pairs that actually touch or overlap. The verifier refuses to judge a trajectory against a cell it cannot see. If the robot's meshes are missing, planning fails with an error rather than passing. ### Limits - **Joint positions.** Checked against each joint's limits in the cell descriptor. A joint with no position limit is reported as unverifiable. - **Joint velocity and acceleration.** Checked against the rate limits in the cell profile, which come from the robot description's [`config.json`](/docs/reference/robot-description#config-json), narrowed by the cell. For connecting moves the check uses the exact extrema of the cubic Hermite curve that is emitted, not only the knots. A joint with no rate limit is reported as unverifiable. - **Travel mobility, welds only.** Whether the joints can deliver the programmed travel speed along the seam direction without exceeding their rate limits. This catches poses near a singularity, where a slow tool speed would need very fast joints. ### The tracking certificate, welds only The verifier also bounds how far the tool point can be from where it belongs on the seam, at every instant between samples, not just at the samples. The tolerance is the seam's `tolerance.position_mm`, 0.5 mm by default. The certificate is exact for straight seam segments. For a curved seam segment the bound does not exist yet, so the certificate reports it as unverifiable, and the weld cannot pass. "Tracking" here means this geometric certificate on the planned path. There is no seam tracking or sensing in RosieOS: nothing measures the real seam while welding. See [Process I/O and sensing](https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing). ### Verdicts Each report has one of three verdicts: | Verdict | Meaning | |---|---| | `PASS` | Every check ran and passed, and nothing was unverifiable | | `REFUSED` | No failure was found, but at least one check could not run (it is listed in `unverifiable`), or the tracking search was undecided | | `FAIL` | A collision, a limit violation or a tracking violation was found | The distinction the verifier keeps is between "checked and clear" and "not checked". A missing rate limit is not a passed rate check. ## What admission requires Admission is the gate between the planner's result and the `.rdt` file. It is `require_native_motion()` in `weldplan/native_admission.py`, and it runs before any dense resampling. A plan gets a `.rdt` only if all of these hold: 1. The connecting-move stage did not fail. Otherwise: `motion_join_failed`. 2. `seams` and `connecting_trajectories` are lists of objects, and every seam has a non-empty, unique `id`. Otherwise: `motion_result_invalid`. 3. Every seam was crossed by the search, with no error. Otherwise: `seam_not_planned`, with where the search stopped and why (poses outside joint travel, rejected by sphere screening, unreachable by inverse kinematics, or with no allowed step from the previous sample). 4. Every seam has a continuous trajectory. Otherwise: `motion_not_verified`. 5. Every seam trajectory's verdict is `PASS`, with `collisions`, `penetrating` and `limit_violations` all exactly 0 and `unverifiable` empty. Its `limit_violations` and `limit_unverifiable` lists are empty. Otherwise: `motion_not_verified`, with the measured numbers and up to the first few findings (which bodies, which joint, how far). 6. Every seam's tracking certificate is `PASS`, with no segment out of tolerance, nothing undecided or unverifiable, and a finite bound between 0 and the declared tolerance. Otherwise: `motion_not_verified`. 7. Every connecting move is an `approach`, `transit` or `retract` whose verdict is `PASS`, with `collisions`, `penetrating` and `limit_violations` all 0 and `unverifiable` empty. Otherwise: `motion_result_invalid` or `motion_not_verified`. A separate candidate-only mode, used for saving authored welds that have not been planned, refuses any continuous motion with `candidate_only_requires_m4`, so it cannot be used to get around a failed report. If you turn verification off (`verify=false`), the trajectories carry no verdict, admission refuses them, and no `.rdt` is written. The OLP server always asks for verification. Admission reads the planner's own reports. It does not authenticate a result document, and it does not re-verify the resampled `.rdt`; see below. ## What is not verified Be precise about the limits of this check: - **Motion outside planned programs.** Jog, joint and Cartesian moves, "All joints to 0°", Home, go-home, **Go to plan start**, and anything sent through `rtctl`, the SDKs or the dense trajectory daemon never pass through the verifier. They rely on rt-core's limit and readiness checks only. See the [motion start gate](https://advancedmetalresearch.com/docs/get-started/safety-model#motion-start-gate). - **Physics.** The verifier is a geometric certificate, not a simulation. It does not model torques, payload, following error, drive behaviour or jerk. - **Anything not in the model.** People, cables, clamps you did not enter as fixtures, a part that differs from its CAD file, or a part placed somewhere other than its placement says. The result is only as good as the cell meshes, the robot description, the cell calibration and the placement. - **The M4 screen.** The seam search checks sampled poses against spheres. Its findings are reported as screening results, "not a continuous mesh collision certificate". Only the continuous M5 and M6 output is certified. - **The exported representation.** The `.rdt` is a resampling of the verified curves on a 0.01 s grid. The exporter slows the start and end of each weld pass with a 0.25 s ramp and adds a 3 s stationary dwell before and after each pass. Positions stay on the certified path, but the verifier does not re-judge the resampled file. The encoder checks its own rules instead: segments start and end at rest, consecutive segments meet within 0.001 rad, no sample-to-sample step is larger than the velocity ceiling allows (with 25 % interpolation slack), and no axis exceeds the cell's per-axis velocity ceiling (`qd_limit_exceeded`). See the [`.rdt` format](/docs/reference/rdt-format). - **Plan speed.** `speed_scale` (1 % to 100 %) slows the whole plan after planning. The path, and so the clearance and tracking certificates, are unchanged, and the original velocity and acceleration checks stay conservative for any scale up to 1. - **The weld.** Nothing checks bead quality, arc behaviour or process parameters. Torch outputs are refused everywhere, so no program can switch a torch on. See [Process outputs are refused](https://advancedmetalresearch.com/docs/get-started/safety-model#process-outputs). You can replay the exact `.rdt` bytes before Load, in the 3D viewer or on the local simulator. That replay is an operator step. The software does not require it. ## The plan result The planner answers with a result document, schema `amr-weld-planner-v1.motion-plan-result.v1`. Angles are in degrees and lengths in mm on the wire. The document is the evidence for a plan: | Block | What it records | |---|---| | `request` | The SHA-256 of the exact `.weldplan` bytes, the request, program and cell ids, the cell descriptor summary, the source STEP and its digest, every member's digest, and the placement | | `producer` | `module` (`amr-weld-planner/v1`) and `source_revision`: the full 40-character Git commit of the planner code, or `null` with `source_revision_state: "unavailable"` when it was not supplied | | `candidate_search` | How M4 searched: `dynamic_programming_over_discrete_redundancy_lattice`, minimising the sum of absolute joint motion, `exact_within_the_sampled_lattice` | | `coverage` | What was and was not evaluated; see below | | `options` | Every planning option actually used, flat, with `verify_margin_mm` in mm | | `timings_ms` | Wall time per stage, in pipeline order | | `seams[]` | Per seam: whether it was crossed, the K candidates, and the continuous `trajectory` with its `verdict` and `tracking` reports | | `connecting_trajectories[]` | The moves, each with `kind`, `from`, `to`, knots, `clearance_mm` and `verdict` | | `dense` or `dense_error` | The `.rdt` summary, or the reason none was written | The `coverage` block states the claim limits in the document itself: | Key | Values | |---|---| | `candidate_collision_screening` | `sampled_lattice_nodes` or `not_evaluated` | | `candidate_limit_checks` | `reported_per_candidate` | | `weld_trajectory_continuous_verification`, `connecting_trajectory_continuous_verification` | `evaluated_for_all_emitted_trajectories`, `partially_evaluated` or `not_evaluated` | | `canonical_motion_server` | Always `not_evaluated` | | `physical_motion` | Always `not_evaluated` | | `execution_authority` | Always `none` | A plan result grants no authority to move anything. Authority comes from the rt-control lease when you Load and Play. See [One controller at a time](https://advancedmetalresearch.com/docs/get-started/safety-model#one-controller). When a plan comes back without a `.rdt`, the planner keeps the request and its full result under `/refused/`, named by the request digest, so you can diagnose it without planning again. It keeps the newest 5. The `.rdt` header carries the plan identity (`plan_id`, `program_id`, `program_digest`, revisions) and the robot description and cell calibration identities the plan was made against. `program_digest` is `sha256:` plus the digest of the `.weldplan` request, and `plan_id` is `:`. OLP's Load refuses a plan made for another robot description or cell calibration. See [Robot description and coordinate frames](https://advancedmetalresearch.com/docs/concepts/robot-description-and-frames#identity). ## Related pages - [Run the weld planner](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner) - [Weld planner HTTP API](https://advancedmetalresearch.com/docs/apis/weld-planner-http) - [Weld program and `.weldplan` container](/docs/reference/weld-program-format) - [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning) - [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `weld_planner/v1/python/weldplan/native_admission.py:1-5,137-250` - `weld_planner/v1/python/weldplan/dense_joint_trajectory.py:90-110,508-509,655-797` - `weld_planner/v1/python/weld_motion_planner/verifier/verify.py:1-127,460-606,844-1015,1640-1687,1877-2060` - `weld_planner/v1/python/weld_motion_planner/verifier/model.py:20-39` - `weld_planner/v1/python/weld_motion_planner/robot_cell/collision/model.py:1-24` - `weld_planner/v1/python/weld_motion_planner/planner/planner_main.py:1-27,64-96,160-180` - `weld_planner/v1/python/weld_motion_planner/planner/dp_seam_search/dp_seam_search_main.py:1-22` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/options.py:21-40` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/weld_trajopt.py:245-294,494-498` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/taught.py:1-60` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/freespace.py:2435-2464` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/control_solver.py:94,175-178` - `weld_planner/v1/python/weld_motion_planner/io/native_result.py:17-164` - `weld_planner/v1/python/weld_motion_planner/io/dense_output.py:1-139` - `weld_planner/v1/python/weld_motion_planner/server/motion_planner_server.py:78-82,137-179,353-377` - `weld_planner/v1/data/dense_joint_trajectory.params.json` - `offline-programming/v1/weld_plan.go:424-438` - `offline-programming/v1/internal/denseexec/session.go:236-258` --- # Cells, machines and positioners > How drive, robot, machine and cell configuration layer and pin each other, how a cell's identity is checked, and how the Rosie 1400 H-frame positioner is modelled. URL: https://advancedmetalresearch.com/docs/concepts/cells-and-positioners Section: RosieOS docs / Concepts Last updated: 2026-10-10 A running cell is described by four layers of configuration. Each layer pins the one below it by content hash, so every process can tell whether it is talking to the cell it expects. This page explains the layers, how they compile into one identity, and how the Rosie 1400's two-table positioner fits in. ## Four layers | Layer | Where | What it holds | |---|---|---| | **Drive config** | `rt-core/config/drives/.json` | One drive or I/O terminal model: EtherCAT identity, PDO layout, scaling semantics, startup parameters, Home transactions, brake and jog policy, collision bounds. | | **Robot description** | `robot_description/robots//` | The robot: `robot.urdf` and meshes, the planner's `config.json`, collision `spheres.json`, and rt-core's `rtcore_definition.json` (joint mapping and mechanics). A manifest registers the files, and the description's identity is a SHA-256 over them. | | **Machine config** | `rt-core/config/machines/`, `machines/bench/`, `machines/simulation/` | Binds drives and, optionally, a robot description to bus positions, cycle timing, limits, cell I/O, control timing and host runtime policy. | | **Cell config** | `rt-core/config/cells/.json` | Binds one deployed cell: which machine config, its compiled digest, the pair identity, and the remote listener. | Each layer has an annotated template in `rt-core/config/templates/` (`drive.json`, `robot.json`, `machine.json`, `cell.json`). Every field there has a `_doc` sibling giving its unit, allowed values and default. The field-by-field tables are in [Configuration files](https://advancedmetalresearch.com/docs/reference/configuration). ## Compiling a machine into an identity `rtctl compile` resolves a machine config, the drive configs it names and any robot description it pins. It writes one immutable directory named after the result, the **configuration digest**: ```bash cd rt-core build/rtctl compile --config config/machines/simulation/simulation.json --out build/config # build/config/ ``` The directory holds `axes.conf`, `argv.json` (the core's command line), the identity artifacts and `resources.json`, the robot files that `rt-control` serves to clients. Machine, deployment and configuration digests are distinct, and Describe reports all three. The digest travels with the cell: - `rt-control` is started with `--configuration-sha256` and refuses a core that reports another identity. - Every `acquire` must present the same digest in its binding, with the pair id and revision. - OLP refuses a cell whose deployed digest differs from its catalogue entry, with `cell_configuration_mismatch`. Change any layer and the digest changes. You then recompile, and update every pin that referred to the old digest. ### Robot-bound limits For an axis mapped to a robot joint, the compiler takes travel and maximum velocity from `robot.urdf`, and acceleration from `config.json` (`planning.joints..acceleration_rad_s2`). A machine config cannot override those values. The per-axis drive speed cap is derived from the same URDF velocity. A cell can narrow its limits further with a per-cell `machine_planning_calibration.json`. That file lives with the cell, not in the repository, and the machine config pins it by path and hash. It can narrow joint limits and apply small, capped kinematic corrections. It can never widen a limit. ## Cell config The cell config is the deployment pin. Its native binding has these fields: | Field | Type | Description | |---|---|---| | `name` | string | Display name. Ignored by the validator. | | `nodes[].runtime` | string | `rt-core` for a native node. | | `nodes[].rt_core.machine_config` | path | Repository-relative path to the machine config. | | `nodes[].rt_core.configuration_sha256` | 64 hex | The exact `rtctl compile` digest. Pin it on every deployed cell. | | `nodes[].rt_core.pair_id` | string | Deployment pair identity: letters, digits, `_`, `.` or `-`. | | `nodes[].rt_core.pair_revision` | uint64 | Positive pair revision. Raise it to retire old authority. | | `nodes[].rt_core.remote_listen` | host:port | The mutual-TLS listener, for example `127.0.0.1:8443`. | The validator in `rt-core/config/cells` recompiles every cell's machine config and refuses the cell if the digest, axis count, pair or listener is wrong, or if the compiler printed warnings: ```bash cd rt-core go test -count=1 ./config/cells -run TestRepositoryCompatibility ``` ## Robot models | Model | Axes | Base, tool, work frames | Notes | |---|---|---|---| | `rosie_1400_v3` | J1–J6 arm, J7–J9 positioner | `world`, `tool0`, `positioner_table_a_top` | Rosie 1400 on an H-frame two-table positioner, the layout of the War Machine cell | | `rosie_1420_v1` | J1–J6 | `world`, `tool0`, `world` | Rosie 1420 six-axis arm on a pedestal | | `bench_one_motor_1to1`, `bench_nine_motors_1to1` | 1 or 9 bare motors | — | Motor benches: a manifest and an rt-core definition, no URDF. OLP does not list or plan for them. | Frames, joint chains and units are covered in [Robot description and coordinate frames](https://advancedmetalresearch.com/docs/concepts/robot-description-and-frames). ## The H-frame positioner The Rosie 1400 description models the positioner as three revolute joints. They are driven by the same core as the arm, at the same 1 kHz cycle. | Joint | Parent → child | Axis | URDF limit | Role | |---|---|---|---|---| | J9 | `floor` → `h_frame` | +z (vertical) | ±π rad, 3.14 rad/s | Turns the whole H-frame | | J7 | `h_frame` → `positioner_table_a` | +y (horizontal) | ±π rad, 3.14 rad/s | Rotates table A | | J8 | `h_frame` → `positioner_table_b` | −y (horizontal) | ±π rad, 3.14 rad/s | Rotates table B | The two tables sit either side of the H-frame. `rosie_1400_v3/config.json` sets what the planner uses: - **Driven axes:** J1 to J7. The planner moves the arm and table A together. - **Held axes:** J8 = 0 and J9 = 0. The planner keeps them fixed. - **Work frame:** `positioner_table_a_top`, a surface on `positioner_table_a` at `xyz_m` [0, −0.622, 0.1]. Weld geometry is expressed on table A. Dense programs always carry nine axis columns in the order J1–J6, J7/J8, J9. The header's axis mask is `0x1ff` for `rosie_1400_v3` and `0x3f` for `rosie_1420_v1`. See [Dense trajectory format](https://advancedmetalresearch.com/docs/reference/rdt-format). > [!NOTE] Gear ratios, encoder resolution and drive parameters live in each model's `rtcore_definition.json` and in the drive configs. These docs describe their schema, not their values. ## Simulation machines `rt-core/config/machines/simulation/` holds machine configs for the simulated backend: | File | Axes | Use | |---|---|---| | `simulation-program.json` | 9 flat axes J1–J9, no Home requirement | Direct SDK examples. `dev-stack.sh` also starts from it, adds the Rosie 1400 URDF limits and requires Home on every axis. | | `simulation-rosie1400.json` | 9 axes bound to `rosie_1400_v3` | Serves the real robot description to OLP. Its Home is refused on the simulated bus, so nothing arms on it yet. | | `simulation.json`, `simulation-linear.json`, `simulation-scalars.json`, `simulation-home.json` | 1–2 flat axes | Small fixtures for tests and compile examples | Choose the dev-stack machine with `DEV_STACK_MACHINE_CONFIG`. See [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation). ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/ipcclient/client_linux.go:369-370,816` - `offline-programming/v1/internal/targets/cells.go:168,218` - `offline-programming/v1/internal/denseexec/rt_core.go:1459` - `robot_description/go/cell.go:6-14,188-196` - `rt-core/tools/rtctl/robot_definition.go:658` - `rt-core/config/templates/cell.json` - `rt-core/config/templates/machine.json:228-233` - `rt-core/config/cells/compatibility.go:20-24,41-170` - `rt-core/tools/rtctl/command.go:25-45` - `rt-core/cmd/rt-control/main.go:27,58-66` - `rt-core/adapters/rosie/control/api_generated.go:385-390` - `rt-core/ipcclient Description (configuration_sha256, machine_sha256, deployment_sha256)` - `robot_description/robots/rosie_1400_v3/config.json` - `robot_description/robots/rosie_1400_v3/robot.urdf:196-218` - `robot_description/robots/rosie_1420_v1/config.json` - `robot_description/go/cell.go:39-114` - `weld_planner/v1/python/weld_motion_planner/io/dense_output.py:142-166` - `motion-server/joint-trajectory/v1/src/dense_joint_trajectory_format.hpp:38` - `rt-core/config/machines/simulation/*.json` - `motion-server/v1/local-rt-core.sh:17-60` --- # Process I/O and sensing: current status > What RosieOS does today with cell digital I/O, why torch outputs are refused everywhere, and the fact that no seam tracking or sensing is implemented. URL: https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing Section: RosieOS docs / Concepts Last updated: 2026-10-10 RosieOS moves the robot. It does not yet run the welding process. This page states exactly what exists, so you do not plan around features that are not there. | Capability | Status | |---|---| | Cell digital inputs and non-torch outputs | Implemented in rt-core. Not qualified on hardware. | | Torch output (arc on/off) | **Refused** everywhere | | Weld parameters (wire, gas, arc, travel speed) | Stored as program and preset data. Nothing sends them to a power source. | | Seam tracking and sensing (laser, vision, touch, through-arc) | **Not implemented** | ## Cell I/O in rt-core A machine config can declare one digital I/O terminal in its `io` block, selected by `io.profile` from `rt-core/config/drives/`. It has up to eight inputs and eight outputs. | Field | Values | Meaning | |---|---|---| | `io.torch_qualified` | `false` only | Torch outputs are unqualified. The compiler accepts no other value. | | `io.inputs[].class` | `fast` or `supervisory` | A `fast` input must be satisfied for outputs to stay on. | | `io.inputs[].polarity` | `active_high` or `active_low` | | | `io.outputs[].class` | `process` or `torch` | `torch` outputs are always refused. | | `io.outputs[].safe_state` | `false` only | Every output's safe state is OFF. | | `io.outputs[].expiry_ns` | 1 to 1,000,000,000 ns | An ON intent lapses after this long unless renewed. | | `io.outputs[].readback_bit`, `readback_polarity` | input bit 0–7 | Independent physical feedback for the output. | A process output turns on only when all of these hold: 1. The session holds a current grant and has armed I/O with `io_arm`. `io_arm` requires a fresh I/O exchange, satisfied fast inputs and OFF readback on every output after the last fence. 2. A trajectory point sets the output's bit in `io_mask` and `io_values`, both 8-bit fields in configured output order. 3. The core's final output permit holds for that cycle. An output goes OFF on its own when its expiry passes. The core drops every output to OFF and disarms I/O when any of these happens: - Stop or a new generation fences the session - the permit lapses - a fast input drops - the I/O exchange is lost - readback still disagrees with the commanded value three exchanges after a change Intent and readback are reported separately in status. A commanded value is never taken as proof that the output switched. Reason codes include `io_not_configured`, `io_not_armed`, `io_fast_input_unsatisfied`, `io_readback_disagreement`, `io_marker_late` and `io_exchange_lost`. See [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes). > [!WARNING] Cell I/O is a process interlock, not a safety function. A `fast` input is not a safety input. Wire guards and the E-stop into the hardware safety chain. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model#hardware-e-stop). The channel counts, expiry ceiling and readback window above are marked unverified on hardware in the code (`io_channels_unverified`, `io_readback_cycles_unverified`). ## Torch outputs are refused Switching the arc is refused at every layer until it is qualified on hardware: - **rt-core** refuses any intent on a `torch`-class output with `io_torch_unqualified`, and every machine config must declare `torch_qualified: false`. - **The dense trajectory daemon** refuses a `.rdt` that contains torch samples with `native_torch_unsupported` and the consumer action `remove_torch_samples`. - **OLP** refuses to load a program that still requires process outputs. Choose the run mode **Dry run · process outputs off**, which strips them before Load. The motion is unchanged. The OLP STOP button's tooltip mentions dropping the torch. That describes the Stop path, which drives every output OFF. It does not mean the torch output works. ## No seam tracking or sensing RosieOS has no laser, vision, touch or through-arc sensing, and no runtime path correction. The robot follows the planned trajectory exactly. The word "tracking" in the weld planner means something else. It is the **tracking certificate**: a geometric bound on how far the planned tool centre point (TCP) may deviate from the seam model. The default tolerance is 0.5 mm, unless the weld's own `tolerance.position_mm` sets another. The verifier checks it before a program is admitted. It says nothing about where the real seam is. See [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification). ## Weld parameters are data Weld presets in `weld_planner/v1/data/presets/weld/` describe a process: method, ISO 4063 number, travel speed, arc, wire, gas, weave, and start and end behaviour. Torch presets are in `data/presets/torch/`. The planner uses travel speed and torch geometry to plan motion. The arc, wire and gas fields are carried with the program, but nothing drives a welding power source from them. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/include/cell_io.hpp:13-24,37-60,134-200` - `rt-core/config/templates/machine.json (io block and _doc fields)` - `rt-core/protocol/application-v1.schema.json (rules.cell_io, types Point io_mask/io_values, reasons io_*)` - `rt-core/engine/cycle_machine.hpp:3540-3545` - `motion-server/joint-trajectory/v1/src/rt_control_executor.hpp:148-156` - `offline-programming/v1/internal/denseexec/rt_core.go:772-777,797-800` - `offline-programming/v1/ui/src/execution/densePanel.ts:79-81,541-544` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/weld_trajopt.py:496-497` - `weld_planner/v1/data/presets/weld/gmaw-steel-fillet.json` --- # Run everything in simulation > Use dev-stack.sh to run the simulated core, rt-control, the dense daemon, NATS, the virtual pendant, the weld planner and the offline programming app on one machine, and check it with the smoke test. URL: https://advancedmetalresearch.com/docs/guides/run-in-simulation Section: RosieOS docs / Guides Last updated: 2026-10-10 `dev-stack.sh`, at the repository root, runs a whole simulated cell on one Linux or WSL2 machine. Each service runs in its own session, with a pid file and a log. `stop` kills exactly what `start` began, and nothing else. ```bash export DEV_STACK_UI_HOST=127.0.0.1 DEV_STACK_FIREWALL=0 ./dev-stack.sh start # every service ./dev-stack.sh start rt-sim rt-control olp ui # or only the ones you name ./dev-stack.sh status ./dev-stack.sh logs olp # tail one or more logs ./dev-stack.sh restart ui ./dev-stack.sh stop ``` With no names, a command applies to every service. If you are new to the stack, start with the [Quickstart](https://advancedmetalresearch.com/docs/get-started/quickstart). ## Services `start` launches the services in this order. It waits for `rt-sim`, `rt-control` and `pendant` to become healthy before it moves on. | Service | What runs | Endpoint | Needs | If missing | |---|---|---|---|---| | `catalog` | External program-catalog script | `http://127.0.0.1:8787` | `CATALOG_DAEMON_SCRIPT` | Skipped. The script is not in this repository. | | `nats` | `nats-server -a 127.0.0.1 -p 14222 -m 18222` | `nats://127.0.0.1:14222` | `nats-server` 2.14+ at `DEV_STACK_NATS_BIN` | Skipped | | `rt-sim` | `rosie-rt-core-sim` with a compiled simulation machine | native `ipc.sock` | `make -C rt-core control sim` (built automatically if missing) | — | | `rt-control` | `rt-control --backend simulation --pair-id local-dev --pair-revision 1` | `control.sock`, `jog.sock` | `rt-sim` healthy | — | | `daemon` | `joint_trajectory_daemon` for cell `dev-cell` | TCP `127.0.0.1:8797` | `make -C motion-server/joint-trajectory/v1 all` | Skipped | | `pendant` | Virtual pendant bridge, `robot-v4-sim` adapter | `http://127.0.0.1:51712` | Go; built from `steamdeck/virtual` on start | — | | `motion` | Weld planner, `pixi run -e motion motion-serve` | `http://127.0.0.1:8796` | `weld_planner/v1` motion env, NVIDIA GPU | Fails. Its log says why. | | `olp` | `offline-programming/v1/start-offline-programming.sh` | `http://127.0.0.1:8794` | Go, the Tesseract env, a C++ compiler | — | | `ui` | Vite for the OLP UI | `http://127.0.0.1:5189/offline-programming/v1/ui/` | `npm ci` in `offline-programming/v1/ui` | — | `daemon`, `pendant` and `olp` all refuse to start until `rt-control` is healthy. Before each one starts, the stack runs `motion-server/v1/local-rt-core.sh bind`. That command writes a consumer binding for the running simulator, so every client targets the same pair and configuration digest. The `olp` log is the one to read when something is off. It reports its state in lines prefixed `olp-start:`: - `rails:` says whether the seam worker (the weld planner's `default` env) is available. - `motion:` names the weld planner origin used for planning and dense playback. `olp` takes the longest to start, because the launcher builds whatever is missing and waits up to 270 s for its local simulator. ## The simulated machine By default, `local-rt-core.sh prepare` starts from `rt-core/config/machines/simulation/simulation-program.json`, nine flat axes named J1 to J9. It rewrites each axis's travel and velocity limits from the Rosie 1400 URDF, switches the axes to a simulation drive profile that supports Home, and requires Home on every axis. It then compiles the result with `rtctl compile`. The simulated robot therefore behaves like a real cell, in order: Home, Arm, then move. To bind the actual Rosie 1400 description instead, set: ```bash export DEV_STACK_MACHINE_CONFIG=rt-core/config/machines/simulation/simulation-rosie1400.json ``` The cell then serves the same robot description that OLP plans against. But that machine uses the real drive profile, and the simulated bus does not answer its Home objects. Home is refused, so nothing arms or jogs on it. Leave it unset unless you are working on that gap. > [!NOTE] The simulator runs rt-core's state machine over a simulated bus. It does not model dynamics, contact or the drives' own control loops, and it does not qualify anything for powered motion. The separate `mujoco-sim/v1` project is an experimental physics model, not a stand-in for the controller. ## Where files go | Path | Default | Contents | |---|---|---| | Stack directory, `ROSIE_DEV_STACK_DIR` | `${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/rosie-stack-$UID` | pid files, `.log`, readiness files | | Runtime directory, `ROSIE_LOCAL_RT_DIR` | `${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/rosie-dev-rt-$UID` | `ipc.sock`, `control.sock`, `jog.sock`, the simulator's `metrics.json` | | Build directory, `ROSIE_LOCAL_RT_BUILD` | `rt-core/build/dev-stack` | `machine.json`, `config//`, `binding.env`, `olp-cells.json`, `olp-rt-core.json`, `daemon-rt-core.json`, `dense-plans/` | `binding.env` exports the variables a client needs to reach the simulator: `ROSIE_RT_CONTROL_SOCKET`, `ROSIE_RT_JOG_SOCKET`, the pair, the configuration digest and the URDF path and hash. Source it to point your own tools at the stack: ```bash source rt-core/build/dev-stack/binding.env rt-core/build/rtctl describe --socket "$ROSIE_RT_CONTROL_SOCKET" --json | head -c 300; echo ``` To change the simulation paths, stop the whole stack first. `start` refuses to change `ROSIE_LOCAL_RT_DIR` or `ROSIE_LOCAL_RT_BUILD` under a running stack. ## Useful overrides | Variable | Default | Effect | |---|---|---| | `DEV_STACK_UI_HOST` | `127.0.0.1`; `0.0.0.0` when a Tailscale address is found | Where the UI binds | | `DEV_STACK_UI_MODE` | `dev`; `built` when a Tailscale address is found | `dev` hot-reloads. `built` serves a compressed bundle and needs `restart ui` after edits. | | `DEV_STACK_FIREWALL` | `1` | `0` skips the helper that adds a `ufw` rule for remote UI access | | `DEV_STACK_MACHINE_CONFIG` | `rt-core/config/machines/simulation/simulation-program.json` | The simulated machine, described above | | `DEV_STACK_PENDANT_PORT` | `51712` | Virtual pendant bridge port | | `DEV_STACK_DENSE_INGEST` | `127.0.0.1:8797` | Dense daemon TCP ingest | | `STACK_WAIT_SECS` | `300` | How long `start` waits for health | The complete list is in [Ports, sockets and environment](https://advancedmetalresearch.com/docs/reference/ports-and-environment). > [!WARNING] `DEV_STACK_OLP_CELLS` and `DEV_STACK_OLP_REMOTE` add real cells to the OLP machine list, alongside the local simulation. Once they are set, the same UI can arm and move hardware. Read [Connect to a cell](https://advancedmetalresearch.com/docs/guides/connect-a-cell) and the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model) first. ## Run the smoke test The composed smoke test checks that the stack works end to end. It builds the core and both motion servers, and starts `rt-sim`, `rt-control` and `pendant` in a private directory. It then runs two Go tests against them: one dense program that must complete, and one OLP jog that must move the simulated axes. ```bash export TMPDIR=/dev/shm/rt-core/smoke mkdir -p "$TMPDIR" bash motion-server/v1/tests/dev-stack-smoke.sh ``` It ends with `dev-stack smoke: rt_core PASS (public jog motion and dense completion)`, and stops what it started. ## Without the dev stack The OLP launcher also works on its own: ```bash bash offline-programming/v1/start-offline-programming.sh ``` If no dev-stack binding is live, it builds rt-core and starts its own simulator and `rt-control` in a temporary directory. It stops them again when you press Ctrl+C. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `dev-stack.sh:1-80,172-193,240-297,299-395,436-470,555-613` - `motion-server/v1/local-rt-core.sh:6-130` - `motion-server/v1/tests/dev-stack-smoke.sh:1-30` - `motion-server/joint-trajectory/v1/Makefile:7,16-17` - `offline-programming/v1/start-offline-programming.sh:16-36,68-110,490-515,620-668` - `weld_planner/v1/pixi.toml:156` - `rt-core/config/machines/simulation/simulation-program.json` - `rt-core/config/machines/simulation/simulation-rosie1400.json` - `steamdeck/virtual/cmd/local/main.go` --- # Simulate a Rosie robot > Download the URDF, MuJoCo model, STEP and robot.json of the Rosie 600, 1000 or 1400 and run it in PyBullet, MuJoCo or ROS 2, with the robot's own kinematics, joint limits, speeds, torques and mass properties. URL: https://advancedmetalresearch.com/docs/guides/simulate-a-rosie-robot Section: RosieOS docs / Guides Last updated: 2026-10-10 Each Rosie robot has a free simulation and CAD kit: a URDF, a MuJoCo model, an SRDF, a STEP assembly, meshes and `robot.json`, plus quickstart scripts and a ROS 2 package. The geometry is the robot's exterior, one filled solid per link: the structural parts keep the CAD's own surfaces, and the motors and gearboxes are plain envelopes of the same outer size. The kinematics, joint limits, speeds, torques, payload ratings and mass properties are the robot's own. To run RosieOS itself against a simulated cell, see [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation). The kits use the same joint names, axes and zero pose as RosieOS's robot descriptions, so joint values carry over unchanged. ## Downloads | Robot | Kit | robot.json | URDF | MuJoCo | STEP | GLB | IK cases | |---|---|---|---|---|---|---|---| | Rosie 600 | [rosie-600-sim-kit.zip](https://advancedmetalresearch.com/assets/sim/rosie-600-sim-kit.zip) | [robot.json](https://advancedmetalresearch.com/assets/sim/600/robot.json) | [rosie_600.urdf](https://advancedmetalresearch.com/assets/sim/600/urdf/rosie_600.urdf) | [rosie_600.xml](https://advancedmetalresearch.com/assets/sim/600/mjcf/rosie_600.xml) | [rosie_600.step](https://advancedmetalresearch.com/assets/sim/600/step/rosie_600.step) | [rosie_600.glb](https://advancedmetalresearch.com/assets/sim/600/glb/rosie_600.glb) | [ik_cases.json](https://advancedmetalresearch.com/assets/sim/600/ik_cases.json) | | Rosie 1000 | [rosie-1000-sim-kit.zip](https://advancedmetalresearch.com/assets/sim/rosie-1000-sim-kit.zip) | [robot.json](https://advancedmetalresearch.com/assets/sim/1000/robot.json) | [rosie_1000.urdf](https://advancedmetalresearch.com/assets/sim/1000/urdf/rosie_1000.urdf) | [rosie_1000.xml](https://advancedmetalresearch.com/assets/sim/1000/mjcf/rosie_1000.xml) | [rosie_1000.step](https://advancedmetalresearch.com/assets/sim/1000/step/rosie_1000.step) | [rosie_1000.glb](https://advancedmetalresearch.com/assets/sim/1000/glb/rosie_1000.glb) | [ik_cases.json](https://advancedmetalresearch.com/assets/sim/1000/ik_cases.json) | | Rosie 1400 | [rosie-1400-sim-kit.zip](https://advancedmetalresearch.com/assets/sim/rosie-1400-sim-kit.zip) | [robot.json](https://advancedmetalresearch.com/assets/sim/1400/robot.json) | [rosie_1400.urdf](https://advancedmetalresearch.com/assets/sim/1400/urdf/rosie_1400.urdf) | [rosie_1400.xml](https://advancedmetalresearch.com/assets/sim/1400/mjcf/rosie_1400.xml) | [rosie_1400.step](https://advancedmetalresearch.com/assets/sim/1400/step/rosie_1400.step) | [rosie_1400.glb](https://advancedmetalresearch.com/assets/sim/1400/glb/rosie_1400.glb) | [ik_cases.json](https://advancedmetalresearch.com/assets/sim/1400/ik_cases.json) | Every file of every kit also has its own URL under `/assets/sim//`, with the same layout as the zip. [`/assets/sim/index.json`](/assets/sim/index.json) lists the kits and all their files. The URDF and MJCF reference their meshes by relative path, so download the zip (or the whole folder) rather than the single file. ## What a kit contains rosie_1000_description/: ```text README.md conventions, joint table, quickstart (the same code as below) LICENSE.txt robot.json kinematics, limits, speeds, torques, accelerations, payloads, mass properties, frames, FK reference poses, benchmark cycle ik_cases.json IK reference cases solved by RosieOS's IK, self-contained benchmark/ the benchmark pick-and-place cycle with 1 kg: t, q, qd, qdd every 1 ms (CSV and JSON) glb/rosie_1000.glb glTF binary, one node per link in the joint hierarchy urdf/rosie_1000.urdf mjcf/rosie_1000.xml MuJoCo 3.1 or later srdf/rosie_1000.srdf planning group "manipulator", named poses, disabled collision pairs (MoveIt) step/rosie_1000.step AP214 assembly, mm, one solid per link (exact CAD surfaces) at the home pose meshes/visual/ one STL per link, link frame, metres meshes/collision/ convex pieces per link, link frame, metres launch/ rviz/ package.xml CMakeLists.txt ROS 2 package rosie_1000_description examples/ pybullet_demo.py, mujoco_demo.py, fk_ik.py ``` ## Frames and conventions - Units: metres, kilograms, radians, seconds. The STEP is in millimetres. - Joints `J1` to `J6` connect `base_link`, `link_1` ... `link_6`. Every link frame is parallel to `base_link` at the zero pose and sits on its joint, so each joint origin is a pure translation. - Axes: J1 z, J2 y, J3 y, J4 x, J5 y, J6 z. Positive follows the right-hand rule. - Zero pose is home: upper arm vertical, forearm along +x, tool flange facing down. - `tool0` is on the tool flange face (the ISO 9409 mounting face), z out of the flange, x along base +x at home. Put your tool's TCP at a fixed offset from `tool0`. - `base_footprint` is the floor or mounting face under J1, and the root of the URDF. `base_link` is the robot base frame above it: 175 mm for the Rosie 600 and 1000, 29.4 mm for the Rosie 1400 (its CAD base frame, inside the base plate). The STEP's origin is `base_link`. | Robot | Wrist centre at J2 = 90°, J3 = -90° | Published reach (wrist / flange) | tool0 at home | Mass | |---|---|---|---|---| | Rosie 600 | 600.0 mm from the J1 axis | 600 / 677 mm | (0.330, 0, 0.423) m | 29.3 kg | | Rosie 1000 | 1,000.0 mm | 1,000 / 1,077 mm | (0.530, 0, 0.623) m | 38.6 kg | | Rosie 1400 | 1,433.7 mm | 1,400 / 1,499 mm | (0.834, 0, 0.756) m | 77.0 kg | The Rosie 1400's published reach is 1,400 mm; its geometry stretches to 1,433.7 mm. `robot.json` gives both, under `reach`. ## Quickstart Download and run a kit: Terminal: ```bash curl -LO https://advancedmetalresearch.com/assets/sim/rosie-1000-sim-kit.zip unzip -q rosie-1000-sim-kit.zip && cd rosie_1000_description pip install mujoco pybullet numpy python examples/mujoco_demo.py # also: pybullet_demo.py, fk_ik.py ``` The snippets below run from inside the kit folder. They are written for the Rosie 1000; for another robot, change `1000` to `600` or `1400`. **MuJoCo** mujoco_hold.py: ```python import mujoco import numpy as np model = mujoco.MjModel.from_xml_path("mjcf/rosie_1000.xml") data = mujoco.MjData(model) data.ctrl[:] = np.radians([30, 45, -10, 0, -35, 0]) # position servos on J1..J6 for _ in range(1500): # 3 s at 2 ms steps mujoco.mj_step(model, data) print(np.degrees(data.qpos).round(2)) # joint angles (deg) print(data.actuator_force.round(1)) # joint torques (N m) print(data.site("tool0").xpos.round(4)) # flange face (m) ``` **PyBullet** pybullet_move.py: ```python import math import pybullet as p p.connect(p.DIRECT) # p.GUI for a window p.setGravity(0, 0, -9.81) robot = p.loadURDF("urdf/rosie_1000.urdf", useFixedBase=True, flags=p.URDF_USE_INERTIA_FROM_FILE) joints = [p.getJointInfo(robot, i) for i in range(p.getNumJoints(robot))] arm = [j for j in joints if j[2] == p.JOINT_REVOLUTE] # J1..J6 tool0 = next(j[0] for j in joints if j[12] == b"tool0") target = [math.radians(a) for a in (30, 45, -10, 0, -35, 0)] for j, q in zip(arm, target): # j[10] peak torque, j[11] max speed p.setJointMotorControl2(robot, j[0], p.POSITION_CONTROL, targetPosition=q, force=j[10], maxVelocity=j[11]) for _ in range(3 * 240): p.stepSimulation() print(p.getLinkState(robot, tool0, computeForwardKinematics=True)[4]) ``` **ROS 2** Terminal, in a ROS 2 workspace: ```bash cp -r rosie_1000_description ~/ros2_ws/src/ cd ~/ros2_ws && colcon build --packages-select rosie_1000_description source install/setup.bash ros2 launch rosie_1000_description display.launch.py # RViz with joint sliders ``` **Kinematics** fk.py, numpy only: ```python import json import urllib.request import numpy as np URL = "https://advancedmetalresearch.com/assets/sim/1000/robot.json" req = urllib.request.Request(URL, headers={"User-Agent": "rosie-sim-kit/1.0"}) robot = json.load(urllib.request.urlopen(req)) def rot(axis, q): x, y, z = axis c, s, t = np.cos(q), np.sin(q), 1 - np.cos(q) return np.array([[t * x * x + c, t * x * y - s * z, t * x * z + s * y], [t * x * y + s * z, t * y * y + c, t * y * z - s * x], [t * x * z - s * y, t * y * z + s * x, t * z * z + c]]) def fk(q): """Pose of tool0 (4x4) in base_link for joint angles q (rad).""" T = np.eye(4) for j, qi in zip(robot["joints"], q): A = np.eye(4) A[:3, 3], A[:3, :3] = j["origin_xyz"], rot(j["axis"], qi) T = T @ A return T @ np.diag([1, -1, -1, 1]) # tool0: z out of the flange print(fk(robot["poses"]["stretched"])[:3, 3]) # (m) ``` `examples/fk_ik.py` in each kit adds a damped least-squares inverse kinematics solver that respects the joint limits. The URDF's mesh paths are relative to the file, which PyBullet, Isaac Sim and most URDF libraries resolve directly. ROS 2 needs `package://` URIs: `display.launch.py` rewrites them on load, or run `sed 's|filename="../meshes/|filename="package://rosie_1000_description/meshes/|'` over the URDF yourself. ## robot.json | Key | Contents | |---|---| | `joints[]` | `name`, `parent`, `child`, `origin_xyz` (m), `axis`, `lower` / `upper` (rad and deg), `max_velocity` (rad/s and deg/s), `effort_peak` and `effort_continuous` (N·m), `drive_peak_torque_at_joint` (N·m), `armature` (kg·m²), `damping`, `derived_max_acceleration` (rad/s² and deg/s²) | | `links[]` | `mass` (kg), `com` (m) and `inertia` (kg·m², about the CoM, link frame), mesh paths | | `frames` | `base_footprint`, `base_link` (height above the floor) and `tool0` | | `home`, `poses` | Joint angles of `home`, `work` and `stretched` (rad) | | `fk_reference[]` | For each pose: wrist centre, `tool0` position and rotation, to check your own kinematics against | | `ik_reference` | Where the IK reference cases are (`ik_cases.json`), the solver and the branch flags | | `motion` | Motion profiles RosieOS uses, its acceleration and jerk settings, and how `derived_max_acceleration` is computed | | `benchmarks` | The cycle behind the published cycle times: path, TCP, payload, profile, accelerations, segment times, trajectory files | | `reach`, `payload`, `repeatability_mm`, `mass` | Published ratings, and the geometric reach | | `geometry` | How the geometry was made and how it compares with the CAD | ## Accelerations and motion profiles - `joints[].derived_max_acceleration` is each joint's peak-torque acceleration from standstill at the stretched pose with the rated payload, computed from `robot.json` itself: (`effort_peak` − |gravity torque|) / (`armature` + the inertia about the joint axis of everything it moves). It is conservative: `effort_peak` is the torque the gearbox passes to the arm, while the motor accelerates its own rotor ahead of the gearbox with its own torque, up to `drive_peak_torque_at_joint`. `motion.derived_max_acceleration` also gives the values at the `work` pose with 1 kg. - `benchmarks.acceleration` lists the accelerations behind the published cycle times: 80 % of each joint's peak-torque acceleration at the cycle poses, from the drive model. - `motion.rosieos_settings` (Rosie 1400 only) lists the planning settings of RosieOS's simulator model, `rosie_1400_v3`. They are software settings for that simulated cell, well under the robot's capability, not ratings. RosieOS states no per-joint jerk limit: its planner bounds jerk at 2 × the acceleration setting / 0.2 s, and jog, stop ramps and Move-to are jerk-limited by the cell's machine jog jerk where one is set. - Profiles: RosieOS plans programs as joint-space cubic splines (free-space moves as clamped cubic B-splines, so acceleration is continuous; welds as cubic Hermite curves) within the velocity and acceleration settings, and plays them as planned. Jog, stops and Move-to are jerk-limited (double-S). The benchmark cycle uses plain trapezoids. ## Benchmark cycle The published cycle times ("Cycle, 25 / 305 / 25 mm, 1 kg" and "Cycle at rated payload" in the [specification](https://advancedmetalresearch.com/rosie#specs)) come from one model, and `robot.json` `benchmarks` gives every condition it used: - Path: the tool points straight down and keeps its heading. Pick point A and place point B are 305 mm apart along y, centred 350 mm from the J1 axis on +x. The TCP lifts 25 mm at each: A low, A high, B high, B low, then back the same way. `benchmarks.path.tcp_points_base_link_m` gives the points in `base_link`. The low points are 50 mm above `base_link` on the Rosie 600 and 1000 and 75.5 mm above it on the Rosie 1400. - TCP: 50 mm out from the flange face along the tool axis. - Payload: everything on the flange as one body, gripper included, with no other tool mass. That is 1 kg for the 1 kg cycle and the rated payload for the other. The CoM is 100 mm out along the tool axis and 50 mm off it, with the inertia of a uniform 100 mm cube. AMR defines no standard gripper or tool. - Moves: six joint-space moves, each from rest to rest, with no blending, dwell or gripper time. Every joint follows a trapezoid at its benchmark acceleration, and the segment takes the slowest joint's time at `max_velocity`. `benchmark/rosie__cycle_1kg.csv` (and `.json`) is that cycle with 1 kg, sampled every 1 ms: time, `q`, `qd` and `qdd` for J1 to J6, and the TCP. Its duration rounds to the published time. `benchmarks.notes` records how the model's payload and mass inputs relate to the published figures. `benchmarks.kit_model_check` gives the most torque each joint of the kit's MuJoCo model needs to follow the trajectory, as a fraction of its force limit; every joint stays within it. ## IK reference cases Each kit's `ik_cases.json` is a standalone file, also online at `/assets/sim//ik_cases.json`. It holds twelve cases solved by RosieOS's own IK: the Cartesian resolver `robot-v4-cartesiand`, the same solver the pendant and offline programming use for Cartesian moves. Each case gives: - a seed pose; - the straight Cartesian move RosieOS resolved from it; - the target pose of `tool0` in `base_link`; - the solution RosieOS reached (`expected`, with its shoulder, elbow and wrist branch); - every other solution inside the joint limits (`all_solutions`). The file carries the kinematic chain, units, frames, tolerance, the solver's method and its RosieOS source files and commit. RosieOS has no robot description of the Rosie 600 or 1000, and its Rosie 1400 description is the simulator's cell model, so the solver ran on each kit's own chain; the `solver.provenance` field says so for each robot. This check needs only numpy and the file: check_ik_cases.py: ```python import json import numpy as np doc = json.load(open("ik_cases.json")) def rot(axis, q): x, y, z = axis c, s, t = np.cos(q), np.sin(q), 1 - np.cos(q) return np.array([[t * x * x + c, t * x * y - s * z, t * x * z + s * y], [t * x * y + s * z, t * y * y + c, t * y * z - s * x], [t * x * z - s * y, t * y * z + s * x, t * z * z + c]]) def fk(q): T = np.eye(4) for j, qi in zip(doc["chain"]["joints"], q): A = np.eye(4) A[:3, 3], A[:3, :3] = j["origin_xyz"], rot(j["axis"], qi) T = T @ A return T @ np.diag([1, -1, -1, 1]) # tool0: link_6 turned 180 deg about x worst = 0.0 for case in doc["cases"]: for sol in [case["expected"]] + case["all_solutions"]: T = fk(sol["q"]) worst = max(worst, np.abs(T[:3, 3] - case["target"]["xyz"]).max(), np.abs(T[:3, :3] - case["target"]["rotation"]).max()) print(len(doc["cases"]), "cases, worst error", worst, "tolerance", doc["tolerance"]["position_m"]) ``` ## GLB `glb/rosie_.glb` is the visual geometry as one glTF 2.0 binary, for three.js, Babylon.js, ``, Blender or a game engine. Its nodes follow the joint chain (`base_footprint` > `base_link` > `link_1` ... `link_6` > `tool0`). Each link node sits on its joint origin, so rotating it about its joint axis (in the node's `extras`) by the joint angle moves the robot. glTF is Y-up, so the root node turns the kit's Z-up frames upright. ## Simulation parameters - URDF `effort` is `effort_peak`, the peak torque the gearbox passes to the arm. MuJoCo `forcerange` is `drive_peak_torque_at_joint`, the motor's peak torque times the gear ratio, which also pays for accelerating the motor's own rotor (`armature`); on the Rosie 1400 J4 it is the drive's set torque limit times the ratio. URDF `velocity` is the maximum joint speed. - MuJoCo actuators are position servos: `ctrl` is the joint target in radians. Gains are the output peak torque (`effort_peak`) per 0.01 rad, critically damped, so a held pose sits within a few tenths of a degree under gravity. Replace them with your own controller as needed. - `armature` is the reflected inertia of each joint's motor and gearbox. Joint damping is a nominal value. - MuJoCo and the SRDF skip collisions between adjacent links only, whose hulls meet at each joint. Every other pair of links is checked. ## Geometry For each link, every part except fasteners, pulleys and belts is fused into one solid whose outside is the original CAD surface: the same planes, cylinders, cones, tori and B-spline faces, so in a CAD system you can pick faces, edges and hole axes and dimension them. Bolt holes, counterbores and openings into the inside are capped flush with the face around them, open channels get a cover that follows their rim, and the inside is solid, with no internal parts. The base mounting face with its holes and bore, and the tool flange, are unchanged. The visual meshes are that solid, tessellated; the collision meshes are convex decompositions of it, up to ten pieces per link. ## Licence The kits are free to use for simulation and integration, including commercially. See `LICENSE.txt` in each kit. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `/assets/sim/index.json and each kit's robot.json and ik_cases.json (generated by AMR from the robot CAD and engineering data; amr-cad-work sim-kit/kitspec.py BENCHMARK, ROSIEOS_MOTION, ROSIEOS_IK)` - `robot_description/robots/rosie_1400_v3/robot.urdf (joint names, axes, zero pose), RosieOS motion-server/v1/src cartesian_resolver.hpp and robot_v4_cartesian_nats_protocol.hpp (IK)` - `src/docs/guides/run-in-simulation.md` --- # Program a weld from CAD > Create a program in offline programming, import and place a STEP part, define welds from its edges with the seam search, plan and verify them with the weld planner, replay the result in simulation, and read a refusal. URL: https://advancedmetalresearch.com/docs/guides/offline-programming Section: RosieOS docs / Guides Last updated: 2026-10-10 This guide takes a STEP part to a verified, simulated weld program in the offline programming (OLP) app. You import the part, place it on the cell, pick welds from its edges, plan them with the weld planner and replay the result. Nothing here moves a robot. Running the program on a cell is the next guide, [Connect to a cell and run a program](https://advancedmetalresearch.com/docs/guides/connect-a-cell). ## Before you start You need three things running: - **The OLP server and UI.** The [quickstart](https://advancedmetalresearch.com/docs/get-started/quickstart) starts both. Open `http://127.0.0.1:5189/offline-programming/v1/ui/`. - **The weld planner**, with an NVIDIA GPU, and OLP pointed at it with `OFFLINE_PROGRAMMING_WELD_PLANNER_MOTION_ORIGIN`. The OLP launcher prints `motion:` with the origin it uses. See [Run the weld planner](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner). - **The CAD environments.** STEP import runs in the `cadquery/v1` pixi environment, and seam detection in the `weld_planner/v1` `default` environment. Install both with `pixi install` in each directory. Programs live in your browser's storage (IndexedDB) for the page's origin. A different host name or port is a different store. Use **Export .weldplan…** or the catalogue to keep a copy elsewhere. ## 1. Create a program Open the **Program** menu (☰) and choose **New program…**. Give it a name and press **Create program**. Your current program is saved first. Every new program is a rails program: once it has a part, the weld planner is its planner. In the **WORKSPACE TREE**, select **Workspace** to see the robot model (**Rosie 1400 V3** or **Rosie 1420 V1**) and the cell settings. If you will run the program on a cell, choose that cell from the **Cells** menu now and use **Pull cell settings**, so the plan is made against the cell's own calibration. Offline, **Load machine_planning_calibration.json…** plans against a calibration file instead. ## 2. Import the part On the toolbar, open **Import** and choose **Weld parts from files…**, then pick one or more `.step` or `.stp` files. Each part gets its own placement and welds. The same menu lists STEP files you imported before, kept in this browser, so you can import them again without the file browser. Add anything else the robot must clear: - **Import › Fixtures / obstacles from files…** adds STEP geometry as fixtures. - Right-click in the tree and choose **Add a stand** for a simple stand under the part. The weld planner plans around every fixture you add, and the verifier checks the arm and the part against them. It knows nothing about bodies you leave out. ## 3. Place the part Turn on **Move part** (M) and drag an arrow to slide the part along one of its axes, or an arc to turn it. The placement updates when you release. The placement is measured from the robot's work frame, so it means something only on that robot model. The planner moves the positioner itself. You do not set its joint angles. ## 4. Choose presets - **Weld presets** hold the process defaults that new welds start from. The shipped preset is "GMAW · mild steel · 6 mm fillet": 6.5 mm/s travel, 0° work angle and 12° travel angle. - **Plan presets** hold the torch angle search and the fitted torch. The search presets are **Steady** (±15° work, ±40° travel) and **Wide** (±30° work, ±60° travel). The torch presets describe the torch body the search casts against. ## 5. Define the welds Turn on **Rails welds** (W). Click an edge of the part, or click near the line where two plates meet. OLP matches the click to a detected seam and searches its torch angles at once, in both directions. The result appears in the welds list: | State | Meaning | |---|---| | searching | The seam worker is casting rays and searching angles | | proposed | The search found a work and travel profile. Decide what to do with it. | | failed | The search found nothing it could use; the note says why | | accepted | The weld is in the program as searched | | trimmed | The weld is in the program, narrowed to its longest clear stretch | For a proposed weld, press **Accept**, or **Trim** to keep only the longest stretch the torch can reach. Click the row for the detail: the work and travel angle profiles, which you can edit; a coverage strip showing which parts of the seam the torch reaches; and **Trim to longest clear run**, **Accept** and **Remove weld**. An accepted weld becomes a weld node in the program, with its approach and retract around it. From then on the node is what gets planned. Select it and press **EDIT** to change its span, direction, angle profiles, travel speed, standoff or weave. The field names and units are in the [program format](https://advancedmetalresearch.com/docs/reference/program-format#weld-nodes) and the [seam model](https://advancedmetalresearch.com/docs/reference/weld-program-format#seams). For a weld that is not an edge of the part, place torch poses by hand instead: - **Waypoint welds**: torch poses joined as C0, C1 or C2 - **Line welds**: a straight weld between two poses - **Arc welds**: a circular weld through three poses OLP turns hand-placed welds into seams before it plans them. You can add taught moves, dwells, I/O events and Home to the same program. See [Teach waypoints and moves](https://advancedmetalresearch.com/docs/guides/teach-waypoints). ## 6. Plan Press **Plan** (P). The **Plan** menu shows which planner runs: **Weld planner · B-spline free space** by default. The cuRobo and Catmull-Rom (deprecated) entries only change the solver for the moves between welds. A progress dialog shows four stages: 1. Packing the program for the weld planner 2. Planning: seam search, weld and connecting trajectory optimisation, verify 3. Keeping the plan and its trajectory 4. Loading the simulation Planning takes minutes. You can cancel it in the dialog until the result is being kept. When it finishes, the status line reads `Planned · samples, s · press play to simulate`. What happened: OLP packed a `.weldplan`, the planner planned every weld and move, and the verifier checked each one against the exact cell meshes, the part, your fixtures and the joint limits. The planner wrote a trajectory only because every segment passed. OLP keeps a copy of that trajectory with the program. See [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification). The planner needs the part's STEP file in this session. After a reload, if the status says the STEP is not loaded, import the same file again. ## 7. Simulate Open the **Simulate & deploy** panel on the right and choose **Simulate**. Press ▶ to replay the planned trajectory in the viewer. These are the exact bytes a cell would play. The transport has stop, step back and forward, a timeline and a playback rate from 1× to 16×. The playback rate does not change the trajectory. The viewport says **PREVIEW · NO MOTION OUTPUT**. The TCP path overlay shows the planned route: blue for travel and green for welds. **PLAN SPEED** scales the whole plan's motion from 1 % to 100 % without changing the path. Change it before you plan; after a change, plan and simulate again. ## When planning is refused A refused plan shows a message and **Technical details** with the planner's own evidence. The common cases: | Message | What to do | |---|---| | Robot settings have changed since this program was created | The robot description changed since you wrote the program. Press **Review robot settings**, then **Accept current settings** in Workspace, and plan again. | | No candidate motion path was found for a seam (`seam_not_planned`) | The seam search could not cross the seam. The details say where it stopped and why: joint travel limits, sphere collision screening, poses the robot cannot reach, or no allowed step between samples. Move the part, widen the search preset, or trim the weld. | | No verified motion path is available (`motion_not_verified`) | A continuous check failed or could not run. The details name the bodies in contact, the joint and limit, or the check that was unverifiable. Review the seam, torch pose, part placement, weld speed or fixtures. | | The welds planned but the moves between them did not (`motion_plan_unjoined`) | Try another free-space solver in the **Plan** menu, or add clearance around the part. | | Motion origin or seam worker unavailable | Start the weld planner, or install the seam worker's environment. See [Run the weld planner](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner). | The planner keeps the last 5 refused requests and their results in its store's `refused/` directory, so a refusal can be studied without planning again. The full list of codes is in the [weld planner HTTP API](https://advancedmetalresearch.com/docs/apis/weld-planner-http#a-plan-without-a-trajectory) and the [OLP HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http#weld-plan). ## Export the request **Program › Export .weldplan…** downloads the exact request the planner would receive. Use it to plan the same program on another machine with the [command line](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner#plan-from-the-command-line), or to keep with the plan result as evidence. The file format is in the [weld program reference](https://advancedmetalresearch.com/docs/reference/weld-program-format#weldplan). ## Next: run it on a cell > [!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). On a cell, **Deploy** in the same panel homes, arms, loads and plays the program. Load only accepts a trajectory from the planner's store, and it refuses one made for a different robot description or cell calibration. Choose **Dry run · process outputs off**: torch outputs are refused in this release, and there is no seam tracking or sensing, so the robot follows the planned path exactly and nothing corrects it against the real part. **Go to plan start** moves the robot to the plan's first pose with a joint move, which is not verified against collisions. Follow [Connect to a cell and run a program](https://advancedmetalresearch.com/docs/guides/connect-a-cell). ## Related pages - [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification) - [Teach waypoints and moves](https://advancedmetalresearch.com/docs/guides/teach-waypoints) - [Weld program and `.weldplan` container](/docs/reference/weld-program-format) - [Offline programming HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http) - [Process I/O and sensing](https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `offline-programming/v1/ui/src/main.ts:446-488,496-560,708,722-812,968-1060,1650-1769,1930-1989,2004-2030,3098-3340,4330-4420,4691-4730` - `offline-programming/v1/ui/src/rails/index.ts:1-18,80-100,930-1010,1150-1230,1700-1766` - `offline-programming/v1/ui/src/authoring/programTree.ts:50-110` - `offline-programming/v1/ui/src/ui/planning-failure.ts:1-73` - `offline-programming/v1/ui/src/execution/densePanel.ts:541-553` - `offline-programming/v1/ui/src/rails/parameters.ts:181-200` - `weld_planner/v1/data/presets/search/steady.json` - `weld_planner/v1/data/presets/search/wide.json` - `weld_planner/v1/data/presets/weld/gmaw-steel-fillet.json` - `weld_planner/v1/python/seam_worker/workers.py:277-316,395-498` - `weld_planner/v1/python/weldplan/plan_request.py:170-190` - `weld_planner/v1/python/weld_motion_planner/server/motion_planner_server.py:80-82,346-377` - `offline-programming/v1/start-offline-programming.sh:499-515` --- # Teach waypoints and moves > Record taught waypoints from a measured robot pose or the preview, make them joint, linear or circular moves, set their speeds, add dwell, I/O and Home events, and plan and verify them with the weld planner. URL: https://advancedmetalresearch.com/docs/guides/teach-waypoints Section: RosieOS docs / Guides Last updated: 2026-10-10 A taught waypoint is a `move` node in the program: a destination you recorded, plus how to get there. You record it by jogging the robot, or the preview robot, to a pose and pressing record. The weld planner then plans every move between your waypoints and verifies it against the cell, just as it does for welds. This guide uses the offline programming (OLP) app on a desktop. The Steam Deck pendant records waypoints the same way, with the same rules; see [Program from the Steam Deck pendant](https://advancedmetalresearch.com/docs/guides/program-from-the-pendant). > [!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). Recording on a real cell means jogging it into place. Jog is not verified against collisions: watch the robot, jog slowly near the part and fixtures, and let go of the control to stop. Only the planned program that results is verified. ## Move types | `motion` | Common name | What the planner makes | |---|---|---| | `joint` | MoveJ | All joints move together, starting and ending at rest, to the recorded joint values | | `linear` | MoveL | The tool point moves in a straight line to the recorded pose, solved by inverse kinematics | | `circular` | MoveC | The tool point moves on an arc through a via pose to the recorded pose | Linear and circular moves cruise at a constant TCP speed between acceleration and deceleration ramps by default. Every planned move, of any type, passes the same continuous collision and limit verifier as the welds. See [What the verifier checks](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification#what-the-verifier-checks). ## Where the pose comes from The **Simulate & deploy** panel decides which robot you record from: - With **Deploy** showing a connected cell, you record the **measured** pose of the real robot. The toolbar says **Teach › Record robot waypoint**. The capture's `source` is `machine`. - With **Simulate** showing, you record the **preview** robot in the viewer. The toolbar says **Teach › Record preview waypoint**. The capture's `source` is `preview`. To connect a cell and jog it, see [Connect to a cell and run a program](https://advancedmetalresearch.com/docs/guides/connect-a-cell). The Jog panel (toolbar **Jog**) has joint and Cartesian jog for the selected, armed cell. ## Record a waypoint 1. Jog the robot to the pose you want. 2. Let go of the jog control and wait for the robot to be still. 3. Press **RECORD WAYPOINT** in the spreadsheet toolbar, or **Teach › Record robot waypoint**. The status line says `Measured waypoint recorded` or `Preview waypoint recorded`, and the new node is selected in the spreadsheet. A measured recording is refused unless the pose can be trusted: | Refusal | Why | |---|---| | `Release jog and wait for fresh stationary feedback before recording` | A jog is held, a request is in flight, or a program is playing; or the status is older than 2 s | | `Record needs verified position and stationary feedback for every described joint` | An axis has no valid Home, coordinate or calibration, its position is not trusted, or it is moving faster than 0.001 rad/s | | `Release jog and wait for fresh stationary position samples before recording` | The drives report no velocity, and the positions have not stayed within 0.00002 rad for 100 ms | | `Adopt the matching robot description before recording a waypoint` | The machine or the program uses a different robot description or model. Accept the current settings in **Workspace** first. | | `Pull machine calibration before recording` | The machine binds a description, but its calibration has not been pulled. Use **Cells › Pull cell settings**. | | `Machine identity changed during capture; record again` | The cell or its calibration changed while recording | | `Stop preview playback before recording` | The preview is playing | A new waypoint is labelled `P1`, `P2` and so on, and starts as a joint move at 100 % joint speed and acceleration, with a TCP speed limit of 25 mm/s. It goes after the selected node. If the selection is inside a weld block, it goes after that block's retract. If the selection is a transit between two welds, OLP splits the transit into a retract, your waypoint and a new approach. ## Reteach and via - **RETEACH** replaces the selected waypoint's destination and its capture record with a fresh one from the current pose. - **RECORD VIA** records the current pose as the selected waypoint's via pose and makes it a circular move. The via must be recorded against the same cell calibration as its destination. If it is not, reteach the destination first. ## Edit a move Select the node and press **EDIT**: | Group | Field | Unit | Stored as | |---|---|---|---| | Motion profile | Move type | | `motion`: `joint`, `linear` or `circular` | | | TCP speed limit | mm/s | `speed_mm_s` | | | Constant TCP speed (linear and circular) | | `constant_tcp_speed`, default on | | Joint limits | Joint speed limit | % | `speed_scale`, 1–100 % | | | Joint acceleration limit | % | `acceleration_scale`, 1–100 %. If unset, it follows the speed limit. | | Destination | x, y, z | mm | `target.xyz_m`, stored in m | | | roll, pitch, yaw | ° | `target.rpy_rad`, stored in rad | | | J1… | ° | `target.joint_values_rad`, stored in rad. Read-only for linear and circular moves, where they seed the inverse kinematics. | | Circular via pose | as Destination | | `via` | | Original observation | Source, Observed | | `capture`, read-only | Editing x, y, z or roll, pitch, yaw makes the destination a world TCP pose that the planner solves again (`target_space: "cartesian"`). Editing a joint angle makes it a joint destination (`target_space: "joint"`). Editing never changes the original observation; only a reteach replaces it. After you plan, the editor shows the planned **JOINT MOTION PROFILE** (joint velocity and acceleration over time) and, for linear and circular moves, the **TCP MOTION PROFILE** (TCP speed and acceleration). The field reference is in the [program format](https://advancedmetalresearch.com/docs/reference/program-format#move-nodes). ## Add events and Home | Button | Adds | Default | |---|---|---| | **+ DWELL** | A `dwell` node after the selection | 0.5 s. The editor opens. | | **+ IO** | An `io` node (a digital output) after the selection | On. The editor opens to set the channel. | | **+ HOME** | A `home` node at the end of the program | | The motion nodes must form one line, in the order Home, approach, weld, retract, Home, with taught moves before, after or between complete weld blocks. See [The single line](https://advancedmetalresearch.com/docs/reference/program-format#the-single-line). OLP refuses an edit that breaks it and says why in the status line. > [!NOTE] The weld planner refuses a program with taught moves that also contains an **IO** node: "Digital-output nodes need an execution I/O schedule; cannot silently omit them". Process outputs are not supported in this release. See [Process I/O and sensing](https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing). ## Plan and simulate A program with taught moves is planned by the weld planner, even without a part. Press **Plan** (P), then replay it under **Simulate**, as in [Program a weld from CAD](https://advancedmetalresearch.com/docs/guides/offline-programming#6-plan). The plan starts from the cell's reset pose and visits your nodes in order. The planner never reorders welds around taught moves. **PLAN SPEED** scales the whole plan afterwards without changing the path. On a cell, **Go to plan start** in the **Deploy** panel moves the robot to the plan's first pose with a joint move, then says `Move completed. Load to robot, then Play.` That move is not planned or verified by the weld planner. It is checked against joint limits only. Make sure the way is clear before you press it. ## Related pages - [Program format: move nodes](https://advancedmetalresearch.com/docs/reference/program-format#move-nodes) - [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification) - [Connect to a cell and run a program](https://advancedmetalresearch.com/docs/guides/connect-a-cell) - [Pendant controls](https://advancedmetalresearch.com/docs/reference/pendant-controls) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `offline-programming/v1/ui/src/execution/teach.ts:1-102` - `offline-programming/v1/ui/src/execution/machineFlow.ts:50` - `offline-programming/v1/ui/src/program-v2/capture.ts:1-70` - `offline-programming/v1/ui/src/program-v2/edits.ts:60-103` - `offline-programming/v1/ui/src/program-v2/nodes.ts:69-79` - `offline-programming/v1/ui/src/program-v2/field-specs.ts:132-160,360-371` - `offline-programming/v1/ui/src/program-v2/move-profile.ts:17-50` - `offline-programming/v1/ui/src/program-v2/node-editor.ts:280-290` - `offline-programming/v1/ui/src/main.ts:722-736,2239-2290,3262-3273,4330-4332` - `offline-programming/v1/ui/src/execution/densePanel.ts:545,1471-1501` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/taught.py:1-80` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/connecting_trajopt_main.py:60-75` - `weld_planner/v1/python/weld_motion_planner/planner/planner_main.py:169-171` --- # Program from the Steam Deck pendant > Use the native v5 teach pendant on a Steam Deck to connect a cell, arm, jog with the hold-to-enable trigger, record waypoints, edit the program, plan it with the weld planner, preview it and run it. URL: https://advancedmetalresearch.com/docs/guides/program-from-the-pendant Section: RosieOS docs / Guides Last updated: 2026-10-10 The v5 teach pendant is a native Qt app for the Steam Deck's 1280×800 screen. It edits the same programs as the offline programming (OLP) desktop app, because it runs OLP's own program and machine logic inside the app. It talks to a headless OLP server on the Deck, and that server holds the machine session through rt-control. > [!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). > [!CAUTION] **The v5 pendant is not qualified for real motion.** Physical stick, trigger and rear-button handling, real robot motion, network fault handling and a full plan, Load and Play on the Deck have not been qualified, and no CI builds it. The hold-to-enable trigger is a software deadman, not a safety-rated enabling device. Use it in [simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation) until the cell owner has qualified it. To install it on a Deck, see [Build and install the pendant](https://advancedmetalresearch.com/docs/guides/install-the-pendant). Every control is listed in [Pendant controls](https://advancedmetalresearch.com/docs/reference/pendant-controls). ## How it fits together - **The app** (`rosie-pendant-v5`) is the only user interface. It reads the gamepad from `/dev/input/js0` to `js3`. - **The OLP server** runs headless on `127.0.0.1:8794` as a systemd user unit, `rosie-v5-olp`. It owns the machine session and serves connect, arm, stop, heartbeat, jog, move, Load and Play, the robot catalogue and planning. The app never talks to rt-control directly. - **Planning** goes from the Deck's OLP server to a weld planner on a workstation the Deck can reach. Because rt-control admits one controller at a time, a desktop OLP or another client holding the same cell locks the pendant out, and the other way round. See [One controller at a time](https://advancedmetalresearch.com/docs/get-started/safety-model#one-controller). Closing the app stops the OLP server. Its shutdown stops any machine it armed before it releases control. Starting the app never arms, homes or moves anything. ## The screen The header shows the program name, its save state (**LOCAL**, **SAVED** or **UNSAVED**), the connected cell and its posture, the **Arm** pill and **■ STOP**. Below it are five pages, which you cycle with **L1** and **R1**: | Page | Use it to | |---|---| | **JOG** | Jog joints or the tool, and record waypoints, beside the 3D view | | **PROGRAM** | Edit the program's node spreadsheet | | **RUN** | Plan, preview, load, play and stop | | **TELEMETRY** | Watch the drives' plots | | **CELL** | Connect a cell, home it, and match the program's robot settings to it | **■ STOP** (B, R5, Esc) works on every page and in every dialog. It stops the robot and also disarms. It is a software stop: it is not the hardware E-stop, and a stop receipt does not prove the robot has stopped moving. **Menu** opens the controls guide. **View** jumps to the CELL page. ## 1. Connect a cell On **CELL**, the list shows the cells in the Deck's cell catalogue (`olp-cells.json`). Tap one to connect. Connecting checks the cell's identity; it never arms. - **HOME AXES** asks the cell to run Home. It appears when the cell supports native Home. Home is not verified against collisions. - **PULL CELL SETTINGS** copies the cell's robot settings and calibration into the program. - **ACCEPT CURRENT SETTINGS** adopts the current robot description when the program was written against an older one. Plan again and verify before you run. - **DISCONNECT** and **REFRESH** do what they say. The steps below the list follow OLP's machine steps, so you can see what is still missing before you can arm or load. ## 2. Arm Hold **R2** and press **A**, or tap the **Arm** pill. On a real machine the pill changes to **CONFIRM ARM · A**: press **A** again to confirm, or **CANCEL**. Arming acquires control and energises the drives. ## 3. Jog On **JOG**, nothing moves unless you hold **R2**, the hold-to-enable trigger. Release it and the jog stops. - **Joints**: press **D-pad ▲▼** to pick a joint, then hold **R2** and push the **left stick** up or down. - **Cartesian**: press **L3** to switch. Hold **R2** and use the left stick for X and Y, the right stick up and down for Z and left and right for RZ. Add **L2** to turn the left stick into RX and RY. - **Speed**: **D-pad ◀▶** steps the jog speed through 5, 10, 25, 50, 75 and 100 %. Cartesian jog moves one axis at a time: whichever stick direction is largest. A direction counts once the stick is past 0.35 of its travel. Choose **BASE** or **TOOL** as the frame, and **HOLD** or **STEP** as the mode. In **STEP** mode each push makes one bounded move of the linear step (100 mm by default) or angular step (15° by default); centre the stick before the next. After a page change, a mode or axis change, a new direction, the Steam overlay taking focus, or a lost controller, the hold ends. You must bring the controls back to neutral before the next jog starts. The touch controls do the same without the pad: **−** and **+** for each joint, an angle move per joint (**± 10°** and **GO**), **HOME JOINTS** and **ALL JOINTS TO 0°**. These are joint moves, checked against joint limits only. Without **R2**, the right stick orbits the 3D view and **L2** with it zooms. **R3** resets the view. ## 4. Record waypoints With the robot still: | Press | To | |---|---| | **A** or **L4** | Record a waypoint at the measured pose | | **X** or **L5** | Reteach the selected waypoint | | **Y** or **R4** | Record a via pose for the selected waypoint, making it a circular move | The pendant records under exactly the same rules as the desktop app: every axis homed and trusted, the robot still, fresh status, and the program's robot description matching the machine's. A refusal appears in the status line as `Record blocked: …`. See [Teach waypoints and moves](https://advancedmetalresearch.com/docs/guides/teach-waypoints#record-a-waypoint) for each refusal. ## 5. Edit the program **PROGRAM** shows the node spreadsheet, a page at a time. **D-pad ▲▼** moves the selected row; **◀ PAGE** and **PAGE ▶** page. | Button | Does | |---|---| | **● RECORD**, **RETEACH**, **VIA** | Record, as on the JOG page | | **+ DWELL**, **+ IO**, **+ HOME** | Add an event or a Home | | **EDIT** | Open the selected node's fields (also a double tap) | | **ON / OFF** | Enable or disable the node | | **▲ UP**, **▼ DOWN**, **DELETE** | Reorder or remove | | **UNDO**, **REDO** | History | | **PLAN SPEED** | The whole-plan speed, 1–100 % | | **NEW**, **OPEN**, **SAVE AS** | Program files | Editing is locked while the program runs. The node fields are the same as the desktop's; see [Edit a move](https://advancedmetalresearch.com/docs/guides/teach-waypoints#edit-a-move). **OPEN** and **SAVE AS** read and write OLP project files (`offline-programming.project-export.v1` JSON) in `~/Rosie programs`. The current program also saves itself to `~/.local/share/Rosie/Rosie Pendant v5/current.olp.json` after every change. Press **Steam** + **X** for the on-screen keyboard when a dialog asks for a name. ## 6. Plan, preview and run On **RUN**: 1. **PLAN PROGRAM** sends the program to the weld planner through the Deck's OLP server. The planner plans and verifies every move, as on the desktop. When it passes, the status says `Planned · samples, s · Preview, or Load to robot`, and the pendant keeps the trajectory bytes in its own data directory. 2. **▶ PREVIEW PLAN** replays the kept trajectory in the 3D view, with a scrubber. **■ END PREVIEW** stops it. 3. Choose **DRY RUN** (process outputs off). Torch outputs are refused in this release, and there is no seam tracking. 4. **GO TO PLAN START** moves the robot to the plan's first pose with a joint move, then says `Move completed. Load to robot, then Play.` This move is not verified against collisions. 5. **LOAD TO ROBOT** loads the trajectory. Load fetches it by digest from the planner's store and checks its identity, the robot description and calibration, and Home. 6. **▶ PLAY** starts it. The executing node is highlighted in the program list. **■ STOP** ends it. The pendant plans with the shipped fitted torch and the B-spline free-space solver. It holds no STEP file and has no seam search, so it can plan programs without a CAD part, such as taught moves. A program whose welds come from a part answers `This program's part (STEP) is not on the pendant; plan it in offline programming, then open it here`. Only planned programs pass the verifier. Jog, the joint moves, Home and Go to plan start do not. See [What is verified before motion](https://advancedmetalresearch.com/docs/get-started/safety-model#what-is-verified). ## Telemetry **TELEMETRY** plots the drives' data. Choose the quantity and the time **WINDOW**. **LIVE** follows the stream and **HOLD** freezes it. **CAPTURE 1 kHz** reads the last few seconds at the full 1 kHz rate. ## Related pages - [Pendant controls](https://advancedmetalresearch.com/docs/reference/pendant-controls) - [Build and install the pendant](https://advancedmetalresearch.com/docs/guides/install-the-pendant) - [Teach waypoints and moves](https://advancedmetalresearch.com/docs/guides/teach-waypoints) - [Use the virtual pendant](https://advancedmetalresearch.com/docs/guides/virtual-pendant) - [Offline programming HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `steamdeck/real/v5/src/main.cpp:11-24` - `steamdeck/real/v5/src/window.cpp:55-59,61-207,215-281,378-523,525-593,661-677,694-792,794-873` - `steamdeck/real/v5/src/input.hpp:1-50` - `steamdeck/real/v5/src/controller.cpp:14-57` - `steamdeck/real/v5/src/pages.hpp:26-150` - `steamdeck/real/v5/src/jog_page.cpp:18-22,67-85,192-233,245-300` - `steamdeck/real/v5/src/program_page.cpp:170-263` - `steamdeck/real/v5/src/run_page.cpp:33-124,170-300` - `steamdeck/real/v5/src/cell_page.cpp:63-126` - `steamdeck/real/v5/src/telemetry_page.cpp:209-252` - `steamdeck/real/v5/olp-core/run.ts:102-113` - `steamdeck/real/v5/run.sh` - `steamdeck/real/v5/start-olp.sh` - `steamdeck/real/v5/controller.vdf` --- # Connect to a cell and run a program > Commission a cell for offline programming, describe it in the cell catalogue, select it, and Home, Arm, Load, Play and Stop a planned weld program, with the checks OLP makes at each step and how to read its refusals. URL: https://advancedmetalresearch.com/docs/guides/connect-a-cell Section: RosieOS docs / Guides Last updated: 2026-10-10 This guide takes the OLP server from offline work to a real cell: you describe the cell in a catalogue, select it, then Home, Arm, Load and Play a planned weld program. Each step shows the UI action and the HTTP call behind it, so you can script it or debug it. Try the whole sequence on the simulated cell first. The dev stack adds a `local-simulation` cell to the catalogue for you. See [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation). > [!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). ## Before you start - The cell host runs `rosie-rt-core` and `rt-control` with the mutual-TLS listener enabled. See [Install rt-core on a cell host](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host) and [Remote access (mTLS)](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#enable-the-listener). - You have a client certificate for OLP from the cell's PKI, issued with `remote-pki.sh issue-client`. It carries exactly one `rosie-pair:` URI, and OLP refuses the connection if that pair differs from the catalogue's binding (`session_principal_mismatch`). See [Provision certificates](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#provision-certificates). - The weld planner is running and OLP can reach it (`OFFLINE_PROGRAMMING_WELD_PLANNER_MOTION_ORIGIN`). Load fetches programs from it. - The hardware E-stop has been tested this session. ## 1. Commission the cell in the repository OLP only drives a cell whose deployment manifest is in the repository, at `rt-core/config/cells/.json`. When it reads the catalogue, it compiles that manifest's machine config and takes the listener address, pair ID, pair revision and configuration digest from it. The catalogue can only repeat these values; it cannot override them. The manifest's native node looks like this. [Cells, machines and positioners](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners#cell-config) describes every field. rt-core/config/cells/cell-a.json (native node): ```json { "rt_core": { "machine_config": "rt-core/config/machines/cell-a.json", "configuration_sha256": "<64 hex from rtctl compile>", "pair_id": "cell-a", "pair_revision": 1, "remote_listen": "127.0.0.1:8443" } } ``` Check it with the repository validator: ```bash cd rt-core go test -count=1 ./config/cells -run TestRepositoryCompatibility ``` > [!IMPORTANT] In this release OLP resolves only the cell IDs it knows by name. The list is `cellManifestName` in `offline-programming/v1/internal/targets/cells.go`. To add a cell, add its manifest and add its ID to that function. An ID that is not listed fails with `cell_id_unknown`. ## 2. Write the cell catalogue The catalogue is a JSON file, schema `offline-programming.cell-catalogue.v1`, that lists the cells the operator can choose from. Unknown fields are refused. olp-cells.json: ```json { "schema": "offline-programming.cell-catalogue.v1", "cells": [ { "cell_id": "cell-a", "label": "Cell A", "role": "WELD CELL", "models": ["rosie_1400_v3"], "requested_mode": "real", "execution": { "address": "https://127.0.0.1:8443", "ca_file": "pki/ca.pem", "certificate_file": "pki/clients/olp-1.pem", "key_file": "pki/clients/olp-1-key.pem", "binding": {"pair_id": "cell-a", "revision": 1, "configuration_sha256": "<64 hex from rtctl compile>"}, "axis_mask": 511, "expected_backend": "ethercat" } } ] } ``` | Field | Required | Rule | |---|---|---| | `cell_id` | yes | Unique. Must be a commissioned cell (step 1). | | `label`, `role` | yes | Non-empty. Shown in the Cells dialog. | | `models` | yes | Exactly one known model: `rosie_1400_v3`, `rosie_1420_v1`, `bench_one_motor_1to1` or `bench_nine_motors_1to1`. It must be the model the manifest's machine config compiles to. | | `requested_mode` | yes | `real` or `simulation`. At selection it must match what the cell reports: `real` for an `ethercat` backend, `simulation` for `simulation`. | | `execution.address` | yes | `https://host:port`, no path or query. Must equal `https://` + the manifest's `remote_listen`. | | `execution.ca_file`, `certificate_file`, `key_file` | yes | PEM files. Relative paths resolve from the catalogue's directory. They must exist. | | `execution.binding.pair_id`, `revision` | yes | Must equal the manifest's `pair_id` and `pair_revision` | | `execution.binding.configuration_sha256` | no | If given, must equal the compiled digest. OLP fills it in from the compile either way. | | `execution.axis_mask` | yes | The axes OLP commands, 1–511, J1 = bit 0. Home, Arm and Load address exactly these axes. | | `execution.expected_backend` | yes | `ethercat` or `simulation`. Describe must report the same. | Because the address must match `remote_listen`, a cell pinned to `127.0.0.1:8443` is reachable only from the cell host itself or through a forwarded port. A simulated cell served on the same machine uses a local entry instead. It needs the local binding file in `OFFLINE_PROGRAMMING_RT_CORE_CONFIG` (the dev stack writes it): ```json {"cell_id": "local-simulation", "label": "Local simulation", "role": "LOCAL SIMULATION", "models": ["rosie_1400_v3"], "requested_mode": "simulation", "execution": {"local": true}} ``` ## 3. Start OLP with the catalogue ```bash export OFFLINE_PROGRAMMING_CELLS=/path/to/olp-cells.json export OFFLINE_PROGRAMMING_WELD_PLANNER_MOTION_ORIGIN=http://127.0.0.1:8796 bash offline-programming/v1/start-offline-programming.sh ``` With the dev stack, set `DEV_STACK_OLP_CELLS=/path/to/olp-cells.json` instead. Its cells are added after `local-simulation`, and relative credential paths are resolved against your file. The server checks the whole catalogue at startup and refuses to start on a bad entry, naming the rule: for example `cell_endpoint_mismatch`, `cell_pair_mismatch`, `cell_configuration_mismatch`, `cell_credentials_missing` or `cell_model_mismatch`. A cell whose manifest names no machine config stays listed but unavailable, with `cell_configuration_missing`. On success it prints: ```text dense execution: choose a machine; joint and Cartesian jog share its armed control session ``` ## 4. Select the cell In the UI, open **Cells** and choose the cell. Over HTTP: ```bash OLP=http://127.0.0.1:8794/api/offline-programming/v1 curl -s $OLP/targets | jq '.targets[] | {cell_id, backend, simulation, reason}' curl -s -X POST $OLP/targets/select -d '{"cell_id":"cell-a","model_id":"rosie_1400_v3"}' ``` Selection connects to `rt-control`, runs Describe and checks it against the catalogue: backend, configuration digest, contract version, axis count. It takes no authority. If another cell was selected, OLP stops and releases it first. Then confirm you have the machine you expect: ```bash curl -s $OLP/dense-execution/status | jq '{target, robot_cell}' curl -s $OLP/dense-execution/capabilities | jq '.rt_core.description | {backend, configuration_sha256}' ``` `target.backend` must say `ethercat` for a real cell. `robot_cell.valid` must be `true`, and `robot_cell.model_id` and `robot_description_sha256` identify the robot description the machine was compiled with. Every later request that can move the robot must carry `X-RT-Target-Generation: ` and `X-RT-Target-Cell: `. If anyone selects another cell in between, the request is refused with `target_changed`. The UI does this for you. See [Target fencing](https://advancedmetalresearch.com/docs/apis/olp-http#target-fencing). For the commands below: ```bash STATUS=$(curl -s $OLP/dense-execution/status) FENCE=(-H "X-RT-Target-Generation: $(echo "$STATUS" | jq -r .target.selection_generation)" -H "X-RT-Target-Cell: $(echo "$STATUS" | jq -r .target.cell_id)") ``` ## 5. Home Clear the cell. In the UI, press **Home**. Over HTTP, with `FENCE` set to the two headers: ```bash curl -s -X POST $OLP/dense-execution/home "${FENCE[@]}" ``` Home acquires the `rt-control` lease, runs native Home on every axis in the catalogue's `axis_mask`, and returns when Home is valid on all of them. Send `{"axes": [0, 1]}` to home only some axes. Home moves the robot but never arms it. ## 6. Arm ```bash curl -s -X POST $OLP/dense-execution/arm "${FENCE[@]}" -d '{"armed": true}' ``` Arm enables and arms the drives and waits until every configured axis reports `ready`. If an execution fault is latched and a reset clears it, Arm resets it first. From now on, while OLP holds the lease: - Send a heartbeat at least every 5 s: `POST /dense-execution/heartbeat` with `{"session_id": ""}`. The UI does this while its tab is open. Without it, OLP stops the machine with `ui_heartbeat_lost`. - Any refused or failed operation makes OLP run Stop and Release before it answers. ## 7. Plan and Load Plan the program in OLP against this cell. While a cell is selected, the plan request carries the cell's own calibration, and the planner stamps the robot and cell identity into the `.rdt`. See [Program a weld from CAD](https://advancedmetalresearch.com/docs/guides/offline-programming). Then Load it. In the UI, choose the **Dry run · process outputs off** run mode and press **Load**. Over HTTP, send the plan identity from the plan response: ```bash curl -s -X POST $OLP/dense-execution/load "${FENCE[@]}" -d '{ "trajectory_digest": "sha256:…", "plan_id": "bracket_fillet:3f1c0a9d2b7e", "program_id": "bracket_fillet", "program_digest": "sha256:…", "manifest_revision": 1, "plan_revision": 3, "dry_run": true}' ``` Load refuses before any byte reaches the robot unless: 1. the `.rdt` exists in the planner's store, which it only does if every segment passed the verifier 2. its header matches the identity you sent 3. the plan's robot description and cell calibration match the machine 4. every configured axis has valid Home It then uploads the program with `prepare_program`, and `rt-control` checks positions, velocities and continuity against the live machine. The answer carries a `session_id` and the trajectory's segments. `dry_run: true` removes the torch bits first. A program that still needs process outputs is refused: torch output is not supported in this release. See [Process I/O and sensing](https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing). ## 8. Play Replay the program in simulation first if you have not: **Simulate** in OLP plays the exact bytes in the viewer or on the local simulator. Then: ```bash curl -s -X POST $OLP/dense-execution/play "${FENCE[@]}" -d '{"session_id": "ds-9b1e4f07a2c3"}' ``` Play waits until the machine is armed and ready, then starts the program. Keep the heartbeat going. Watch `status.session.state` (`playing`) and `status.rt_core.status` for execution progress. The first point may start with a short alignment ramp from the held position; see [Starting from rest](https://advancedmetalresearch.com/docs/concepts/motion-and-planning#starting-from-rest). ## 9. Stop and disconnect Press **STOP** in the UI, or: ```bash curl -s -X POST $OLP/dense-execution/stop ``` Stop needs no fence and no body. It inhibits outputs at once, then releases the lease. Retry it until it answers `"ok": true`. If a Stop or Release cannot be confirmed, OLP blocks further motion with `rt_core_inhibited` until a Stop succeeds. A Stop receipt is not proof of standstill. Watch the robot. The STOP button is software; the hardware E-stop is the emergency stop. To give up the cell, disarm (`{"armed": false}` on `/arm`, which runs Stop and Release) and clear the selection with `POST /dense-execution/target` and `{"cell": ""}`. ## When something is refused | Code | Step | What to do | |---|---|---| | `cell_id_unknown` | 3, 4 | The ID is not a commissioned cell known to OLP. See step 1. | | `cell_endpoint_mismatch`, `cell_pair_mismatch`, `cell_pair_revision_mismatch`, `cell_configuration_mismatch` | 3, 4 | The catalogue disagrees with the manifest, or the running `rt-control` serves another configuration. Recompile and update the pins. | | `cell_mode_mismatch`, `cell_backend_mismatch` | 4 | The cell reports `simulation` where you asked for `real`, or the reverse | | `cell_unreachable`, `rt_core_transport_lost` | 4 | OLP cannot reach the listener. Check the address, the port forward and the certificates. | | `session_principal_mismatch` | 4 | The client certificate's pair differs from the binding | | `target_changed` | 5–8 | Another client selected a cell. Read the status again and use the new generation. | | `control_already_owned` | 5–8 | Another controller (a pendant, a motion server, another OLP) holds `rt-control`. Release it there. | | `home_required` | 7 | Home the machine first | | `robot_cell_mismatch`, `robot_cell_missing` | 7 | The plan was made for another robot description or cell calibration, or before the cell was checked. Replan against this cell. | | `robot_cell_unavailable` | 7 | The machine cannot say which robot it is. Check its compiled configuration. | | `dense_blob_not_found` | 7 | The planner's store no longer has that digest. Replan. | | `dense_identity_mismatch` | 7 | The identity you sent differs from the `.rdt` header | | `program_identity_or_process_mismatch` | 7 | The program still needs process outputs. Load with `dry_run: true`. | | `native_limit_exceeded`, `native_segment_rate_exceeded`, `outside_limits_outward` | 7 | The program leaves the machine's limits. `limit_violation` in the answer names the segment, sample and axis. | | `not_ready`, `jog_not_ready` | 6–8 | An axis is not ready. `rt_core.status` shows the first failing gate per axis. | | `ui_heartbeat_lost` | 6–8 | The client stopped sending heartbeats. Arm again. | | `rt_core_inhibited` | any | Retry Stop until it succeeds | All codes are in the [Offline programming HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http#error-codes) and [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes). ## Related pages - [Safety model](https://advancedmetalresearch.com/docs/get-started/safety-model) - [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning#olp) - [Offline programming HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http) - [Cells, machines and positioners](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners) - [Dense trajectory (.rdt) format](https://advancedmetalresearch.com/docs/reference/rdt-format#robot-cell) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `offline-programming/v1/internal/targets/cells.go:20-237` - `offline-programming/v1/internal/targets/picker.go:16-250` - `offline-programming/v1/internal/targets/descriptors.go:69-102` - `offline-programming/v1/internal/denseexec/rt_core.go:30-44,148-173,189-239,399-504,556-616,660-825,827-871,1024-1030,1292-1406,1428-1518` - `offline-programming/v1/internal/denseexec/robot.go:12-104` - `offline-programming/v1/dense_execution.go:24-63,115-305` - `offline-programming/v1/cell_targets.go:10-47` - `offline-programming/v1/dense_cells.go:37-57` - `offline-programming/v1/main.go:311-346,896-918` - `offline-programming/v1/start-offline-programming.sh:649-650` - `motion-server/v1/local-rt-core.sh:103-127` - `rt-core/config/templates/cell.json:8-18` - `weld_planner/v1/python/weldplan/dense_joint_trajectory.py:726` --- # Install rt-core on a cell host > Provision a Linux cell host for rt-core, build and install a runtime package with inactive systemd units, bind it to a compiled machine configuration, optionally set up mutual TLS, check the host, start the services and roll back. URL: https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host Section: RosieOS docs / Guides Last updated: 2026-10-10 This guide takes a PREEMPT_RT Linux host from a bare OS to running `rosie-rt-core` and `rt-control` as systemd services against real drives. It uses the scripts in `rt-core/host/` and `rt-core/tools/`. > [!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). Installing is owner-operated work. The scripts refuse to start anything on their own: every install leaves the units inactive, and you start them when the cell is ready. Passing every check in this guide does not qualify the cell for powered motion. Hardware bring-up and qualification are manual steps for the cell owner. To try rt-core without hardware, use [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation) instead. ## What you need - A cell host that matches a supported foundation profile (below), with its EtherCAT NIC wired to the drives. - A **live** machine config for this cell (`"backend": "live"`) that compiles cleanly. See [Configuration files](https://advancedmetalresearch.com/docs/reference/configuration). - A deployment pair id and a positive pair revision for this cell. - A build machine of the **same CPU architecture** as the host, with the [toolchain](https://advancedmetalresearch.com/docs/get-started/installation), to build the package. - Root on the host, and network access from it to the IgH EtherCAT source archive during provisioning. ## 1. Provision the EtherCAT foundation `host/ethercat-foundation.sh` installs the IgH EtherCAT master for the running kernel, pinned by version and archive SHA-256, and configures its device access. It accepts only these host profiles: | Profile | OS | Kernel | IgH | |---|---|---|---| | `revpi-debian12` | Debian 12 | `*-revpi*-rpi-v8` | 1.6.9 | | `rpi5-debian13` | Debian 13 | `6.18.*-rpi-v8-rt` | 1.6.12 | Any other host is refused. On other hardware you provision PREEMPT_RT and IgH yourself, and `rtctl hostcheck` (step 5) checks the result. The script needs these variables. It refuses to run without `NO_REAL_MOVEMENT=1`. | Variable | Description | |---|---| | `NO_REAL_MOVEMENT` | Must be `1`. | | `ROSIE_RT_FOUNDATION_ETHERCAT_INTERFACE` | The EtherCAT NIC's interface name. | | `ROSIE_RT_FOUNDATION_ETHERCAT_MAC`, `…_ETHERCAT_PERMANENT_MAC` | Its current and permanent MAC. | | `ROSIE_RT_FOUNDATION_UPLINK_INTERFACE`, `…_UPLINK_MAC`, `…_UPLINK_PERMANENT_MAC` | The host's other (uplink) NIC, so the two are never confused. | | `ROSIE_RT_FOUNDATION_EXPECTED_SLAVES` | Number of EtherCAT slaves expected on the bus. | | `ROSIE_RT_FOUNDATION_RECEIPT_PATH` | Where to write the foundation receipt. | | `ROSIE_RT_FOUNDATION_PROFILE` | Optional. Must match the detected profile. | | `ROSIE_RT_ETHERCAT_GROUP` | Optional. Group given access to `/dev/EtherCAT0`. Default `ethercat`. | Run it in three stages: `inspect` changes nothing, `install` provisions, `verify` checks the result and runs `rtctl hostcheck`. ```bash export NO_REAL_MOVEMENT=1 \ ROSIE_RT_FOUNDATION_ETHERCAT_INTERFACE=eth1 \ ROSIE_RT_FOUNDATION_ETHERCAT_MAC= ROSIE_RT_FOUNDATION_ETHERCAT_PERMANENT_MAC= \ ROSIE_RT_FOUNDATION_UPLINK_INTERFACE=eth0 \ ROSIE_RT_FOUNDATION_UPLINK_MAC= ROSIE_RT_FOUNDATION_UPLINK_PERMANENT_MAC= \ ROSIE_RT_FOUNDATION_EXPECTED_SLAVES=9 \ ROSIE_RT_FOUNDATION_RECEIPT_PATH=/etc/rosie-rt-core/ethercat-foundation.receipt sudo -E bash rt-core/host/ethercat-foundation.sh inspect sudo -E bash rt-core/host/ethercat-foundation.sh install sudo -E bash rt-core/host/ethercat-foundation.sh verify # ethercat_foundation_status=verified profile=… kernel=… expected_slaves=9 ``` `install` installs build dependencies with `apt-get`, downloads and checks the IgH archive, builds and installs the master and its kernel module, writes `/etc/ethercat.conf`, keeps NetworkManager off the EtherCAT NIC, adds a udev rule for `/dev/EtherCAT0`, and enables and restarts `ethercat.service`. It writes the receipt last. Running it again on a host whose receipt matches does nothing. ## 2. Build a runtime package On the build machine, build the live daemon against the pinned IgH userspace library, then stage a package: ```bash cd rt-core bash tools/build-igh-userlib.sh # 1.6.9 by default; pass 1.6.12 for rpi5-debian13 export PKG_CONFIG_PATH="$PWD/build/deps/igh-prefix/lib/pkgconfig" make live control bash tools/package-runtime.sh /tmp/rt-package # package status=staged backend=live # component status=staged output=/tmp/rt-package.components bash tools/verify-package.sh /tmp/rt-package ``` The package holds `bin/` (`rosie-rt-core`, `rt-control`, `rtctl`, `rt-package`, and `rt-natspublisher` if built), `lib/libethercat.so.1` with its licences, `host/` (the install scripts and the three unit files), `config/`, every robot description's registered files, and `rt_package.json`, the manifest that `verify-package.sh` checks. [Releasing](https://advancedmetalresearch.com/docs/contributing/releasing) describes the package and its companion archive. The unit files in the package point at `/opt/rosie-rt-core/current`. Set `ROSIE_RT_PACKAGE_SLOT_ROOT` before packaging to use another absolute path (not under `/home`, `/root` or `/run/user`). Copy `/tmp/rt-package` to the host. ## 3. Install the package, inactive ```bash sudo bash /tmp/rt-package/host/install.sh --from-package /tmp/rt-package --live # Installed. No units were started. ``` This: 1. verifies the package, and with `--live` refuses a simulation package 2. creates the groups `rosie-rt`, `rosie-ctl`, `rosie-rt-clients` and the device group, and the system users `rosie-rt` (the core) and `rosie-ctl` (the adapter), with no home and no login shell 3. copies the package to `/opt/rosie-rt-core//` and points `/opt/rosie-rt-core/current` at it, keeping the old target as `previous` 4. installs `rosie-rt-core.service`, `rosie-rt-control.service` and `rosie-rt-natspublisher.service` in `/etc/systemd/system/`, and runs `systemctl daemon-reload` It refuses to replace a stored release with different bytes under the same git identity. Nothing is enabled or started, and there is no deployment binding yet, so the units cannot start: each has `ConditionPathExists=/etc/rosie-rt-core/control.env`. To configure the optional NATS observer, set `ROSIE_RT_NATS_URL`, `ROSIE_RT_NATS_HOST` and `ROSIE_RT_NATS_CADENCE` when you run `install.sh`. It then writes `/etc/rosie-rt-core/natspublisher.env`. ## 4. Set up remote access (optional) Skip this if every client runs on the cell host and uses the local socket. For remote clients, `host/remote-pki.sh` creates one CA per deployment pair. Run it after step 3, because it needs the `rosie-ctl` group. Keep the PKI directory outside the repository. ```bash export ROSIE_RT_REMOTE_SERVER_SAN="DNS:rosie.local,DNS:localhost,IP:127.0.0.1" sudo -E bash /opt/rosie-rt-core/current/host/remote-pki.sh init /var/lib/rosie-rt-pki/cell-a cell-a sudo bash /opt/rosie-rt-core/current/host/remote-pki.sh issue-client /var/lib/rosie-rt-pki/cell-a pendant-1 ``` The identity given to `init` is the pair id and must match step 5. `issue-client` writes `clients/.pem` and `clients/-key.pem`; hand those and `ca.pem` to that client. Certificate details, SAN identity and revocation are in [Remote access (mTLS)](https://advancedmetalresearch.com/docs/apis/remote-access-mtls). > [!WARNING] **The CRL expires after 7 days.** Every `init`, `issue-client` and `revoke` regenerates `crl.pem` with a 7-day validity, and `remote-pki.sh` has no refresh command. rt-control checks the CRL on every handshake and refuses all remote connections once it has expired (`remote CRL not current`). Worse, if the CRL has already expired when rt-control starts, rt-control exits at startup, which also takes the local control socket down. Reissue the CRL before it expires, as below. To reissue the CRL, run the same command the script ends with, in the PKI directory, and publish the result where rt-control reads it: ```bash cd /var/lib/rosie-rt-pki/cell-a sudo openssl ca -batch -config ca.cnf -gencrl -out crl.pem sudo install -m 0640 -o root -g rosie-ctl crl.pem /etc/rosie-rt-core/pki/crl.pem ``` rt-control reloads a changed CRL before each full handshake, so no restart is needed. Run this on a schedule shorter than 7 days. ## 5. Bind the deployment Put the machine config on the host (keeping a copy at `/etc/rosie-rt-core/machine.json` lets `hostcheck` find it later), then compile it into the installed binding: ```bash sudo install -m 0644 machine.json /etc/rosie-rt-core/machine.json sudo bash /opt/rosie-rt-core/current/host/generate-control-env.sh \ --remote-pki /var/lib/rosie-rt-pki/cell-a \ /etc/rosie-rt-core/machine.json cell-a 1 # configuration_sha256=<64 hex> ``` Drop `--remote-pki` if you skipped step 4. The script runs [`rtctl control-env`](/docs/reference/rtctl#control-env), which refuses any machine config that is not `live`. Only if compilation succeeds does it atomically replace: - `/etc/rosie-rt-core/control.env` (mode 0640, group `rosie-ctl`), with the pair, the configuration digest, the core's arguments and `ROSIE_RT_REMOTE_ARGS` - `/etc/rosie-rt-core/compiled//axes.conf` and `argv.json` (group `rosie-rt`) - with `--remote-pki`, `/etc/rosie-rt-core/pki/`: the CA certificate, server certificate and key, and CRL readable by `rosie-ctl`, and the CA key and database readable by root only The remote listener address comes from `ROSIE_RT_REMOTE_LISTEN` and defaults to `127.0.0.1:8443`, loopback only. Set it to a host address to accept remote clients. Record the printed digest. Put it in the cell config's `nodes[].rt_core.configuration_sha256`, and use it in every client's `acquire` binding. See [Cells, machines and positioners](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners#cell-config). > [!CAUTION] At this revision the installer copies only `axes.conf` and `argv.json` into the compiled directory, and that directory is readable by the core's group only. `control.env` also points rt-control at it through `ROSIE_RT_COMPILED_CONFIG`, and rt-control reads `resources.json` and `configuration.identity` from there when it starts. Check that `rosie-rt-control` starts in step 7 before you rely on an installed cell. ## 6. Check the host ```bash sudo /opt/rosie-rt-core/current/bin/rtctl hostcheck --config /etc/rosie-rt-core/machine.json sudo /opt/rosie-rt-core/current/bin/rtctl inventory --config /etc/rosie-rt-core/machine.json ``` - [`hostcheck`](/docs/reference/rtctl#hostcheck) is read-only. It checks PREEMPT_RT, the IgH master version, EtherCAT device permissions, CPU isolation and `nohz_full` for the RT CPU, the frequency governor, NIC IRQ affinity, timers, and the service users and directories. Exit 0 means every check passed; it never measures cycle latency. - [`inventory`](/docs/reference/rtctl#inventory) reads each slave's identity from the bus and compares it with the compiled configuration. Exit 0 means every slave matched. If you have an owner-supplied host identity manifest, add `--expected-release ` to `hostcheck` to compare the installed units and loaded executables with it once the services are running. ## 7. Start the services When the cell is clear and the hardware E-stop is tested: ```bash sudo systemctl enable rosie-rt-core.service rosie-rt-control.service sudo systemctl start rosie-rt-control.service # also starts rosie-rt-core systemctl status rosie-rt-core rosie-rt-control ``` The core runs as `rosie-rt` with only `CAP_SYS_NICE` and `CAP_IPC_LOCK`, the adapter as `rosie-ctl` with no capabilities. The public sockets are `/run/rosie-rt-core/control.sock` and `/run/rosie-rt-core/jog.sock` (symlinks into `public/`), group `rosie-rt-clients`. Add the account that runs your clients to that group: ```bash sudo usermod -aG rosie-rt-clients "$USER" # log in again afterwards /opt/rosie-rt-core/current/bin/rtctl describe ``` Describe should report the `ethercat` backend and the digest from step 5. Starting the services does not arm or move anything: a client must acquire the lease, Home and arm first. The NATS observer, if configured, is `rosie-rt-natspublisher.service`; it has no command authority. ## Update and roll back To update, build and install a new package (steps 2 and 3). The new release becomes `current` and the old one `previous`. Rerun step 5 if the machine config changed, then restart the units. To go back one release: ```bash sudo bash /opt/rosie-rt-core/current/host/install.sh --rollback # Rolled back current to . No units were started. sudo systemctl restart rosie-rt-control.service ``` Rollback verifies both stored releases and swaps `current` and `previous`. It works once: a second rollback is refused until you install another package. Clients must acquire again after the restart. ## Installing from a source tree `host/install.sh` run from a checkout, without `--from-package`, installs the binaries from `rt-core/build/` into `/opt/rosie/rt-core/` instead, and with `machine.json pair-id pair-revision` arguments also generates the binding (and `--enable` enables the units). This form exists for the repository's own tests. Use a package for a cell. ## Related pages - [rtctl command reference](https://advancedmetalresearch.com/docs/reference/rtctl) - [Configuration files](https://advancedmetalresearch.com/docs/reference/configuration) - [Remote access (mTLS)](https://advancedmetalresearch.com/docs/apis/remote-access-mtls) - [Ports, sockets and environment variables](https://advancedmetalresearch.com/docs/reference/ports-and-environment) - [Releasing](https://advancedmetalresearch.com/docs/contributing/releasing) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/host/ethercat-foundation.sh:1-60,86-97,125-176,264-265,462-596` - `rt-core/host/install.sh:1-224` - `rt-core/host/generate-control-env.sh:1-79` - `rt-core/host/remote-pki.sh:1-101` - `rt-core/host/rosie-rt-core.service:1-43` - `rt-core/host/rosie-rt-control.service:1-36` - `rt-core/host/rosie-rt-natspublisher.service:1-41` - `rt-core/tools/package-runtime.sh:1-101` - `rt-core/tools/verify-package.sh:1-9` - `rt-core/tools/build-igh-userlib.sh:9-40` - `rt-core/tools/packageinfo/component.go:57-110` - `rt-core/tools/rtctl/control_env.go:22-90` - `rt-core/tools/rtctl/hostcheck.go:169-294` - `rt-core/tools/rtctl/inventory.go:74-146` - `rt-core/cmd/rt-control/main.go:24-68` - `rt-core/adapters/rosie/control/controller.go:109-130` - `rt-core/adapters/rosie/control/resources.go:80-100` - `rt-core/adapters/rosie/control/remote_tls.go:48-140` - `.github/workflows/rt-core.yml:62-72` --- # Build and install the pendant > Build the v5 Steam Deck teach pendant, test it, run it on a workstation, package and deploy it to a Deck with hash checks, add it as a Steam shortcut with its controller layout, and write the per-Deck site files. URL: https://advancedmetalresearch.com/docs/guides/install-the-pendant Section: RosieOS docs / Guides Last updated: 2026-10-10 The v5 pendant ships as one directory, `RosiePendantV5`, that you build on a Linux workstation and copy to a Steam Deck. It holds the native app, OLP's program logic bundled for Qt's JavaScript engine, the OLP server it runs headless on the Deck, and the source trees that server reads. To use it once it is installed, see [Program from the Steam Deck pendant](https://advancedmetalresearch.com/docs/guides/program-from-the-pendant). > [!CAUTION] **Not qualified for real motion.** The v5 pendant has not been qualified on physical stick, trigger and rear-button input, real robot motion, network faults, or a full plan, Load and Play on the Deck, and no CI workflow builds it. Its hold-to-enable trigger is a software deadman, not a safety-rated enabling device. Keep the cell's hardware E-stop within reach whenever the drives are powered, and use the pendant in simulation until the cell owner has qualified it. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). ## Before you start On the build workstation, Ubuntu 22.04 or WSL2: ```bash sudo apt install g++ make pkg-config qtbase5-dev libassimp-dev curl ``` You also need: - **Qt5Qml.** Install `qtdeclarative5-dev`, or, without root, extract it to a sysroot (below). - **Node 22** and the OLP UI's packages, for esbuild and OLP's TypeScript sources. - **Go 1.26.2**, to build the OLP server for the Deck. - The C++17 toolchain for `motion-server/v1`, which the build uses for the pendant's Cartesian solver. - `ssh` access to the Deck, for deploying. See [Install the toolchain](https://advancedmetalresearch.com/docs/get-started/installation). ### Qt5Qml without root The Makefile looks for a sysroot at `~/.rosie/qt5qml-sysroot`, or wherever `QML_SYSROOT` points: ```bash mkdir -p ~/.rosie/qt5qml-sysroot/debs && cd ~/.rosie/qt5qml-sysroot/debs apt-get download qtdeclarative5-dev libqt5qml5 libqt5qmlmodels5 libqt5quick5 for d in *.deb; do dpkg-deb -x "$d" ..; done ``` ## Build From the repository root: ```bash (cd offline-programming/v1/ui && npm ci) make -C steamdeck/real/v5 -j4 all ``` This builds three things in `steamdeck/real/v5/build/`: | File | What it is | |---|---| | `rosie-pendant-v5` | The native Qt 5.15 app | | `olp-core.js` | OLP's program and machine logic, from `offline-programming/v1/ui/src` and `steamdeck/real/v5/olp-core`, bundled by esbuild as ES2016 for Qt's engine | | `robot-v4-cartesiand` | The Cartesian solver the OLP server uses, built from `motion-server/v1` | The bundle depends on every non-test `.ts` file under OLP's `ui/src`, so a change to OLP rebuilds it. That is how the pendant and the desktop stay one implementation. ## Test ```bash make -C steamdeck/real/v5 check ``` | Check | What it covers | |---|---| | `session.test.ts`, `machine.test.ts` | The pendant's session and machine logic, under Node | | `core-check` | The built `olp-core.js` inside Qt's JavaScript engine, as the app runs it | | `capture-check` | The app's C++ capture TCP pose against OLP's own poses, for both robot descriptions | | `view-check` | Offscreen: the input interlock, display kinematics, description identity and both robots' mesh sets | ## Run it on the workstation Start an OLP server on port 8794 (the [quickstart](https://advancedmetalresearch.com/docs/get-started/quickstart) does), then: ```bash cd steamdeck/real/v5 LD_LIBRARY_PATH=~/.rosie/qt5qml-sysroot/usr/lib/x86_64-linux-gnu \ build/rosie-pendant-v5 --windowed --robots ../../../robot_description/robots ``` Leave out `LD_LIBRARY_PATH` if Qt5Qml is installed system-wide. | Option | Default | Description | |---|---|---| | `--olp ` | `http://127.0.0.1:8794` | The OLP server the pendant fronts | | `--bundle ` | `/olp-core.js` | The OLP logic bundle | | `--robots ` | `/../olp/robot_description/robots` | Robot descriptions, for the 3D view | | `--programs ` | `~/Rosie programs` | The folder for **OPEN** and **SAVE AS** | | `--windowed` | full screen | Run in a desktop window | | `--snapshot ` | | Render every page at 1280×800 to `-.png` after 3 s, then exit | ## Package ```bash steamdeck/real/v5/package.sh # the app and the OLP tree steamdeck/real/v5/package.sh --with-tesseract-env # also the Tesseract pixi environment (about 1 GB) ``` `package.sh` runs `make all`, builds the OLP server for Linux x86-64, and stages `steamdeck/real/v5/build/package/RosiePendantV5`: | Path | Contents | |---|---| | `run.sh`, `start-olp.sh`, `controller.vdf` | The launcher, the OLP server's start script, and the Steam Input layout | | `bin/` | `rosie-pendant-v5`, `olp-core.js`, `robot-v4-cartesiand` | | `lib/` | `libassimp.so.5`, `libdraco.so.4` and `libminizip.so.1` from the build host, with their Debian copyright files. SteamOS ships Qt 5.15 but not Assimp. | | `olp/` | The files of `offline-programming/v1`, `motion-server/v1`, `robot_description`, `tesseract/v1`, `weld_planner/v1`, `cadquery/v1`, and `rt-core/config/{cells,machines,drives}`, which the server needs to resolve remote cells. Also the server binary and a stub `ui/dist/index.html`, because `serve` refuses to start without one. | | `SHA256SUMS` | A hash of every file except the Tesseract environment | Pass `--with-tesseract-env` on the first deploy, and again whenever `tesseract/v1/pixi.lock` changes. The OLP server needs that environment. `package.sh` adds the `~/.rosie` toolchain directories to the front of `PATH`. If your Go and Node live elsewhere, have them on `PATH` already. The package never contains site files: the cell catalogue, the rt-core binding, `planner.env` or credentials. ## Deploy Exit the pendant on the Deck first. Then, with your Deck's address: ```bash steamdeck/real/v5/deploy.sh deck@rosie-deck.local ``` The optional second argument is the directory on the Deck. It defaults to `devkit-game/RosiePendantV5` in the `deck` user's home. `deploy.sh`: 1. refuses while the pendant or its `rosie-v5-olp` unit is running, so nothing that holds a machine grant is replaced underneath it 2. removes the files of the retired browser kiosk from the app directory 3. copies the package over `ssh` 4. checks every staged hash on the Deck with `sha256sum -c SHA256SUMS`, and prints `deploy: all staged hashes match` Site files on the Deck are left alone. ## Add it to Steam 1. Add `run.sh` as a non-Steam game (or devkit) shortcut. 2. Set `controller.vdf` as that shortcut's Steam Input layout. It maps the rear buttons L4, L5, R4 and R5 to F1, F2, F3 and F4, makes the right trackpad an absolute mouse, and makes the left trackpad scroll. See [Pendant controls](https://advancedmetalresearch.com/docs/reference/pendant-controls#steam-input-layout). `run.sh`: - starts `start-olp.sh` as the systemd user unit `rosie-v5-olp`, unless it is already running - waits up to 30 s for `http://127.0.0.1:8794/api/offline-programming/v1/health` - starts the app full screen, with Steam's `LD_PRELOAD` and `LD_LIBRARY_PATH` removed, `LD_LIBRARY_PATH` set to the package's `lib/`, `QT_QPA_PLATFORM=xcb` and a scale factor of 1 - stops the unit when the app exits. The server's shutdown stops any machine it armed before it releases control. Arguments to `run.sh` go to the app. The server's log is `journalctl --user -u rosie-v5-olp`. ## Site files Put these beside `run.sh` on each Deck. They are never committed and never packaged: | File | Read by | Contents | |---|---|---| | `olp-cells.json` | OLP server, as `OFFLINE_PROGRAMMING_CELLS` | The cell catalogue: which cells the CELL page lists. See [Connect to a cell and run a program](https://advancedmetalresearch.com/docs/guides/connect-a-cell). | | `olp-rt-core.json` | OLP server, as `OFFLINE_PROGRAMMING_RT_CORE_CONFIG` | The server-only rt-core backend configuration, for a local simulation entry | | `planner.env` | `start-olp.sh`, if present | Where the weld planner is, and how to run the seam worker | | client credentials (`*.pem`) | OLP server, through the catalogue | The mutual-TLS credentials for remote cells, named by the catalogue. See [Remote access (mTLS)](https://advancedmetalresearch.com/docs/apis/remote-access-mtls). | `start-olp.sh` also sets `OFFLINE_PROGRAMMING_EXECUTION_BACKEND=rt_core`, `OFFLINE_PROGRAMMING_JOG_BACKEND=rt_core` and the solver path, and starts `offline-programming-linux serve --listen 127.0.0.1:8794 --enable-local-simulator`. It leaves `ROSIE_RT_*` unset on purpose: OLP's disconnected local jog would otherwise take the simulation adapter's only controller slot and lock the pendant out. ### planner.env Planning and Load both need a weld planner on a workstation the Deck can reach. Every plan also runs the seam worker once on the Deck, to pack the `.weldplan`. The package ships the seam worker's sources but no pixi environment. Packing uses only the Python standard library, so the Deck's system Python can run it: planner.env: ```bash OFFLINE_PROGRAMMING_WELD_PLANNER_MOTION_ORIGIN=http://planner.local:8796 SEAM_WORKER_PYTHON=/usr/bin/python3 # Spare workers preload OpenCASCADE, which the system Python does not have. SEAM_WORKER_SPARES=0 ``` Without an origin, the RUN page shows the server's refusal and nothing is planned or loaded. Without `SEAM_WORKER_PYTHON` and without pixi on the Deck, planning fails because the seam worker cannot start. The alternative is pixi on the Deck and `pixi install -e default` in `olp/weld_planner/v1`. > [!WARNING] The weld planner must listen on the network for the Deck to reach it, and it has no authentication. Firewall port 8796 so that only the Deck and your own machine can reach it. See [Bind it safely](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner#bind). A complete plan, Load and Play from the Deck against the weld planner has not been qualified. ## Related pages - [Program from the Steam Deck pendant](https://advancedmetalresearch.com/docs/guides/program-from-the-pendant) - [Pendant controls](https://advancedmetalresearch.com/docs/reference/pendant-controls) - [Run the weld planner](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner) - [Use the virtual pendant](https://advancedmetalresearch.com/docs/guides/virtual-pendant) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `steamdeck/real/v5/Makefile:1-56` - `steamdeck/real/v5/package.sh:1-86` - `steamdeck/real/v5/deploy.sh:1-26` - `steamdeck/real/v5/run.sh:1-37` - `steamdeck/real/v5/start-olp.sh:1-28` - `steamdeck/real/v5/controller.vdf` - `steamdeck/real/v5/src/main.cpp:11-24` - `steamdeck/real/v5/olp-core/run.ts:102-113` - `offline-programming/v1/main.go:301-312` - `offline-programming/v1/internal/seam/worker.go:152-178,483-495` - `offline-programming/v1/internal/seam/spares.go:152` - `offline-programming/v1/internal/denseexec/session.go:236-244` - `weld_planner/v1/python/seam_worker/workers.py:548-623` - `weld_planner/v1/python/weldplan/urdf.py:51-56` - `weld_planner/v1/python/weldplan/program_v2.py:51-60` --- # Run the weld planner > Install the weld planner's pixi environments, check the GPU, serve the motion planner on port 8796 bound safely, connect OLP to it, plan from the command line, and run its tests. URL: https://advancedmetalresearch.com/docs/guides/run-the-weld-planner Section: RosieOS docs / Guides Last updated: 2026-10-10 The weld planner lives in `weld_planner/v1`. It has two halves with different needs: - The **seam worker** (CAD topology, weld joints, torch angle search, packing `.weldplan` files) runs on the CPU in the `default` environment. OLP starts it as a subprocess for every request; you never run it as a server. - The **motion planner** (seam search, trajectory optimisation, the verifier and the dense trajectory encoder) needs an NVIDIA GPU and runs in the `motion` environment, as an HTTP server on port 8796. For what the planner does and what its verification proves, see [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification). ## Before you start - Linux on x86-64 for the `motion` environment. For an NVIDIA Jetson Thor, see [Environments](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner#environments). The `default` environment also runs on Windows and macOS. - An NVIDIA GPU and a driver that supports CUDA 12 or later. The CUDA runtime comes from the environment; only the driver is the system's. - [pixi](https://pixi.sh). Nothing else: pixi installs Python, CadQuery, PyTorch and the rest. - A clone of RosieOS with its Git LFS files, because the planner reads the robot meshes from `robot_description/`. ## Install ```bash cd weld_planner/v1 pixi install -e default # seam worker and authoring tools (CPU) pixi install -e motion # motion planner (CUDA PyTorch, several GB) ``` Always pass `-e` to `pixi run` for motion tasks. Some task names, such as `motion-test`, exist in both environments. ### Environments | Environment | Platforms | Used for | |---|---|---| | `default` | linux-64, linux-aarch64, win-64, osx-64, osx-arm64 | The seam worker, CAD inspection tools and the fast self-tests | | `motion` | linux-64 | The motion planner and its tests, with CUDA PyTorch | | `motion-thor` | linux-aarch64 | The motion planner on an NVIDIA Jetson Thor. pixi provides everything except PyTorch, which comes from NVIDIA's own image. | | `bench` | linux-64 | Benchmarks against reference libraries. Not needed to plan. | The `motion` environment pins `TORCH_ALLOW_TF32_CUBLAS_OVERRIDE=0`, so matrix maths keeps full float32 precision on every GPU. The planner was qualified that way. ## Check the GPU ```bash pixi run -e motion motion-gpu # the device, its capability, and whether this PyTorch has kernels for it pixi run -e motion motion-self-test # every motion worker's smoke check pixi run -e motion motion-verify # the verifier's self-test ``` ## Serve the motion planner ```bash cd weld_planner/v1 pixi run -e motion motion-serve ``` `motion-serve` starts the FastAPI server on `0.0.0.0:8796` and sets `AMR_WELD_PLANNER_SOURCE_REVISION` to the checkout's `git rev-parse HEAD`, so every result names the code that planned it. The server logs one line per plan to stderr: MiB in, how many seams were crossed, moves, MiB out, time queued and time served. Check it: ```bash curl -s http://localhost:8796/api/motion/health # {"cuda": true, "device": "…"} ``` `"cuda": false` means PyTorch cannot see the GPU. Planning will not work until it can. ### Bind it safely > [!WARNING] **The planner listens on every interface by default, with no authentication.** Anyone who can reach port 8796 can queue plans on your GPU and download every stored trajectory. Bind it to localhost when OLP runs on the same machine, or allow only the hosts that need it through a firewall. When OLP runs on the same machine, bind to localhost: ```bash cd weld_planner/v1 AMR_WELD_PLANNER_SOURCE_REVISION=$(git rev-parse HEAD) \ pixi run -e motion python -m weld_motion_planner.server.motion_planner_server --host 127.0.0.1 ``` A Steam Deck pendant plans through a workstation's planner, so there the planner must listen on the network. Firewall port 8796 so that only the Deck (and your own machine) can reach it. For example, with ufw, with the Deck's address in place of the placeholder: ```bash sudo ufw allow from to any port 8796 proto tcp sudo ufw deny 8796/tcp ``` The dev stack's `motion` service runs `pixi run -e motion motion-serve`, so it too listens on every interface. See [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation). ### Server options | Flag | Environment variable | Default | Description | |---|---|---|---| | `--host` | | `0.0.0.0` | Listen address | | `--port` | | `8796` | Listen port | | `--dense-store-dir` | `WELD_PLANNER_DENSE_STORE_DIR` | `~/.cache/rosieos-olp/weld-planner-dense` | Where verified `.rdt` files are stored, by digest. Refused requests are kept under `refused/` in the same directory. | | | `AMR_WELD_PLANNER_SOURCE_REVISION` | Set by `motion-serve` | A full 40-character lowercase Git commit, stamped into results. Any other non-empty value makes every plan fail. | The store sits outside the checkout on purpose, so resetting the workspace does not delete a trajectory a program still plays. It is still a cache: OLP keeps its own copy of each trajectory, and a Load after the file is gone answers `dense_blob_not_found`. Plan again. ## Connect OLP to it OLP reaches the planner through one variable, which the OLP launcher reads: ```bash export OFFLINE_PROGRAMMING_WELD_PLANNER_MOTION_ORIGIN=http://127.0.0.1:8796 bash offline-programming/v1/start-offline-programming.sh ``` The launcher prints a `motion:` line. `motion: DISABLED` means the variable is not set. Without it, Plan answers `motion_origin_unavailable` and Load answers `dense_blob_source_unavailable`. The dev stack sets it for you. See the [OLP server's variables](https://advancedmetalresearch.com/docs/reference/ports-and-environment#offline-programming-server). OLP also runs the seam worker from `weld_planner/v1` for every seam request and every plan. By default it launches `pixi run -e default python -m seam_worker.workers --stdin`, so the `default` environment must be installed on the machine that runs the OLP server. Two variables change that: | Variable | Default | Description | |---|---|---| | `SEAM_WORKER_PYTHON` | unset (use `pixi run`) | A Python executable to run the worker directly | | `SEAM_WORKER_SPARES` | 2 | Workers started ahead of their request. `0` starts every request cold. | ## Plan from the command line You can plan a `.weldplan` without the server. This is useful for looking at one part in detail: ```bash cd weld_planner/v1 pixi run -e motion motion-plan data/motion/bracket_a_2x.weldplan --trajectories --output result.json ``` It prints a summary per seam and writes the full result document to `--output`. The command line never writes a `.rdt`. Only the server does, with `dense=true`. | Flag | Default | Description | |---|---|---| | `request` | required | Path to a `.weldplan` | | `--k` | 5 | Candidate paths per seam | | `--samples-per-seam` | by time | Space the lattice by sample count | | `--no-collide` | off | Skip collision screening and avoidance. The result says it is unscreened. | | `--trajectories` | off | Also run M5 and the connecting moves (minutes rather than seconds) | | `--no-moves` | off | With `--trajectories`, skip the connecting moves | | `--no-verify` | off | Skip the verifier. Nothing planned this way can become a `.rdt`. | | `--output PATH` | none | Write the result document as JSON | | `--source-revision` | `AMR_WELD_PLANNER_SOURCE_REVISION` | The full Git commit to record | The stages also run alone: `motion-seams` (M4), `motion-weld-trajopt` (M5) and `motion-link` (M6), each with a `.weldplan` argument. The committed example requests are in `weld_planner/v1/data/motion/`. `pixi run -e default motion-fixtures` rebuilds them from the STEP files in `data/fixtures/`. ## Inspect inputs | Task | What it shows | |---|---| | `pixi run -e motion motion-request --read ` | A `.weldplan` as the planner sees it | | `pixi run -e default plan-request --read ` | The container's manifest, with every digest verified | | `pixi run -e default program-v2 ` | Problems in a `robot.v4.program.v2` document | | `pixi run -e motion motion-cell` | Which axes are the arm, which move the work, which belong to neither | | `pixi run -e motion motion-profile` | The cell profile: axis roles, rates, reset pose, and what is missing | | `pixi run -e motion motion-spheres` | The arm's sphere model and the torch built from `tooling.json` | Run any task with `--help` for its arguments. ## Run the tests ```bash cd weld_planner/v1 pixi run -e motion motion-test # the motion planner's tests, on the GPU pixi run -e default test # the whole suite in the authoring environment pixi run -e default self-test # fast smoke checks, no GPU ``` CI runs only a subset of the planner's tests, on a CPU build of PyTorch. Run `motion-test` on a GPU before you rely on a change. ## When something goes wrong | Symptom | Cause | |---|---| | `/api/motion/health` says `"cuda": false` | PyTorch cannot see the GPU. Check the driver with `nvidia-smi`, then `pixi run -e motion motion-gpu`. | | A plan fails with `no cell meshes at …` | The robot's meshes are missing. Fetch the Git LFS files. The verifier refuses to judge a cell it cannot see. | | A plan fails at once with a `source_revision` error | `AMR_WELD_PLANNER_SOURCE_REVISION` is set to something other than a full commit hash. Unset it, or use `motion-serve`. | | A plan answers but has no trajectory | Read `dense_error`. The request and result are kept under `/refused/`. See [A plan without a trajectory](https://advancedmetalresearch.com/docs/apis/weld-planner-http#a-plan-without-a-trajectory). | | A second plan waits | Only one plan runs at a time. `GET /api/motion/progress` shows the running stage. | ## Related pages - [Weld planner HTTP API](https://advancedmetalresearch.com/docs/apis/weld-planner-http) - [Program a weld from CAD](https://advancedmetalresearch.com/docs/guides/offline-programming) - [Weld program and `.weldplan` container](/docs/reference/weld-program-format) - [Install the toolchain](https://advancedmetalresearch.com/docs/get-started/installation) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `weld_planner/v1/pixi.toml:1-242` - `weld_planner/v1/python/weld_motion_planner/server/motion_planner_server.py:57-82,338-344,527-535,556-571` - `weld_planner/v1/python/weld_motion_planner/io/native_result.py:18-36` - `weld_planner/v1/python/weld_motion_planner/planner/planner_main.py:308-448` - `weld_planner/v1/python/weld_motion_planner/verifier/model.py:28-33` - `weld_planner/v1/python/weld_motion_planner/io/plan_request.py:432-433` - `weld_planner/v1/python/weldplan/plan_request.py:656-668` - `weld_planner/v1/python/weldplan/program_v2.py:817-818` - `weld_planner/v1/tools/make_motion_fixtures.py` - `offline-programming/v1/internal/seam/worker.go:47-58,68-90,152-178` - `offline-programming/v1/internal/denseexec/session.go:236-258` - `offline-programming/v1/start-offline-programming.sh:499-515` - `dev-stack.sh:65,302-303` --- # Add a robot model > Create a new RosieOS robot description, from URDF and meshes through config.json, rtcore_definition.json and collision spheres, register it with the manifest tool, and pin it from a machine config. URL: https://advancedmetalresearch.com/docs/guides/add-a-robot-model Section: RosieOS docs / Guides Last updated: 2026-10-10 This guide creates a new robot description in `robot_description/robots//`, checks it, and pins it from an rt-core machine config so a cell can run it and OLP can plan for it. Read [Robot description and coordinate frames](https://advancedmetalresearch.com/docs/concepts/robot-description-and-frames) first; every field is in [Robot description files](https://advancedmetalresearch.com/docs/reference/robot-description). The checks along the way: ```bash python3 robot_description/tools/manifest.py --check # manifest and meshes (cd robot_description/go && go run ./cmd/identity ../robots/) (cd rt-core && build/rtctl compile --config config/machines/.json --out build/config) ``` ## Before you start - Pick a model id: ASCII letters, digits and underscores, for example `rosie_1600_v1`. It is the directory name, the URDF robot name, the `model_id` in every file, and the cell id that programs carry. Changing it later means a new model. - Have the robot's kinematics, joint limits and meshes, and its drive gearing, encoder scale and joint directions from your mechanical design. - Build `rt-core` (`make -C rt-core control`) and install the weld planner's `motion` environment if you will fit spheres (see [Run the weld planner](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner)). Everything you register becomes part of the model's identity, and changing it later invalidates every plan made against it. That is intended: finish the files before you pin them. ## 1. Create the directory ```bash mkdir -p robot_description/robots/rosie_1600_v1/meshes ``` Put the link meshes in `meshes/`. Keep CAD exports, the scripts that produced the files and measurement reports in a `provenance/` folder. They are not registered, so editing them never changes the identity. ## 2. Write `robot.urdf` and `robot.srdf` - Set ``, equal to the directory name. - Give every movable joint a bounded ``, in rad and rad/s. rt-core takes each robot-bound axis's travel and maximum velocity from here, and refuses a joint without bounded limits. - Reference meshes as `package:///meshes/`. - End the arm in a `tool0` link at the tool centre point, as the Rosie models do, and put the torch's electrode along +x of that frame. - In `robot.srdf`, define a `manipulator` group with the planned joints and a `home` group state. Name the arm links `link_1` to `link_6` if you can. The sphere-fitting tool assumes those names. The sphere tool in `urdf/v1/sphere_tool` can also help find joint limits by driving each joint to where the arm stops. See [step 5](https://advancedmetalresearch.com/docs/guides/add-a-robot-model#spheres). ## 3. Write `config.json` Copy a sibling's `config.json` and replace every value with this robot's. State only what URDF cannot: do not repeat a URDF number. robot_description/robots/rosie_1600_v1/config.json (shape): ```json { "schema": "rosie.robot-config.v1", "model_id": "rosie_1600_v1", "frames": { "base": "world", "tool": "tool0", "work": "world" }, "kinematic_correction_caps": { "xyz_m": 0.002, "rpy_rad": 0.017453292519943295 }, "planning": { "speed_ceiling_rad_s": 1.5, "joints": { "J1": { "velocity_rad_s": 1.0, "acceleration_rad_s2": 2.0 } } }, "axes": { "driven": ["J1", "J2", "J3", "J4", "J5", "J6"], "held": {} }, "reset_pose": { "J1": 0.0 }, "torch": { "tool_frame": "tool0", "electrode_axis": [1.0, 0.0, 0.0] } } ``` The loader refuses: - a `schema` or `model_id` that does not match - a planning `velocity_rad_s` above that joint's URDF `velocity`, or a `speed_ceiling_rad_s` above the fastest URDF joint - a non-positive rate, a negative cap, or a non-finite held value - any joint in `planning`, `axes` or `reset_pose` that is not a revolute URDF joint - unknown fields Give **every** joint an `acceleration_rad_s2`. rt-core uses it for trajectories and jog on each robot-bound axis and refuses to compile without it (`robot_acceleration_required`). For a positioner, add `frames.work_surface` so the work frame sits on the table surface rather than at the link origin, and hold any axis the planner should not move. See [Cells, machines and positioners](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners#the-h-frame-positioner) for the Rosie 1400 example. ## 4. Write `rtcore_definition.json` This file tells rt-core how each joint maps to a drive. Start from the annotated template `rt-core/config/templates/robot.json`: every field there has a `_doc` sibling with its unit, range and default. Remove the `_doc` fields and fill in your robot. > [!NOTE] The gear ratio, encoder counts, joint signs and encoder battery facts come from your robot's mechanical design and drives. These docs describe the fields but give no values. Every numeric fact needs a `_source` ("cited: …") or `_unverified` ("unverified: …") sibling, or compile refuses it. Checklist: - `schema` is `rosie.robot-definition.v1`, and `robot_id` equals the directory name. - `calibration` is `{"identity": "home_calibration", "note": "…"}`. Home offsets are never stored here. - `defaults` names the drive config (`drive_profile`), `coordinate_evidence_mode` and `home_policy` (`required` unless you have a reason). - `joints[]` has one entry per movable URDF joint, at most 16: `name`, `kind` (`arm`, `positioner` or `external`), `urdf_joint`, `cartesian_participates`, `gear_ratio`, `motor_encoder_counts_per_rev`, `sign`, and, for joints on a supported absolute-encoder drive, `encoder_battery` with its source. - `arm` joints must map to a URDF joint. Only an `external` joint with no URDF joint may declare `mechanical_travel`. A robot-bound machine cannot override any of these, so get them right here. ## 5. Fit the collision spheres The weld planner refuses a model without `spheres.json`. Author a first model in the sphere tool, then refine it: ```bash (cd urdf/v1/sphere_tool && npm ci && npm run build) python3 urdf/v1/sphere_tool/serve.py 8795 rosie_1600_v1 # open http://127.0.0.1:8795/ # save the result as robot_description/robots/rosie_1600_v1/spheres.json cd weld_planner/v1 pixi run -e motion python tools/fit_spheres.py --cell rosie_1600_v1 pixi run -e motion motion-spheres ``` `fit_spheres.py` refits an existing `spheres.json` in place so every sphere sits inside its link, and stamps the URDF and mesh hashes it fitted against into `provenance`. By default it refits `link_3` to `link_6`; pass `--links` to choose others. After a URDF edit that moves no mesh, `--restamp` updates the hashes and changes nothing else. `motion-spheres` reports which links are covered. ## 6. Register the files ```bash python3 robot_description/tools/manifest.py rosie_1600_v1 # manifest: rosie_1600_v1 files sha256:<64 hex> python3 robot_description/tools/manifest.py --check ``` The manifest registers `robot.urdf`, `robot.srdf`, `config.json`, `spheres.json`, `rtcore_definition.json` and everything in `meshes/`. Do not add a `machine_planning_calibration.json` to the description: that file belongs to a cell. `--check` also fails if the URDF names a mesh that is not in `meshes/`. Print the identity with the Go tool too; both must agree: ```bash (cd robot_description/go && go run ./cmd/identity ../robots/rosie_1600_v1) ``` ## 7. Pin it from a machine config Create a machine config under `rt-core/config/machines/` that pins the directory and its identity, and maps one axis per definition joint: rt-core/config/machines/bench/rosie1600.json (shape): ```json { "schema_version": 1, "backend": "live", "cycle_ns": 1000000, "robot": { "robot_description": { "path": "robot_description/robots/rosie_1600_v1", "sha256": "sha256:", "source": "cited: robot_description/robots/rosie_1600_v1" } }, "axes": [ { "robot_joint": "J1", "slave_position": 0, "limits": { "max_target_lead": 0.02, "following_error": 0.04, "following_error_timeout_ms": 100, "completion_tolerance": 0.0002, "completion_timeout_ms": 500 } } ], "max_cycle_lateness_ns": 20000000, "runtime": { "cpu": 2, "priority": 90 } } ``` Robot-bound axes carry only `robot_joint`, `slave_position` and the tracking limits. They inherit travel, velocity, acceleration, gearing and Home policy from the description, and compile refuses any attempt to redeclare them. List a definition joint that has no drive on this machine in `robot.absent_joints`. Compile it: ```bash cd rt-core build/rtctl compile --config config/machines/bench/rosie1600.json --out build/config ``` | If compile says | Fix | |---|---| | `robot_description_mismatch: … hashes to X and the machine pins Y` | Update `sha256` to the current identity. | | `robot_definition_unavailable` | Register `rtcore_definition.json` in the manifest. | | `robot_acceleration_required` | Add `acceleration_rad_s2` for that joint in `config.json`. | | `robot_joint_missing` | Add an axis for the joint, or list it in `robot.absent_joints`. | | `robot_urdf_limits_required` | Remove the limit, velocity or gearing field from the axis. | Every reason is listed in [robot compile refusals](https://advancedmetalresearch.com/docs/reference/configuration#robot-compile-refusals). Then deploy it as in [Install rt-core on a cell host](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host), and update every cell pin with the new configuration digest. ## 8. Plan against it OLP lists every description with a URDF, so the new model appears in its robot catalogue. The planner finds it by directory name. A program planned for it records the model id and identity, and Load refuses it on a cell with any other description (`robot_cell_mismatch`). To try the model in simulation, write a `"backend": "simulation"` machine that pins it the same way. Home works on a simulated robot-bound machine only if the definition's drive profile supports Home on the simulated bus. The shipped Rosie 1400 simulation machine does not, which is why the dev stack's default simulated cell uses flat axes. See [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation). ## When the robot changes Edit the files, rerun `manifest.py `, and update the identity in every machine config that pins the model. Recompile, redeploy and update the cell pins. Plans made against the old identity are refused and must be planned again. That is the point: the plan was for a different robot. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `robot_description/tools/manifest.py:1-131` - `robot_description/go/identity.go:60-118` - `robot_description/go/description.go:122-285` - `robot_description/go/cmd/identity/main.go:1-32` - `robot_description/robots/rosie_1420_v1/config.json` - `robot_description/robots/rosie_1400_v3/robot.srdf` - `rt-core/config/templates/robot.json:1-109` - `rt-core/config/templates/machine.json (robot, axes robot-bound branch)` - `rt-core/tools/rtctl/robot_definition.go:130-160,250-290,313-420,426-500,500-735` - `rt-core/config/machines/simulation/simulation-rosie1400.json` - `weld_planner/v1/tools/fit_spheres.py:1-60,168-226` - `weld_planner/v1/pixi.toml:144` - `urdf/v1/sphere_tool/serve.py:1-45` - `urdf/v1/sphere_tool/package.json:6-11` - `robot_description/go/description.go:327-360` - `motion-server/v1/local-rt-core.sh:17-60` --- # Use the virtual pendant > Run the browser Steam Deck pendant against the simulated core, take control, arm, home and jog joints, and know what it cannot do. URL: https://advancedmetalresearch.com/docs/guides/virtual-pendant Section: RosieOS docs / Guides Last updated: 2026-10-10 The virtual pendant is a browser copy of the Steam Deck v4 pendant's screen, drawn at the Deck's native 1280×800. A small loopback bridge connects it to the simulated core. Use it to try the pendant workflow, or to work on its layout, without a Deck or a robot. It is simulation-only by construction: - The bridge binds only to a loopback address. - It checks that the backend it drives is the simulation, with the expected configuration digest and pair. - The browser never sees a control socket or a credential. ## Start it Start the simulated core, `rt-control` and the bridge with the dev stack. Then start the browser UI: ```bash export DEV_STACK_UI_HOST=127.0.0.1 DEV_STACK_FIREWALL=0 ./dev-stack.sh start rt-sim rt-control pendant cd steamdeck/virtual/v1/ui npm ci npm run dev ``` Open **http://127.0.0.1:51711/steamdeck/virtual/v1/**. Vite serves the page on port 51711. It proxies `/healthz` and `/api/v1` (including the WebSocket session) to the bridge on `127.0.0.1:51712`. The pendant's 3D view loads the Rosie 1400 model from `urdf/v1/ui/public/robot/rosie_1400_v3`. ## Drive the simulated robot On the rt-core backend, the pendant does joint jog only. The usual sequence: 1. **Take the leader.** The bridge acquires the rt-control lease for your browser session and renews it every 100 ms. A second browser gets `control_already_owned`. 2. **Home.** This sends a public `home` for all axes. The dev-stack simulation requires Home before anything moves. 3. **Arm.** This sends `enable` for all axes, then `arm`. 4. **Jog.** Select a joint, J1 to J9, and push the left stick up or down, or the right stick sideways. Holding one trigger scales the speed to 50%, both triggers to 10%. 5. **Disarm or Stop.** Either one sends a Stop and releases the lease. **Release the leader** when you are done. Jog speed is capped at 10% of the joint's maximum velocity from Describe, times the stick deflection and the trigger scale. The bridge refuses any input sample older than 150 ms. If samples stop arriving, the core's jog deadline ramps the joint to a hold. **Clear Fault** maps to `reset_fault`, which is an interim capability in rt-core. ## Keyboard and mouse | Key | Deck control | |---|---| | `[` / `]`, or Page Up / Page Down | LB / RB | | Arrow keys | D-pad | | A (or Space, Enter), B (or Esc), X, Y | Face buttons | | Tab | Select (View) | | M | Menu | | Shift (hold) | LT | | Ctrl (hold) | RT | | 1 / 2 | Latch LT / RT on or off | | G | Toggle the layout grid | Drag the on-screen sticks with the mouse, or plug in a gamepad. The browser Gamepad API maps its sticks, face buttons, bumpers and D-pad. ## What it cannot do On the rt-core backend, these return a typed refusal instead of acting: | Feature | Refusal code | |---|---| | Cartesian or TCP jog | `rt_core_cartesian_unavailable` | | Program list, program editing, planning | `rt_core_planning_unavailable` | | Reading or applying drive configuration | `rt_core_configuration_unavailable` | | Log sessions | `rt_core_logs_unavailable` | Use the [offline programming app](https://advancedmetalresearch.com/docs/get-started/quickstart) for Cartesian jog and programs. Use `rtctl` and the events and telemetry streams for logs and configuration. ## Run the bridge by hand The dev-stack `pendant` service builds the bridge from `steamdeck/virtual` and starts it with the simulator's binding. To do the same yourself: ```bash (cd steamdeck/virtual && go build -o ../../rt-core/build/virtual-deck ./cmd/local) source rt-core/build/dev-stack/binding.env rt-core/build/virtual-deck bridge start --adapter robot-v4-sim --backend rt_core \ --rt-core-socket "$ROSIE_RT_CONTROL_SOCKET" --rt-core-pair "$ROSIE_RT_PAIR_ID" \ --rt-core-revision "$ROSIE_RT_PAIR_REVISION" --rt-core-sha256 "$ROSIE_RT_CONFIGURATION_SHA256" ``` `bridge start` flags: | Flag | Default | Description | |---|---|---| | `--adapter` | `fixture` | `robot-v4-sim` drives the simulated core. `fixture` serves a deterministic, command-disabled fixture set. | | `--backend` | `rt_core` | The only supported backend. Any other value is refused as `backend_retired`. | | `--rt-core-socket` | — | The public `control.sock`. Required for `robot-v4-sim`. | | `--rt-core-pair`, `--rt-core-revision` | — | The pair binding. Required for `robot-v4-sim`. | | `--rt-core-sha256` | — | The configuration digest, 64 hex. Required for `robot-v4-sim`. | | `--fixture` | `v1-complete` | Fixture set for the `fixture` adapter | | `--host` | `127.0.0.1` | Bind host. Must be a loopback address. | | `--port` | `51712` | Bind port | | `--dry-run` | off | Validate the flags and print the address without starting | `bridge status [--host] [--port]` probes a running bridge's `/healthz`. `bridge contract` prints the bridge's protocol contract as JSON. The rt-core adapter needs Linux or WSL. On other platforms it fails with `rt_core_platform_unavailable`. To point the UI at a bridge on another port, set `STEAMDECK_VIRTUAL_BRIDGE_URL` before `npm run dev`, for example `http://127.0.0.1:51713`. Start the bridge on that port with `DEV_STACK_PENDANT_PORT=51713`. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `steamdeck/virtual/bridge/cli.go:20-45,99-150` - `steamdeck/virtual/bridge/backend.go:25-27` - `steamdeck/virtual/bridge/server.go:119-128,731-739` - `steamdeck/virtual/bridge/rt_core.go:20-32,108,133-340,378-404,465-523` - `steamdeck/virtual/bridge/rt_core_unsupported.go:1-9` - `steamdeck/virtual/cmd/local/main.go` - `steamdeck/virtual/v1/ui/vite.config.ts:10-20,108-131` - `steamdeck/virtual/v1/ui/package.json:6-10` - `steamdeck/virtual/v1/ui/src/input.ts:69-160,800-851` - `steamdeck/virtual/v1/ui/src/state.ts:651-653` - `steamdeck/virtual/v1/ui/src/sticks.ts:1-90` - `dev-stack.sh:38-40,262-264,356-360` --- # rt-control HTTP API > Reference for rt-control, the public control API of RosieOS. It covers all 33 operations, the request and response envelopes, every schema type and the reason codes each operation can return. URL: https://advancedmetalresearch.com/docs/apis/rt-control-http Section: RosieOS docs / APIs Last updated: 2026-10-10 rt-control is the only public way to command a RosieOS cell. It serves HTTP/1.1 and JSON over a Unix socket on the cell host, and optionally over mutual TLS for remote clients. Every client uses it: the offline programming server, the motion servers, the pendant, `rtctl` and your own code. Behind it, rt-control talks to the real-time core over a private IPC channel, which is internal and not part of this API. This page is generated from the contract file `rt-core/protocol/application-v1.schema.json` (`contract_version` 2) and the adapter code. It lists all 33 capabilities, every request and response field, and the reason codes each operation can return. The full catalogue of 153 reason codes is on [Error codes and fault states](https://advancedmetalresearch.com/docs/reference/error-codes). > [!WARNING] **Commands on this page move hardware.** `enable`, `arm`, `home`, jog and every Start energise the drives. The hardware E-stop is the only emergency stop: RosieOS has no software E-stop, and `stop` is not one. Only planned weld programs pass the planner's collision and limit check before they can be loaded. Jog, Home and point-list moves rely on the core's limit and readiness checks only. Read the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model) before you arm a real cell. > [!TIP] **Machine-readable.** The same contract as an [OpenAPI 3.1 document](https://advancedmetalresearch.com/docs/openapi/rt-control.json), the [contract file itself](https://advancedmetalresearch.com/docs/openapi/rt-control.contract.json), and every reason code as [JSON](https://advancedmetalresearch.com/docs/data/error-codes.json). ## Quick start Read the description and status, then take and release authority, from a shell on the cell host (or on a machine running the simulated core). The socket path is the installed default. rt-control from curl: ```bash SOCK=/run/rosie-rt-core/control.sock rt() { curl -sS --unix-socket "$SOCK" "http://localhost$1" "${@:2}"; } # 1. Who is this cell? Check the backend, identity and axis order. rt /v1/describe | jq '.data | {backend, machine_sha256, cycle_ns, axes: [.axes[] | {id, position_unit, min_position, max_position}]}' # 2. What is it doing now? rt /v1/status | jq '{armed: .data.core.armed, homed: .data.core.home_valid_mask, faults: .data.core.safety_fault_mask}' # 3. Take authority, then give it back. Use the pair ID and revision rt-control was started with. D=$(rt /v1/describe) GRANT=$(rt /v1/control -H 'Content-Type: application/json' -d "$(jq -n --argjson d "$D" '{ schema: "rosie.rt-control.request.v1", operation: "acquire", controller: "curl-demo", binding: {pair_id: "cell-a", revision: 1, configuration_sha256: $d.data.configuration_sha256, machine_sha256: $d.data.machine_sha256}}')") echo "$GRANT" | jq '.data | {session, generation, lease_ms}' rt /v1/control -d "$(echo "$GRANT" | jq '{schema: "rosie.rt-control.request.v1", operation: "release", session: .data.session, generation: .data.generation}')" | jq '.data.session == ""' ``` The default lease is 500 ms, and a shell cannot renew it between steps. For anything beyond this round trip, use a client that renews in the background: the [Go SDK](https://advancedmetalresearch.com/docs/apis/go-sdk) or the [C++ client](https://advancedmetalresearch.com/docs/apis/cpp-client). `rtctl` wraps the same calls for one-off commands; see [rtctl](https://advancedmetalresearch.com/docs/reference/rtctl). ## Operations at a glance Each capability has a state. `implemented` means the interface exists and is tested in software; it is not a hardware qualification. `interim` is the current reset API, `test_only` is for fixtures, and `unimplemented` returns `capability_unimplemented`. Describe returns this list at run time. | Operation | State | Transport | Lease | Result | |---|---|---|---|---| | [`telemetry`](#telemetry) | `implemented` | `GET /v1/telemetry?after=`
`GET /v1/telemetry/stream?after=` | none | [`TelemetryBatch`](#type-telemetrybatch) | | [`mark_telemetry`](#mark-telemetry) | `implemented` | `POST /v1/control` | session and generation | `sequence` | | [`acquire`](#acquire) | `implemented` | `POST /v1/control` | no fence; checks `binding` | [`Grant`](#type-grant) | | [`renew`](#renew) | `implemented` | `POST /v1/control` | session and generation | [`Grant`](#type-grant) | | [`release`](#release) | `implemented` | `POST /v1/control` | session and generation | [`Grant`](#type-grant) | | [`stop`](#stop) | `implemented` | `POST /v1/control` | session and generation | [`Grant`](#type-grant) | | [`enable`](#enable) | `implemented` | `POST /v1/control` | session and generation | `sequence` | | [`arm`](#arm) | `implemented` | `POST /v1/control` | session and generation | `sequence` | | [`home`](#home) | `implemented` | `POST /v1/control` | session and generation | `sequence` | | [`reset_fault`](#reset-fault) | `interim` | `POST /v1/control` | session and generation | [`RecoveryStatus`](#type-recoverystatus) | | [`recovery_status`](#recovery-status) | `implemented` | `POST /v1/control` | session and generation | [`RecoveryStatus`](#type-recoverystatus) | | [`jog`](#jog) | `test_only` | `POST /v1/control` | session and generation | `sequence` | | [`begin_jog`](#begin-jog) | `implemented` | `POST /v1/control` | session and generation | `handle` | | [`end_jog`](#end-jog) | `implemented` | `POST /v1/control` | session and generation | `handle` | | [`prepare_trajectory`](#prepare-trajectory) | `implemented` | `POST /v1/control` | session and generation | `handle` | | [`start_trajectory`](#start-trajectory) | `implemented` | `POST /v1/control` | session and generation | `sequence` | | [`discard_trajectory`](#discard-trajectory) | `implemented` | `POST /v1/control` | session and generation | `sequence` | | [`start_program`](#start-program) | `implemented` | `POST /v1/control` | session and generation | `sequence` | | [`describe`](#describe) | `implemented` | `GET /v1/describe` | none | [`Description`](#type-description) | | [`status`](#status) | `implemented` | `GET /v1/status` | none | [`ProcessStatus`](#type-processstatus) | | [`jog_clock`](#jog-clock) | `implemented` | `GET /v1/jog/clock` | none | [`LocalJogClock`](#type-localjogclock) | | [`jog_status`](#jog-status) | `implemented` | `GET /v1/jog` | none | [`JogObservation`](#type-jogobservation) | | [`jog_ingress`](#jog-ingress) | `implemented` | `GET /v1/jog/ingress` | none | [`JogIngressObservation`](#type-jogingressobservation) | | [`prepare_program`](#prepare-program) | `implemented` | `POST /v1/program` | session and generation headers | [`Program`](#type-program) | | [`update_jog`](#update-jog) | `implemented` | `jog.sock datagram or WSS /v1/jog`
`protocol/control.json local_jog_update` | session and generation | [`JogObservation`](#type-jogobservation) | | [`halt`](#halt) | `implemented` | `POST /v1/control` | session and generation | `Response.sequence/native_result` | | [`abort`](#abort) | `unimplemented` | none | none | `none` | | [`subscribe_events`](#subscribe-events) | `implemented` | `GET /v1/events?after=`
`GET /v1/events/stream?after= (SSE)` | none | [`EventBatch`](#type-eventbatch) | | [`readiness`](#readiness) | `unimplemented` | none | none | `none` | | [`restore_anchor`](#restore-anchor) | `implemented` | `POST /v1/control` | session and generation | [`RecoveryStatus`](#type-recoverystatus) | | [`io_arm`](#io-arm) | `implemented` | `POST /v1/control` | session and generation | `sequence` | | [`io_disarm`](#io-disarm) | `implemented` | `POST /v1/control` | session and generation | `sequence` | | [`resource`](#resource) | `implemented` | `GET /v1/resources/` | none | `binary` | ## Connecting | Transport | Address | Who can use it | |---|---|---| | HTTP over a Unix socket | `/run/rosie-rt-core/control.sock` (a link to `public/control.sock` in the same directory) | Local processes in the socket's group. File permissions are the authentication. | | Jog datagrams | `/run/rosie-rt-core/jog.sock` | Local jog producers. Binary frames only; see [`update_jog`](#update-jog). | | HTTPS with mutual TLS 1.3 | `--remote-listen`, which cell configs set to `127.0.0.1:8443` | Remote clients with a certificate from the cell's component CA. See [Remote access](https://advancedmetalresearch.com/docs/apis/remote-access-mtls). | rt-control's own flags set these paths: `--socket` (default `/run/rosie-rt-core/control.sock`; `jog.sock` is created beside it) and `--remote-listen`. Use `localhost` as the HTTP host name on the Unix socket. Keep connections alive: the listener's idle timeout is `control_idle_timeout_ns` from Describe (90 s). ## Request envelope Every JSON command is a `POST /v1/control` whose body is one `Request` object. `schema` and `operation` are always required. After `acquire`, every command also carries the `session` and `generation` of the current grant (the *fence*). Fields an operation does not use can be omitted. The server decodes strictly: unknown fields, duplicate fields and trailing data are refused. | Name | Type | Required | Description | |---|---|---|---| | `request_id` | `string` | No | Optional, non-null string of 1..64 printable ASCII bytes (0x20..0x7e). | | `schema` | `string` | Yes | Exactly `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | Operation name, for example `acquire`. Only `POST /v1/control` operations dispatch here. | | *(embedded)* | [`Fence`](#type-fence) | Yes | All fields of `Fence` appear at this level of the object. | | `label` | `string` | No | mark_telemetry requires 1..128 UTF-8 bytes; free-text dump label. | | `controller` | `string` | No | Acquire requires 1..63 bytes; opaque controller name. | | `binding` | [`Binding`](#type-binding) | No | Acquire requires exact equality with the configured Binding. | | `axis_mask` | `uint32` | No | Uint32 configured axis group mask; interpretation is native operation-specific; reset_fault defaults zero to all axes. | | `velocity` | `float64[]` | No | Finite logical velocities in described axis order for legacy jog; native vector and velocity-limit validation applies. | | `timeout_ms` | `uint32` | No | Legacy jog requires an integer 1..250 milliseconds. | | `handle` | `uint64` | No | Nonzero opaque uint64 handle for start/discard, owned by the current daemon incarnation and grant. | | `points` | [`Point[]`](#type-point) | No | 2..250000 Point records; declared axis order, native limits and continuity apply. Large bodies require the envelope before this array. | | `identity` | [`Identity`](#type-identity) | No | Exact prepared program Identity for start_program. | | `jog_generation` | `uint64` | No | EndJog requires the exact current nonzero independent jog generation. | | `source_sequence` | `uint64` | No | Accepted envelope field retained for compatibility; current /v1/control dispatch does not consume it. Independent updates use the binary lane. | | `source_origin_host_ns` | `uint64` | No | BeginJog origin in the negotiated host monotonic clock, uint64 nanoseconds; native freshness validation applies. | | `deadline_host_ns` | `uint64` | No | BeginJog producer deadline in host monotonic nanoseconds; after origin and never extended by admission or renewal. | | `clock_incarnation` | `string` | No | BeginJog requires exact equality with GET /v1/jog/clock incarnation. | | `requested_lease_ms` | `int` | No | Acquire requested lease in whole ms, 1..10000; omitted or zero retains the LAN default capped by the cell. Adapter returns min(request, cell ceiling) in Grant.lease_ms. Clients require at least three measured round trips plus renewal cadence. | `session` and `generation` come from the embedded `Fence`, so they sit at the top level of the object. - **Large uploads.** For a large `prepare_trajectory`, send `schema`, `operation`, `session` and a non-zero `generation` before `points`, within the first 16384 bytes. rt-control admits the fence and reserves the upload slot before it reads the rest. - **Size caps.** 16384 bytes for ordinary commands, 222516384 bytes for `prepare_trajectory` and 39298580 bytes for a `.rdt` upload to `/v1/program`. Larger bodies get HTTP 413 `body_too_large`. - **Units.** Positions are in each axis's `position_unit` (rad or m), velocities per second, times in ns. Host times use the cell's `CLOCK_MONOTONIC`. ### Idempotent retries Add `request_id` (1..64 printable ASCII bytes) to make a JSON command safe to retry. The current session remembers its last 256 outcomes. Resending the same decoded payload with the same ID joins the in-flight request or replays its final reply. Changing the payload under the same ID returns `request_id_conflict`. Release and lease expiry drop the cache, and so does an adapter restart. Keep one ID for one logical attempt. Use a fresh ID for every renewal and after you correct a refused request. `/v1/program` uploads have no deduplication. After an ambiguous Start, inspect handle state and incarnations before you try new motion. The Go and C++ clients add a random `request_id` to every command. ## Responses and errors Ordinary replies are one `Response` object with `schema: rosie.rt-control.response.v1`. `data` holds the operation's result type; `sequence` and `handle` are top-level fields. Fields that are zero or empty are omitted. | Name | Type | Required | Description | |---|---|---|---| | `native_jog_result` | [`JogObservation`](#type-jogobservation) | No | The core's `JogObservation` when the jog lane refused. | | `native_result` | [`CommandResult`](#type-commandresult) | No | The core's `CommandResult` when the core refused a command. Field names are case-sensitive (`Reason`, `Sequence` …). | | `schema` | `string` | Yes | `rosie.rt-control.response.v1`. | | `operation` | `string` | Yes | The operation this reply answers. | | `sequence` | `uint64` | No | Native command sequence, for operations that return one. Exact uint64. | | `handle` | `uint64` | No | Trajectory handle, or jog generation for jog calls. Exact uint64. | | `data` | `any` | No | The operation's result type (see each operation). On some refusals, structured evidence such as `limit_violation`. | | `error` | `string` | No | Present only on failure: a reason code, or a diagnostic string that starts with one. | | HTTP status | Meaning | |---|---| | 200 | Admitted. For motion this acknowledges admission, not physical completion. | | 409 | Refused. `error` holds the reason. This covers every refusal except the two below. | | 413 | `body_too_large`. | | 404 | `resource_unknown`, from `/v1/resources/` only. | `error` is usually one catalogue label. It can also be an open diagnostic: JSON decoder errors, I/O and context errors, native receipts such as `RTCore rejected operation 0x124: reason 2`, or two causes joined with a newline. Some labels carry detail after a colon, for example `native_limit_exceeded: segment= sample= axis=`. Match on the leading label. Treat anything you do not recognise, and any transport failure, as an unknown outcome: stop producing motion, issue an authenticated `stop` if you can, and reconcile Status before you acquire again. ### Common reasons These sets apply in addition to each operation's own table. The operation sections say which sets apply. #### Envelope reasons (every `POST /v1/control`) | Reason | When | |---|---| | [`body_too_large`](/docs/reference/error-codes#reasons-envelope) | The body is larger than the operation's cap. HTTP 413. | | [`invalid_request_envelope`](/docs/reference/error-codes#reasons-envelope) | The body is not a JSON object, or a key is not a string. | | [`duplicate_request_field`](/docs/reference/error-codes#reasons-envelope) | A field appears twice in the envelope prefix. | | [`schema_mismatch`](/docs/reference/error-codes#reasons-envelope) | `schema` is not `rosie.rt-control.request.v1`. | | [`unknown_operation`](/docs/reference/error-codes#reasons-envelope) | `operation` is not a POST `/v1/control` operation. | | [`trailing_request_data`](/docs/reference/error-codes#reasons-envelope) | Data follows the JSON object. | | [`request_envelope_changed`](/docs/reference/error-codes#reasons-envelope) | The fully decoded envelope differs from the admitted prefix. | | [`invalid_request_id`](/docs/reference/error-codes#reasons-envelope) | `request_id` is null, not a string, or not 1..64 printable ASCII bytes. | | [`request_id_conflict`](/docs/reference/error-codes#reasons-envelope) | The `request_id` was already used in this session with a different payload. | | [`session_principal_mismatch`](/docs/reference/error-codes#reasons-authority) | The session belongs to another TLS principal or to the local transport. | #### Authority reasons (every fenced call) | Reason | When | |---|---| | [`control_session_stale`](/docs/reference/error-codes#reasons-authority) | Wrong or stale session or generation, lease expired, or a Stop is in flight. | | [`daemon_restarted`](/docs/reference/error-codes#reasons-authority) | The session belongs to a previous native daemon incarnation. | | [`fence`](/docs/reference/error-codes#reasons-authority) | A well-formed session this adapter never issued (for example, from before an adapter restart), or authority was revoked during Start. | #### Native reasons (calls the core admits) | Reason | When | |---|---| | [`native_rejected`](/docs/reference/error-codes#reasons-native) | The core refused the command. Read the numeric [native reason](https://advancedmetalresearch.com/docs/reference/error-codes#native-command-reasons) in `native_result.Reason`. | | [`outside_limits_outward`](/docs/reference/error-codes#reasons-native) | The command would move an axis further outside its limits (native reason 8). | #### Cell I/O reasons (calls that can carry outputs) | Reason | When | |---|---| | [`no_grant`](/docs/reference/error-codes#reasons-authority) | Native cell I/O refusal: no current grant. | | [`wrong_generation`](/docs/reference/error-codes#reasons-authority) | Native cell I/O refusal: generation mismatch. | | [`inhibited`](/docs/reference/error-codes#reasons-authority) | Native cell I/O refusal: outputs are inhibited. | | [`io_not_configured`](/docs/reference/error-codes#reasons-io) | No cell I/O is configured. | | [`io_not_armed`](/docs/reference/error-codes#reasons-io) | An ON intent needs `io_arm` first. | | [`io_fast_input_unsatisfied`](/docs/reference/error-codes#reasons-io) | A cyclic fast input contact is invalid or not satisfied. | | [`io_readback_disagreement`](/docs/reference/error-codes#reasons-io) | Physical feedback disagrees with the commanded output. | | [`io_torch_unqualified`](/docs/reference/error-codes#reasons-io) | A torch-class output was requested. Always refused. | | [`io_marker_late`](/docs/reference/error-codes#reasons-io) | A process marker missed its one-cycle delivery bound. | | [`io_exchange_lost`](/docs/reference/error-codes#reasons-io) | Cell I/O has no current complete exchange. | ## Observation Reads need no lease. On the remote listener they still need a valid client certificate. ### `describe` **Endpoint: `GET /v1/describe`** Read the machine description, identities and capability states. State: `implemented`. Lease: none. Returns the native description (backend, protocol version, digests, cycle period, axis order, units and limits), the contract version and digest, every capability with its state, the prepared program if there is one, and the compiled robot description and drive identities. Call it first. Check `backend`, `machine_sha256`, the axis order and `capabilities_digest` against what your client was built for before you acquire authority. `max_grant_lease_ns` and `max_jog_input_age_ns` are the cell's timing ceilings. #### Request No parameters. #### Response `data` is a [`Description`](#type-description). | Name | Type | Required | Description | |---|---|---|---| | *(embedded)* | [`NativeDescription`](#type-nativedescription) | Yes | All fields of `NativeDescription` appear at this level of the object. | | `contract_version` | `uint32` | Yes | Exactly 1. | | `capabilities_digest` | `string` | Yes | Lowercase SHA-256 of the exact committed UTF-8 LF schema file bytes, including its final newline; no self-referential digest field. | | `capabilities` | [`CapabilityInfo[]`](#type-capabilityinfo) | Yes | Every target capability with its implementation state and transport. | | `control_idle_timeout_ns` | `uint64` | No | Compiled link idle timeout in ns; clients keep reserved connections alive at one third of this bound, independently of grants. Unverified LAN and internet policy: 90000000000 ns. An omitted legacy field uses 30000000000 ns. | | `program` | [`Program`](#type-program) | No | Detached prepared program metadata including both identity digests; absent when no program is prepared. | | `robot` | [`RobotDescription`](#type-robotdescription) | No | Null when no robot definition is bound or no compiled artefact is loaded; never guessed from axis count. | | `drives` | [`DriveDescription[]`](#type-drivedescription) | Yes | Per-axis compiled drive configuration and current verified bring-up identity; empty without a compiled artefact. | #### Example ```http GET /v1/describe HTTP/1.1 Host: localhost ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "describe", "data": { "backend": "simulation", "schema": "…", "protocol_major": 1, "protocol_minor": 10, "configuration_sha256": "c2b097096fef301cad3b9c3e594f32266c625feaccba02be3410ed08df2b63ea", "machine_sha256": "4f1d9a7c2e6b8d0f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f", "deployment_sha256": "…", "max_trajectory_points": 250000, "resident_plans": 3, "cycle_ns": 1000000, "interpolation": "…", "stop": "…", "axes": [ { "index": 0, "id": "J1", "position_unit": "rad", "min_position": -2.96, "max_position": 2.96, "max_velocity": 1.5, "…": "…" } ], "bus": {}, "max_grant_lease_ns": 500000000, "max_jog_input_age_ns": 250000000, "contract_version": 2, "capabilities_digest": "d63b2aee7bbcb7e4f11eca7148ce9ffda0e5d8db421f165d20918ccb4f119158", "capabilities": [ { "name": "acquire", "state": "implemented", "transport": "POST /v1/control" }, "…" ], "control_idle_timeout_ns": 90000000000, "robot": null, "drives": [] } } ``` Axis values in the example are illustrative. Read the real limits from your cell. #### Reason codes No operation-specific reason codes. Unknown diagnostics mean the request failed. **Client libraries.** Go: `Client.Describe`. C++: `describe()`. ### `status` **Endpoint: `GET /v1/status`** Read one timestamped snapshot of the core, the grant, jog, execution and every axis. State: `implemented`. Lease: none. Every successful snapshot belongs to one daemon incarnation: `daemon_incarnation` equals `core.daemon_incarnation`. `time_ns` is the native publication time, not an adapter estimate. Check validity flags and timestamps before you treat a logical position as valid. `execution.state` reports the program lifecycle: `prepared`, `executing`, `completed`, `faulted`, `cancelled`, `discarded` or `released` (empty before any observation). A faulted program carries `native_execution_failed` in `execution.error`. #### Request No parameters. #### Response `data` is a [`ProcessStatus`](#type-processstatus). | Name | Type | Required | Description | |---|---|---|---| | `daemon_incarnation` | `string` | Yes | Core process identity for this snapshot. | | `adapter_incarnation` | `string` | Yes | rt-control process identity. | | `grant` | [`GrantObservation`](#type-grantobservation) | Yes | Native grant observation. | | `jog` | [`JogObservation`](#type-jogobservation) | Yes | Native jog observation. | | `jog_ingress` | [`JogIngressObservation`](#type-jogingressobservation) | Yes | Adapter-lifetime ingress outcomes across datagram, WSS and UpdateJog; counters and latest refusal match GET /v1/jog/ingress. | | `core` | [`NativeStatus`](#type-nativestatus) | Yes | Native core status. | | `motion` | [`MotionState`](#type-motionstate) | Yes | Native motion state. | | `execution` | [`Execution`](#type-execution) | Yes | Program and handle lifecycle. | | `time_ns` | `uint64` | Yes | Native publication time, ns. | | `axes` | [`LogicalAxisStatus[]`](#type-logicalaxisstatus) | Yes | Per-axis logical status, in Describe order. | | `generations` | [`StatusGenerations`](#type-statusgenerations) | Yes | Current generations and epochs. | | `plan_cursor` | [`PlanCursor`](#type-plancursor) | Yes | Native plan cursor. | | `buffer_health` | [`BufferHealth`](#type-bufferhealth) | Yes | Native buffer health. | | `adapter` | [`AdapterStatus`](#type-adapterstatus) | Yes | Adapter runtime observations; not native motion state. | #### Example ```http GET /v1/status HTTP/1.1 Host: localhost ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "status", "data": { "daemon_incarnation": "3b9d…", "adapter_incarnation": "a41c…", "grant": {}, "jog": {}, "jog_ingress": {}, "core": { "armed": 0, "axis_enable_mask": 0, "home_valid_mask": 511, "safety_fault_mask": 0, "…": "…" }, "motion": { "mode": 0, "state": 0, "done": false, "…": "…" }, "execution": { "state": "", "generation": 0, "handles": [], "…": "…" }, "time_ns": 81234567890123, "axes": [ { "logical_position": 0, "logical_valid": true, "readiness": "not_enabled", "…": "…" } ], "generations": {}, "plan_cursor": {}, "buffer_health": {}, "adapter": {} } } ``` #### Reason codes | Reason | When | |---|---| | [`daemon_restarted`](/docs/reference/error-codes#reasons-authority) | The native daemon was replaced while the snapshot was read. Re-read Describe and reconcile before any motion. | **Client libraries.** Go: `Client.Status`. C++: `status()`. ### `jog_clock` **Endpoint: `GET /v1/jog/clock`** Sample the host monotonic clock and its incarnation for `begin_jog`. State: `implemented`. Lease: none. Returns `domain` (`CLOCK_MONOTONIC`), the clock `incarnation`, `mapping_generation` and `now_host_ns`. Pass `incarnation` to `begin_jog` as `clock_incarnation`; every jog time you send uses this clock, in ns. #### Request No parameters. #### Response `data` is a [`LocalJogClock`](#type-localjogclock). | Name | Type | Required | Description | |---|---|---|---| | `domain` | `string` | Yes | `CLOCK_MONOTONIC`. | | `incarnation` | `string` | Yes | Clock incarnation; pass it to `begin_jog`. | | `mapping_generation` | `uint64` | Yes | Clock mapping generation (1 for the local lane). | | `now_host_ns` | `uint64` | Yes | Current host monotonic time, ns. | #### Example ```http GET /v1/jog/clock HTTP/1.1 Host: localhost ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "jog_clock", "data": { "domain": "CLOCK_MONOTONIC", "incarnation": "0f3c9a…", "mapping_generation": 1, "now_host_ns": 81234567890123 } } ``` #### Reason codes | Reason | When | |---|---| | [`independent_jog_unavailable`](/docs/reference/error-codes#reasons-jog) | The native client offers no independent jog lane. | **Client libraries.** Go: `Client.JogClock`. C++: `jog_clock()`. ### `jog_status` **Endpoint: `GET /v1/jog`** Read the native jog-lane observation (local socket only). State: `implemented`. Lease: none. On the local socket this returns the native `JogObservation`. Its fields keep their native, case-sensitive names (`Open`, `JogGeneration`, `InputDeadlineHostNs`, `StateReason` …). The reasons are the numeric [native jog reasons](https://advancedmetalresearch.com/docs/reference/error-codes#native-jog-reasons). On the mutual-TLS listener the same path is the WebSocket upgrade for remote jog. See [Remote access](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#remote-jog-over-websocket). #### Request No parameters. #### Response `data` is a [`JogObservation`](#type-jogobservation). | Name | Type | Required | Description | |---|---|---|---| | `Ticket` | `uint64` | Yes | — | | `ConnectionId` | `uint64` | Yes | — | | `NativeGeneration` | `uint64` | Yes | — | | `CapabilityId` | `uint8[16]` | Yes | — | | `AppGrantGeneration` | `uint64` | Yes | — | | `ConfigurationEpoch` | `uint64` | Yes | — | | `HomeEpoch` | `uint64` | Yes | — | | `ClockMappingGeneration` | `uint64` | Yes | — | | `JogGeneration` | `uint64` | Yes | Current jog generation. | | `SourceSequence` | `uint64` | Yes | Sequence of the latest applied input. | | `InputDeadlineHostNs` | `uint64` | Yes | Deadline of the latest applied input, host ns. | | `NowHostNs` | `uint64` | Yes | Publication time, host ns. | | `ObservedSourceSequence` | `uint64` | Yes | — | | `ObservedOriginHostNs` | `uint64` | Yes | — | | `FirstObservedHostNs` | `uint64` | Yes | — | | `ControlReason` | `uint32` | Yes | — | | `ControlAccepted` | `uint32` | Yes | — | | `UpdateReason` | `uint32` | Yes | — | | `StateReason` | `uint32` | Yes | Native jog reason for the current state. | | `AxisMask` | `uint32` | Yes | Axes of the jog session. | | `Open` | `uint32` | Yes | 1 while the jog session accepts input. | | `HasInput` | `uint32` | Yes | 1 once an input has been applied. | | `VelocityScalePpm` | `uint32` | Yes | — | #### Example ```http GET /v1/jog HTTP/1.1 Host: localhost ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "jog_status", "data": { "Open": 1, "JogGeneration": 4, "AxisMask": 1, "InputDeadlineHostNs": 81234817890123, "StateReason": 0, "…": "…" } } ``` #### Reason codes | Reason | When | |---|---| | [`independent_jog_unavailable`](/docs/reference/error-codes#reasons-jog) | The native client offers no independent jog lane. | **Client libraries.** Go: `Client.JogStatus`. C++: `jog_status()`. ### `jog_ingress` **Endpoint: `GET /v1/jog/ingress`** Read cumulative jog-input refusal counters for this adapter process. State: `implemented`. Lease: none. Counts every refused jog input across `jog.sock` datagrams, WebSocket frames and internal updates. `refused_by_reason` is indexed by [native jog reason](https://advancedmetalresearch.com/docs/reference/error-codes#native-jog-reasons) 0..14. The counters reset only when rt-control restarts. Local datagrams get no reply, so this endpoint is how a local jog producer sees its refusals. #### Request No parameters. #### Response `data` is a [`JogIngressObservation`](#type-jogingressobservation). | Name | Type | Required | Description | |---|---|---|---| | `source_sequence` | `uint64` | Yes | — | | `reason` | `uint32` | Yes | — | | `now_host_ns` | `uint64` | Yes | — | | `refused` | `uint64` | Yes | Cumulative refused ingress count for this adapter process; accepted frames do not increment it. | | `refused_by_reason` | `uint64[15]` | Yes | Cumulative refusal counts indexed by protocol/control.json jog reason 0..14; index 0 remains zero. Unverified fixed capacity: 15 reason slots. | | `latest_refusal` | [`JogIngressRefusal`](#type-jogingressrefusal) | Yes | Most recent refusal across all ingress transports; zero before any refusal and preserved through acceptance and grant changes. | #### Example ```http GET /v1/jog/ingress HTTP/1.1 Host: localhost ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "jog_ingress", "data": { "source_sequence": 120, "reason": 0, "now_host_ns": 81234567890123, "refused": 2, "refused_by_reason": [ 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], "latest_refusal": { "source_sequence": 7, "reason": 4, "now_host_ns": 81230000000000 } } } ``` #### Reason codes No operation-specific reason codes. Unknown diagnostics mean the request failed. **Client libraries.** Go: `Client.JogIngress`. C++: `jog_ingress()`. ### `subscribe_events` **Endpoint: `GET /v1/events?after=`** Poll or stream the event log from a cursor. State: `implemented`. Lease: none. **Endpoint: `SSE /v1/events/stream?after=`** Same operation, alternative transport. `/v1/events` returns one `EventBatch` inside the normal response envelope. `/v1/events/stream` is Server-Sent Events: each message has `id` (the sequence), `event` (the type) and `data` (one `Event` as JSON, with no envelope). `after` is the last sequence you handled; omit it to start from 0. The ring keeps 256 events and a batch holds at most 64. The full model, including loss handling, is in [Events and telemetry](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry#events). #### Request | Name | Type | Required | Description | |---|---|---|---| | `after` (query) | `uint64` | No | Last consumed sequence, as unsigned decimal text. Default 0. | #### Response `data` is a [`EventBatch`](#type-eventbatch). | Name | Type | Required | Description | |---|---|---|---| | `events` | [`Event[]`](#type-event) | Yes | At most 64 records, including at most one leading events_dropped record; 256 retained events. | | `next_sequence` | `uint64` | Yes | Resume cursor after the last returned event; unchanged when empty. | | `latest_sequence` | `uint64` | Yes | Newest retained sequence at batch capture. | | `adapter_incarnation` | `string` | Yes | — | #### Example ```http GET /v1/events?after=41 HTTP/1.1 Host: localhost ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "subscribe_events", "data": { "events": [ { "sequence": 42, "time_ns": 81234567890123, "type": "grant_acquired", "daemon_incarnation": "3b9d…", "adapter_incarnation": "a41c…", "grant": { "…": "…" } } ], "next_sequence": 42, "latest_sequence": 42, "adapter_incarnation": "a41c…" } } ``` #### Reason codes | Reason | When | |---|---| | [`invalid_event_cursor`](/docs/reference/error-codes#reasons-observation) | `after` is duplicated, empty or not one unsigned decimal uint64. | | [`event_cursor_ahead`](/docs/reference/error-codes#reasons-observation) | `after` is newer than this adapter incarnation's newest event. | | [`session_principal_mismatch`](/docs/reference/error-codes#reasons-authority) | An `X-Control-Session` header or `session` query names a session owned by another principal. | | [`event_cursor_lost`](/docs/reference/error-codes#reasons-observation) | Raised by the C++ client (`EventCursorLost`) when a stream reports `events_dropped`. The server never returns it. | **Client libraries.** Go: `Client.Events`, `Client.StreamEvents`. C++: `events()`, `events_stream()`. ### `telemetry` **Endpoint: `GET /v1/telemetry?after=`** Read full-rate binary cycle records from a cursor. State: `implemented`. Lease: none. **Endpoint: `GET /v1/telemetry/stream?after=`** Same operation, alternative transport. Returns `application/octet-stream`: one 312-byte `TelemetryBatchHeaderV1` followed by `record_count` 5392-byte `CycleCaptureRecordV2` records. It is never JSON. `/v1/telemetry` returns one batch (gzip if you send `Accept-Encoding: gzip`). `/v1/telemetry/stream` writes one complete batch per flush, about every 20 ms, including empty batches. Errors come back as a JSON `Response`. The binary layout and decoders are in [Events and telemetry](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry#telemetry). The contract describes it as: “Full-rate binary cycle records; observation needs no session or authority.” #### Request | Name | Type | Required | Description | |---|---|---|---| | `after` (query) | `uint64` | No | Last consumed record sequence. 0 (the default) starts at sequence 1 and reports overwritten history in `dropped`. | #### Response The body is binary: a [`TelemetryBatch`](#type-telemetrybatch). #### Example ```http GET /v1/telemetry?after=0 HTTP/1.1 Host: localhost Accept-Encoding: gzip ``` 200 OK: ```text HTTP/1.1 200 OK Content-Type: application/octet-stream Content-Encoding: gzip Cache-Control: no-store <312-byte header> ``` #### Reason codes | Reason | When | |---|---| | [`invalid_telemetry_cursor`](/docs/reference/error-codes#reasons-observation) | `after` is duplicated, empty or not one unsigned decimal uint64. | | [`telemetry_cursor_ahead`](/docs/reference/error-codes#reasons-observation) | `after` is beyond the ring's current sequence. Reconcile the incarnation first. | | [`telemetry_busy`](/docs/reference/error-codes#reasons-observation) | The ring header stayed torn after bounded retries. Retry the same cursor; the reply carries `Retry-After: 1`. | | [`telemetry_unavailable`](/docs/reference/error-codes#reasons-observation) | No compatible live telemetry ring could be observed. | | [`session_principal_mismatch`](/docs/reference/error-codes#reasons-authority) | A supplied session header or query belongs to another principal. | **Client libraries.** Go: `Client.TelemetryBatches`, `Client.TelemetryStream`. C++: `telemetry()`, `telemetry_stream()`. ### `resource` **Endpoint: `GET /v1/resources/`** Download one immutable compiled robot resource by digest. State: `implemented`. Lease: none. Serves the files of the compiled robot description (`robot_description_manifest.json`, `robot.urdf`, meshes, `machine_planning_calibration.json` …) listed in Describe `robot.resources`. The path component is the lowercase SHA-256 of the exact bytes. The reply is the raw bytes with `Content-Type`, `Content-Length` and a quoted-digest `ETag`. rt-control never looks anything up in a repository at request time: only the loaded compiled resource set is served. Remote clients need the mutual-TLS listener, like every remote call. The contract describes it as: “Read one bounded content-addressed compiled resource without a lease; remote mutual TLS required.” #### Request | Name | Type | Required | Description | |---|---|---|---| | `sha256` (path) | `string` | Yes | 64 lowercase hex characters, from `ResourceInfo.sha256`. | #### Response The body is the resource itself. `ResourceInfo` (from Describe) describes it. #### Example ```http GET /v1/resources/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 HTTP/1.1 Host: localhost ``` 200 OK: ```text HTTP/1.1 200 OK Content-Type: application/xml Content-Length: 48213 ETag: "9f86d081…" X-Content-Type-Options: nosniff … ``` #### Reason codes | Reason | When | |---|---| | [`resource_unknown`](/docs/reference/error-codes#reasons-observation) | The digest is malformed or not in the loaded resource set. HTTP 404. | **Client libraries.** Go: `Client.Resource`. C++: none. ## Authority One controller at a time. See [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority) for the model behind these four calls. ### `acquire` **Endpoint: `POST /v1/control`** Take control of the cell and receive a session and fence. State: `implemented`. Lease: no fence; checks `binding`. Only one controller holds authority at a time. `acquire` checks your `binding` against the pair and digests rt-control was started with. When you supply `machine_sha256` it must match exactly and there is no fallback; without it, `configuration_sha256` must match. On success you get a fresh 64-hex `session`, a `generation` one higher than the last, and the effective `lease_ms`. The effective lease is `min(requested_lease_ms, cell ceiling)`. Omit `requested_lease_ms` (or send 0) for the 500 ms default, still capped by the cell. Renew before it runs out. Local socket permissions or the remote TLS identity authenticate the connection; `controller` is only a label. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `acquire`. | | `controller` | `string` | Yes | Opaque controller name, 1..63 bytes. Not a credential. | | `binding` | [`Binding`](#type-binding) | Yes | Pair ID and revision rt-control was started with, plus `configuration_sha256` and, preferably, `machine_sha256` from Describe. | | `requested_lease_ms` | `int` | No | Requested lease in whole ms, 1..10000. Default 0: keep the 500 ms LAN default, capped by the cell ceiling. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `controller`, `binding`. #### Response `data` is a [`Grant`](#type-grant). | Name | Type | Required | Description | |---|---|---|---| | `stopping` | `bool` | Yes | True means renewal keeps the lease alive while Stop is in flight; it grants no movement permission. | | `deadline_host_ns` | `uint64` | Yes | Uint64 absolute lease deadline in host monotonic nanoseconds. | | `session` | `string` | Yes | 64-character unpredictable lowercase hexadecimal token while owned; empty after release/expiry. | | `generation` | `uint64` | Yes | Monotonically increasing uint64 application generation, fenced by Stop and acquisition. | | `controller` | `string` | Yes | The `controller` label sent to `acquire`. | | `binding` | [`Binding`](#type-binding) | Yes | The binding, completed with the adapter's digests. | | `lease_ms` | `int` | Yes | Effective negotiated lease in whole milliseconds: min(client requested lease, compiled cell ceiling); zero/omitted request retains 500 ms capped by the cell. Renew preserves this value. Longer leases increase worst-case unattended-stop delay after link loss. Unverified absolute bound: 10000 ms. | #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "acquire", "controller": "my-app", "binding": { "pair_id": "cell-a", "revision": 1, "configuration_sha256": "c2b097096fef301cad3b9c3e594f32266c625feaccba02be3410ed08df2b63ea", "machine_sha256": "4f1d9a7c2e6b8d0f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f" }, "requested_lease_ms": 500 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "acquire", "data": { "stopping": false, "deadline_host_ns": 81234567890123, "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "controller": "my-app", "binding": { "pair_id": "cell-a", "revision": 1, "configuration_sha256": "c2b097096fef301cad3b9c3e594f32266c625feaccba02be3410ed08df2b63ea", "machine_sha256": "4f1d9a7c2e6b8d0f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f" }, "lease_ms": 500 } } ``` #### Reason codes | Reason | When | |---|---| | [`authority_binding_mismatch`](/docs/reference/error-codes#reasons-authority) | `controller` is empty or longer than 63 bytes, or `binding` does not match the configured pair, revision and digest. | | [`control_already_owned`](/docs/reference/error-codes#reasons-authority) | Another session holds authority, or a Stop is still draining. | | [`application_generation_exhausted`](/docs/reference/error-codes#reasons-authority) | The uint64 grant generation is exhausted. Restart and reconcile. | | [`invalid_requested_lease`](/docs/reference/error-codes#reasons-authority) | `requested_lease_ms` is negative or above 10000. | | [`control_session_stale`](/docs/reference/error-codes#reasons-authority) | The new grant was lost while it was being mirrored to the core. | | [`session_principal_mismatch`](/docs/reference/error-codes#reasons-authority) | A retried `request_id` was first used by another transport principal. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope) reasons. **Client libraries.** Go: `Client.Acquire`, `Client.AcquireLease`. C++: `acquire()`. ### `renew` **Endpoint: `POST /v1/control`** Extend the lease of the current session. State: `implemented`. Lease: session and generation. Renew keeps the session alive. The deadline moves to now plus the effective `lease_ms`, which never changes after Acquire. Renew at most every third of the lease, on its own connection, and use a fresh `request_id` each time: a reused ID replays the old receipt. An older non-zero generation of the same live session may renew and learns the current fence from the reply, but it cannot move the robot. While a Stop is in flight, renew returns `stopping: true`: the lease is kept alive, and no motion permission is granted. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `renew`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`. #### Response `data` is a [`Grant`](#type-grant). | Name | Type | Required | Description | |---|---|---|---| | `stopping` | `bool` | Yes | True means renewal keeps the lease alive while Stop is in flight; it grants no movement permission. | | `deadline_host_ns` | `uint64` | Yes | Uint64 absolute lease deadline in host monotonic nanoseconds. | | `session` | `string` | Yes | 64-character unpredictable lowercase hexadecimal token while owned; empty after release/expiry. | | `generation` | `uint64` | Yes | Monotonically increasing uint64 application generation, fenced by Stop and acquisition. | | `controller` | `string` | Yes | The `controller` label sent to `acquire`. | | `binding` | [`Binding`](#type-binding) | Yes | The binding, completed with the adapter's digests. | | `lease_ms` | `int` | Yes | Effective negotiated lease in whole milliseconds: min(client requested lease, compiled cell ceiling); zero/omitted request retains 500 ms capped by the cell. Renew preserves this value. Longer leases increase worst-case unattended-stop delay after link loss. Unverified absolute bound: 10000 ms. | #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "renew", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "request_id": "5f0c2a9e6b1d4c3a8e7f0b2d4c6a8e1f" } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "renew", "data": { "stopping": false, "deadline_host_ns": 81234567890123, "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "controller": "my-app", "binding": { "pair_id": "cell-a", "revision": 1, "configuration_sha256": "c2b097096fef301cad3b9c3e594f32266c625feaccba02be3410ed08df2b63ea", "machine_sha256": "4f1d9a7c2e6b8d0f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f" }, "lease_ms": 500 } } ``` #### Reason codes | Reason | When | |---|---| | [`expired`](/docs/reference/error-codes#reasons-authority) | The lease deadline passed, or the core reported the grant expired. Stop producing, reconcile and acquire again. | | [`control_session_stale`](/docs/reference/error-codes#reasons-authority) | The session is not the current one, or `generation` is 0 or newer than the current generation. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope) and [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) reasons. **Client libraries.** Go: `Client.Renew`, `Client.StartRenewal`. C++: `renew()`, `start_renewal()`. ### `release` **Endpoint: `POST /v1/control`** Stop, then give up authority. State: `implemented`. Lease: session and generation. Release runs the same inhibiting sequence as Stop and then clears the session: the returned `Grant` has an empty `session`. Stop your renewal loop first. The Go and C++ clients do that for you and never replay a Release automatically after a transport failure. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `release`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`. #### Response `data` is a [`Grant`](#type-grant). | Name | Type | Required | Description | |---|---|---|---| | `stopping` | `bool` | Yes | True means renewal keeps the lease alive while Stop is in flight; it grants no movement permission. | | `deadline_host_ns` | `uint64` | Yes | Uint64 absolute lease deadline in host monotonic nanoseconds. | | `session` | `string` | Yes | 64-character unpredictable lowercase hexadecimal token while owned; empty after release/expiry. | | `generation` | `uint64` | Yes | Monotonically increasing uint64 application generation, fenced by Stop and acquisition. | | `controller` | `string` | Yes | The `controller` label sent to `acquire`. | | `binding` | [`Binding`](#type-binding) | Yes | The binding, completed with the adapter's digests. | | `lease_ms` | `int` | Yes | Effective negotiated lease in whole milliseconds: min(client requested lease, compiled cell ceiling); zero/omitted request retains 500 ms capped by the cell. Renew preserves this value. Longer leases increase worst-case unattended-stop delay after link loss. Unverified absolute bound: 10000 ms. | #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "release", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "release", "data": { "stopping": false, "deadline_host_ns": 81234567890123, "session": "", "generation": 4, "controller": "my-app", "binding": { "pair_id": "cell-a", "revision": 1, "configuration_sha256": "c2b097096fef301cad3b9c3e594f32266c625feaccba02be3410ed08df2b63ea", "machine_sha256": "4f1d9a7c2e6b8d0f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f" }, "lease_ms": 500 } } ``` #### Reason codes | Reason | When | |---|---| | [`control_session_stale`](/docs/reference/error-codes#reasons-authority) | The session was never issued by this adapter, or `generation` is 0 or newer than current. | | [`expired`](/docs/reference/error-codes#reasons-authority) | The lease expired while the release was being processed. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope) and [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) reasons. **Client libraries.** Go: `Client.Release`. C++: `release()`. ### `stop` **Endpoint: `POST /v1/control`** Inhibit outputs immediately and fence all motion. State: `implemented`. Lease: session and generation. > [!WARNING] This call can energise the drives or move the robot. Keep the hardware E-stop within reach. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). Stop cancels execution, uploads, handles and jog at once, inhibits the drive outputs and increments the generation. It does not wait for a ramp. If the session is still valid you get it back with the new generation, and you need a fresh Enable and Arm before moving again. A session that has expired or been revoked can still send Stop to inhibit, but it cannot regain motion permission. Concurrent Stop and Release calls join one cancellation. A Stop receipt does not prove the robot is at standstill. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `stop`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`. #### Response `data` is a [`Grant`](#type-grant). | Name | Type | Required | Description | |---|---|---|---| | `stopping` | `bool` | Yes | True means renewal keeps the lease alive while Stop is in flight; it grants no movement permission. | | `deadline_host_ns` | `uint64` | Yes | Uint64 absolute lease deadline in host monotonic nanoseconds. | | `session` | `string` | Yes | 64-character unpredictable lowercase hexadecimal token while owned; empty after release/expiry. | | `generation` | `uint64` | Yes | Monotonically increasing uint64 application generation, fenced by Stop and acquisition. | | `controller` | `string` | Yes | The `controller` label sent to `acquire`. | | `binding` | [`Binding`](#type-binding) | Yes | The binding, completed with the adapter's digests. | | `lease_ms` | `int` | Yes | Effective negotiated lease in whole milliseconds: min(client requested lease, compiled cell ceiling); zero/omitted request retains 500 ms capped by the cell. Renew preserves this value. Longer leases increase worst-case unattended-stop delay after link loss. Unverified absolute bound: 10000 ms. | #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "stop", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "stop", "data": { "stopping": false, "deadline_host_ns": 81234567890123, "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 4, "controller": "my-app", "binding": { "pair_id": "cell-a", "revision": 1, "configuration_sha256": "c2b097096fef301cad3b9c3e594f32266c625feaccba02be3410ed08df2b63ea", "machine_sha256": "4f1d9a7c2e6b8d0f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f" }, "lease_ms": 500 } } ``` #### Reason codes | Reason | When | |---|---| | [`control_session_stale`](/docs/reference/error-codes#reasons-authority) | The session was never issued by this adapter, or `generation` is 0 or newer than current. | | [`expired`](/docs/reference/error-codes#reasons-authority) | The lease expired while the stop was reacquiring native authority. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope) and [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) reasons. **Client libraries.** Go: `Client.Stop`. C++: `stop()`. ## Machine control Every call here needs the current `session` and `generation`. ### `enable` **Endpoint: `POST /v1/control`** Request CiA402 enable for the selected axes. State: `implemented`. Lease: session and generation. > [!WARNING] This call can energise the drives or move the robot. Keep the hardware E-stop within reach. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). Sends a native enable for `axis_mask` and waits for the native result. The receipt only confirms admission. Poll Status until each axis reports the expected enable bit and readiness. Enable and Arm on their own do not establish Home or permit a Start. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `enable`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `axis_mask` | `uint32` | Yes | Axes to enable, by Describe index (bit 0 = first axis). A nine-axis cell uses 511. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`, `axis_mask`. #### Response `sequence` holds the native command sequence. There is no `data`. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "enable", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "axis_mask": 511 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "enable", "sequence": 17 } ``` #### Reason codes Only the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. **Client libraries.** Go: `Client.Enable`. C++: `enable()`. ### `arm` **Endpoint: `POST /v1/control`** Arm the core so that motion commands can be admitted. State: `implemented`. Lease: session and generation. > [!WARNING] This call can energise the drives or move the robot. Keep the hardware E-stop within reach. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). Sends a native arm and waits for its result. Observe `core.armed == 1` in Status. A successful Arm does not mean every axis is ready: the [motion start gate](https://advancedmetalresearch.com/docs/concepts/real-time-core#the-motion-start-gate) is still checked at every Start and jog. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `arm`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`. #### Response `sequence` holds the native command sequence. There is no `data`. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "arm", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "arm", "sequence": 18 } ``` #### Reason codes Only the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. **Client libraries.** Go: `Client.Arm`. C++: `arm()`. ### `home` **Endpoint: `POST /v1/control`** Run the drives' native Home on the selected axes. State: `implemented`. Lease: session and generation. > [!WARNING] This call can energise the drives or move the robot. Keep the hardware E-stop within reach. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). Runs native Home (commissioning) on `axis_mask` and waits up to 60 s for the result. Home leaves the selected axes disabled: observe the new `home_epoch`, an idle `commissioning_phase` and every selected bit in `home_valid_mask`, then Enable and Arm again. If Home fails, rt-control issues a Stop. When `ROSIE_RT_ANCHOR_DIR` is set in rt-control's environment, a successful Home also saves one anchor file per selected axis, for later use by `restore_anchor`. Losing the HTTP reply does not cancel Home; Stop, lease expiry or transport loss do. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `home`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `axis_mask` | `uint32` | Yes | Non-zero mask of configured axes whose Describe entry has `native_home: true`. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`, `axis_mask`. #### Response `sequence` holds the native command sequence. There is no `data`. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "home", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "axis_mask": 511 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "home", "sequence": 19 } ``` #### Reason codes | Reason | When | |---|---| | [`anchor_source_invalid`](/docs/reference/error-codes#reasons-recovery) | Anchor capture after Home found the machine armed or enabled, or a selected axis had no valid source. | | [`anchor_identity_mismatch`](/docs/reference/error-codes#reasons-recovery) | The captured anchor does not match the adapter's pair ID and revision. | | [`anchor_store_io`](/docs/reference/error-codes#reasons-recovery) | The anchor directory could not be written. Details go to the local log only. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. **Client libraries.** Go: `Client.Home`. C++: `home()`. ### `halt` **Endpoint: `POST /v1/control`** Decelerate to an enabled hold, keeping authority and Arm. State: `implemented`. Lease: session and generation. > [!WARNING] This call can energise the drives or move the robot. Keep the hardware E-stop within reach. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). Halt requests a controlled deceleration using the active trajectory's acceleration, or the jog acceleration when no trajectory bound is declared. It retires the active trajectory handle and jog generation, but keeps the grant, Enable and Arm. A new Start must begin at the held target; jogging needs a new `begin_jog`. Halt does not replace Stop. Stop, faults and lease expiry always inhibit immediately. The receipt confirms admission, not a completed hold. A native refusal also carries `native_result`. The contract describes it as: “Controlled deceleration to enabled hold using the active trajectory acceleration, or jog acceleration when no trajectory bound is declared. Retires active motion; preserves authority and Arm. Stop and faults always inhibit immediately.” #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `halt`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`. #### Response `sequence` holds the native command sequence. There is no `data`. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "halt", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "halt", "sequence": 20 } ``` #### Reason codes | Reason | When | |---|---| | [`no_grant`](/docs/reference/error-codes#reasons-authority) | There is no current valid grant for this session, or its lease has expired. | | [`wrong_generation`](/docs/reference/error-codes#reasons-authority) | `generation` differs from the current application or native generation. | | [`inhibited`](/docs/reference/error-codes#reasons-authority) | A Stop is in flight, or the core is not armed, enabled and ready (native Home and service also refuse Halt). | | [`capability_unimplemented`](/docs/reference/error-codes#reasons-envelope) | The native client has no Halt support. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope) and [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) reasons. **Client libraries.** Go: `Client.Command with Operation "halt"`. C++: `command() with operation halt`. ### `restore_anchor` **Endpoint: `POST /v1/control`** Re-establish Home from saved absolute-encoder anchors, without moving. State: `implemented`. Lease: session and generation. An alternative to Home after a restart. The machine must be disarmed, disabled and not commissioning. For every selected axis, the saved anchor's pair, revision, coordinate identity, drive identity and absolute source must match current evidence, and the core checks them again independently. Set `ROSIE_RT_ANCHOR_DIR` to the same directory for rt-control (write) and the core (read). On success the reply carries the restore receipt in `sequence` and a `RecoveryStatus` read after it, with the selected bits set in `home_valid_mask`. Restoring never enables or arms. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `restore_anchor`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `axis_mask` | `uint32` | Yes | Non-zero mask of configured axes to restore. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`, `axis_mask`. #### Response `sequence` holds the native command sequence and `data` is a [`RecoveryStatus`](#type-recoverystatus). | Name | Type | Required | Description | |---|---|---|---| | `home_valid_mask` | `uint32` | Yes | Native per-axis home evidence remaining after observed recovery. | | `faults` | [`FaultRecovery[]`](#type-faultrecovery) | Yes | Currently latched fault bits with axis masks and recovery policy. | | `reset_sequence` | `uint64` | Yes | Exact native command sequence of the observed reset, zero before any reset; response sequence must match for reset_fault. | | `reason` | `string` | Yes | Native recovery summary: empty when no fault persists, otherwise fault_persists; FaultResetRecovery refuses a persistent fault. | | `outcomes` | [`FaultRecovery[]`](#type-faultrecovery) | Yes | Per-bit outcomes of the correlated reset; never inferred from elapsed time. | #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "restore_anchor", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "axis_mask": 511 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "restore_anchor", "sequence": 21, "data": { "home_valid_mask": 511, "faults": [], "reset_sequence": 0, "reason": "", "outcomes": [] } } ``` #### Reason codes | Reason | When | |---|---| | [`invalid_axis_mask`](/docs/reference/error-codes#reasons-recovery) | `axis_mask` is 0 or names an axis outside the configured group. | | [`mode_conflict`](/docs/reference/error-codes#reasons-native) | The machine is armed, enabled or commissioning. | | [`anchor_missing`](/docs/reference/error-codes#reasons-recovery) | No saved anchor exists for a selected axis. Run Home. | | [`anchor_identity_mismatch`](/docs/reference/error-codes#reasons-recovery) | The anchor was saved under another pair, revision, configuration, drive identity or Home epoch. | | [`anchor_source_invalid`](/docs/reference/error-codes#reasons-recovery) | The drive reports no valid absolute source for the axis. | | [`anchor_disagrees`](/docs/reference/error-codes#reasons-recovery) | The current absolute source disagrees with the anchor beyond tolerance. Investigate, then Home. | | [`anchor_store_io`](/docs/reference/error-codes#reasons-recovery) | The anchor directory could not be read. | | [`recovery_observation_unavailable`](/docs/reference/error-codes#reasons-recovery) | No matching native observation arrived within 1 s of the receipt. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. **Client libraries.** Go: `Client.RestoreAnchor`. C++: `restore_anchor()`. ### `reset_fault` **Endpoint: `POST /v1/control`** Clear latched execution faults (interim). Ends the session. State: `interim`. Lease: session and generation. `reset_fault` is labelled `interim`. It needs an inhibited, idle machine: not armed, no active jog, no commissioning, no native Home and no executing program. It submits one native fault reset for `axis_mask` (0 means every configured axis) and reports the correlated result: `sequence`, `native_result` and a `RecoveryStatus` whose `reset_sequence` matches. A submitted reset **retires your session**, even when the core refuses it. Acquire again afterwards. Any persistent condition refuses the whole reset (`fault_persists`); there is no partial clear. Reset never starts motion and never grants Home. See [fault recovery](https://advancedmetalresearch.com/docs/concepts/real-time-core#faults-and-recovery). #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `reset_fault`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `axis_mask` | `uint32` | No | Axes to reset. Default 0: all configured axes. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`. #### Response `sequence` holds the native command sequence and `data` is a [`RecoveryStatus`](#type-recoverystatus). `native_result` carries the native `CommandResult` of the reset. | Name | Type | Required | Description | |---|---|---|---| | `home_valid_mask` | `uint32` | Yes | Native per-axis home evidence remaining after observed recovery. | | `faults` | [`FaultRecovery[]`](#type-faultrecovery) | Yes | Currently latched fault bits with axis masks and recovery policy. | | `reset_sequence` | `uint64` | Yes | Exact native command sequence of the observed reset, zero before any reset; response sequence must match for reset_fault. | | `reason` | `string` | Yes | Native recovery summary: empty when no fault persists, otherwise fault_persists; FaultResetRecovery refuses a persistent fault. | | `outcomes` | [`FaultRecovery[]`](#type-faultrecovery) | Yes | Per-bit outcomes of the correlated reset; never inferred from elapsed time. | #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "reset_fault", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "axis_mask": 0 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "reset_fault", "sequence": 22, "native_result": { "Sequence": 22, "Handle": 0, "Generation": 5, "Operation": 260, "Result": 0, "Reason": 0, "AxisMask": 511 }, "data": { "home_valid_mask": 511, "faults": [], "reset_sequence": 22, "reason": "", "outcomes": [] } } ``` #### Reason codes | Reason | When | |---|---| | [`program_already_executing`](/docs/reference/error-codes#reasons-program) | A trajectory or program is still executing. Stop first. | | [`reset_requires_inhibited`](/docs/reference/error-codes#reasons-recovery) | The machine is armed, jogging, commissioning or homing, or no fresh observation arrived within 1 s. | | [`recovery_observation_unavailable`](/docs/reference/error-codes#reasons-recovery) | No correlated recovery publication arrived within 1 s of the receipt. | | [`fault_persists`](/docs/reference/error-codes#reasons-recovery) | At least one selected fault condition is still present. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. **Client libraries.** Go: `Client.ResetFault`. C++: `reset_fault()`. ### `recovery_status` **Endpoint: `POST /v1/control`** Read the latched faults and their recovery classes. Changes nothing. State: `implemented`. Lease: session and generation. Requires the same inhibited, idle machine as `reset_fault`, but clears nothing and keeps the session. Each `faults[]` entry names the fault bit, the affected axes and its recovery class. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `recovery_status`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`. #### Response `data` is a [`RecoveryStatus`](#type-recoverystatus). | Name | Type | Required | Description | |---|---|---|---| | `home_valid_mask` | `uint32` | Yes | Native per-axis home evidence remaining after observed recovery. | | `faults` | [`FaultRecovery[]`](#type-faultrecovery) | Yes | Currently latched fault bits with axis masks and recovery policy. | | `reset_sequence` | `uint64` | Yes | Exact native command sequence of the observed reset, zero before any reset; response sequence must match for reset_fault. | | `reason` | `string` | Yes | Native recovery summary: empty when no fault persists, otherwise fault_persists; FaultResetRecovery refuses a persistent fault. | | `outcomes` | [`FaultRecovery[]`](#type-faultrecovery) | Yes | Per-bit outcomes of the correlated reset; never inferred from elapsed time. | #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "recovery_status", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "recovery_status", "data": { "home_valid_mask": 511, "faults": [ { "bit": 5, "name": "following_error", "axis_mask": 4, "recovery": "reset_after_condition_clears", "outcome": "persists", "rehome_axis_mask": 0 } ], "reset_sequence": 0, "reason": "fault_persists", "outcomes": [] } } ``` #### Reason codes | Reason | When | |---|---| | [`program_already_executing`](/docs/reference/error-codes#reasons-program) | A trajectory or program is executing. | | [`reset_requires_inhibited`](/docs/reference/error-codes#reasons-recovery) | The machine is armed, jogging, commissioning or homing, or no fresh observation arrived within 1 s. | | [`recovery_observation_unavailable`](/docs/reference/error-codes#reasons-recovery) | The core published no recovery status within 1 s. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope) and [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) reasons. **Client libraries.** Go: `Client.Command with Operation "recovery_status"`. C++: `recovery_status()`. ### `io_arm` **Endpoint: `POST /v1/control`** Grant fenced permission for configured, non-torch cell outputs. State: `implemented`. Lease: session and generation. Cell I/O outputs are only driven after an explicit `io_arm` under a fresh grant, with valid OFF readback observed after the last Stop. Stop commands every output OFF. Torch-class outputs are refused everywhere: see [Process I/O and sensing](https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing). The receipt confirms native permission only. It does not establish physical feedback. The contract describes it as: “Native cell I/O permission; torch remains unqualified.” #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `io_arm`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`. #### Response `sequence` holds the native command sequence. There is no `data`. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "io_arm", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "io_arm", "sequence": 23 } ``` #### Reason codes | Reason | When | |---|---| | [`capability_unimplemented`](/docs/reference/error-codes#reasons-envelope) | The native client has no cell I/O support. | | [`no_grant`](/docs/reference/error-codes#reasons-authority) | Native cell I/O refusal: no current grant. | | [`wrong_generation`](/docs/reference/error-codes#reasons-authority) | Native cell I/O refusal: generation mismatch. | | [`inhibited`](/docs/reference/error-codes#reasons-authority) | Native cell I/O refusal: outputs are inhibited. | | [`io_not_configured`](/docs/reference/error-codes#reasons-io) | No cell I/O is configured. | | [`io_not_armed`](/docs/reference/error-codes#reasons-io) | An ON intent needs `io_arm` first. | | [`io_fast_input_unsatisfied`](/docs/reference/error-codes#reasons-io) | A cyclic fast input contact is invalid or not satisfied. | | [`io_readback_disagreement`](/docs/reference/error-codes#reasons-io) | Physical feedback disagrees with the commanded output. | | [`io_torch_unqualified`](/docs/reference/error-codes#reasons-io) | A torch-class output was requested. Always refused. | | [`io_marker_late`](/docs/reference/error-codes#reasons-io) | A process marker missed its one-cycle delivery bound. | | [`io_exchange_lost`](/docs/reference/error-codes#reasons-io) | Cell I/O has no current complete exchange. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. **Client libraries.** Go: `Client.IOArm`. C++: `io_arm()`. ### `io_disarm` **Endpoint: `POST /v1/control`** Withdraw cell-output permission and intent. State: `implemented`. Lease: session and generation. Clears output permission and intent in the native cycle. The contract describes it as: “Native cell I/O permission; torch remains unqualified.” #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `io_disarm`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`. #### Response `sequence` holds the native command sequence. There is no `data`. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "io_disarm", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "io_disarm", "sequence": 24 } ``` #### Reason codes | Reason | When | |---|---| | [`capability_unimplemented`](/docs/reference/error-codes#reasons-envelope) | The native client has no cell I/O support. | | [`no_grant`](/docs/reference/error-codes#reasons-authority) | Native cell I/O refusal: no current grant. | | [`wrong_generation`](/docs/reference/error-codes#reasons-authority) | Native cell I/O refusal: generation mismatch. | | [`inhibited`](/docs/reference/error-codes#reasons-authority) | Native cell I/O refusal: outputs are inhibited. | | [`io_not_configured`](/docs/reference/error-codes#reasons-io) | No cell I/O is configured. | | [`io_not_armed`](/docs/reference/error-codes#reasons-io) | An ON intent needs `io_arm` first. | | [`io_fast_input_unsatisfied`](/docs/reference/error-codes#reasons-io) | A cyclic fast input contact is invalid or not satisfied. | | [`io_readback_disagreement`](/docs/reference/error-codes#reasons-io) | Physical feedback disagrees with the commanded output. | | [`io_torch_unqualified`](/docs/reference/error-codes#reasons-io) | A torch-class output was requested. Always refused. | | [`io_marker_late`](/docs/reference/error-codes#reasons-io) | A process marker missed its one-cycle delivery bound. | | [`io_exchange_lost`](/docs/reference/error-codes#reasons-io) | Cell I/O has no current complete exchange. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. **Client libraries.** Go: `Client.IODisarm`. C++: `io_disarm()`. ### `mark_telemetry` **Endpoint: `POST /v1/control`** Write a labelled marker into the telemetry stream. State: `implemented`. Lease: session and generation. Adds a diagnostic mark. An accepted mark publishes one `telemetry_mark` event carrying the label, the native sequence and the grant generation. It does not take the motion lock, so it works during a long Home. A receipt does not mean anything was written to disk. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `mark_telemetry`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `label` | `string` | Yes | Free text, 1..128 bytes of valid UTF-8. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`, `label`. #### Response `sequence` holds the native command sequence. There is no `data`. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "mark_telemetry", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "label": "before weld 3" } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "mark_telemetry", "sequence": 25 } ``` #### Reason codes | Reason | When | |---|---| | [`telemetry_label_invalid`](/docs/reference/error-codes#reasons-observation) | `label` is empty, longer than 128 bytes or not valid UTF-8. | | [`capability_unimplemented`](/docs/reference/error-codes#reasons-envelope) | The native client cannot mark telemetry. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. **Client libraries.** Go: `Client.Command with Operation "mark_telemetry"`. C++: `command() with operation mark_telemetry`. ## Jog Jogging uses its own lane with its own generation and input deadlines. The JSON calls open and close a jog session; the velocity updates themselves are binary frames. ### `begin_jog` **Endpoint: `POST /v1/control`** Open an independent jog session and get its jog generation. State: `implemented`. Lease: session and generation. > [!WARNING] This call can energise the drives or move the robot. Keep the hardware E-stop within reach. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). Reserves native jog mode for `axis_mask`. Get `clock_incarnation` from `GET /v1/jog/clock`. `source_origin_host_ns` is when your input was captured and `deadline_host_ns` is when it must stop applying, both in that host clock. The deadline is never extended by admission or renewal. The reply's `handle` is the new **jog generation**. Send velocities with [`update_jog`](#update-jog), then close with `end_jog`. Jog velocities are joint-space, in each axis's logical unit per second. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `begin_jog`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `axis_mask` | `uint32` | Yes | Axes this jog session may move. | | `source_origin_host_ns` | `uint64` | Yes | Input capture time, host `CLOCK_MONOTONIC` ns. | | `deadline_host_ns` | `uint64` | Yes | Absolute input deadline, host `CLOCK_MONOTONIC` ns, after the origin. | | `clock_incarnation` | `string` | Yes | `incarnation` from `GET /v1/jog/clock`. Must match exactly. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`, `axis_mask`, `source_origin_host_ns`, `deadline_host_ns`, `clock_incarnation`. #### Response `handle` holds the result. `handle` is the jog generation. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "begin_jog", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "axis_mask": 1, "source_origin_host_ns": 81234567890123, "deadline_host_ns": 81234667890123, "clock_incarnation": "0f3c9a…" } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "begin_jog", "handle": 4 } ``` #### Reason codes | Reason | When | |---|---| | [`jog_clock_incarnation_mismatch`](/docs/reference/error-codes#reasons-jog) | `clock_incarnation` is empty or is not the current host clock incarnation. | | [`independent_jog_unavailable`](/docs/reference/error-codes#reasons-jog) | The native client offers no independent jog lane. | | [`mode_conflict`](/docs/reference/error-codes#reasons-native) | A trajectory, program or commissioning is active (native jog reason 2). | | [`outside_limits_outward`](/docs/reference/error-codes#reasons-native) | An axis is outside its limits (native jog reason 15). | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope) and [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) reasons. Other native jog refusals return the diagnostic `RTCore rejected jog (reason N)` with `native_jog_result` set; see [native jog reasons](https://advancedmetalresearch.com/docs/reference/error-codes#native-jog-reasons). **Client libraries.** Go: `Client.NewJogSession`, `Client.PrepareJogSession`, `Client.BeginJog`. C++: `RtJogProducer`, `begin_jog()`. ### `update_jog` **Endpoint: `DGRAM jog.sock`** Send the latest jog velocity: a binary frame, not an HTTP request. State: `implemented`. Lease: session and generation. **Endpoint: `WSS /v1/jog`** Same operation, alternative transport. > [!WARNING] This call can energise the drives or move the robot. Keep the hardware E-stop within reach. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). Updates are 224-byte little-endian `local_jog_update` frames. Local producers send them as Unix datagrams to `jog.sock`, next to `control.sock`. Remote producers send them as binary WebSocket frames on `/v1/jog` over mutual TLS (see [Remote access](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#remote-jog-over-websocket)). Build frames with the SDK (`BuildJogFrame`, `RtJogProducer`) rather than by hand. Each frame carries the session, grant generation, jog generation, a strictly increasing non-zero `source_sequence`, its own origin and deadline, an axis mask and a finite velocity vector. `deadline_host_ns − origin_host_ns` may not exceed the cell's `max_jog_input_age_ns` (250 ms on LAN). Only the newest valid frame in a burst is applied, and a refused frame never extends the previous input's deadline. Local datagrams get no reply: read refusals from `GET /v1/jog/ingress`. #### Request A 224-byte little-endian `local_jog_update` frame, defined in `protocol/control.json`. Reserved bytes are zero. | Name | Type | Offset (bytes) | Description | |---|---|---|---| | `session_id` | `u8[32]` | 0 | The 32 bytes of the session token (its 64 hex characters, decoded). | | `clock_incarnation` | `u8[16]` | 32 | The 16 bytes of the jog clock incarnation (hex-decoded). On WSS the server maps source time instead. | | `grant_generation` | `u64` | 48 | Current grant generation. | | `jog_generation` | `u64` | 56 | Jog generation from `begin_jog`. | | `source_sequence` | `u64` | 64 | Strictly increasing, non-zero. | | `origin_host_ns` | `u64` | 72 | Input capture time, host ns (source clock on WSS). | | `deadline_host_ns` | `u64` | 80 | Absolute deadline; at most the cell's input-age ceiling after the origin. | | `axis_mask` | `u32` | 88 | Axes in this frame; a subset of the Begin mask. | | `reserved` | `u32` | 92 | Zero. | | `velocity` | `f64[16]` | 96 | Per-axis velocity in Describe order, rad/s or m/s. Axes outside the mask must be 0. | #### Response No reply on `jog.sock`. On WSS, only refusals are answered, as `{"type":"rejected","seq":N,"reason":"…"}`. #### Reason codes | Reason | When | |---|---| | [`jog_session_stale`](/docs/reference/error-codes#reasons-jog) | The frame's session, grant generation or jog generation is not the current one (WSS). | | [`jog_session_or_sequence_stale`](/docs/reference/error-codes#reasons-jog) | No open jog session for this generation, or `source_sequence` did not increase. | | [`jog_publisher_busy`](/docs/reference/error-codes#reasons-jog) | Another producer is publishing. Replace your unsent input with a fresh sample. | | [`jog_invalid_frame`](/docs/reference/error-codes#reasons-jog) | The binary frame could not be decoded (WSS). | | [`jog_stream_idle`](/docs/reference/error-codes#reasons-jog) | No complete WSS frame arrived within the cell's input-age ceiling; rt-control ends the jog and closes. | | [`control_session_stale`](/docs/reference/error-codes#reasons-authority) | Sent on WSS just before closing, when the grant was stopped or expired. | | [`jog_clock_unqualified`](/docs/reference/error-codes#reasons-jog) | Remote clock qualification is disabled (the `--remote-jog-*` flags are 0) or the calibration exchange is incomplete. | | [`jog_clock_invalid_budget_or_exchange`](/docs/reference/error-codes#reasons-jog) | A malformed calibration or Begin message, or an invalid timing budget. | | [`jog_clock_incarnation_mismatch`](/docs/reference/error-codes#reasons-jog) | The source clock incarnation changed during the WSS session. | | [`jog_clock_mapping_generation_mismatch`](/docs/reference/error-codes#reasons-jog) | The frame was mapped with an out-of-date clock mapping. | | [`jog_clock_moved_backwards`](/docs/reference/error-codes#reasons-jog) | A source timestamp went backwards, or input predates the Begin sample. | | [`jog_clock_arithmetic_range`](/docs/reference/error-codes#reasons-jog) | Clock conversion would overflow. | | [`jog_clock_uncertainty_exceeded`](/docs/reference/error-codes#reasons-jog) | The calibrated offset interval is wider than `--remote-jog-max-uncertainty-ns`. | | [`jog_clock_calibration_expired`](/docs/reference/error-codes#reasons-jog) | The last calibration is older than `--remote-jog-calibration-max-age-ns`. Recalibrate. | | [`jog_clock_exchange_inconsistent`](/docs/reference/error-codes#reasons-jog) | The calibration timestamps are not causally consistent. | | [`jog_input_too_old`](/docs/reference/error-codes#reasons-jog) | The conservatively mapped input age exceeds the cell ceiling. | | [`jog_input_entirely_future`](/docs/reference/error-codes#reasons-jog) | The whole input interval lies in the host's future. | | [`jog_input_deadline_expired`](/docs/reference/error-codes#reasons-jog) | The input deadline has already passed. | | [`session_principal_mismatch`](/docs/reference/error-codes#reasons-authority) | The WSS connection's TLS principal does not own the session. | | [`independent_jog_unavailable`](/docs/reference/error-codes#reasons-jog) | No independent jog lane, or the remote listener is shutting down (HTTP 503 before upgrade). | A frame refused by the native lane is answered on WSS as `jog_native_rejected_`, where `n` is the [native jog reason](https://advancedmetalresearch.com/docs/reference/error-codes#native-jog-reasons). On `jog.sock`, every refusal is only counted in `/v1/jog/ingress`. **Client libraries.** Go: `JogSession.Update / UpdateAt`, `RemoteJogSession.Update`. C++: `RtJogProducer::update`, `RtJogRemoteProducer::update`. ### `end_jog` **Endpoint: `POST /v1/control`** End a jog generation. Motion ramps to a hold. State: `implemented`. Lease: session and generation. Revokes input for `jog_generation` before waiting for the native receipt, then the core ramps the jog to a stop. The reply's `handle` echoes the generation. `end_jog` does not release authority: call `release` separately. Ending during the expiry ramp can return a native `closed` refusal. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `end_jog`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `jog_generation` | `uint64` | Yes | The current non-zero jog generation from `begin_jog`. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`, `jog_generation`. #### Response `handle` holds the result. `handle` echoes the ended jog generation. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "end_jog", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "jog_generation": 4 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "end_jog", "handle": 4 } ``` #### Reason codes | Reason | When | |---|---| | [`jog_session_stale`](/docs/reference/error-codes#reasons-jog) | No jog session is open, or `jog_generation` is not the current one. | | [`independent_jog_unavailable`](/docs/reference/error-codes#reasons-jog) | The native client offers no independent jog lane. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope) and [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) reasons. Native jog refusals return `RTCore rejected jog (reason N)` with `native_jog_result`. **Client libraries.** Go: `JogSession.End`, `Client.EndJog`. C++: `RtJogProducer::end`, `end_jog()`. ### `jog` **Endpoint: `POST /v1/control`** Legacy JSON velocity jog. Test only. State: `test_only`. Lease: session and generation. Labelled `test_only`: it is kept for test fixtures. Applications use `begin_jog`, `update_jog` and `end_jog` instead. The contract describes it as: “Legacy velocity jog is test-only for oracle ports and fixtures. Consumers use begin_jog/update_jog/end_jog; update_jog uses jog.sock or WSS /v1/jog.” #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `jog`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `axis_mask` | `uint32` | Yes | Axes to jog. | | `velocity` | `[]float64` | Yes | Finite velocities in Describe axis order, rad/s or m/s. | | `timeout_ms` | `uint32` | Yes | Input lifetime, 1..250 ms. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`, `axis_mask`, `velocity`, `timeout_ms`. #### Response `sequence` holds the native command sequence. There is no `data`. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "jog", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "axis_mask": 1, "velocity": [ 0.01, 0, 0, 0, 0, 0, 0, 0, 0 ], "timeout_ms": 100 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "jog", "sequence": 26 } ``` #### Reason codes | Reason | When | |---|---| | [`mode_conflict`](/docs/reference/error-codes#reasons-native) | A program is executing. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. **Client libraries.** Go: `Client.Jog`. C++: `jog()`. ## Trajectories A low-level point-list path, used by tests and short moves. Programs from the planner use [Programs](https://advancedmetalresearch.com/docs/apis/rt-control-http#programs). ### `prepare_trajectory` **Endpoint: `POST /v1/control`** Upload a point list and get an inert handle. State: `implemented`. Lease: session and generation. Stages 2..250000 `Point` records for `axis_mask` and returns a `handle` in the `prepared` state. Nothing moves until `start_trajectory`. The first point's `time_ns` is 0 and times strictly increase; positions and optional velocities are in Describe axis order and logical rad or m units. Native limits and continuity are enforced at Prepare, and again at Start. For a large body, put `schema`, `operation`, `session` and a non-zero `generation` **before** `points`, within the first 16384 bytes, so rt-control can check the fence before reading the rest. Do not use a key-sorting JSON encoder. One JSON upload is admitted at a time across all listeners, and the whole body is capped at 222516384 bytes. This is the low-level path for tests and short moves; production programs use [`prepare_program`](#prepare-program). #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `prepare_trajectory`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `axis_mask` | `uint32` | Yes | Axes the trajectory commands. | | `points` | [`Point`](#type-point)`[]` | Yes | 2..250000 points. See `Point` for units. | | `identity` | [`Identity`](#type-identity) | No | Immutable identity checked again at Start. Default: all zero. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`, `axis_mask`, `points`. #### Response `handle` is the new handle and `data` is its [`HandleRecord`](#type-handlerecord). | Name | Type | Required | Description | |---|---|---|---| | `handle` | `uint64` | Yes | — | | `state` | `string` | Yes | One of the handle_states labels; terminal states never regain permission. | | `generation` | `uint64` | Yes | Application grant generation that prepared this handle. | | `execution_generation` | `uint64` | Yes | Count of acknowledged native Starts when this handle started; zero before Start. | | `native_sequence` | `uint64` | Yes | Correlated native Start sequence; zero before Start. | | `identity` | [`Identity`](#type-identity) | No | Immutable raw trajectory identity supplied at Prepare and checked at Start; echoed unchanged through terminal observation. | #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "prepare_trajectory", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "axis_mask": 1, "points": [ { "time_ns": 0, "position": [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ] }, { "time_ns": 1000000000, "position": [ 0.05, 0, 0, 0, 0, 0, 0, 0, 0 ] } ] } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "prepare_trajectory", "handle": 7, "data": { "handle": 7, "state": "prepared", "generation": 3, "execution_generation": 0, "native_sequence": 0, "identity": {} } } ``` #### Reason codes | Reason | When | |---|---| | [`busy`](/docs/reference/error-codes#reasons-program) | Another JSON upload holds the preparation slot, or the lifecycle lock is busy. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority), [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) and [cell I/O](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-io) reasons. A native `invalid_trajectory` refusal (reason 6) retires every prepared handle and the prepared program. **Client libraries.** Go: `Client.PrepareTrajectory`. C++: `prepare_trajectory()`. ### `start_trajectory` **Endpoint: `POST /v1/control`** Start a prepared handle. State: `implemented`. Lease: session and generation. > [!WARNING] This call can energise the drives or move the robot. Keep the hardware E-stop within reach. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). Starts `handle` if it is still `prepared` and belongs to this grant. The full [motion start gate](https://advancedmetalresearch.com/docs/concepts/real-time-core#the-motion-start-gate) is evaluated in the core: lease, Arm, bus, faults, configuration, Home, limits, CSP mode, enable and a first point continuous with the held position. A definite `not_ready` or `mode_conflict` refusal restores the prepared plan so you can retry. Any uncertain outcome retires the handle: prepare again rather than replay. Completion is observed in Status and events, never inferred from the receipt. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `start_trajectory`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `handle` | `uint64` | Yes | Non-zero handle from `prepare_trajectory`. | | `identity` | [`Identity`](#type-identity) | No | Must equal the identity supplied at Prepare (all zero if none was). | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`, `handle`. #### Response `sequence` holds the native command sequence and `data` is a [`HandleRecord`](#type-handlerecord). | Name | Type | Required | Description | |---|---|---|---| | `handle` | `uint64` | Yes | — | | `state` | `string` | Yes | One of the handle_states labels; terminal states never regain permission. | | `generation` | `uint64` | Yes | Application grant generation that prepared this handle. | | `execution_generation` | `uint64` | Yes | Count of acknowledged native Starts when this handle started; zero before Start. | | `native_sequence` | `uint64` | Yes | Correlated native Start sequence; zero before Start. | | `identity` | [`Identity`](#type-identity) | No | Immutable raw trajectory identity supplied at Prepare and checked at Start; echoed unchanged through terminal observation. | #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "start_trajectory", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "handle": 7 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "start_trajectory", "sequence": 27, "data": { "handle": 7, "state": "started", "generation": 3, "execution_generation": 1, "native_sequence": 27, "identity": {} } } ``` #### Reason codes | Reason | When | |---|---| | [`unknown_handle`](/docs/reference/error-codes#reasons-handles) | No such handle in this adapter incarnation. | | [`trajectory_identity_mismatch`](/docs/reference/error-codes#reasons-handles) | `identity` differs from the one given at Prepare. | | [`handle_started`](/docs/reference/error-codes#reasons-handles) | The handle has already started. | | [`handle_consumed`](/docs/reference/error-codes#reasons-handles) | The handle completed, or a later execution replaced it. | | [`handle_discarded`](/docs/reference/error-codes#reasons-handles) | The handle was discarded. | | [`handle_superseded`](/docs/reference/error-codes#reasons-handles) | A newer preparation replaced it. | | [`handle_retired`](/docs/reference/error-codes#reasons-handles) | Stop, grant loss or an uncertain outcome retired it. Prepare again. | | [`mode_conflict`](/docs/reference/error-codes#reasons-native) | A program is executing, or the core reports another active mode. The handle is kept. | | [`not_ready`](/docs/reference/error-codes#reasons-native) | The start gate failed. The handle is kept; fix readiness and retry. | | [`fence`](/docs/reference/error-codes#reasons-authority) | Authority was revoked or the request cancelled while starting. The handle is retired. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. **Client libraries.** Go: `Client.StartTrajectory`. C++: `start_trajectory()`. ### `discard_trajectory` **Endpoint: `POST /v1/control`** Discard a prepared handle and free its native storage. State: `implemented`. Lease: session and generation. Discards a `prepared` handle. Discarding an already discarded handle is a no-op. A started handle cannot be discarded: you get `handle_active`; use Halt or Stop instead. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `discard_trajectory`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `handle` | `uint64` | Yes | Non-zero handle to discard. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`, `handle`. #### Response `sequence` holds the native command sequence. There is no `data`. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "discard_trajectory", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "handle": 7 } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "discard_trajectory", "sequence": 28 } ``` #### Reason codes | Reason | When | |---|---| | [`unknown_handle`](/docs/reference/error-codes#reasons-handles) | No such handle in this adapter incarnation. | | [`handle_active`](/docs/reference/error-codes#reasons-handles) | The handle has started. Discard is refused; end it with Halt or Stop. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. When native cleanup fails, the error reads `discard preparation : `. **Client libraries.** Go: `Client.DiscardTrajectory`. C++: `discard_trajectory()`. ## Programs The production path for planned weld programs: upload a verified `.rdt`, then start it by identity. ### `prepare_program` **Endpoint: `POST /v1/program`** Upload a dense `.rdt` program. It stays inert until `start_program`. State: `implemented`. Lease: session and generation headers. The body is the raw `.rdt` bytes (see [the .rdt format](https://advancedmetalresearch.com/docs/reference/rdt-format)); authority travels in headers. rt-control decodes and validates the whole file (format, digests, time grid, continuity, native position, velocity and declared acceleration limits, axis mapping and process markers) before staging it. It replaces any previously prepared program. The reply's `Program` carries the full `Identity`. Echo it unchanged to `start_program`. `prepare_program` has no `request_id` deduplication: never replay an uncertain upload; inspect Describe or Status, or Stop, first. One binary upload is admitted at a time, and the body is capped at 39298580 bytes. #### Request | Name | Type | Required | Description | |---|---|---|---| | `X-Control-Session` (header) | `string` | Yes | Current session token. | | `X-Control-Generation` (header) | `uint64` | Yes | Current grant generation, as decimal text. | | `Content-Type` (header) | `string` | No | `application/octet-stream` (sent by the SDKs). | Body: Binary `.rdt` blob. Required: `X-Control-Session`, `X-Control-Generation`. #### Response `data` is a [`Program`](#type-program). | Name | Type | Required | Description | |---|---|---|---| | `process_markers` | [`ProcessMarker[]`](#type-processmarker) | Yes | Process-I/O markers in the program. | | `identity` | [`Identity`](#type-identity) | Yes | Echo this unchanged to `start_program`. | | `requires_process_io` | `bool` | Yes | True if the program has output markers. | | `segments` | `int` | Yes | Segment count. | | `samples` | `int` | Yes | Source sample count. | | `normalised_samples` | `int` | Yes | Exact execution sample count after coincident segment endpoints are shared. | | `axis_mask` | `uint32` | Yes | Axes the program commands. | #### Example ```http POST /v1/program HTTP/1.1 Host: localhost Content-Type: application/octet-stream X-Control-Session: 8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e X-Control-Generation: 3 Content-Length: 1530412 <.rdt bytes> ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "prepare_program", "data": { "process_markers": [], "identity": { "plan_id": "weld-demo:1a2b3c4d5e6f", "program_id": "weld-demo", "program_digest": "sha256:…", "trajectory_digest": "sha256:…", "source_digest": "sha256:…", "normalised_digest": "sha256:…", "manifest_revision": 1, "plan_revision": 1 }, "requires_process_io": false, "segments": 3, "samples": 10002, "normalised_samples": 10000, "axis_mask": 511 } } ``` #### Reason codes | Reason | When | |---|---| | [`invalid_control_generation`](/docs/reference/error-codes#reasons-envelope) | `X-Control-Generation` is missing or not a decimal uint64. | | [`body_too_large`](/docs/reference/error-codes#reasons-envelope) | The body exceeds 39298580 bytes. HTTP 413. | | [`session_principal_mismatch`](/docs/reference/error-codes#reasons-authority) | The session belongs to another principal. | | [`busy`](/docs/reference/error-codes#reasons-program) | Another `.rdt` upload is in progress, the lifecycle lock is busy, or the core has no free plan slot (then `native_result.Reason` is 5, capacity). | | [`program_already_executing`](/docs/reference/error-codes#reasons-program) | A program is executing. Stop it or wait. | | [`manifest_revision_mismatch`](/docs/reference/error-codes#reasons-program) | The program's `manifest_revision` is not the adapter's pair revision. | | [`process_io_executor_not_qualified`](/docs/reference/error-codes#reasons-program) | The program sets the torch flag. Always refused. | | [`program_identity_missing`](/docs/reference/error-codes#reasons-program) | The `.rdt` header lacks a required identity field. | | [`dense_axis_map_requires_nine_ids`](/docs/reference/error-codes#reasons-program) | The cell describes more rotary axes than the nine-column format carries. | | [`invalid_dense_axis_map`](/docs/reference/error-codes#reasons-program) | The cell's rotary axes cannot be mapped onto the dense columns. | | [`unmapped_dense_axis`](/docs/reference/error-codes#reasons-program) | A commanded dense column has no native axis. The error reads `unmapped_dense_axis: `. | | [`native_limit_exceeded`](/docs/reference/error-codes#reasons-program) | A sample exceeds a native position, velocity or declared acceleration limit. `data.limit_violation` names it. | | [`native_segment_rate_exceeded`](/docs/reference/error-codes#reasons-program) | An interpolated segment exceeds a velocity limit. `data.limit_violation` names it. | | [`outside_limits_outward`](/docs/reference/error-codes#reasons-native) | A recovery segment would bow further outside the limits. | | [`segment_boundary_discontinuous`](/docs/reference/error-codes#reasons-program) | Adjacent moving segments do not meet. | | [`io_not_configured`](/docs/reference/error-codes#reasons-io) | The program has process markers but no cell I/O is configured. | | [`io_torch_unqualified`](/docs/reference/error-codes#reasons-io) | A marker names a torch-class output. | | [`io_marker_invalid`](/docs/reference/error-codes#reasons-io) | A marker names an unknown output or does not land on a sample. | | [`axis_count_mismatch`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`blob_length_mismatch`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`block_layout_invalid`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`block_sha256_mismatch`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`dense_schema_mismatch`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`duration_mismatch`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`header_json_invalid`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`header_truncated`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`kind_invalid`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`limits_invalid`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`nonfinite_sample`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`q_step_exceeded`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`qd_limit_exceeded`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`reserved_flags_set`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`sample_count_overflow`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`sample_encoding_mismatch`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`segment_index_out_of_order`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`segment_too_short`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`segments_empty`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`time_grid_invalid`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`torch_outside_weld`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`total_sample_count_mismatch`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`trajectory_digest_mismatch`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`boundary_q_discontinuity`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | | [`boundary_qd_nonzero`](/docs/reference/error-codes#reasons-dense) | The `.rdt` file failed format validation. | Also the common [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority) and [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) reasons. The dense format reasons are defined with the [.rdt format](https://advancedmetalresearch.com/docs/reference/rdt-format). Every one of them is a catalogue label. **Client libraries.** Go: `Client.PrepareProgram`. C++: `prepare_program()`. ### `start_program` **Endpoint: `POST /v1/control`** Start the prepared program by its exact identity. State: `implemented`. Lease: session and generation. > [!WARNING] This call can energise the drives or move the robot. Keep the hardware E-stop within reach. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). `identity` must equal the prepared program's `Identity` field for field. The same start gate as `start_trajectory` applies. Record `sequence`; completion appears in Status as `execution.state: completed` for the next `execution.generation`, or `faulted` with `native_execution_failed`. To retry a lost receipt, resend the same request with the same `request_id`. A new ID is a new command. #### Request | Name | Type | Required | Description | |---|---|---|---| | `schema` | `string` | Yes | `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | `start_program`. | | `session` | `string` | Yes | Session token from the current `Grant`. | | `generation` | `uint64` | Yes | Current grant generation. | | `identity` | [`Identity`](#type-identity) | Yes | The complete `Identity` returned by `prepare_program`. | | `request_id` | `string` | No | Idempotence key, 1..64 printable ASCII bytes. See [retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | The contract's semantically required fields: `schema`, `operation`, `session`, `generation`, `identity`. #### Response `sequence` holds the native command sequence. There is no `data`. #### Example ```http POST /v1/control HTTP/1.1 Host: localhost Content-Type: application/json { "schema": "rosie.rt-control.request.v1", "operation": "start_program", "session": "8c2f4e1a9b7d3c5e0f6a2b4c8d1e3f5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e", "generation": 3, "request_id": "9b2e41c07d5f4a3e8c6b1d0f2a4e6c8b", "identity": { "plan_id": "weld-demo:1a2b3c4d5e6f", "program_id": "weld-demo", "program_digest": "sha256:…", "trajectory_digest": "sha256:…", "source_digest": "sha256:…", "normalised_digest": "sha256:…", "manifest_revision": 1, "plan_revision": 1 } } ``` 200 OK: ```json { "schema": "rosie.rt-control.response.v1", "operation": "start_program", "sequence": 29 } ``` #### Reason codes | Reason | When | |---|---| | [`program_identity_mismatch`](/docs/reference/error-codes#reasons-program) | No program is prepared, or `identity` differs from it. | | [`program_already_executing`](/docs/reference/error-codes#reasons-program) | A program is already executing. | | [`process_io_executor_not_qualified`](/docs/reference/error-codes#reasons-program) | The program requires torch output. Always refused. | | [`unknown_handle`](/docs/reference/error-codes#reasons-handles) | No such handle in this adapter incarnation. | | [`handle_started`](/docs/reference/error-codes#reasons-handles) | The handle has already started. | | [`handle_consumed`](/docs/reference/error-codes#reasons-handles) | The handle completed, or a later execution replaced it. | | [`handle_discarded`](/docs/reference/error-codes#reasons-handles) | The handle was discarded. | | [`handle_superseded`](/docs/reference/error-codes#reasons-handles) | A newer preparation replaced it. | | [`handle_retired`](/docs/reference/error-codes#reasons-handles) | Stop, grant loss or an uncertain outcome retired it. Prepare again. | | [`mode_conflict`](/docs/reference/error-codes#reasons-native) | A program is executing, or the core reports another active mode. The handle is kept. | | [`not_ready`](/docs/reference/error-codes#reasons-native) | The start gate failed. The handle is kept; fix readiness and retry. | | [`fence`](/docs/reference/error-codes#reasons-authority) | Authority was revoked or the request cancelled while starting. The handle is retired. | Also the common [envelope](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-envelope), [authority](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-authority), [native](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-native) and [cell I/O](https://advancedmetalresearch.com/docs/apis/rt-control-http#common-io) reasons. **Client libraries.** Go: `Client.StartProgram`. C++: `start_program()`. ## Reserved operations Named in the contract so clients can detect them, but they perform no operation. ### `abort` **Endpoint: `POST /v1/control`** Reserved. Not implemented. State: `unimplemented`. Lease: none. Advertised as `unimplemented`, with transport `none`. Sending `operation: "abort"` performs nothing and returns `capability_unimplemented` with the `CapabilityInfo` in `data`. Use `stop` or `halt`. #### Request The standard envelope with `operation` set to the operation name. No other fields. #### Response Always refused; see below. #### Reason codes | Reason | When | |---|---| | [`capability_unimplemented`](/docs/reference/error-codes#reasons-envelope) | Always. | ### `readiness` **Endpoint: `POST /v1/control`** Reserved. Not implemented. State: `unimplemented`. Lease: none. Advertised as `unimplemented`. Returns `capability_unimplemented`. Read per-axis `readiness` from `GET /v1/status` instead. #### Request The standard envelope with `operation` set to the operation name. No other fields. #### Response Always refused; see below. #### Reason codes | Reason | When | |---|---| | [`capability_unimplemented`](/docs/reference/error-codes#reasons-envelope) | Always. | ## Types Every type in the contract, generated from `types` in the schema. `Required` is the codec designation; which fields an operation needs is in its request table. JSON integers are exact uint64 values: parse them without converting to floating point. Fields of native receipts (`CommandResult`, `JogObservation`, `GrantObservation`) keep their case-sensitive native names. ### Authority #### Fence | Name | Type | Required | Description | |---|---|---|---| | `session` | `string` | Yes | Current unguessable session token; acquire omits the fence. Tokens issued by the adapter are 64 lowercase hexadecimal characters. | | `generation` | `uint64` | Yes | Nonzero uint64 grant generation. Motion requires exact equality; renew and stop accept an older nonzero generation not above the current one for the same session. | #### Binding | Name | Type | Required | Description | |---|---|---|---| | `pair_id` | `string` | Yes | Exact configured nonempty pair ID. | | `revision` | `uint64` | Yes | Nonzero uint64 equal to configured pair revision. | | `configuration_sha256` | `string` | Yes | Exact configured native SHA-256 label. | | `machine_sha256` | `string` | No | Machine-semantics digest; omitted by clients that only know the whole-artifact digest. | #### Grant | Name | Type | Required | Description | |---|---|---|---| | `stopping` | `bool` | Yes | True means renewal keeps the lease alive while Stop is in flight; it grants no movement permission. | | `deadline_host_ns` | `uint64` | Yes | Uint64 absolute lease deadline in host monotonic nanoseconds. | | `session` | `string` | Yes | 64-character unpredictable lowercase hexadecimal token while owned; empty after release/expiry. | | `generation` | `uint64` | Yes | Monotonically increasing uint64 application generation, fenced by Stop and acquisition. | | `controller` | `string` | Yes | The `controller` label sent to `acquire`. | | `binding` | [`Binding`](#type-binding) | Yes | The binding, completed with the adapter's digests. | | `lease_ms` | `int` | Yes | Effective negotiated lease in whole milliseconds: min(client requested lease, compiled cell ceiling); zero/omitted request retains 500 ms capped by the cell. Renew preserves this value. Longer leases increase worst-case unattended-stop delay after link loss. Unverified absolute bound: 10000 ms. | #### GrantObservation | Name | Type | Required | Description | |---|---|---|---| | `Ticket` | `uint64` | Yes | — | | `ConnectionId` | `uint64` | Yes | — | | `NativeGeneration` | `uint64` | Yes | — | | `AppGrantGeneration` | `uint64` | Yes | — | | `CapabilityId` | `uint8[16]` | Yes | — | | `DeadlineHostNs` | `uint64` | Yes | — | | `NowHostNs` | `uint64` | Yes | — | | `ConfigurationEpoch` | `uint64` | Yes | — | | `HomeEpoch` | `uint64` | Yes | — | | `StopAckId` | `uint64` | Yes | — | | `GrantReason` | `uint32` | Yes | — | | `GrantActive` | `uint32` | Yes | — | | `AxisMask` | `uint32` | Yes | — | | `Reserved` | `uint32` | Yes | — | | `Reserved1` | `uint64` | Yes | — | | `Reserved2` | `uint64` | Yes | — | | `Reserved3` | `uint64` | Yes | — | ### Requests and responses #### Request | Name | Type | Required | Description | |---|---|---|---| | `request_id` | `string` | No | Optional, non-null string of 1..64 printable ASCII bytes (0x20..0x7e). | | `schema` | `string` | Yes | Exactly `rosie.rt-control.request.v1`. | | `operation` | `string` | Yes | Exact capability name; only POST /v1/control operations dispatch here. Unknown names are rejected. | | *(embedded)* | [`Fence`](#type-fence) | Yes | All fields of `Fence` appear at this level of the object. | | `label` | `string` | No | mark_telemetry requires 1..128 UTF-8 bytes; free-text dump label. | | `controller` | `string` | No | Acquire requires 1..63 bytes; opaque controller name. | | `binding` | [`Binding`](#type-binding) | No | Acquire requires exact equality with the configured Binding. | | `axis_mask` | `uint32` | No | Uint32 configured axis group mask; interpretation is native operation-specific; reset_fault defaults zero to all axes. | | `velocity` | `float64[]` | No | Finite logical velocities in described axis order for legacy jog; native vector and velocity-limit validation applies. | | `timeout_ms` | `uint32` | No | Legacy jog requires an integer 1..250 milliseconds. | | `handle` | `uint64` | No | Nonzero opaque uint64 handle for start/discard, owned by the current daemon incarnation and grant. | | `points` | [`Point[]`](#type-point) | No | 2..250000 Point records; declared axis order, native limits and continuity apply. Large bodies require the envelope before this array. | | `identity` | [`Identity`](#type-identity) | No | Exact prepared program Identity for start_program. | | `jog_generation` | `uint64` | No | EndJog requires the exact current nonzero independent jog generation. | | `source_sequence` | `uint64` | No | Accepted envelope field retained for compatibility; current /v1/control dispatch does not consume it. Independent updates use the binary lane. | | `source_origin_host_ns` | `uint64` | No | BeginJog origin in the negotiated host monotonic clock, uint64 nanoseconds; native freshness validation applies. | | `deadline_host_ns` | `uint64` | No | BeginJog producer deadline in host monotonic nanoseconds; after origin and never extended by admission or renewal. | | `clock_incarnation` | `string` | No | BeginJog requires exact equality with GET /v1/jog/clock incarnation. | | `requested_lease_ms` | `int` | No | Acquire requested lease in whole ms, 1..10000; omitted or zero retains the LAN default capped by the cell. Adapter returns min(request, cell ceiling) in Grant.lease_ms. Clients require at least three measured round trips plus renewal cadence. | #### Response | Name | Type | Required | Description | |---|---|---|---| | `native_jog_result` | [`JogObservation`](#type-jogobservation) | No | Native jog observation on a jog refusal. | | `native_result` | [`CommandResult`](#type-commandresult) | No | Native receipt on a native refusal. | | `schema` | `string` | Yes | `rosie.rt-control.response.v1`. | | `operation` | `string` | Yes | The operation this reply answers. | | `sequence` | `uint64` | No | Native command sequence, when returned. | | `handle` | `uint64` | No | Handle or jog generation, when returned. | | `data` | `any` | No | The operation's result type. | | `error` | `string` | No | Reason code or diagnostic, on failure only. | #### RawResponse | Name | Type | Required | Description | |---|---|---|---| | `native_jog_result` | [`JogObservation`](#type-jogobservation) | No | Native jog observation on a jog refusal. | | `native_result` | [`CommandResult`](#type-commandresult) | No | Native receipt on a native refusal. | | `schema` | `string` | Yes | `rosie.rt-control.response.v1`. | | `operation` | `string` | Yes | The operation this reply answers. | | `sequence` | `uint64` | No | Native command sequence, when returned. | | `handle` | `uint64` | No | Handle or jog generation, when returned. | | `data` | `object` | No | Undecoded result JSON. | | `error` | `string` | No | Reason code or diagnostic, on failure only. | #### CommandResult | Name | Type | Required | Description | |---|---|---|---| | `Sequence` | `uint64` | Yes | Native command sequence. | | `Handle` | `uint64` | Yes | Native handle, when relevant. | | `Generation` | `uint64` | Yes | Native control generation. | | `Operation` | `uint32` | Yes | Native message code (`MSG_CMD_*`). | | `Result` | `uint32` | Yes | 0 accepted, 1 rejected, 2 prepared. | | `Reason` | `uint32` | Yes | Native command reason; see [native command reasons](https://advancedmetalresearch.com/docs/reference/error-codes#native-command-reasons). | | `AxisMask` | `uint32` | Yes | Axes the result applies to. | #### CapabilityInfo | Name | Type | Required | Description | |---|---|---|---| | `name` | `string` | Yes | Canonical capability name. | | `state` | `string` | Yes | implemented, interim, test_only or unimplemented. test_only operations are retained for oracle ports and fixtures; consumers use implemented operations. | | `transport` | `string` | Yes | Actual ingress; unimplemented has no transport. | ### Motion #### Point | Name | Type | Required | Description | |---|---|---|---| | `time_ns` | `int64` | Yes | Signed int64 nanoseconds from plan start; first point zero, later timestamps strictly increasing. | | `position` | `float64[]` | Yes | Finite logical rad/m positions in described axis order, within native configured bounds. | | `velocity` | `float64[]` | No | Optional finite logical rad/s or m/s velocities in described axis order; nil/empty omits feedforward. | | `io_mask` | `uint32` | No | Eight-bit mask in configured output order; set bits require configured non-torch outputs. | | `io_values` | `uint32` | No | Eight-bit output intents; every set bit must also be set in io_mask. Physical readback is independent. | #### Identity | Name | Type | Required | Description | |---|---|---|---| | `plan_id` | `string` | Yes | Plan identifier from the `.rdt` header. | | `program_id` | `string` | Yes | Program identifier from the `.rdt` header. | | `program_digest` | `string` | Yes | Program digest from the `.rdt` header. | | `trajectory_digest` | `string` | Yes | Content digest of the dense trajectory. | | `source_digest` | `string` | Yes | Verified source trajectory digest; start_program must echo the prepared value exactly. | | `normalised_digest` | `string` | Yes | Canonical segment-record digest including local clocks and process bits; start_program must echo the prepared value exactly. | | `manifest_revision` | `uint64` | Yes | Must equal the pair revision rt-control was started with. | | `plan_revision` | `uint64` | Yes | Plan revision from the `.rdt` header. | #### HandleRecord | Name | Type | Required | Description | |---|---|---|---| | `handle` | `uint64` | Yes | — | | `state` | `string` | Yes | One of the handle_states labels; terminal states never regain permission. | | `generation` | `uint64` | Yes | Application grant generation that prepared this handle. | | `execution_generation` | `uint64` | Yes | Count of acknowledged native Starts when this handle started; zero before Start. | | `native_sequence` | `uint64` | Yes | Correlated native Start sequence; zero before Start. | | `identity` | [`Identity`](#type-identity) | No | Immutable raw trajectory identity supplied at Prepare and checked at Start; echoed unchanged through terminal observation. | #### Execution | Name | Type | Required | Description | |---|---|---|---| | `handles` | [`HandleRecord[]`](#type-handlerecord) | Yes | Every handle this adapter knows, with its state. | | `generation` | `uint64` | Yes | Count of acknowledged native Starts; distinct from native control generation. | | `state` | `string` | Yes | Application execution state: empty before observation, prepared, executing, completed, faulted, cancelled, discarded or released. | | `identity` | [`Identity`](#type-identity) | Yes | Identity of the prepared or running program. | | `segment` | `int` | Yes | Current zero-based program segment when a program is observed. | | `native_sequence` | `uint64` | Yes | Native sequence of the program Start. | | `error` | `string` | No | Failure detail, for example `native_execution_failed`. | #### MotionState | Name | Type | Required | Description | |---|---|---|---| | `mode` | `uint32` | Yes | 0 idle, 2 trajectory, 3 jog. | | `state` | `uint32` | Yes | Native execution state: 0 idle, 1 accepted, 2 queued, 3 executing, 4 completed, 5 aborted, 6 faulted, 7 underrun. | | `trajectory_id` | `uint64` | Yes | Active trajectory handle. | | `point_index` | `uint32` | Yes | Current point index. | | `queue_depth` | `uint32` | Yes | — | | `last_event` | `uint32` | Yes | — | | `done` | `bool` | Yes | True when the active execution has finished. | | `command_sequence` | `uint64` | Yes | Native command sequence of the active or last motion. | | `time_ns` | `uint64` | Yes | Native observation time, ns. | #### Program | Name | Type | Required | Description | |---|---|---|---| | `process_markers` | [`ProcessMarker[]`](#type-processmarker) | Yes | Process-I/O markers in the program. | | `identity` | [`Identity`](#type-identity) | Yes | Echo this unchanged to `start_program`. | | `requires_process_io` | `bool` | Yes | True if the program has output markers. | | `segments` | `int` | Yes | Segment count. | | `samples` | `int` | Yes | Source sample count. | | `normalised_samples` | `int` | Yes | Exact execution sample count after coincident segment endpoints are shared. | | `axis_mask` | `uint32` | Yes | Axes the program commands. | #### ProcessMarker | Name | Type | Required | Description | |---|---|---|---| | `time_ns` | `int64` | Yes | Marker time from program start, ns. | | `segment` | `int` | Yes | Segment index. | | `sample` | `int` | Yes | Sample index. | | `action` | `string` | Yes | Output name. | | `value` | `bool` | Yes | Requested output value. | #### ProgramLimitData | Name | Type | Required | Description | |---|---|---|---| | `limit_violation` | [`ProgramLimitViolation`](#type-programlimitviolation) | Yes | Logical joint-limit diagnostic for native_limit_exceeded or native_segment_rate_exceeded; supplements the unchanged refusal reason and is not a native command receipt. | #### ProgramLimitViolation | Name | Type | Required | Description | |---|---|---|---| | `kind` | `string` | Yes | Diagnostic quantity: position or velocity. | | `segment` | `int` | Yes | Zero-based source segment index, matching the refusal detail. | | `sample` | `int` | Yes | Zero-based source sample index, matching the refusal detail. | | `axis` | `string` | Yes | Canonical joint identifier reported by admission, for example J6. | | `value` | `float64` | Yes | Finite logical joint position or peak velocity in unit; command-count direction and Home offset have been removed. | | `limit` | `float64` | Yes | Finite logical admission bound in unit; invalid numeric details are omitted as a whole without discarding the refusal. | | `unit` | `string` | Yes | rad for position; rad/s for velocity. | #### PlanCursor | Name | Type | Required | Description | |---|---|---|---| | `time_ns` | `uint64` | Yes | — | | `active_handle` | `uint64` | Yes | — | | `sample_index` | `uint32` | Yes | — | | `native_clock_ns` | `uint64` | Yes | — | #### BufferHealth | Name | Type | Required | Description | |---|---|---|---| | `time_ns` | `uint64` | Yes | — | | `slots_free` | `uint32` | Yes | — | | `staging_in_progress` | `bool` | Yes | — | | `ring_drops` | `uint64` | Yes | — | | `native_slot_occupancy` | `object` | Yes | — | ### Jog #### LocalJogClock | Name | Type | Required | Description | |---|---|---|---| | `domain` | `string` | Yes | `CLOCK_MONOTONIC`. | | `incarnation` | `string` | Yes | Clock incarnation; pass it to `begin_jog`. | | `mapping_generation` | `uint64` | Yes | Clock mapping generation (1 for the local lane). | | `now_host_ns` | `uint64` | Yes | Current host monotonic time, ns. | #### JogObservation | Name | Type | Required | Description | |---|---|---|---| | `Ticket` | `uint64` | Yes | — | | `ConnectionId` | `uint64` | Yes | — | | `NativeGeneration` | `uint64` | Yes | — | | `CapabilityId` | `uint8[16]` | Yes | — | | `AppGrantGeneration` | `uint64` | Yes | — | | `ConfigurationEpoch` | `uint64` | Yes | — | | `HomeEpoch` | `uint64` | Yes | — | | `ClockMappingGeneration` | `uint64` | Yes | — | | `JogGeneration` | `uint64` | Yes | Current jog generation. | | `SourceSequence` | `uint64` | Yes | Sequence of the latest applied input. | | `InputDeadlineHostNs` | `uint64` | Yes | Deadline of the latest applied input, host ns. | | `NowHostNs` | `uint64` | Yes | Publication time, host ns. | | `ObservedSourceSequence` | `uint64` | Yes | — | | `ObservedOriginHostNs` | `uint64` | Yes | — | | `FirstObservedHostNs` | `uint64` | Yes | — | | `ControlReason` | `uint32` | Yes | — | | `ControlAccepted` | `uint32` | Yes | — | | `UpdateReason` | `uint32` | Yes | — | | `StateReason` | `uint32` | Yes | Native jog reason for the current state. | | `AxisMask` | `uint32` | Yes | Axes of the jog session. | | `Open` | `uint32` | Yes | 1 while the jog session accepts input. | | `HasInput` | `uint32` | Yes | 1 once an input has been applied. | | `VelocityScalePpm` | `uint32` | Yes | — | #### JogIngressObservation | Name | Type | Required | Description | |---|---|---|---| | `source_sequence` | `uint64` | Yes | — | | `reason` | `uint32` | Yes | — | | `now_host_ns` | `uint64` | Yes | — | | `refused` | `uint64` | Yes | Cumulative refused ingress count for this adapter process; accepted frames do not increment it. | | `refused_by_reason` | `uint64[15]` | Yes | Cumulative refusal counts indexed by protocol/control.json jog reason 0..14; index 0 remains zero. Unverified fixed capacity: 15 reason slots. | | `latest_refusal` | [`JogIngressRefusal`](#type-jogingressrefusal) | Yes | Most recent refusal across all ingress transports; zero before any refusal and preserved through acceptance and grant changes. | #### JogIngressRefusal | Name | Type | Required | Description | |---|---|---|---| | `source_sequence` | `uint64` | Yes | Refused source sequence; zero when no complete input frame is available. | | `reason` | `uint32` | Yes | Typed native jog reason; zero only before the first refusal. | | `now_host_ns` | `uint64` | Yes | Host monotonic ns when the adapter recorded the refusal; not an RT application timestamp. | ### Description #### Description | Name | Type | Required | Description | |---|---|---|---| | *(embedded)* | [`NativeDescription`](#type-nativedescription) | Yes | All fields of `NativeDescription` appear at this level of the object. | | `contract_version` | `uint32` | Yes | Exactly 1. | | `capabilities_digest` | `string` | Yes | Lowercase SHA-256 of the exact committed UTF-8 LF schema file bytes, including its final newline; no self-referential digest field. | | `capabilities` | [`CapabilityInfo[]`](#type-capabilityinfo) | Yes | Every target capability with its implementation state and transport. | | `control_idle_timeout_ns` | `uint64` | No | Compiled link idle timeout in ns; clients keep reserved connections alive at one third of this bound, independently of grants. Unverified LAN and internet policy: 90000000000 ns. An omitted legacy field uses 30000000000 ns. | | `program` | [`Program`](#type-program) | No | Detached prepared program metadata including both identity digests; absent when no program is prepared. | | `robot` | [`RobotDescription`](#type-robotdescription) | No | Null when no robot definition is bound or no compiled artefact is loaded; never guessed from axis count. | | `drives` | [`DriveDescription[]`](#type-drivedescription) | Yes | Per-axis compiled drive configuration and current verified bring-up identity; empty without a compiled artefact. | #### NativeDescription | Name | Type | Required | Description | |---|---|---|---| | `backend` | `string` | Yes | Core backend, for example `simulation`. | | `schema` | `string` | Yes | — | | `protocol_major` | `uint32` | Yes | Native protocol major version. | | `protocol_minor` | `uint32` | Yes | Native protocol minor version. | | `configuration_sha256` | `string` | Yes | Digest of the compiled configuration. | | `machine_sha256` | `string` | Yes | Canonical machine-semantics digest recomputed by the daemon at startup. | | `deployment_sha256` | `string` | Yes | Deployment identity digest (host, NIC, CPUs, sockets, pair, users). | | `max_trajectory_points` | `uint32` | Yes | Maximum points per trajectory. | | `resident_plans` | `uint32` | Yes | Native plan slots. | | `cycle_ns` | `uint64` | Yes | Cycle period, ns. | | `interpolation` | `string` | Yes | — | | `stop` | `string` | Yes | — | | `axes` | [`AxisDescription[]`](#type-axisdescription) | Yes | Axes in native index order. | | `bus` | `object` | Yes | Current native bus policy and held_slaves inventory; the machine digest binds configured policy and declarations, not discovered devices. A declared cell I/O terminal has disposition io_terminal and contributes one required responding slave. Its declared identity and presence remain mandatory under both hold and refuse; after activation it must be OP or admission reports io_terminal_not_operational. Held devices alone receive no PDOs or output capability. | | `max_grant_lease_ns` | `uint64` | Yes | Compiled cell lease ceiling in ns; a longer effective lease increases unattended-stop delay after link loss. | | `max_jog_input_age_ns` | `uint64` | Yes | Compiled cell capture-age and lifetime ceiling in ns; longer ages prolong stale velocity application before the existing expiry ramp. | | `io` | `object` | No | Configured cell I/O terminal identity, torch_qualified=false, named input classes and polarities, and output classes, OFF safe states, expiry_ns budgets and independent readback wiring. Omitted when no terminal is configured; this policy is separate from NativeStatus.io observations. | #### AxisDescription | Name | Type | Required | Description | |---|---|---|---| | `completion_tolerance` | `float64` | Yes | Settle tolerance at the final sample, in `position_unit`. | | `feedback_fields` | `string[]` | Yes | — | | `index` | `uint32` | Yes | Native index; the bit position in every axis mask. | | `id` | `string` | Yes | Axis identifier, for example `J1`. | | `position_unit` | `string` | Yes | `rad` or `m`. | | `counts_per_unit` | `float64` | Yes | Drive counts per `position_unit`, compiled from the robot definition. | | `sign` | `int` | Yes | Command direction, +1 or −1. | | `feedback_wrap` | `bool` | Yes | — | | `command_wrap` | `bool` | Yes | — | | `velocity_command_mapped` | `bool` | Yes | — | | `native_home` | `bool` | Yes | True if the axis supports native Home. | | `require_home` | `bool` | Yes | True if motion requires a valid Home. | | `min_position` | `float64` | Yes | Lower position limit, in `position_unit`. | | `max_position` | `float64` | Yes | Upper position limit, in `position_unit`. | | `max_velocity` | `float64` | Yes | Velocity limit, `position_unit`/s. | | `jog_acceleration` | `float64` | Yes | Jog acceleration, `position_unit`/s². | | `max_target_lead` | `float64` | Yes | Largest allowed command lead over feedback, in `position_unit`. | | `following_error` | `float64` | Yes | Following-error bound, in `position_unit`. | | `following_error_timeout_ns` | `uint64` | Yes | How long a following error may persist, ns. | | `completion_timeout_ns` | `uint64` | Yes | Settle deadline after the final sample, ns. | | `interpolation` | `string` | Yes | hermite_position_with_velocity, linear_without: cubic Hermite q when both knots supply qd, otherwise linear q; configured shortest-step command wrapping applies. | | `feedforward` | `string` | Yes | qd_optional: both supplied knot velocities shape Hermite position; feedforward is its derivative. If either knot lacks qd, feedforward is the linear segment slope. Endpoint hold velocity is zero. | | `checks` | `object` | Yes | Object with exactly position: declared, velocity: declared, acceleration: declared or not_declared, jerk: unsupported. Prepare checks position range and supplied qd/segment velocity. With max_acceleration, check consecutive qd differences per segment or second q differences across segment midpoints when qd is absent; mixed qd presence rejects. Without the profile field acceleration is not_declared. Sampled derivatives give no continuous acceleration guarantee at linear corners or endpoint hold. Jerk is never checked. | | `endpoint` | `string` | Yes | hold_last_sample_then_settle: hold final q with zero velocity; fresh ready feedback within completion_tolerance by completion_timeout_ns, as evaluated by completion.hpp. | | `max_acceleration` | `float64` | No | Positive finite profile bound in position units per s^2; absent only when acceleration is not_declared. | | `brake_override_reason` | `string` | No | Nonempty reason for an explicit bench brake override (effective present=false); absent without an override. Does not claim the physical brake is absent or qualified. | #### RobotDescription | Name | Type | Required | Description | |---|---|---|---| | `model_id` | `string` | Yes | The robot description's model id: the directory name under robot_description/robots/ the machine was compiled with. | | `robot_description_sha256` | `string` | Yes | sha256:<64 lowercase hex>, the description's manifest identity over the resources it registers (docs/ROBOT-DESCRIPTION-CONTRACT.md); the identity a plan's header must carry. | | `machine_planning_calibration_sha256` | `string` | Yes | sha256:<64 lowercase hex> of the served machine_planning_calibration.json bytes, this machine's deviation from the description; the identity a plan's header must carry. | | `resources` | [`ResourceInfo[]`](#type-resourceinfo) | Yes | The description's manifest and every file it registers, by description-relative path, plus machine_planning_calibration.json; unverified maximum 1024 entries and 134217728 distinct bytes. | #### ResourceInfo | Name | Type | Required | Description | |---|---|---|---| | `path` | `string` | Yes | Description-relative path (robot_description_manifest.json, robot.urdf, meshes/, rtcore_definition.json, ...) or machine_planning_calibration.json. Never a server filesystem path. | | `sha256` | `string` | Yes | Lowercase SHA-256 of the exact resource bytes. | | `bytes` | `uint64` | Yes | Exact resource byte count; unverified maximum 33554432 bytes. | | `media_type` | `string` | Yes | application/json, application/xml, model/vnd.collada+xml, model/stl or application/octet-stream. | #### DriveDescription | Name | Type | Required | Description | |---|---|---|---| | `axis` | `string` | Yes | Configured axis ID in native axis order. | | `config_name` | `string` | Yes | Compiled drive configuration name. | | `config_sha256` | `string` | Yes | SHA-256 of canonical drive configuration content used in the compiled identity. | | `slave_position` | `uint16` | Yes | Configured EtherCAT slave position. | | `verified_identity` | [`DriveIdentity`](#type-driveidentity) | No | Null unless current native configuration verification and CoE revision readback are valid. Vendor/product are configured expectations, revision is observed. Simulation synthesizes vendor/product from the profile and cannot independently inject their mismatch. | #### DriveIdentity | Name | Type | Required | Description | |---|---|---|---| | `expected_vendor_id` | `uint32` | Yes | Configured vendor expectation used by IgH slave matching; not an independent vendor readback. | | `expected_product_code` | `uint32` | Yes | Configured product expectation used by IgH slave matching; not an independent product readback. | | `observed_coe_revision` | `uint32` | Yes | Observed CoE 0x1018:3 firmware revision; distinct from the configured SII revision and never substituted from configuration. | ### Status #### ProcessStatus | Name | Type | Required | Description | |---|---|---|---| | `daemon_incarnation` | `string` | Yes | Core process identity for this snapshot. | | `adapter_incarnation` | `string` | Yes | rt-control process identity. | | `grant` | [`GrantObservation`](#type-grantobservation) | Yes | Native grant observation. | | `jog` | [`JogObservation`](#type-jogobservation) | Yes | Native jog observation. | | `jog_ingress` | [`JogIngressObservation`](#type-jogingressobservation) | Yes | Adapter-lifetime ingress outcomes across datagram, WSS and UpdateJog; counters and latest refusal match GET /v1/jog/ingress. | | `core` | [`NativeStatus`](#type-nativestatus) | Yes | Native core status. | | `motion` | [`MotionState`](#type-motionstate) | Yes | Native motion state. | | `execution` | [`Execution`](#type-execution) | Yes | Program and handle lifecycle. | | `time_ns` | `uint64` | Yes | Native publication time, ns. | | `axes` | [`LogicalAxisStatus[]`](#type-logicalaxisstatus) | Yes | Per-axis logical status, in Describe order. | | `generations` | [`StatusGenerations`](#type-statusgenerations) | Yes | Current generations and epochs. | | `plan_cursor` | [`PlanCursor`](#type-plancursor) | Yes | Native plan cursor. | | `buffer_health` | [`BufferHealth`](#type-bufferhealth) | Yes | Native buffer health. | | `adapter` | [`AdapterStatus`](#type-adapterstatus) | Yes | Adapter runtime observations; not native motion state. | #### NativeStatus | Name | Type | Required | Description | |---|---|---|---| | `daemon_incarnation` | `string` | Yes | Identity of this core process. | | `rt_cpu` | `uint32` | Yes | Configured RT CPU index as an exact uint32 integer. | | `housekeeping_cpus` | `string` | Yes | Effective Linux CPU-list mask for housekeeping workers, excluding the RT CPU. | | `affinity_applied` | `object` | Yes | Worker name to affinity-application success; false is not qualified placement and null means no observations. | | `recovery` | [`RecoveryStatus`](#type-recoverystatus) | Yes | Latched faults and recovery classes. | | `time_ns` | `uint64` | Yes | Native publication time, ns. | | `backend` | `string` | Yes | Core backend. | | `configuration_sha256` | `string` | Yes | Digest of the compiled configuration. | | `control_generation` | `uint64` | Yes | Native control generation (the core's fence). | | `lease_valid` | `uint32` | Yes | 1 while the core holds a valid lease. | | `config_verified_mask` | `uint32` | Yes | Axes whose drive configuration is verified. | | `home_valid_mask` | `uint32` | Yes | Axes with valid Home evidence. | | `home_epoch` | `uint64` | Yes | Increments when Home evidence changes. | | `commissioning_phase` | `uint32` | Yes | 0 when no Home or commissioning is running. | | `safety_fault_mask` | `uint32` | Yes | Non-zero blocks all motion. | | `execution_fault_reasons` | `uint32` | Yes | Latched execution fault bits; see error codes. | | `last_bus_failure_operation` | `uint32` | Yes | — | | `last_bus_failure_code` | `int64` | Yes | — | | `armed` | `uint32` | Yes | 1 when armed. | | `axis_enable_mask` | `uint32` | Yes | Axes that are enabled. | | `native_home_active_axis_mask` | `uint32` | Yes | Axes running native Home. | | `axes` | [`AxisStatus[]`](#type-axisstatus) | Yes | Per-axis native status. | | `configuration_epoch` | `uint64` | Yes | — | | `buffer_health` | [`BufferHealth`](#type-bufferhealth) | Yes | — | | `plan_cursor` | [`PlanCursor`](#type-plancursor) | Yes | — | | `bus` | `object` | Yes | Native bus policy and held_slaves with retained, observed and expected declared identities, presence, AL state and disposition; unreadable identity fields are null. Dispositions: held for an undeclared device allowed by hold; declared_unused for a matching declaration; refused_unknown for an undeclared device under refuse; refused_identity for a declared identity mismatch; identity_unreadable for an unavailable SII identity; refused_missing for a missing required declaration; identity_changed for a post-admission identity change under hold; disappeared for a missing unused device under hold; refused_state for a held device outside PREOP or INIT, or a configured terminal outside OP after activation. Refuse continuously requires declared identities and presence; post-admission refusal latches a readiness fault and inhibits outputs before submission, requiring public reset_fault (FaultReset), reacquisition of the application session, then explicit Enable and Arm after restoring the device and verified readiness. Enable before reset is rejected with native not_ready (reason 2); reset clears the readiness fault (reason 128) and safety mask while keeping outputs inhibited and retiring the session. Lost Home evidence requires separate qualified recovery before motion. Hold reports post-admission unused-device identity or presence changes and continues axes while axis readiness remains valid. Admission reason and reason_position identify identity_unreadable, unknown_slave, declared_identity_mismatch, declared_slave_missing, unsafe_held_state or capacity_exceeded; none means no observed refusal. No output capability is granted to held devices. A declared cell I/O terminal has disposition io_terminal and contributes one required responding slave. Its declared identity and presence remain mandatory under both hold and refuse; after activation it must be OP or admission reports io_terminal_not_operational. Held devices alone receive no PDOs or output capability. | | `io` | [`IoState`](#type-iostate) | Yes | — | #### AxisStatus | Name | Type | Required | Description | |---|---|---|---| | `drive_alarm` | `string` | Yes | Drive display alarm code decoded from the drive's error-code objects; unresolved bus candidates joined by \|; empty when clear, unmapped, or another drive family. | | `drive_alarm_text` | `string` | Yes | Text meaning of drive_alarm; multiple candidates joined by \| in matching order. | | `drive_alarm_aux_code` | `uint32` | Yes | Raw auxiliary alarm word read from the drive. Retained for the current nonzero drive error event and configuration epoch, zero when invalid or cleared. Separate from manufacturer_error_code, which carries the cyclic manufacturer_err PDO. | | `drive_alarm_aux_valid` | `bool` | Yes | True only for an admitted auxiliary reply matching the current nonzero drive error event and configuration epoch; false when missing, stale, or cleared, preventing zero from being mistaken for a valid reply. | | `logical_position` | `float64` | Yes | — | | `logical_valid` | `bool` | Yes | — | | `coordinate_counts` | `int64` | Yes | — | | `absolute_source_counts` | `int32` | Yes | — | | `independent_anchor_source` | [`IndependentAnchorSource`](#type-independentanchorsource) | No | Fresh independent raw acquisition; does not grant a command-frame binding or Home. | | `coordinate_valid` | `uint32` | Yes | — | | `coordinate_source_valid` | `uint32` | Yes | — | | `pdo_fresh` | `uint32` | Yes | — | | `native_home_position_offset` | `int32` | Yes | — | | `observed_revision_no` | `uint32` | Yes | — | | `revision_readback_valid` | `uint32` | Yes | — | | `serial_no` | `uint32` | Yes | — | | `serial_readback_valid` | `uint32` | Yes | — | | `anchor_period_counts` | `uint32` | Yes | — | | `anchor_tolerance_counts` | `uint64` | Yes | — | | `coordinate_reason` | `uint32` | Yes | — | | `coordinate_identity_sha256` | `string` | Yes | Canonical coordinate digest; empty means persisted anchors are unavailable. | | `coordinate_identity` | `string` | Yes | Canonical per-axis fields; empty means persisted anchors are unavailable. | | `anchor_identity_changed_field` | `string` | Yes | Differing identity component for anchor_identity_mismatch, or empty. | | `anchor_reason` | `string` | Yes | — | | `restored_anchor_home_epoch` | `uint64` | Yes | — | | `pos_counts` | `int32` | Yes | — | | `statusword` | `uint16` | Yes | — | | `error_code` | `uint16` | Yes | — | | `vendor_id` | `uint32` | Yes | — | | `product_code` | `uint32` | Yes | — | | `slave_position` | `uint16` | Yes | — | | `time_ns` | `uint64` | Yes | — | | `logical_target` | `float64` | Yes | — | | `readiness` | `string` | Yes | — | | `brake_state` | `uint32` | Yes | — | | `home_valid` | `bool` | Yes | — | | `logical_velocity` | `float64` | No | — | | `velocity_actual_counts_per_s` | `int32` | Yes | Drive 0x606C signed reference counts/s in the position frame, sampled each cycle. Diagnostics only; never a speed-safety certification or an execution guard. | | `following_error_counts` | `int32` | Yes | Drive 0x60F4 signed reference counts in the position frame; preferred input to the configured collision watchdog. | | `velocity_actual_valid` | `bool` | Yes | True only when velocity_actual is mapped and this cycle has fresh PDO feedback. An absent signal is zero with false validity. | | `following_error_valid` | `bool` | Yes | True only when following_error is mapped and this cycle has fresh PDO feedback. An absent signal is zero with false validity. | | `max_abs_velocity_actual_counts_per_s` | `uint32` | Yes | Maximum absolute valid drive velocity in counts/s since the last accepted Arm(true); zero before Arm. Retained through Stop/disarm; INT32_MIN has magnitude 2147483648. | | `max_abs_following_error_counts` | `uint32` | Yes | Maximum absolute valid drive following error in counts since the last accepted Arm(true); zero before Arm. Retained through Stop/disarm; INT32_MIN has magnitude 2147483648. | | `di_bits` | `uint32` | Yes | Current 0x60FD DI logic bits; interpret only when di_valid. | | `di_valid` | `bool` | Yes | True only for a fresh PDO and verified digital-input mapping. | | `external_enable_active` | `bool` | Yes | Drive servo-on (S-ON) input active; interpret only when external_enable_valid. Diagnostics only; does not prove STO or torque state. | | `external_enable_valid` | `bool` | Yes | True only with valid DI feedback and exactly one explicitly configured, verified S-ON digital-input function. | | `torque_raw` | `int32` | Yes | Drive 0x6077 raw per-mille of rated torque; use only with fresh PDO feedback. | | `collision_watchdog` | [`CollisionWatchdogStatus`](#type-collisionwatchdogstatus) | Yes | Native cycle evaluation and latched trip evidence for this axis. | | `calibration_valid` | `bool` | No | Persistent calibration applicability; null/absent means legacy unknown. | | `position_state` | `string` | No | Native state: unverified, recovering, trusted or lost; empty means legacy unknown. | | `position_reason` | `string` | No | Native CoordinateReason name; none means no coordinate refusal; absent means legacy unknown. | | `audit_status` | `string` | No | Last stationary audit: pending, verified or unavailable; never motion authority. | | `audit_reason` | `string` | No | Native SampleInvalidity name; none means no reported audit failure; absent means legacy unknown. | | `audit_last_verified_ns` | `uint64` | No | Monotonic acquisition completion of last accepted independent evidence; zero means never. | #### LogicalAxisStatus | Name | Type | Required | Description | |---|---|---|---| | `drive_alarm` | `string` | Yes | Drive display alarm code decoded from the drive's error-code objects; unresolved bus candidates joined by \|; empty when clear, unmapped, or another drive family. | | `drive_alarm_text` | `string` | Yes | Text meaning of drive_alarm; multiple candidates joined by \| in matching order. | | `drive_alarm_aux_code` | `uint32` | Yes | Raw auxiliary alarm word read from the drive. Retained for the current nonzero drive error event and configuration epoch, zero when invalid or cleared. Separate from manufacturer_error_code, which carries the cyclic manufacturer_err PDO. | | `drive_alarm_aux_valid` | `bool` | Yes | True only for an admitted auxiliary reply matching the current nonzero drive error event and configuration epoch; false when missing, stale, or cleared, preventing zero from being mistaken for a valid reply. | | `time_ns` | `uint64` | Yes | — | | `logical_position` | `float64` | Yes | — | | `logical_valid` | `bool` | Yes | — | | `logical_target` | `float64` | Yes | — | | `logical_velocity` | `float64` | No | — | | `readiness` | `string` | Yes | ready or first failing axis gate in this order: faulted, mode_mismatch, home_required, coordinate_invalid, not_enabled, not_operation_enabled, brake_wait; group authority still applies. | | `position_counts` | `int32` | Yes | — | | `statusword` | `uint16` | Yes | — | | `brake_state` | `uint32` | Yes | — | | `home_valid` | `bool` | Yes | — | | `velocity_actual_counts_per_s` | `int32` | Yes | Drive 0x606C signed reference counts/s in the position frame, sampled each cycle. Diagnostics only; never a speed-safety certification or an execution guard. | | `following_error_counts` | `int32` | Yes | Drive 0x60F4 signed reference counts in the position frame; preferred input to the configured collision watchdog. | | `velocity_actual_valid` | `bool` | Yes | True only when velocity_actual is mapped and this cycle has fresh PDO feedback. An absent signal is zero with false validity. | | `following_error_valid` | `bool` | Yes | True only when following_error is mapped and this cycle has fresh PDO feedback. An absent signal is zero with false validity. | | `max_abs_velocity_actual_counts_per_s` | `uint32` | Yes | Maximum absolute valid drive velocity in counts/s since the last accepted Arm(true); zero before Arm. Retained through Stop/disarm; INT32_MIN has magnitude 2147483648. | | `max_abs_following_error_counts` | `uint32` | Yes | Maximum absolute valid drive following error in counts since the last accepted Arm(true); zero before Arm. Retained through Stop/disarm; INT32_MIN has magnitude 2147483648. | | `di_bits` | `uint32` | Yes | Current 0x60FD DI logic bits; interpret only when di_valid. | | `di_valid` | `bool` | Yes | True only for a fresh PDO and verified digital-input mapping. | | `external_enable_active` | `bool` | Yes | Drive servo-on (S-ON) input active; interpret only when external_enable_valid. Diagnostics only; does not prove STO or torque state. | | `external_enable_valid` | `bool` | Yes | True only with valid DI feedback and exactly one explicitly configured, verified S-ON digital-input function. | | `torque_raw` | `int32` | Yes | Drive 0x6077 raw per-mille of rated torque; use only with fresh PDO feedback. | | `collision_watchdog` | [`CollisionWatchdogStatus`](#type-collisionwatchdogstatus) | Yes | Native cycle evaluation and latched trip evidence for this axis. | | `calibration_valid` | `bool` | No | Persistent calibration applicability; null/absent means legacy unknown. | | `position_state` | `string` | No | Native state: unverified, recovering, trusted or lost; empty means legacy unknown. | | `position_reason` | `string` | No | Native CoordinateReason name; none means no coordinate refusal; absent means legacy unknown. | | `audit_status` | `string` | No | Last stationary audit: pending, verified or unavailable; never motion authority. | | `audit_reason` | `string` | No | Native SampleInvalidity name; none means no reported audit failure; absent means legacy unknown. | | `audit_last_verified_ns` | `uint64` | No | Monotonic acquisition completion of last accepted independent evidence; zero means never. | #### StatusGenerations | Name | Type | Required | Description | |---|---|---|---| | `time_ns` | `uint64` | Yes | — | | `native` | `uint64` | Yes | — | | `configuration_epoch` | `uint64` | Yes | — | | `home_epoch` | `uint64` | Yes | — | | `execution` | `uint64` | Yes | Adapter handle execution generation; correlated native Start acknowledgement. | | `jog` | `uint64` | Yes | — | | `grant` | `uint64` | Yes | — | | `grant_time_ns` | `uint64` | Yes | Daemon fast-grant publication timestamp; zero if grant observation is unavailable. Independent of metrics time_ns. | | `jog_time_ns` | `uint64` | Yes | Daemon jog publication timestamp; zero if jog observation is unavailable. Independent of metrics time_ns. | #### AdapterStatus | Name | Type | Required | Description | |---|---|---|---| | `heap_inuse` | `uint64` | Yes | Go runtime bytes at request observation. | | `sys` | `uint64` | Yes | Go runtime bytes at request observation. | | `goroutines` | `uint32` | Yes | Go runtime goroutine count at request observation. | | `open_preparation_slots` | `uint32` | Yes | Available adapter preparation admissions, from the two atomic reservations. | #### CollisionWatchdogStatus | Name | Type | Required | Description | |---|---|---|---| | `armed` | `bool` | Yes | True only while enabled, armed and receiving valid feedback; false after a trip. | | `torque_cycles` | `uint32` | Yes | Consecutive torque breaches; equality resets. Frozen at trip. | | `following_error_cycles` | `uint32` | Yes | Consecutive following-error breaches independent of torque. Frozen at trip. | | `peak_torque_raw` | `uint32` | Yes | Peak absolute 0x6077 raw per-mille since arm; frozen at trip. | | `peak_following_error_counts` | `uint64` | Yes | Peak absolute counts since arm; drive 0x60F4 when valid, otherwise prior wire target minus feedback with configured wrap handling. | | `last_trip_quantity` | `string` | Yes | none, torque, following_error or torque_and_following_error; retained across explicit reset. | | `trip_sustained_cycles` | `uint32` | Yes | Configured consecutive count reached at last trip, 2..1000. | | `trip_count` | `uint64` | Yes | Monotonic per-axis trip sequence for the daemon incarnation. | | `samples` | `uint64` | Yes | Fresh normal armed/enabled cycles accumulated since arm, even if the watchdog is disabled; peaks include pre-motion hold. Frozen after trip or disarm. | | `last_trip_peak_torque_raw` | `uint32` | Yes | Peak absolute torque at last trip, retained across reset and rearm. | | `last_trip_peak_following_error_counts` | `uint64` | Yes | Peak absolute following error at last trip, retained across reset and rearm. | #### IndependentAnchorSource | Name | Type | Required | Description | |---|---|---|---| | `valid` | `bool` | Yes | Native acquisition validity; never motion permission. | | `encoder_counts` | `int64` | Yes | Independent signed raw encoder count. | | `completed_ns` | `uint64` | Yes | Host-monotonic acquisition completion timestamp in nanoseconds. | | `maximum_age_ns` | `uint64` | Yes | Configured acquisition freshness bound in nanoseconds. | ### Recovery #### RecoveryStatus | Name | Type | Required | Description | |---|---|---|---| | `home_valid_mask` | `uint32` | Yes | Native per-axis home evidence remaining after observed recovery. | | `faults` | [`FaultRecovery[]`](#type-faultrecovery) | Yes | Currently latched fault bits with axis masks and recovery policy. | | `reset_sequence` | `uint64` | Yes | Exact native command sequence of the observed reset, zero before any reset; response sequence must match for reset_fault. | | `reason` | `string` | Yes | Native recovery summary: empty when no fault persists, otherwise fault_persists; FaultResetRecovery refuses a persistent fault. | | `outcomes` | [`FaultRecovery[]`](#type-faultrecovery) | Yes | Per-bit outcomes of the correlated reset; never inferred from elapsed time. | #### FaultRecovery | Name | Type | Required | Description | |---|---|---|---| | `bit` | `uint32` | Yes | Native execution-fault bit index. | | `name` | `string` | Yes | Native fault name for that bit. | | `axis_mask` | `uint32` | Yes | Affected axis evidence mask. | | `recovery` | `string` | Yes | Native recovery-policy label for the fault bit. | | `outcome` | `string` | Yes | Native per-bit outcome: persists, cleared or rehome_required. | | `rehome_axis_mask` | `uint32` | Yes | Axes requiring qualified re-home after this outcome. | ### Events #### Event | Name | Type | Required | Description | |---|---|---|---| | `sequence` | `uint64` | Yes | Strictly increasing within adapter_incarnation; drop records use last_lost_sequence; no renumbering. | | `time_ns` | `uint64` | Yes | Native observation host time, except telemetry_mark uses adapter host monotonic time after native acceptance; never proof of motion or disk output. | | `type` | `string` | Yes | grant_acquired, grant_renewed, grant_released, grant_expired, grant_revoked, enable_changed, arm_changed, handle_transition, execution_started, execution_completed, execution_faulted, execution_aborted, jog_begin, jog_end, jog_expired, jog_ramping, jog_limited, fault_latched, fault_cleared, home_epoch_changed, daemon_incarnation_changed, adapter_incarnation_changed, publisher_overflow, events_dropped, telemetry_mark | | `daemon_incarnation` | `string` | Yes | — | | `adapter_incarnation` | `string` | Yes | — | | `grant` | [`GrantEvent`](#type-grantevent) | No | — | | `handle` | [`HandleTransitionEvent`](#type-handletransitionevent) | No | — | | `execution` | [`ExecutionEvent`](#type-executionevent) | No | — | | `jog` | [`JogEvent`](#type-jogevent) | No | — | | `fault` | [`FaultEvent`](#type-faultevent) | No | — | | `enable` | [`EnableEvent`](#type-enableevent) | No | — | | `epoch` | [`EpochEvent`](#type-epochevent) | No | — | | `incarnation` | [`IncarnationEvent`](#type-incarnationevent) | No | — | | `overflow` | [`PublisherOverflowEvent`](#type-publisheroverflowevent) | No | — | | `events_dropped` | [`EventsDropped`](#type-eventsdropped) | No | — | | `mark` | [`TelemetryMarkEvent`](#type-telemetrymarkevent) | No | Present for telemetry_mark only, once per accepted mark command; rejected commands produce no mark event. | #### EventBatch | Name | Type | Required | Description | |---|---|---|---| | `events` | [`Event[]`](#type-event) | Yes | At most 64 records, including at most one leading events_dropped record; 256 retained events. | | `next_sequence` | `uint64` | Yes | Resume cursor after the last returned event; unchanged when empty. | | `latest_sequence` | `uint64` | Yes | Newest retained sequence at batch capture. | | `adapter_incarnation` | `string` | Yes | — | #### GrantEvent | Name | Type | Required | Description | |---|---|---|---| | `generation` | `uint64` | Yes | — | | `stopping` | `bool` | Yes | True for a local keepalive while Stop drains; does not confer native authority. | #### HandleTransitionEvent | Name | Type | Required | Description | |---|---|---|---| | `handle` | `uint64` | Yes | — | | `from` | `string` | Yes | — | | `to` | `string` | Yes | — | | `execution_generation` | `uint64` | Yes | — | | `native_sequence` | `uint64` | Yes | — | | `identity` | [`Identity`](#type-identity) | No | Immutable raw trajectory identity supplied at Prepare and checked at Start; echoed unchanged through terminal observation. | | `generation` | `uint64` | No | Application grant generation that owns this immutable preparation. | #### ExecutionEvent | Name | Type | Required | Description | |---|---|---|---| | `handle` | `uint64` | Yes | — | | `generation` | `uint64` | Yes | — | | `native_sequence` | `uint64` | Yes | — | | `fault_bits` | `uint32` | Yes | — | | `recovery` | [`FaultRecovery[]`](#type-faultrecovery) | Yes | — | | `identity` | [`Identity`](#type-identity) | No | Immutable raw trajectory identity supplied at Prepare and checked at Start; echoed unchanged through terminal observation. | #### JogEvent | Name | Type | Required | Description | |---|---|---|---| | `generation` | `uint64` | Yes | — | | `reason` | `uint32` | Yes | — | | `axis_mask` | `uint32` | Yes | — | | `velocity_scale_ppm` | `uint32` | Yes | — | #### FaultEvent | Name | Type | Required | Description | |---|---|---|---| | `bit` | `uint32` | Yes | — | | `axis_mask` | `uint32` | Yes | — | | `recovery_class` | `string` | Yes | — | #### EnableEvent | Name | Type | Required | Description | |---|---|---|---| | `enabled_mask` | `uint32` | Yes | — | | `armed` | `bool` | Yes | — | #### EpochEvent | Name | Type | Required | Description | |---|---|---|---| | `previous` | `uint64` | Yes | — | | `current` | `uint64` | Yes | — | #### IncarnationEvent | Name | Type | Required | Description | |---|---|---|---| | `previous` | `string` | Yes | — | | `current` | `string` | Yes | — | #### PublisherOverflowEvent | Name | Type | Required | Description | |---|---|---|---| | `previous_drops` | `uint64` | Yes | — | | `total_drops` | `uint64` | Yes | — | #### EventsDropped | Name | Type | Required | Description | |---|---|---|---| | `first_lost_sequence` | `uint64` | Yes | Inclusive first missing event. | | `last_lost_sequence` | `uint64` | Yes | Inclusive last missing event; synthetic drop event sequence equals this cursor, so retained sequences are never renumbered. | #### TelemetryMarkEvent | Name | Type | Required | Description | |---|---|---|---| | `label` | `string` | Yes | Accepted mark label, 1..128 UTF-8 bytes; receipt of queued dump work, not proof of disk output. | | `native_sequence` | `uint64` | Yes | Accepted mark command sequence returned by POST /v1/control; distinct from event and telemetry sample cursors. | | `generation` | `uint64` | Yes | Authorizing application grant generation; session capability is never published. | ### Telemetry #### TelemetryBatch A binary transport type: it is never encoded as JSON. The fields below are the decoded header plus the record body. | Name | Type | Required | Description | |---|---|---|---| | `daemon_incarnation` | `string` | Yes | 32 lowercase hex bytes identifying the daemon owning these sequences; reconnect must reconcile changes. | | `adapter_incarnation` | `string` | Yes | 32 lowercase hex bytes identifying the adapter incarnation. | | `machine_sha256` | `string` | Yes | 64 lowercase hex machine digest bytes. | | `deployment_sha256` | `string` | Yes | 64 lowercase hex deployment digest bytes. | | `cycle_period_ns` | `uint64` | Yes | Cycle period in ns. | | `axis_count` | `uint32` | Yes | Active axes, 1 through 16. | | `record_layout_digest` | `string` | Yes | 64 ASCII hex bytes from the generated transitive CycleCaptureRecordV2 layout digest. | | `first_sequence` | `uint64` | Yes | First included sequence; zero when empty. | | `last_sequence` | `uint64` | Yes | Last included sequence or after+dropped when empty; resume cursor. | | `dropped` | `uint64` | Yes | Exact number of records lost after the requested cursor and before this batch. | | `record_count` | `uint32` | Yes | Number of complete records, at most min(ring capacity,4096). | | `records` | `bytes` | Yes | Opaque binary body; never encoded as JSON or base64. Each CycleCaptureAxisV2 appends velocity_actual_counts_per_s (i32), following_error_counts (i32), velocity_actual_valid (u32 0/1), following_error_valid (u32 0/1). Drive position-frame counts/s and counts; following error also feeds the configured collision watchdog. No speed-safety certification. | ### Cell I/O #### IoState | Name | Type | Required | Description | |---|---|---|---| | `time_ns` | `uint64` | Yes | — | | `cycle` | `uint64` | Yes | — | | `armed` | `bool` | Yes | — | | `reason` | `uint32` | Yes | — | | `inputs` | [`IoInput[]`](#type-ioinput) | Yes | — | | `outputs` | [`IoOutput[]`](#type-iooutput) | Yes | — | #### IoInput | Name | Type | Required | Description | |---|---|---|---| | `name` | `string` | Yes | — | | `value` | `bool` | Yes | — | | `valid` | `bool` | Yes | — | | `observed_ns` | `uint64` | Yes | — | | `bit` | `uint32` | Yes | — | | `fast` | `bool` | Yes | — | #### IoOutput | Name | Type | Required | Description | |---|---|---|---| | `name` | `string` | Yes | — | | `intent` | `bool` | Yes | — | | `commanded` | `bool` | Yes | — | | `readback` | `bool` | Yes | — | | `valid` | `bool` | Yes | — | | `expiry_ns` | `uint64` | Yes | — | | `observed_ns` | `uint64` | Yes | — | | `changed_ns` | `uint64` | Yes | — | | `marker_sample_ns` | `uint64` | Yes | — | | `marker_cycle` | `uint64` | Yes | — | | `bit` | `uint32` | Yes | — | | `torch` | `bool` | Yes | — | ## Related pages - [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority): leases, fences and the jog lane. - [Events and telemetry](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry): SSE events, cursors and the binary telemetry layout. - [Remote access (mTLS)](https://advancedmetalresearch.com/docs/apis/remote-access-mtls): the remote listener, PKI and WSS jog. - [Go SDK](https://advancedmetalresearch.com/docs/apis/go-sdk), [C++ client](https://advancedmetalresearch.com/docs/apis/cpp-client) and [TypeScript contracts](https://advancedmetalresearch.com/docs/apis/typescript-types). - [Error codes and fault states](https://advancedmetalresearch.com/docs/reference/error-codes). ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/protocol/application-v1.schema.json:3 (capabilities)` - `rt-core/protocol/application-v1.schema.json:421 (reasons)` - `rt-core/protocol/application-v1.schema.json:1037 (rules)` - `rt-core/protocol/application-v1.schema.json:1115 (types)` - `rt-core/protocol/control.json (local_jog_update)` - `rt-core/adapters/rosie/control/http.go:52-283` - `rt-core/adapters/rosie/control/admission.go:19-217` - `rt-core/adapters/rosie/control/idempotence.go:12-111` - `rt-core/adapters/rosie/control/controller.go:546-1649` - `rt-core/adapters/rosie/control/handles.go:122-207` - `rt-core/adapters/rosie/control/halt_linux.go:25-79` - `rt-core/adapters/rosie/control/reset_linux.go:22-154` - `rt-core/adapters/rosie/control/cell_io_linux.go:66-88` - `rt-core/adapters/rosie/control/telemetry_linux.go:18-47` - `rt-core/adapters/rosie/control/jog_linux.go:26-396` - `rt-core/adapters/rosie/control/jog_ws.go:103-393` - `rt-core/adapters/rosie/control/events.go:334-421` - `rt-core/adapters/rosie/control/telemetry_publication.go:104-216` - `rt-core/adapters/rosie/control/resources.go:358-373` - `rt-core/adapters/rosie/control/remote_listener.go:60-145` - `rt-core/adapters/rosie/control/link_timing.go:8` - `rt-core/cmd/rt-control/main.go:25-39` - `rt-core/host/rosie-rt-core.service:24-26` - `rt-core/sdk/control/operations.go:14-157` - `rt-core/clients/cpp/include/rosie/rt_control_client.hpp:588-906` --- # Events and telemetry streams > How to follow rt-control's event log over polling or Server-Sent Events, and how to read the full-rate binary telemetry stream, with cursors, loss reporting and the exact record layout. URL: https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry Section: RosieOS docs / APIs Last updated: 2026-10-10 rt-control publishes two observation streams. Neither needs a lease, so any client that can reach the socket can watch the cell while another client controls it. - **Events** are a JSON log of state changes: grants, enable and arm, handle and execution transitions, jog sessions, faults, Home epochs and restarts. They are good for reacting to changes. - **Telemetry** is binary: one record per control cycle with positions, targets, drive state and fault masks. It is good for plotting, logging and replay. Both use the same cursor model: you pass `after`, the last sequence you handled, and the server tells you exactly what you missed. ## Follow the event stream **curl** ```bash # Server-Sent Events from the beginning of the retained log; Ctrl-C to stop. curl -sN --unix-socket /run/rosie-rt-core/control.sock \ 'http://localhost/v1/events/stream?after=0' ``` **Go** ```go c, err := control.Dial("/run/rosie-rt-core/control.sock") if err != nil { log.Fatal(err) } defer c.Close() var cursor uint64 err = c.StreamEvents(ctx, cursor, func(e control.Event) error { if e.Type == "events_dropped" { // History was lost: re-read Status before trusting your local view. log.Printf("lost events %d..%d", e.Dropped.FirstLostSequence, e.Dropped.LastLostSequence) } log.Printf("%d %s", e.Sequence, e.Type) cursor = e.Sequence return nil }) ``` SSE output: ```text : rt-core events id: 1 event: adapter_incarnation_changed data: {"sequence":1,"time_ns":81230000000000,"type":"adapter_incarnation_changed","daemon_incarnation":"3b9d…","adapter_incarnation":"a41c…","incarnation":{"previous":"","current":"a41c…"}} id: 2 event: grant_acquired data: {"sequence":2,"time_ns":81234567890123,"type":"grant_acquired","daemon_incarnation":"3b9d…","adapter_incarnation":"a41c…","grant":{"generation":1,"stopping":false}} ``` ## Events **Endpoint: `GET /v1/events?after=`** One batch of up to 64 events after the cursor, in the normal `Response` envelope with an `EventBatch` in `data`. **Endpoint: `SSE /v1/events/stream?after=`** The same events as Server-Sent Events. Each message is `id` (the sequence), `event` (the type) and `data` (one `Event` as JSON, with no envelope). ### Cursors and loss - `after` is the sequence of the last event you handled. Omit it (or send 0) to start at the oldest retained event. It must be one unsigned decimal number, otherwise you get 409 `invalid_event_cursor`. A cursor newer than the log gets 409 `event_cursor_ahead`. - Sequences increase by one within an `adapter_incarnation`. A poll returns `next_sequence` (use it as your next `after`) and `latest_sequence`. - The log keeps the last **256** events. If your cursor is older than that, the first record you receive is a synthetic `events_dropped` event whose `first_lost_sequence`..`last_lost_sequence` range (inclusive) is what you missed. Its own `sequence` equals `last_lost_sequence`, so it is also your resume cursor. The retained events that follow keep their original sequence numbers. - The SSE handler does not read `Last-Event-ID`. To resume, reconnect with `?after=` set to the last `id` you handled. - When rt-control restarts, the sequence starts again under a new `adapter_incarnation`. Discard your cursor, read Status and start from 0. Events are observations, not a lossless trace. `time_ns` is the core's publication time, and native snapshots can merge transitions that happen between publications. After any loss or incarnation change, reconcile from `GET /v1/status` rather than replaying motion. ### Event types Every event has `sequence`, `time_ns`, `type`, `daemon_incarnation` and `adapter_incarnation`, plus one payload field for its type: | Type | Payload field | Payload type | Emitted when | |---|---|---|---| | `grant_acquired` | `grant` | [`GrantEvent`](/docs/apis/rt-control-http#type-grantevent) | `acquire` succeeded. | | `grant_renewed` | `grant` | [`GrantEvent`](/docs/apis/rt-control-http#type-grantevent) | `renew` succeeded. `stopping: true` while a Stop drains. | | `grant_released` | `grant` | [`GrantEvent`](/docs/apis/rt-control-http#type-grantevent) | `release`. | | `grant_expired` | `grant` | [`GrantEvent`](/docs/apis/rt-control-http#type-grantevent) | The lease ran out. | | `grant_revoked` | `grant` | [`GrantEvent`](/docs/apis/rt-control-http#type-grantevent) | `stop`, or authority lost to a native or transport failure. | | `enable_changed` | `enable` | [`EnableEvent`](/docs/apis/rt-control-http#type-enableevent) | `axis_enable_mask` changed. | | `arm_changed` | `enable` | [`EnableEvent`](/docs/apis/rt-control-http#type-enableevent) | `armed` changed. | | `handle_transition` | `handle` | [`HandleTransitionEvent`](/docs/apis/rt-control-http#type-handletransitionevent) | A trajectory handle changed state (`from` → `to`). | | `execution_started` | `execution` | [`ExecutionEvent`](/docs/apis/rt-control-http#type-executionevent) | A handle started. | | `execution_completed` | `execution` | [`ExecutionEvent`](/docs/apis/rt-control-http#type-executionevent) | A started handle was consumed: it completed, or a later execution replaced it. | | `execution_aborted` | `execution` | [`ExecutionEvent`](/docs/apis/rt-control-http#type-executionevent) | A started handle was retired with no fault bits set. | | `execution_faulted` | `execution` | [`ExecutionEvent`](/docs/apis/rt-control-http#type-executionevent) | A started handle was retired with fault bits set; `fault_bits` and `recovery` say which. | | `jog_begin` | `jog` | [`JogEvent`](/docs/apis/rt-control-http#type-jogevent) | A jog session opened. | | `jog_end` | `jog` | [`JogEvent`](/docs/apis/rt-control-http#type-jogevent) | A jog session closed for a reason other than expiry. | | `jog_expired` | `jog` | [`JogEvent`](/docs/apis/rt-control-http#type-jogevent) | Jog input expired. | | `jog_ramping` | `jog` | [`JogEvent`](/docs/apis/rt-control-http#type-jogevent) | The jog is ramping down. | | `jog_limited` | `jog` | [`JogEvent`](/docs/apis/rt-control-http#type-jogevent) | The jog is being slowed at a position limit (an accepted state, not a failure). | | `fault_latched` | `fault` | [`FaultEvent`](/docs/apis/rt-control-http#type-faultevent) | An execution fault bit was set. | | `fault_cleared` | `fault` | [`FaultEvent`](/docs/apis/rt-control-http#type-faultevent) | An execution fault bit was cleared. | | `home_epoch_changed` | `epoch` | [`EpochEvent`](/docs/apis/rt-control-http#type-epochevent) | Home evidence changed. | | `daemon_incarnation_changed` | `incarnation` | [`IncarnationEvent`](/docs/apis/rt-control-http#type-incarnationevent) | The core process changed (and once at adapter start). | | `adapter_incarnation_changed` | `incarnation` | [`IncarnationEvent`](/docs/apis/rt-control-http#type-incarnationevent) | Emitted once when rt-control starts. | | `publisher_overflow` | `overflow` | [`PublisherOverflowEvent`](/docs/apis/rt-control-http#type-publisheroverflowevent) | Native observations were dropped before they reached the event log. | | `events_dropped` | `events_dropped` | [`EventsDropped`](/docs/apis/rt-control-http#type-eventsdropped) | Your cursor fell behind the 256-event ring; the lost range is inclusive. | | `telemetry_mark` | `mark` | [`TelemetryMarkEvent`](/docs/apis/rt-control-http#type-telemetrymarkevent) | `mark_telemetry` was accepted. | > [!NOTE] In `FaultEvent`, `bit` is the bit's **value** (for example 32 for bit 5), not its index. In `RecoveryStatus.faults[]`, `bit` is the index. The bits are listed in [Execution fault bits](https://advancedmetalresearch.com/docs/reference/error-codes#fault-bits). Events never contain the session token. The field tables for every payload type are in the [types section](https://advancedmetalresearch.com/docs/apis/rt-control-http#types-events) of the HTTP reference. ## Telemetry **Endpoint: `GET /v1/telemetry?after=`** One binary batch after the cursor. Sends gzip when you ask for it with `Accept-Encoding: gzip`. **Endpoint: `GET /v1/telemetry/stream?after=`** Concatenated binary batches over chunked HTTP, one complete batch per flush, about every 20 ms. Telemetry is `application/octet-stream`, never JSON or base64. A batch is a 312-byte `TelemetryBatchHeaderV1` followed by exactly `record_count` records of 5392 bytes (`CycleCaptureRecordV2`), all little-endian. A batch is at most 312 + 4096 × 5392 = 22085944 bytes. Go: follow telemetry: ```go err := c.TelemetryStream(ctx, 0, func(b control.TelemetryBatch) error { if b.Header.Dropped != 0 { log.Printf("lost %d records", b.Header.Dropped) } for _, r := range b.Records { j1 := r.Ax[0] _ = j1.Position // drive counts; convert with Describe counts_per_unit and sign } return nil }) ``` ### Cursors and loss - `after` is the last record sequence you consumed. 0 (the default) starts at sequence 1, and history that the ring has already overwritten is reported in `dropped`. - A non-empty batch starts at `after + dropped + 1` and its sequences are contiguous. An empty batch has `first_sequence` 0 and `last_sequence` equal to `after + dropped`. Always resume from `last_sequence`, after you have handled the batch and its loss. - Errors come back as JSON with HTTP 409: `invalid_telemetry_cursor`, `telemetry_cursor_ahead`, `telemetry_unavailable`, or `telemetry_busy` with `Retry-After: 1` (retry the same cursor). On an established stream, a busy read becomes an empty batch with the cursor unchanged. - A stream closes when the core disconnects or a write blocks for 5 s. HTTP proxies may re-chunk the stream, so find batch boundaries from the header, not from chunks. - If `adapter_incarnation` or `daemon_incarnation` changes, stop. Re-read Describe and Status before you reset the cursor. Telemetry needs no session, but on the remote listener it still needs a valid client certificate. ### Batch header `TelemetryBatchHeaderV1`, 312 bytes. Check `magic`, `version`, `header_bytes`, `record_bytes`, the layout digest, `axis_count` (1..16) and `record_count` (≤ 4096) before reading the body. | Field | Offset | Size | Type | Notes | |---|---|---|---|---| | `magic` | 0 | 4 | `u32` | `0x31425452` (`RTB1`). | | `version` | 4 | 2 | `u16` | 1. | | `header_bytes` | 6 | 2 | `u16` | 312. | | `adapter_incarnation` | 8 | 32 | `u8[32]` | Lowercase hex, rt-control process identity. | | `machine_sha256` | 40 | 64 | `u8[64]` | Lowercase hex machine digest. | | `deployment_sha256` | 104 | 64 | `u8[64]` | Lowercase hex deployment digest. | | `record_layout_digest` | 168 | 64 | `u8[64]` | Must equal `CycleCaptureRecordV2LayoutDigest`. | | `cycle_period_ns` | 232 | 8 | `u64` | Cycle period, ns. | | `first_sequence` | 240 | 8 | `u64` | First record's sequence; 0 when the batch is empty. | | `last_sequence` | 248 | 8 | `u64` | Last record's sequence, or `after + dropped` when empty. Your next cursor. | | `dropped` | 256 | 8 | `u64` | Records lost after your cursor and before this batch. | | `axis_count` | 264 | 4 | `u32` | Active axes, 1..16. | | `record_count` | 268 | 4 | `u32` | Records in this batch, at most min(ring capacity, 4096). | | `record_bytes` | 272 | 4 | `u32` | 5392. | | `reserved` | 276 | 4 | `u32` | Reserved, zero. | | `daemon_incarnation` | 280 | 32 | `u8[32]` | Lowercase hex, core process identity; owns the sequence numbers. | The current record layout digest is `1173439681672348f35f9652f2a4821d251638c70b60362ff59b37032a794b6b`. A reader that sees a different digest must refuse the batch: the Go, C++ and TypeScript decoders all raise `TelemetryLayoutMismatchError` with both digests. ### Cycle record `CycleCaptureRecordV2`, 5392 bytes: 52 group fields followed by `ax`, 16 per-axis entries. Positions and targets are in drive counts, not radians; convert them with the axis's `counts_per_unit` and `sign` from Describe. | Field | Offset | Size | Type | Notes | |---|---|---|---|---| | `t_ns` | 0 | 8 | `u64` | Cycle time, host monotonic ns. | | `seq` | 8 | 8 | `u64` | Record sequence. | | `cycle` | 16 | 8 | `u64` | Cycle counter. | | `work_ns` | 24 | 8 | `u64` | Cycle work time, ns. | | `axes` | 32 | 4 | `u32` | Active axis count. | | `reserved` | 36 | 4 | `u32` | Reserved, zero. | | `cycle_jitter_ns` | 40 | 8 | `i64` | Wake time minus scheduled time, ns (signed). | | `active_traj_id` | 48 | 8 | `u64` | | | `active_command_seq` | 56 | 8 | `u64` | | | `jog_generation` | 64 | 8 | `u64` | | | `app_grant_generation` | 72 | 8 | `u64` | | | `control_generation` | 80 | 8 | `u64` | | | `configuration_epoch` | 88 | 8 | `u64` | | | `home_epoch` | 96 | 8 | `u64` | | | `grant_deadline_host_ns` | 104 | 8 | `u64` | | | `jog_input_deadline_host_ns` | 112 | 8 | `u64` | | | `jog_source_sequence` | 120 | 8 | `u64` | | | `bus_submission_return_code` | 128 | 8 | `i64` | | | `armed` | 136 | 4 | `u32` | 1 when armed. | | `axis_enable_mask` | 140 | 4 | `u32` | Enabled axes. | | `active_mode` | 144 | 4 | `u32` | 0 idle, 2 trajectory, 3 jog. | | `state` | 148 | 4 | `u32` | | | `current_point_index` | 152 | 4 | `u32` | | | `queue_depth` | 156 | 4 | `u32` | | | `last_event_code` | 160 | 4 | `u32` | | | `underrun_count` | 164 | 4 | `u32` | | | `stale_command_flag` | 168 | 4 | `u32` | | | `motion_done` | 172 | 4 | `u32` | | | `capability_flags` | 176 | 4 | `u32` | | | `jog_session_open` | 180 | 4 | `u32` | 1 while jog input is accepted. | | `jog_has_input` | 184 | 4 | `u32` | | | `jog_axis_mask` | 188 | 4 | `u32` | | | `jog_ramping` | 192 | 4 | `u32` | | | `grant_active` | 196 | 4 | `u32` | 1 while the effective grant is active. | | `grant_axis_mask` | 200 | 4 | `u32` | | | `safety_fault_mask` | 204 | 4 | `u32` | Non-zero blocks motion. | | `execution_fault_reasons` | 208 | 4 | `u32` | Latched fault bits. | | `submitted_axis_mask` | 212 | 4 | `u32` | | | `bus_submission_operation` | 216 | 4 | `u32` | | | `pdo_fresh_axis_mask` | 220 | 4 | `u32` | Axes with fresh feedback this cycle. | | `config_verified_mask` | 224 | 4 | `u32` | | | `home_valid_mask` | 228 | 4 | `u32` | Axes with valid Home. | | `service_mode_axis_mask` | 232 | 4 | `u32` | | | `wkc_actual` | 236 | 4 | `u32` | | | `wkc_expected` | 240 | 4 | `u32` | | | `master_state` | 244 | 4 | `u32` | | | `io_configured` | 248 | 4 | `u32` | | | `io_input_word` | 252 | 4 | `u32` | | | `io_output_word` | 256 | 4 | `u32` | | | `io_input_valid` | 260 | 4 | `u32` | | | `io_armed` | 264 | 4 | `u32` | | | `io_reason` | 268 | 4 | `u32` | | | `ax` | 272 | 5120 | `CycleCaptureAxisV2[16]` | One entry per axis; unused axes are zero. | ### Axis entry `CycleCaptureAxisV2`, 320 bytes, 16 per record. Check `pdo_fresh` and each field's validity flag before you use a value; invalid values are not meaningful, and `di_valid = 0` means unavailable, not "inputs off". | Field | Offset | Size | Type | Notes | |---|---|---|---|---| | `position` | 0 | 4 | `i32` | Feedback position, drive counts. | | `target` | 4 | 4 | `i32` | Commanded target, drive counts. | | `statusword` | 8 | 2 | `u16` | CiA402 statusword. | | `error_code` | 10 | 2 | `u16` | CiA402 error code. | | `manufacturer_error_code` | 12 | 4 | `u32` | | | `absolute_value` | 16 | 64 | `i32[16]` | | | `absolute_valid` | 80 | 16 | `u8[16]` | | | `velocity_actual_counts_per_s` | 96 | 4 | `i32` | Actual velocity, counts/s; valid only with `velocity_actual_valid`. | | `following_error_counts` | 100 | 4 | `i32` | Following error, counts; valid only with `following_error_valid`. | | `velocity_actual_valid` | 104 | 4 | `u32` | | | `following_error_valid` | 108 | 4 | `u32` | | | `di_bits` | 112 | 4 | `u32` | | | `di_valid` | 116 | 4 | `u32` | | | `external_enable_active` | 120 | 4 | `u32` | | | `external_enable_valid` | 124 | 4 | `u32` | | | `coordinate_counts` | 128 | 8 | `i64` | | | `coordinate_epoch` | 136 | 8 | `u64` | | | `pdo_observed_time_ns` | 144 | 8 | `u64` | | | `collision_peak_following_error_counts` | 152 | 8 | `u64` | | | `collision_trip_count` | 160 | 8 | `u64` | | | `collision_samples` | 168 | 8 | `u64` | | | `collision_last_trip_peak_following_error_counts` | 176 | 8 | `u64` | | | `absolute_source_counts` | 184 | 4 | `i32` | | | `coordinate_reason` | 188 | 4 | `u32` | | | `target_velocity_counts_per_s` | 192 | 4 | `i32` | | | `collision_armed` | 196 | 4 | `u32` | | | `collision_torque_cycles` | 200 | 4 | `u32` | | | `collision_following_error_cycles` | 204 | 4 | `u32` | | | `collision_peak_torque_raw` | 208 | 4 | `u32` | | | `collision_last_trip_quantity` | 212 | 4 | `u32` | | | `collision_trip_sustained_cycles` | 216 | 4 | `u32` | | | `collision_last_trip_peak_torque_raw` | 220 | 4 | `u32` | | | `brake_state` | 224 | 4 | `u32` | | | `native_home_position_offset` | 228 | 4 | `i32` | | | `native_home_last_abort_code` | 232 | 4 | `u32` | | | `ext_position_error_counts` | 236 | 4 | `i32` | | | `ext_multi_turn_lo` | 240 | 4 | `i32` | | | `ext_multi_turn_hi` | 244 | 4 | `i32` | | | `max_abs_velocity_actual_counts_per_s` | 248 | 4 | `u32` | | | `max_abs_following_error_counts` | 252 | 4 | `u32` | | | `torque_raw` | 256 | 2 | `i16` | Torque, signed per-mille of rated; valid only with `torque_valid`. | | `ext_bus_voltage_raw` | 258 | 2 | `u16` | | | `ext_load_rate_raw` | 260 | 2 | `u16` | | | `ext_igbt_temp_raw` | 262 | 2 | `i16` | | | `ext_motor_temp_raw` | 264 | 2 | `i16` | | | `ext_drive_not_ready_bits` | 266 | 2 | `u16` | | | `ext_motor_not_rotating_code` | 268 | 2 | `u16` | | | `ext_valid_mask` | 270 | 2 | `u16` | Bits 0..8 qualify the `ext_*` fields; ignore any field whose bit is clear. | | `mode_display` | 272 | 1 | `i8` | Drive mode (8 = CSP). | | `ds402_state` | 273 | 1 | `u8` | DS402 state code (see the real-time core page). | | `torque_valid` | 274 | 1 | `u8` | | | `coordinate_valid` | 275 | 1 | `u8` | 1 when the coordinate is trustworthy. | | `coordinate_source_valid` | 276 | 1 | `u8` | | | `native_home_state` | 277 | 1 | `u8` | | | `slave_al_state` | 278 | 1 | `u8` | | | `slave_online` | 279 | 1 | `u8` | | | `slave_operational` | 280 | 1 | `u8` | | | `pdo_fresh` | 281 | 1 | `u8` | 1 when this axis's feedback is fresh. | | `target_velocity_valid` | 282 | 1 | `u8` | | | `ext_valid` | 283 | 1 | `u8` | | | `calibration_valid` | 284 | 1 | `u8` | | | `position_state` | 285 | 1 | `u8` | | | `audit_reason` | 286 | 2 | `u16` | | | `audit_last_verified_ns` | 288 | 8 | `u64` | | | `audit_started_ns` | 296 | 8 | `u64` | | | `audit_completed_ns` | 304 | 8 | `u64` | | | `audit_coherence_error_counts` | 312 | 8 | `u64` | | Decoders for these layouts are generated from `protocol/control.json`: Go in `rosieos/rt-core/ipcclient` (used by `Client.TelemetryBatches` and `TelemetryStream`), C++ in `protocol_generated.hpp` (used by `RtControlClient::telemetry`), and TypeScript in [`rt_protocol_generated.ts`](/docs/apis/typescript-types#telemetry-decoders). ## Resources Describe's `robot.resources` lists the compiled robot description files (manifest, URDF, meshes, calibration) with their SHA-256 digests. Fetch one with [`GET /v1/resources/`](/docs/apis/rt-control-http#resource). The bytes are immutable, and an unknown digest returns 404 `resource_unknown`. ## Related pages - [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http): `subscribe_events`, `telemetry`, `mark_telemetry` and `resource`. - [The real-time core](https://advancedmetalresearch.com/docs/concepts/real-time-core#telemetry): where the telemetry ring comes from. - [TypeScript contracts](https://advancedmetalresearch.com/docs/apis/typescript-types): decoding telemetry in the browser or Node. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/adapters/rosie/control/events.go:17-421` - `rt-core/adapters/rosie/control/telemetry_publication.go:18-216` - `rt-core/adapters/rosie/control/remote_listener.go:88-104` - `rt-core/protocol/control.json (native_records TelemetryBatchHeaderV1, CycleCaptureRecordV2, CycleCaptureAxisV2)` - `rt-core/protocol/application-v1.schema.json (rules.events` - `rules.telemetry, types Event, EventBatch)` - `rt-core/clients/ts/rt_protocol_generated.ts:3` - `rt-core/sdk/control/events.go:19-80` - `rt-core/sdk/control/telemetry.go:52-121` - `rt-core/ipcclient/telemetry_batch.go:18-21` --- # Remote access (mTLS) and remote jog > How to expose rt-control to other machines over mutual TLS 1.3, provision the component CA and client certificates, connect from Go, C++ or curl, and jog over the WebSocket lane. URL: https://advancedmetalresearch.com/docs/apis/remote-access-mtls Section: RosieOS docs / APIs Last updated: 2026-10-10 By default rt-control listens only on a local Unix socket. To control a cell from another machine (a Steam Deck pendant, or an offline programming server on a workstation), you enable its **remote listener**: HTTPS with mutual TLS 1.3, where both sides present certificates from the cell's own component CA. The remote listener serves the same [HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http) as the local socket, plus a WebSocket lane for jogging. Read Describe over mutual TLS: ```bash curl --cacert ca.pem --cert clients/pendant-1.pem --key clients/pendant-1-key.pem \ https://rosie.local:8443/v1/describe ``` > [!WARNING] A remote client can do everything a local client can, including energising the drives and jogging. The hardware E-stop is the only emergency stop, and RosieOS has no software E-stop. Keep the E-stop within reach of whoever operates the robot, and read the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). ## How the listener authenticates - **TLS 1.3 only, client certificate required.** rt-control verifies the client against one configured CA certificate, which must be a self-signed CA. The client certificate must be issued directly by that CA; intermediate CAs are refused. - **Identity comes from URI SANs.** A client certificate must carry exactly one `rosie-principal:` and exactly one `rosie-pair:` URI, both opaque tokens (`[A-Za-z0-9_.:-]+`). The pair must equal rt-control's `--pair-id`. The principal becomes the owner of any session that client acquires, so no other principal, and no local client, can use that session (`session_principal_mismatch`). - **The CRL is checked on every handshake.** A revoked serial, a CRL that the CA did not sign, a CRL with an unsupported critical extension, or a CRL outside its validity window all refuse the handshake. - **Unauthenticated traffic never reaches the API.** A failed handshake gets a TLS alert or a closed connection. Even a plaintext request, such as a Stop sent without TLS, receives no HTTP response. - **Files are reloaded before each full handshake** when their modification time changes. A reload that fails refuses new handshakes; it never falls back to the old credentials. Connections that are already open are not revoked, and TLS session tickets are disabled. ## Enable the listener rt-control takes these flags. All four files are required when `--remote-listen` is set, and rt-control exits with status 2 if any check fails at startup. | Flag | Default | Description | |---|---|---| | `--remote-listen` | empty (disabled) | `host:port` to listen on. | | `--remote-ca` | — | PEM file with exactly one self-signed component CA certificate. | | `--remote-cert` | — | Server certificate PEM. | | `--remote-key` | — | Server private key PEM. | | `--remote-crl` | — | CRL PEM, signed by the CA. Checked on every handshake. | | `--pair-id` | — | Deployment pair; client certificates must carry the same `rosie-pair`. | | `--remote-jog-max-uncertainty-ns` | 0 | Largest allowed clock-offset interval for remote jog, ns. | | `--remote-jog-drift-ppb` | 0 | Remote clock drift bound, parts per billion. | | `--remote-jog-calibration-max-age-ns` | 0 | Oldest usable jog clock calibration, ns. | The three `--remote-jog-*` values must all be non-zero for remote jog to work. With the defaults, every remote jog frame is refused with `jog_clock_unqualified`. They describe your measured network and clocks, and the code marks them as unverified; there are no recommended values. On an installed cell host, `host/install.sh --remote-pki ` writes these flags into `/etc/rosie-rt-core/control.env`, pointing at `/etc/rosie-rt-core/pki/{ca.pem,server.pem,server-key.pem,crl.pem}`. The listen address comes from `ROSIE_RT_REMOTE_LISTEN` and defaults to `127.0.0.1:8443`: loopback only, so you must choose a host interface explicitly to expose it. The cell configuration template records the same address in `nodes[].rt_core.remote_listen`. Installing a cell host is covered in [Install rt-core on a cell host](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host). The remote listener's HTTP timeouts follow the cell's link profile: 2 s to read request headers on the LAN defaults, 10 s otherwise, 30 s to read a request, 65 s to write a reply, and the Describe `control_idle_timeout_ns` (90 s) for idle connections. ## Provision certificates `rt-core/host/remote-pki.sh` owns one CA per deployment pair. Run it as root on a host where the `rosie-ctl` group exists. Every subcommand takes a PKI directory and an identity token. ```bash # Create the CA, the server certificate and the first CRL. The identity is the pair ID. sudo ROSIE_RT_REMOTE_SERVER_SAN='DNS:rosie.local,DNS:localhost,IP:127.0.0.1' \ rt-core/host/remote-pki.sh init /var/lib/rosie-rt-pki/cell-a cell-a # Issue a client certificate for one principal. sudo rt-core/host/remote-pki.sh issue-client /var/lib/rosie-rt-pki/cell-a pendant-1 # Revoke it. The CRL is regenerated. sudo rt-core/host/remote-pki.sh revoke /var/lib/rosie-rt-pki/cell-a pendant-1 ``` > [!NOTE] Keep the CA directory separate from `/etc/rosie-rt-core/pki/`. The installer copies the CA and server files from the directory you pass to `--remote-pki` into `/etc/rosie-rt-core/pki/`, so passing the same directory makes it copy files onto themselves. See [Install on a cell host](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host). | Subcommand | What it creates | |---|---| | `init ` | A new directory (it refuses to overwrite one) with an EC P-256 CA valid for 3650 days, a server certificate valid for 365 days, and a CRL. The server certificate's names come from `ROSIE_RT_REMOTE_SERVER_SAN`, which defaults to `DNS:localhost,IP:127.0.0.1,IP:::1`; add the host name your clients will use. | | `issue-client ` | `clients/.pem` and `clients/-key.pem`, valid for 365 days, with `URI:rosie-principal:` and `URI:rosie-pair:`. Each principal can be issued once. | | `revoke ` | Revokes that client certificate. | Every subcommand ends by regenerating `crl.pem`. The CRL is valid for 7 days, and rt-control refuses every handshake once it has expired (`remote CRL not current`). The script has no separate refresh command, so plan to regenerate the CRL before it expires. Keys and certificates are published `root:rosie-ctl` mode 0640, and the CA key is mode 0600. Copy `ca.pem` and the client's certificate and key to the client machine. Treat the client key as a credential: anyone who holds it can control the cell. ## Connect a client **Go** ```go import ( "crypto/tls" "crypto/x509" "os" "rosieos/rt-core/sdk/control" ) caPEM, _ := os.ReadFile("ca.pem") roots := x509.NewCertPool() roots.AppendCertsFromPEM(caPEM) cert, err := tls.LoadX509KeyPair("clients/pendant-1.pem", "clients/pendant-1-key.pem") if err != nil { log.Fatal(err) } c, err := control.DialTLS("https://rosie.local:8443", &tls.Config{ RootCAs: roots, Certificates: []tls.Certificate{cert}, }) ``` **C++** ```cpp #include using namespace rosie::rt_control; // The header-only client does no TLS itself. Supply a factory that returns a // fresh mutual-TLS HttpStream per connection, verifying the server certificate // and host name and presenting the client certificate and key. StreamFactory tls_factory = [](Deadline d) -> std::unique_ptr { return open_my_mtls_stream("rosie.local", 8443, d); // your TLS implementation }; RtControlClient client(tls_factory); auto description = client.describe(); ``` **curl** ```bash curl --cacert ca.pem --cert clients/pendant-1.pem --key clients/pendant-1-key.pem \ https://rosie.local:8443/v1/status ``` `control.DialTLS` refuses a config without `RootCAs` or a client certificate, and one with `InsecureSkipVerify`. It forces TLS 1.3 and HTTP/1.1, and the address must be a bare HTTPS origin. In C++, `open_my_mtls_stream` stands for your own TLS code: the stream must honour the absolute deadline on every read and write and never replay bytes. See [C++ client](https://advancedmetalresearch.com/docs/apis/cpp-client#remote-transport). ## Remote jog over WebSocket On the remote listener, `GET /v1/jog` is always a WebSocket upgrade (on the local socket the same path is the [`jog_status`](/docs/apis/rt-control-http#jog-status) read). The WebSocket carries the same binary `local_jog_update` frames as the local `jog.sock` lane, after a clock calibration that maps the pendant's clock onto the cell's. > [!WARNING] Remote jog moves the robot from another machine over a network. The input deadline, the idle timeout and the lease stop motion when the link stalls, but none of them is an emergency stop. **Endpoint: `GET /v1/jog?session=&generation=&jog_generation=`** Upgrades the mutual-TLS connection to the jog WebSocket. | Query parameter | Type | Required | Description | |---|---|---|---| | `session` | `string` | Yes | Current session, owned by this client's principal. | | `generation` | `uint64` | Yes | Current grant generation, decimal. | | `jog_generation` | `uint64` | Yes | 0 to set up the transport and begin over the socket (the normal path), or an existing jog generation. | The request needs `Connection: Upgrade`, `Upgrade: websocket`, `Sec-WebSocket-Version: 13` and a base64 `Sec-WebSocket-Key` of 16 bytes. Success is HTTP 101. Failures before the upgrade are `text/plain`: 400 `invalid websocket upgrade`, 403 `session_principal_mismatch`, 409 for a stale session or generation (`jog_session_stale`, `control_session_stale`), and 503 `independent_jog_unavailable` while the listener shuts down. Only one remote jog connection is active at a time: opening a second one closes both with code 1008. ### The exchange With `jog_generation=0`, the client and rt-control exchange text frames, then switch to binary updates: ```text client → {"type":"calibrate","source_incarnation":"<32 hex>","source_ns":S1,"seq":1} server ← {"type":"calibrated","seq":1,"source_ns":S1,"host_rx_ns":R1,"host_tx_ns":T1} client → {"type":"calibrate","source_incarnation":"<32 hex>","source_ns":S2,"seq":2,"host_tx_ns":T1} server ← {"type":"calibrated","seq":2,"source_ns":S2,"host_rx_ns":R2,"host_tx_ns":T2} client → {"type":"begin","seq":3,"axis_mask":1,"source_ns":S3,"host_tx_ns":T2} server ← {"type":"begun","seq":3,"jog_generation":4} client → server ← {"type":"rejected","seq":N,"reason":"…"} (only on refusal) ``` 1. **Calibrate twice.** `source_incarnation` identifies your monotonic clock (16 non-zero bytes, hex-encoded) and must stay the same. Take the second source sample *after* you receive the first reply, and echo that reply's `host_tx_ns`. This gives rt-control a causal bound on the offset between your clock and the cell's. A single one-way timestamp never qualifies. You can send more calibrations later to refresh the mapping. 2. **Begin.** Send `begin` with `seq: 3`, the axis mask, a fresh `source_ns` and the second reply's `host_tx_ns`. rt-control calls `begin_jog` for you and replies `begun` with the new jog generation. Setup does not use up the first input's allowance. 3. **Stream updates.** Send binary frames built with the SDK, carrying source-clock times. rt-control maps each input's capture time conservatively (the earliest possible host time, including drift) and never subtracts measured latency. Input captured before the Begin sample is refused. The receiver closes the stream when no complete frame arrives within the cell's input-age ceiling (250 ms on LAN), answering `jog_stream_idle` first and ending the jog. During setup the idle limit is 5 s. When the grant is stopped or expires, it answers `control_session_stale` and closes. Neither the Go nor the C++ producer sends keepalives, so send updates at a steady cadence. Frames are limited to 4096 bytes and must not be fragmented. Ping and pong are answered. Protocol violations close the connection with a WebSocket close code (1002, 1007 or 1009); the [WebSocket reasons](https://advancedmetalresearch.com/docs/reference/error-codes#reasons-websocket) list them. Jog refusals, which keep the connection open, are listed under [`update_jog`](/docs/apis/rt-control-http#update-jog). ### With the client libraries **Go** ```go // After Acquire, StartRenewal, Enable, Arm and observed readiness: jog, err := c.NewRemoteJogSession(ctx, 1, 100_000_000, nil) // mask, 100 ms input lifetime, CLOCK_MONOTONIC if err != nil { log.Fatal(err) // e.g. independent_jog_unavailable, jog_clock_* reasons } v := make([]float64, len(d.Axes)) v[0] = 0.05 // rad/s on J1 if err := jog.Update(v, ipcclient.HostMonotonicNS()); err != nil { log.Print(err) } _ = jog.End(ctx) // ends the jog generation; call Release separately ``` **C++** ```cpp #include // client was built with a mutual-TLS StreamFactory and holds a renewed grant. RtJogRemoteProducer jog(client, "rosie.local", /*mask*/ 1, /*input_duration_ns*/ 100000000); std::vector v(axis_count, 0.0); v[0] = 0.05; // rad/s if (!jog.update(v, host_monotonic_ns())) { /* another writer held the lock: drop this sample */ } jog.end(); ``` Both constructors perform the calibration and Begin. The input lifetime may not exceed the cell's input-age ceiling. `Rejection` / `rejection()` read typed refusals, and `Observe` / `observe()` return the receiver's jog observation. ## Related pages - [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http) - [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority#the-jog-lane): the jog lane's deadlines and generations. - [Error codes: remote TLS](https://advancedmetalresearch.com/docs/reference/error-codes#reasons-tls) and [WebSocket](https://advancedmetalresearch.com/docs/reference/error-codes#reasons-websocket) reasons. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/cmd/rt-control/main.go:32-39,43-50,99-120,155-180,199-213` - `rt-core/adapters/rosie/control/remote_tls.go:17-160` - `rt-core/adapters/rosie/control/remote_listener.go:24-145` - `rt-core/adapters/rosie/control/jog_ws.go:33-393` - `rt-core/adapters/rosie/control/websocket.go:20-183` - `rt-core/host/remote-pki.sh:1-107` - `rt-core/host/generate-control-env.sh:25-26,65-67` - `rt-core/host/install.sh:20-33,127-164` - `rt-core/config/templates/cell.json:17` - `rt-core/protocol/application-v1.schema.json (rules.remote_jog_upgrade)` - `rt-core/sdk/control/client.go:90-111` - `rt-core/sdk/control/jog_remote.go:30,90-114,313-377` - `rt-core/clients/cpp/include/rosie/rt_jog_remote_producer.hpp:176-270` - `rt-core/clients/cpp/include/rosie/rt_control_client.hpp:80-90,524` --- # Go SDK > The Go client for rt-control. Dial the local socket or mutual TLS, acquire and renew authority, jog, upload and start programs, follow events and telemetry, and handle typed refusals. URL: https://advancedmetalresearch.com/docs/apis/go-sdk Section: RosieOS docs / APIs Last updated: 2026-10-10 Package `rosieos/rt-core/sdk/control` is the Go client for [rt-control](https://advancedmetalresearch.com/docs/apis/rt-control-http). It is what the offline programming server and the v4 pendant tooling use. It keeps separate connections for lifecycle, urgent (Stop, Release), bulk upload and renewal traffic, so a large upload can never delay a Stop or a renewal. It fills in the fence and a `request_id` on every command, and it returns refusals as a typed `*control.Rejected`. The package builds on Linux only (`//go:build linux`). > [!WARNING] The example below enables, arms and jogs the robot. Run it against the simulated core (`rosie-rt-core-sim`) first. On real hardware, keep the hardware E-stop within reach: it is the only emergency stop, and RosieOS has no software E-stop. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). ## Example: acquire, jog, stop, release This program is the simulation example from the rt-core README. It acquires, renews in the background, enables and arms all axes, waits for every drive to reach Operation Enabled, jogs axis 0 at 0.01 rad/s for one second, and then stops and releases. Start the simulated core and rt-control first (see [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation)), then set `TMPDIR` to the directory holding `control.sock` and `SHA` to the compiled configuration digest. jog.go: ```go package main import ( "context" "fmt" "os" "time" "rosieos/rt-core/ipcclient" "rosieos/rt-core/sdk/control" ) func must[T any](v T, err error) T { if err != nil { panic(err) }; return v } func check(err error) { if err != nil { panic(err) } } func main() { // Simulation inputs: nine axes, 0.01 rad/s, 1 s of input at a 10 ms cadence, // 100 ms input lifetime, 10 s overall budget. const mask, velocity, duration, cadence, age, budget = 511, 0.01, time.Second, 10 * time.Millisecond, 100 * time.Millisecond, 10 * time.Second ctx, cancel := context.WithTimeout(context.Background(), budget) defer cancel() c := must(control.Dial(control.UnixPath(os.Getenv("TMPDIR") + "/control.sock"))) defer c.Close() d := must(c.Describe(ctx)) if d.Backend != "simulation" || d.ConfigurationSHA256 != os.Getenv("SHA") { panic("simulation identity mismatch") } g := must(c.Acquire(ctx, "readme", control.Binding{PairID: "readme", Revision: 1, ConfigurationSHA256: d.ConfigurationSHA256})) released := false defer func() { if !released { c.Stop(context.Background()) c.Release(context.Background()) } }() fmt.Printf("acquire generation=%d\n", g.Generation) renewals := must(c.StartRenewal(ctx, 0)) fmt.Printf("enable sequence=%d\n", must(c.Enable(ctx, mask)).Sequence) fmt.Printf("arm sequence=%d\n", must(c.Arm(ctx)).Sequence) tick := time.NewTicker(cadence) defer tick.Stop() for !ipcclient.AllOperationEnabled(must(c.Status(ctx)).Core) { select { case <-tick.C: case <-ctx.Done(): panic(ctx.Err()) } } before := must(c.Status(ctx)).Core.Axes[0].PositionCounts jog := must(c.PrepareJogSession(ctx, mask)) velocities := make([]float64, len(d.Axes)) velocities[0] = velocity for end := time.Now().Add(duration); time.Now().Before(end); { origin := ipcclient.HostMonotonicNS() check(jog.UpdateAt(velocities, origin, origin+uint64(age))) select { case r, ok := <-renewals: if !ok { panic("renewal ended") } check(r.Err) case <-tick.C: case <-ctx.Done(): panic(ctx.Err()) } } check(jog.End(ctx)) delta := must(c.Status(ctx)).Core.Axes[0].PositionCounts - before fmt.Printf("jog requested_ns=%d delta_counts=%d\n", duration, delta) fmt.Printf("stop generation=%d\n", must(c.Stop(ctx)).Generation) fmt.Printf("release session_empty=%t\n", must(c.Release(ctx)).Session == "") released = true } ``` Output recorded in the rt-core README: ```text acquire generation=1 enable sequence=1 arm sequence=2 jog requested_ns=1000000000 delta_counts=207 stop generation=2 release session_empty=true ``` `sdk/control/example_motion_server_test.go` is a longer runnable example: it Homes, uploads an `.rdt` program, starts it and observes completion against the simulator. `make test-go` runs it. ## Add the module The module path is `rosieos/rt-core`, which is not a fetchable URL. Point a `replace` directive at your checkout of the repository, as the offline programming server does: go.mod: ```text require rosieos/rt-core v0.0.0 replace rosieos/rt-core => ../RosieOS/rt-core ``` The module declares `go 1.24` with toolchain `go1.26.2`. ## Connect | Function | Use | |---|---| | `control.Dial(path UnixPath) (*Client, error)` | The local socket, for example `control.Dial("/run/rosie-rt-core/control.sock")`. Local jog sessions dial `jog.sock` in the same directory. | | `control.DialTLS(addr string, config *tls.Config) (*Client, error)` | The remote listener. `config` must have `RootCAs` and a client certificate, and must not set `InsecureSkipVerify`. TLS 1.3 is forced. See [Remote access](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#connect-a-client). | | `(*Client).Close() error` | Closes connections and stops renewal. It does **not** release authority: call `Release` first. | Neither constructor connects: the first call does, bounded by its context. ## Authority | Method | Returns | Notes | |---|---|---| | `Acquire(ctx, controller string, binding Binding)` | `Grant, error` | Measures Describe if needed and requests a lease sized to the round trip. | | `AcquireLease(ctx, controller, binding, requested time.Duration)` | `Grant, error` | As Acquire, with a minimum requested lease. It never goes below the measured requirement. | | `Renew(ctx)` | `Grant, error` | One renewal. | | `StartRenewal(ctx, interval time.Duration)` | `<-chan Renewal, error` | Starts one background renewer. `interval` 0 means a third of the lease, and larger values are refused. It refuses to start (`control_session_stale`) when the lease cannot cover three round trips plus the interval. The channel holds only the latest `Renewal{Grant, Err, StartedHostMonotonicNS}`; any error ends renewal. | | `StopRenewal()` | — | Stops and joins the renewer. It does not release. | | `Stop(ctx)` | `Grant, error` | Uses the urgent connection. The reply carries the new generation. | | `Release(ctx)` | `Grant, error` | Stops renewal, then releases. Never replayed automatically. | | `Grant()`, `Fence()` | `Grant`, `Fence` | The client's current authority. | | `SetFence(f Fence)` | — | Adopts a fence obtained elsewhere, before concurrent use. | | `Timing()` | `LeaseTiming` | Measured round trip, effective lease, renewal interval and call budget. | | `PrepareAcquisition(ctx)` | `AcquisitionSnapshot, error` | Warms remote connections and reads Status and events before `Acquire`. Grants nothing. | `TimingForLease(lease, roundTrip)` and `RequestedLease(roundTrip, ceiling)` are the helpers behind the lease sizing. See [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority#lease-timing). ## Commands Each method sends one [rt-control operation](https://advancedmetalresearch.com/docs/apis/rt-control-http) with the current fence and a fresh `request_id`. | Method | Operation | Result | |---|---|---| | `Enable(ctx, mask uint32)` | `enable` | `Response` (`.Sequence`) | | `Arm(ctx)` | `arm` | `Response` | | `Home(ctx, mask uint32)` | `home` | `Response` | | `RestoreAnchor(ctx, mask uint32)` | `restore_anchor` | `RecoveryStatus` | | `ResetFault(ctx, mask uint32)` | `reset_fault` | `Response`; the client also forgets the retired fence | | `IOArm(ctx)`, `IODisarm(ctx)` | `io_arm`, `io_disarm` | `Response` | | `PrepareTrajectory(ctx, mask uint32, points []Point)` | `prepare_trajectory` | `Response` (`.Handle`) | | `StartTrajectory(ctx, handle uint64)` | `start_trajectory` | `Response` | | `DiscardTrajectory(ctx, handle uint64)` | `discard_trajectory` | `Response` | | `PrepareProgram(ctx, blob []byte)` | `prepare_program` | `Program` | | `StartProgram(ctx, identity Identity)` | `start_program` | `Response` | | `BeginJog(ctx, mask, clock string, originNS, deadlineNS uint64)` | `begin_jog` | `Response` (`.Handle` is the jog generation) | | `EndJog(ctx, generation uint64)` | `end_jog` | `Response` | | `Jog(ctx, mask, velocity []float64, timeoutMS uint32)` | `jog` (test only) | `Response` | There are no convenience methods for `halt`, `recovery_status` or `mark_telemetry`. Send them with `Command`: ```go resp, err := c.Command(ctx, &control.Request{Operation: "halt"}) ``` `Command(ctx, *Request)` fills in `schema`, the fence and a random `request_id` the first time, and writes them back into the request. To retry a lost reply, call `Command` again with **the same** `*Request` and a fresh context; the adapter then replays the first outcome instead of running the command twice. Never share one request between goroutines or change it between attempts. `NewRequestID()` returns a random 32-character hex ID if you want to set your own. `PrepareProgram` sends the `.rdt` bytes with the session and generation headers. Uploads are not deduplicated and never retried: if one fails uncertainly, inspect Describe and Status, or Stop, before uploading again. ## Jog | API | Use | |---|---| | `PrepareJogSession(ctx, mask)` | Begin a local jog session without an input sample; the first input's allowance is the cell's input-age ceiling. | | `NewJogSession(ctx, mask, deadlineNS)`, `NewJogSessionAt(ctx, mask, originNS, deadlineNS)` | Begin with a first input captured now or at `originNS`. | | `(*JogSession).UpdateAt(velocities, originNS, deadlineNS)`, `Update(velocities, deadlineNS)` | Send one datagram. Capture times must increase. | | `(*JogSession).End(ctx)` | Close the socket, then end the jog generation. | | `(*JogSession).Generation()` | The jog generation. | | `NewRemoteJogSession(ctx, mask, durationNS, sourceNow)` | The WebSocket lane on a `DialTLS` client; see [remote jog](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#remote-jog-over-websocket). | | `BuildJogFrame(...)` | Encode one 224-byte `local_jog_update` frame yourself. | Times are host `CLOCK_MONOTONIC` nanoseconds; `ipcclient.HostMonotonicNS()` reads that clock. A deadline may be at most the cell's `max_jog_input_age_ns` after its origin. `Update` never blocks: when the socket is congested it returns `ErrJogWouldBlock`, and you should drop that sample and send a fresh one. Local datagrams get no reply, so read refusals from `JogIngress`. ## Observe | Method | Returns | |---|---| | `Describe(ctx)` | `Description`. Also records the round trip and the cell's lease and jog ceilings. | | `Status(ctx)` | `Status` (the schema's `ProcessStatus`) | | `JogClock(ctx)`, `JogStatus(ctx)`, `JogIngress(ctx)` | `JogClock`, `JogObservation`, `JogIngressObservation` | | `Events(after)`, `EventsContext(ctx, after)` | `EventBatch`, validated for order and loss | | `StreamEvents(ctx, after, func(Event) error)` | Runs until the context ends or the handler returns an error | | `TelemetryBatches(ctx, after)` | One `TelemetryBatch` (`Header`, `Records`) | | `TelemetryStream(ctx, after, func(TelemetryBatch) error)` | Follows the stream; returns on an incarnation change | | `TelemetryPeek(ctx, after)` | The header and first record only | | `Resource(ctx, sha)` | `io.ReadCloser`, returned only after the size and ETag match the digest | See [Events and telemetry](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry) for cursor handling. ## Errors ```go g, err := c.Acquire(ctx, "my-app", binding) var rejected *control.Rejected switch { case errors.As(err, &rejected): // A 409 refusal. rejected.Reason is the reason code; NativeResult, // NativeJogResult and ResponseData carry any structured evidence. log.Printf("refused: %s", rejected.Reason) case errors.Is(err, control.ErrConnectionUnsent): // The request never left this process. Safe to retry. case errors.Is(err, control.ErrTransport): // The outcome is unknown. Stop, then reconcile Status before acquiring again. case err != nil: log.Fatal(err) } _ = g ``` | Error | Meaning | |---|---| | `*Rejected` | HTTP 409. `Reason` is a [reason code](https://advancedmetalresearch.com/docs/reference/error-codes). `NativeResult` and `NativeJogResult` are the native receipts; `ResponseData` keeps `data` (for example `limit_violation`) without losing integer precision. | | `*LeaseTimingRejected` | The lease cannot cover the measured round trip. Wraps a `*Rejected` with reason `control_session_stale` and reports `RoundTrip` and `Ceiling`. | | `ErrTransport` | The connection failed. The outcome may be unknown. | | `ErrConnectionUnsent` | The request was not sent. | | `ErrRequestTimeout` | The request budget expired (joined with `context.DeadlineExceeded`). | | `ErrProtocol` | A malformed reply, or an unexpected status such as 413 or 404. | | `ErrClosed` | The client is closed. | | `ErrJogWouldBlock` | A jog datagram was not sent. Replace it with fresh input. | The client retries a request at most once, after a closed or stale connection, reusing the same `request_id`. It never retries a `release` or an `.rdt` upload whose outcome is uncertain. ## Types The package re-exports the contract types, so you rarely need the adapter package directly: `Request`, `Fence`, `Binding`, `Grant`, `Identity`, `Program`, `Execution`, `HandleRecord`, `ProcessMarker`, `Description`, `RecoveryStatus`, `CapabilityInfo`, `Point`, `JogClock`, `JogObservation`, `JogIngressObservation`, `JogIngressRefusal`, `Response` (the raw envelope, with `Data` as `json.RawMessage`), `Status`, `Event`, `EventBatch`, `EventsDropped`, `TelemetryBatch`, `ResourceInfo`, `RobotDescription`, `DriveDescription` and `DriveIdentity`. Their fields are in the [types reference](https://advancedmetalresearch.com/docs/apis/rt-control-http#types). The capability states are exported as `CapabilityStateImplemented`, `CapabilityStateInterim`, `CapabilityStateTestOnly` and `CapabilityStateUnimplemented`. The reason codes are constants in the generated adapter package `rosieos/rt-core/adapters/rosie/control`, for example `ReasonControlSessionStale`. ## Related pages - [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http) - [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority) - [C++ client](https://advancedmetalresearch.com/docs/apis/cpp-client) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/go.mod:1-5` - `rt-core/sdk/control/client.go:27-112,182-280,329-440,444-500` - `rt-core/sdk/control/operations.go:14-157` - `rt-core/sdk/control/renewal.go:13-110` - `rt-core/sdk/control/timing.go:14-90` - `rt-core/sdk/control/jog.go:18-190` - `rt-core/sdk/control/jog_remote.go:30-114` - `rt-core/sdk/control/events.go:19-80` - `rt-core/sdk/control/telemetry.go:17-121` - `rt-core/sdk/control/resources.go:17-40` - `rt-core/sdk/control/types.go:13-48` - `rt-core/sdk/control/reopen.go:22` - `rt-core/sdk/control/request_deadline.go:14` - `rt-core/sdk/control/warm.go:253-280` - `rt-core/sdk/control/example_motion_server_test.go:1-60` - `rt-core/ipcclient/client_linux.go:1166` - `rt-core/ipcclient/application_grant_linux.go:69` - `offline-programming/v1/go.mod:10,21 (replace directive); example and output: rt-core/README.md:198-276 (narrative, recorded run)` --- # C++ client > The header-only C++17 client for rt-control, used by the motion servers. Connect, acquire and renew, upload and start programs, jog locally or over WebSocket, and handle the exception types. URL: https://advancedmetalresearch.com/docs/apis/cpp-client Section: RosieOS docs / APIs Last updated: 2026-10-10 `rosie::rt_control::RtControlClient` is a header-only C++17 client for [rt-control](https://advancedmetalresearch.com/docs/apis/rt-control-http). The Cartesian motion server and the dense trajectory daemon are built on it. It uses the generated contract types from `rt_control_api_generated.hpp`, keeps separate persistent connections for lifecycle, urgent, bulk and renewal traffic, and reports refusals as a `Rejected` exception carrying the reason code. The headers are in `rt-core/clients/cpp/include/rosie/`: | Header | Contents | |---|---| | `rt_control_client.hpp` | `RtControlClient`, the exception types, `HttpStream` and `StreamFactory`. | | `rt_control_api_generated.hpp` | The generated contract types (`Grant`, `Binding`, `Description`, `ProcessStatus` …) and reason constants. | | `rt_jog_producer.hpp` | `RtJogProducer`: local jog over `jog.sock`. | | `rt_jog_remote_producer.hpp` | `RtJogRemoteProducer`: remote jog over WebSocket. | > [!WARNING] The example below enables and arms the drives and starts a program. Run it against the simulated core first. On real hardware, keep the hardware E-stop within reach: it is the only emergency stop, and RosieOS has no software E-stop. Only planned weld programs pass the planner's collision and limit check; see the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). ## Example: run one program `clients/cpp/examples/rt_execute.cpp` acquires, renews every 50 ms, enables and arms every axis, waits for readiness, uploads an `.rdt` program, starts it, waits for completion and releases. This is its core, shortened: rt_execute.cpp (abridged): ```cpp #include "rosie/rt_control_client.hpp" using namespace rosie::rt_control; int main(int argc, char** argv) { // argv: CONTROL_SOCKET PAIR_ID PAIR_REVISION CONFIGURATION_SHA256 PROGRAM.rdt RtControlClient client(argv[1]); try { Binding binding; binding.PairID = argv[2]; binding.Revision = std::stoull(argv[3]); binding.ConfigurationSHA256 = argv[4]; client.acquire("motion-server", binding); client.start_renewal(std::chrono::milliseconds(50)); const auto count = client.describe().Description.Axes.size(); const std::uint32_t mask = (1U << count) - 1; client.enable(mask); client.arm(); // ... poll client.status() until Core.Armed == 1, every axis is Operation Enabled // and Core.SafetyFaultMask == 0 ... std::string bytes = read_file(argv[5]); // the immutable .rdt blob Program program = client.prepare_program(bytes); auto before = client.status(); client.start_program(program.Identity); // ... poll client.status() until Execution.State == "completed" for // Execution.Generation == before.Execution.Generation + 1 ... client.release(); return 0; } catch (const Rejected& e) { std::cerr << e.reason << '\n'; // a reason code, e.g. not_ready client.stop_renewal(); try { client.stop(); client.release(); } catch (...) {} return 2; } catch (const std::exception& e) { // TransportError and friends: outcome unknown std::cerr << e.what() << '\n'; client.stop_renewal(); try { client.stop(); client.release(); } catch (...) {} return 3; } } ``` Build and run the full example against a running rt-control: ```bash make -C rt-core clients rt-core/build/rt-execute /run/rosie-rt-core/control.sock cell-a 1 "$CONFIGURATION_SHA256" program.rdt ``` It prints the final status JSON. The exit code is 0 on completion, 2 for a rejection (it prints the reason) and 3 for a transport or observation failure. On failure it attempts Stop and Release. Two more examples sit beside it: `motion_server_sequence.cpp` (Home, program upload and completion) and `remote_jog_sequence.cpp` (remote jog). ## Build Compile with C++17, `-Irt-core/clients/cpp/include` and `-pthread`. The jog producers and the telemetry decoders also need the generated frame codec, so add `-Irt-core/include`. The client includes `protocol_generated.hpp` by a relative path, so if you copy the client headers elsewhere, copy `rt-core/include/protocol_generated.hpp` with them. ## Connect | Constructor | Use | |---|---| | `RtControlClient(const std::string& socket)` | The local Unix socket, for example `/run/rosie-rt-core/control.sock`. | | `RtControlClient(StreamFactory factory)` | Any other transport, in practice mutual TLS; see [below](https://advancedmetalresearch.com/docs/apis/cpp-client#remote-transport). | The client is not copyable. Join your other calls before destroying it; the destructor stops renewal but does not release authority. Every call takes an optional absolute `Deadline` (a `std::chrono::steady_clock::time_point`). The default is 5 s from the call, and it bounds the call including its retry. ## Methods | Method | Operation | Returns | |---|---|---| | `describe(d)` | `describe` | `Description`; also records the round trip and the cell's lease and jog ceilings | | `status(d)` | `status` | `ProcessStatus` | | `jog_clock(d)`, `jog_status(d)`, `jog_ingress(d)` | `jog_clock`, `jog_status`, `jog_ingress` | `LocalJogClock`, `JogObservation`, `JogIngressObservation` | | `acquire(controller, binding, d)` | `acquire` | `Grant`; sizes the lease to the measured round trip, and remembers the fence | | `renew(d)` | `renew` | `Grant` | | `stop(d)` | `stop` | `Grant` (urgent connection) | | `release(d)` | `release` | `Grant`; stops renewal first | | `enable(mask, d)`, `arm(d)`, `home(mask, d)` | `enable`, `arm`, `home` | `Response` | | `restore_anchor(mask, d)` | `restore_anchor` | `RecoveryStatus` | | `reset_fault(mask, d)`, `recovery_status(d)` | `reset_fault`, `recovery_status` | `Response`, `RecoveryStatus` | | `io_arm(d)`, `io_disarm(d)` | `io_arm`, `io_disarm` | `Response` | | `begin_jog(mask, clock, origin_ns, deadline_ns, d)`, `end_jog(generation, d)` | `begin_jog`, `end_jog` | `Response` | | `jog(mask, velocity, timeout_ms, d)` | `jog` (test only) | `Response` | | `prepare_trajectory(mask, points, d)` | `prepare_trajectory` | `Response` (`Handle`) | | `start_trajectory(handle, d)`, `discard_trajectory(handle, d)` | `start_trajectory`, `discard_trajectory` | `Response` | | `prepare_program(bytes, d)` | `prepare_program` | `Program`; takes the `.rdt` as a binary `std::string`, NUL bytes included | | `start_program(identity, d)` | `start_program` | `Response` | | `events(after, d)` | `subscribe_events` | `EventBatch` | | `events_stream(after, callback, stop, d)` | `subscribe_events` (SSE) | Calls `callback` per `Event` until `stop()` returns true | | `telemetry(after, d)` | `telemetry` | `TelemetryPublication` (`Header`, `Records`) | | `telemetry_stream(after, callback, stop, d)` | `telemetry` (stream) | Calls `callback` per batch until `stop()` returns true | | `command(request, d, stop)` | any JSON operation | `Response` | There are no named methods for `halt`, `mark_telemetry` or `resource`; send `halt` and `mark_telemetry` with `command()`. ### Retries and request IDs Every mutation carries a random `request_id`. To retry a lost reply, keep the `Request` object and pass it to `command()` again without changing its ID, fence or body: the adapter replays the original outcome. The client itself retries once with the same request. `release()` and `.rdt` uploads are never replayed automatically. An uncertain upload must not lead to a Start: inspect state or Stop first. ### Renewal `start_renewal(interval)` starts a background thread that renews at `interval` (0 means a third of the lease; anything larger is refused). It throws `Rejected` with `control_session_stale` when the lease cannot cover three round trips plus the interval. Poll `take_renewal()`, which returns the latest `Renewal{grant, error}` if there is one. A grant with `Stopping` set is a successful renewal during a Stop. Any error ends renewal, and the thread never reacquires authority. `stop_renewal()` joins the thread. `start_connection_keepalive()` keeps idle connections open at a third of the Describe `control_idle_timeout_ns`. ## Exceptions | Exception | Base | Meaning | |---|---|---| | `Rejected` | `std::runtime_error` | HTTP 409. `reason` is the [reason code](https://advancedmetalresearch.com/docs/reference/error-codes); `native_result` and `native_jog_result` are optional native receipts. | | `LeaseTimingRejected` | `Rejected` | The lease cannot cover the measured round trip. Carries `round_trip` and `ceiling`. | | `EventCursorLost` | `Rejected` | An event stream reported `events_dropped`. `event` is the loss record; resume with `after = event.Sequence` after reconciling Status. | | `TransportError` | `std::runtime_error` | The connection failed; the outcome may be unknown. | | `ProtocolError` | `TransportError` | A malformed or unexpected reply. | | `TelemetryLayoutMismatchError` | `ProtocolError` | A telemetry batch uses another record layout. Carries `stored_digest` and `expected_digest`. | | `DeadlineExceeded` | `TransportError` | The call's deadline passed; the outcome may be unknown. | | `ConnectionFailure` | `TransportError` | The connection closed. `sent` says whether any request bytes were transmitted. | Catch `Rejected` first for refusals, then `TransportError` for everything whose outcome is uncertain. After an uncertain outcome, stop producing motion, attempt `stop()`, and reconcile `status()` before acquiring again. ## Jog `RtJogProducer` jogs over the local `jog.sock`: ```cpp #include "rosie/rt_jog_producer.hpp" // After acquire(), start_renewal(), enable(), arm() and observed readiness: auto deadline = host_monotonic_ns() + 100000000; // 100 ms input lifetime RtJogProducer jog(client, "/run/rosie-rt-core/jog.sock", /*mask*/ 1, deadline); std::vector v(axis_count, 0.0); v[0] = 0.05; // rad/s on axis 0 if (!jog.update(v, host_monotonic_ns() + 100000000)) { // Congested: this sample was not sent. Drop it and send a fresh one. } jog.end(); // ends the jog generation ``` The constructor reads `/v1/jog/clock` and calls `begin_jog`. `update()` returns false when the datagram could not be sent; discard that input and sample again. Stop cancels the producer, so never reuse it after a Stop. For remote jog, use `RtJogRemoteProducer` on a client built with a mutual-TLS factory; see [remote jog](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#remote-jog-over-websocket). ## Remote transport The header-only client does no TLS itself. To reach the [remote listener](https://advancedmetalresearch.com/docs/apis/remote-access-mtls), construct the client with a `StreamFactory`: a function that takes a `Deadline` and returns a new `std::unique_ptr` connected over mutual TLS. `HttpStream` has two methods, `read(data, size, deadline)` and `write(data, size, deadline)`. Your factory must return an independent stream for every call, verify the server certificate and host name, and present the client certificate and key of the paired principal. Every read and write must honour the absolute deadline and never replay bytes. A read that times out throws `DeadlineExceeded` without consuming data. Keep the lifecycle, renewal and jog connections under the same authenticated identity. The client's own tests exercise the WebSocket jog path through a bridge that performs TLS on the Go side, so the C++ TLS path itself is not covered by CI. ## Related pages - [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http) - [Go SDK](https://advancedmetalresearch.com/docs/apis/go-sdk) - [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/clients/cpp/include/rosie/rt_control_client.hpp:22-90 (exceptions, HttpStream, StreamFactory)` - `rt-core/clients/cpp/include/rosie/rt_control_client.hpp:359-404 (Renewal, telemetry decoding)` - `rt-core/clients/cpp/include/rosie/rt_control_client.hpp:405-910 (RtControlClient)` - `rt-core/clients/cpp/include/rosie/rt_jog_producer.hpp:16-80` - `rt-core/clients/cpp/include/rosie/rt_jog_remote_producer.hpp:89-270` - `rt-core/clients/cpp/examples/rt_execute.cpp:1-86` - `rt-core/Makefile:138 (clients); narrative: rt-core/clients/cpp/README.md (build flags, CI note)` --- # TypeScript contracts > The generated TypeScript types, constants and telemetry decoders for rt-control. There is no HTTP client; use them with your own transport, and mind uint64 precision. URL: https://advancedmetalresearch.com/docs/apis/typescript-types Section: RosieOS docs / APIs Last updated: 2026-10-10 `rt-core/clients/ts/` holds two generated TypeScript files for applications that talk to [rt-control](https://advancedmetalresearch.com/docs/apis/rt-control-http) from Node or from a web front end behind a server: | File | Contents | |---|---| | `rt_control_api_generated.ts` | An interface for every contract type, the `Reason…` and `Operation…` constants, the `Capabilities` list, `ContractVersion`, `RequestSchema`, `ResponseSchema` and `CapabilitiesDigest`. | | `rt_protocol_generated.ts` | Little-endian decoders for the binary telemetry stream: `decodeTelemetryBatchHeaderV1`, `decodeCycleCaptureRecordV2`, `decodeCycleCaptureAxisV2`, their byte sizes, `CycleCaptureRecordV2LayoutDigest` and `TelemetryLayoutMismatchError`. | They have no imports and no runtime dependencies. Compile them with your own TypeScript toolchain, targeting ES2020 or later; nothing needs installing in rt-core. **There is no HTTP client**: rt-control listens on a Unix socket or on mutual TLS, which a browser cannot reach directly. Call it from Node, or through a server of your own. Both files are regenerated from the contract by `make generate-api` and `make generate-protocol`, and `make check-api` and `make check-protocol` fail if they drift. ## Example: typed calls from Node describe.ts: ```ts import http from "node:http"; import { CapabilitiesDigest, RequestSchema, ReasonControlAlreadyOwned, type Description, type Grant, type Response, } from "./rt_control_api_generated.ts"; const socketPath = "/run/rosie-rt-core/control.sock"; function call(method: string, path: string, body?: object): Promise { return new Promise((resolve, reject) => { const req = http.request({ socketPath, method, path, headers: { "Content-Type": "application/json" } }, (res) => { let text = ""; res.setEncoding("utf8"); res.on("data", (chunk) => (text += chunk)); res.on("end", () => resolve(JSON.parse(text) as Response)); }); req.on("error", reject); if (body) req.write(JSON.stringify(body)); req.end(); }); } const d = (await call("GET", "/v1/describe")).data as Description; if (d.capabilities_digest !== CapabilitiesDigest) throw new Error("contract mismatch"); console.log(d.backend, d.axes.map((a) => `${a.id} [${a.min_position}, ${a.max_position}] ${a.position_unit}`)); const r = await call("POST", "/v1/control", { schema: RequestSchema, operation: "acquire", controller: "ts-demo", binding: { pair_id: "cell-a", revision: 1, configuration_sha256: d.configuration_sha256, machine_sha256: d.machine_sha256 }, }); if (r.error === ReasonControlAlreadyOwned) console.log("someone else has control"); else if (!r.error) console.log("generation", (r.data as Grant).generation); ``` This sketch acquires but never renews or releases, so the grant simply expires after its lease. A real client must renew at a third of the lease and release when done; see [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority). ## uint64 precision JSON integer fields are typed `number`, which is exact only up to 2^53. Values such as `deadline_host_ns`, `time_ns` and event sequences can exceed that on a long-running host. When you need exact uint64 JSON values, parse with a lossless JSON parser. The binary telemetry decoders have no such problem: every 64-bit field decodes to `bigint`. `Request` is declared with `request_id?: string | null`, but the server refuses `null`: omit the field, or send a string of 1..64 printable ASCII bytes. ## Telemetry decoders The telemetry stream is a sequence of batches: a 312-byte header, then `RecordCount` records of `CycleCaptureRecordV2Bytes` (5392) bytes. Each decoder takes an exact-size `Uint8Array` and throws on a wrong length or a non-zero reserved field. `decodeTelemetryBatchHeaderV1` also throws `TelemetryLayoutMismatchError` (with `storedDigest` and `expectedDigest`) when the batch uses another record layout. telemetry.ts: ```ts import { decodeTelemetryBatchHeaderV1, decodeCycleCaptureRecordV2, TelemetryBatchHeaderV1Bytes, CycleCaptureRecordV2Bytes, } from "./rt_protocol_generated.ts"; // Feed raw chunks from GET /v1/telemetry/stream?after=. HTTP chunks need // not line up with batches, so buffer until a whole batch is available. let pending = new Uint8Array(0); let cursor = 0n; export function onChunk(chunk: Uint8Array) { const joined = new Uint8Array(pending.length + chunk.length); joined.set(pending); joined.set(chunk, pending.length); pending = joined; for (;;) { if (pending.length < TelemetryBatchHeaderV1Bytes) return; const h = decodeTelemetryBatchHeaderV1(pending.subarray(0, TelemetryBatchHeaderV1Bytes)); if (h.Magic !== 0x31425452 || h.Version !== 1 || h.RecordBytes !== CycleCaptureRecordV2Bytes) throw new Error("bad header"); const size = TelemetryBatchHeaderV1Bytes + h.RecordCount * CycleCaptureRecordV2Bytes; if (pending.length < size) return; if (h.Dropped !== 0n) console.warn(`lost ${h.Dropped} records`); for (let i = 0; i < h.RecordCount; i++) { const at = TelemetryBatchHeaderV1Bytes + i * CycleCaptureRecordV2Bytes; const rec = decodeCycleCaptureRecordV2(pending.subarray(at, at + CycleCaptureRecordV2Bytes)); console.log(rec.Seq, rec.Ax[0].Position); // Position is in drive counts } cursor = h.LastSequence; // resume point pending = pending.slice(size); } } ``` Before accepting a batch, also check `AxisCount` (1..16), `RecordCount` (at most 4096), that each record's `Seq` follows on from the last and that `Axes` matches the header. If the adapter or daemon incarnation changes, stop and reconcile before you reset the cursor. The layout, field by field, is in [Events and telemetry](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry#telemetry). ## What is exported | Export | Kind | |---|---| | `ContractVersion`, `RequestSchema`, `ResponseSchema`, `CapabilitiesDigest` | Constants: `2`, `rosie.rt-control.request.v1`, `rosie.rt-control.response.v1` and the schema digest. | | `Reason…` (for example `ReasonNotReady`) | One string constant per [reason code](https://advancedmetalresearch.com/docs/reference/error-codes). | | `Operation…` (for example `OperationStartProgram`) | One string constant per operation. | | `Capabilities` | The 33 capabilities with `name`, `state` and `transport`, `as const`. | | `Fence`, `Binding`, `Grant`, `Request`, `Response`, `Description`, `ProcessStatus`, `Event`, `EventBatch` … | One interface per [contract type](https://advancedmetalresearch.com/docs/apis/rt-control-http#types). Native receipts (`CommandResult`, `JogObservation`, `GrantObservation`) keep their PascalCase field names. | | `TelemetryBatchHeaderV1`, `CycleCaptureRecordV2`, `CycleCaptureAxisV2` | Decoded telemetry interfaces, with 64-bit fields as `bigint`. | ## Related pages - [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http) - [Events and telemetry](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/clients/ts/rt_control_api_generated.ts:1-1029 (constants L3-7, Capabilities L381-415, Request L456, Response L510, Description L793)` - `rt-core/clients/ts/rt_protocol_generated.ts:1-323 (TelemetryLayoutMismatchError L5, CycleCaptureRecordV2Bytes L161, Ax L215, decodeTelemetryBatchHeaderV1 L299)` - `rt-core/adapters/rosie/control/http.go:29-50 (null request_id refused)` - `rt-core/Makefile (generate-api, check-api); narrative: rt-core/clients/ts/README.md` --- # Offline programming HTTP API > Every route of the OLP server on port 8794, including the dense-execution machine-control API for Home, Arm, jog, moves, Load, Play and Stop, with request bodies, units, target fencing and error codes. URL: https://advancedmetalresearch.com/docs/apis/olp-http Section: RosieOS docs / APIs Last updated: 2026-10-10 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](https://advancedmetalresearch.com/docs/get-started/safety-model). > [!TIP] **Machine-readable.** This page's routes, request fields and error codes as an [OpenAPI 3.1 document](https://advancedmetalresearch.com/docs/openapi/offline-programming.json), 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. ```bash 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](https://advancedmetalresearch.com/docs/apis/olp-http#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`): ```json {"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: ```json {"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: | Header | Value | |---|---| | `X-RT-Target-Generation` | `status.target.selection_generation`, a decimal integer (sent as a string in JSON) | | `X-RT-Target-Cell` | `status.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 path | Purpose | |---|---| | `GET /health` | Server status | | `GET /capabilities` | Feature flags | | `GET /robots`, `POST /robots/capture-model` | Robot catalogue; compiled URDF for a recorded pose | | `POST /cadquery/topology` | STEP topology and tessellation | | `POST /seam` | Seam worker operations | | `POST /weld-plan`, `POST /weld-plan/export` | Plan a program; export the `.weldplan` | | `GET /targets`, `POST /targets/select` | List and select cells | | `GET /dense-execution/cells`, `POST /dense-execution/cells`, `DELETE /dense-execution/cells/{id}`, `POST /dense-execution/target` | Cell registry | | `GET /dense-execution/capabilities`, `GET /dense-execution/status`, `GET /dense-execution/cell`, `GET /dense-execution/telemetry/window` | Observe the selected cell | | `POST /dense-execution/home`, `POST /dense-execution/arm` | Home and arm | | `POST /dense-execution/load`, `play`, `pause`, `stop`, `heartbeat` | Run a planned program | | `GET /dense-execution/jog/state`, `POST /dense-execution/jog/{begin,update,end}` | Joint jog | | `POST /dense-execution/move` | Joint move | | `GET /dense-execution/cartesian/state`, `POST /dense-execution/cartesian/{start,intent,stop,halt}` | Cartesian jog | | `POST /dense-execution/cartesian/move` | Cartesian move | | `POST /dense-execution/go-home` | Not available on `rt_core` | | `/local-simulator/*` | The loopback simulator and its preview jog | | `/programs*` | Program catalog proxy | ## Service **Endpoint: `GET /api/offline-programming/v1/health`** Server status. Always 200. ```json {"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. **Endpoint: `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. **Endpoint: `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. ```json {"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. **Endpoint: `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. | Field | Type | Required | Description | |---|---|---|---| | `model_id` | string | yes | Robot model | | `robot_description_sha256` | string | yes | Description identity, `sha256:<64 hex>` | | `machine_planning_calibration_base64` | string | no | The cell's `machine_planning_calibration.json`, base64. Empty uses the empty calibration. | Returns `{"urdf": "", "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 **Endpoint: `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. **Endpoint: `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. | Code | HTTP | Meaning | |---|---|---| | `invalid_request` | 400 | The worker refused the request | | `payload_too_large` | 413 | Body over 12 MiB | | `busy` | 429 | No worker free | | `canceled` | 408 | The client went away | | `timeout` | 504 | The worker ran out of time | | `unavailable` | 503 | The worker's pixi environment is missing | | `worker_failed`, `invalid_response` | 502 | The worker crashed or answered with invalid JSON | See the [weld program format](https://advancedmetalresearch.com/docs/reference/weld-program-format) for the operations' payloads. ### Plan a program **Endpoint: `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. | Field | Type | Required | Description | |---|---|---|---| | `program` | object | yes | A [`robot.v4.program.v2`](/docs/reference/program-format) document | | `tooling` | object | yes | The fitted torch, packed as `tooling.json` | | `step_base64` | string | yes | The workpiece STEP file, base64. Empty only when `workpiece_absent` is true. | | `step_filename` | string | no | Its file name | | `workpiece_absent` | boolean | no | Plan with no workpiece | | `cell_id` | string | yes | The robot model whose description to plan against | | `free_space_backend` | string | no | Passed to the planner: `bspline`, `curobo` or `legacy` | | `machine_planning_calibration_base64` | string | no | Only read when no cell is connected. While a cell is selected, the server fetches the cell's own calibration. | | `program_id` | string | no | Stamped into the plan identity | | `program_digest` | string | no | Echoed in the response | | `manifest_revision`, `plan_revision` | integer | no | Stamped into the plan identity | | `fixtures` | object | no | Workcell 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 OK: ```json {"result": { … }, "weldplan_sha256": "…", "program_digest": "sha256:…", "dense": {"trajectory_digest": "sha256:…", "plan_id": "bracket_fillet:3f1c0a9d2b7e", … }, "dense_blob_base64": "…"} ``` | Field | Description | |---|---| | `result` | The planner's result document, `amr-weld-planner-v1.motion-plan-result.v1`, unchanged. See the [Weld planner HTTP API](https://advancedmetalresearch.com/docs/apis/weld-planner-http). | | `weldplan_sha256` | SHA-256 of the packed request | | `dense` | The dense trajectory summary: digest, `plan_id`, samples, segments, `robot_cell` | | `dense_blob_base64` | The `.rdt` bytes, base64, so the project keeps a copy | | Code | HTTP | Meaning | |---|---|---| | `weld_plan_invalid` | 400 | Invalid JSON, or missing STEP | | `seam_worker_unavailable`, `motion_origin_unavailable` | 503 | The seam worker or `OFFLINE_PROGRAMMING_WELD_PLANNER_MOTION_ORIGIN` is not configured | | `motion_plan_seam_refused` | 502 | One or more welds could not be planned | | `motion_plan_unjoined` | 502 | The welds planned but the connecting moves did not | | `motion_plan_no_trajectory` | 502 | No dense trajectory was produced; `dense_error` gives `{reason, detail, segment_index}` | | `motion_plan_failed`, `motion_plan_invalid_response`, `dense_blob_unreachable` | 502 | The planner call failed | | seam worker codes | as above | Packing failed | **Endpoint: `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 `.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](https://advancedmetalresearch.com/docs/apis/olp-http#target-fencing). Selection grants no authority; Arm or Load does. See [Connect to a cell](https://advancedmetalresearch.com/docs/guides/connect-a-cell) for the catalogue file. **Endpoint: `GET /api/offline-programming/v1/targets`** Lists the catalogue's cells with what each one reports about itself. ```json {"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`. **Endpoint: `POST /api/offline-programming/v1/targets/select`** Selects a cell. Stops and releases any session on the previous cell first. | Field | Type | Required | Description | |---|---|---|---| | `cell_id` | string | yes | A catalogue cell | | `model_id` | string | yes | One 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`. **Endpoint: `POST /api/offline-programming/v1/dense-execution/target`** The Cells dialog's selection. Body `{"cell": ""}`; an empty string clears the selection. Returns the dense-execution capabilities. Not fenced. **Endpoint: `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](https://advancedmetalresearch.com/docs/apis/olp-http#target-fencing) needs the fencing headers. ### Observe **Endpoint: `GET /api/offline-programming/v1/dense-execution/capabilities`** What the selected controller is and whether it is reachable. ```json {"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`. **Endpoint: `GET /api/offline-programming/v1/dense-execution/status`** The machine and session state. Takes no authority. Poll it. ```json { "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 } ``` | Field | Description | |---|---| | `target` | The selection this status belongs to. Use it for the [fencing headers](https://advancedmetalresearch.com/docs/apis/olp-http#target-fencing). | | `session.id` | The session to name in heartbeat, Play, jog and moves | | `session.kind` | `authority` (after Arm or Home), `trajectory` (after Load) | | `session.state` | `idle`, `loaded`, `playing` or `stopped` | | `session.lease.held` | OLP holds the `rt-control` lease | | `session.stop_reason`, `last_error` | Why the session stopped | | `armed` | Armed, lease held and no refusal pending | | `robot_cell` | Which robot the machine says it is. See [Robot and cell identity](https://advancedmetalresearch.com/docs/reference/rdt-format#robot-cell). | | `rt_core.status` | `rt-control`'s full status. See [`status`](/docs/apis/rt-control-http#status). | | `rt_core.events` | Events since the last poll | | `rt_core.refusal`, `refusal_kind` | The last refused operation and which kind it was | | `rt_core.inhibited`, `stop_uncertain`, `recovery_action` | A Stop or Release was not confirmed. `recovery_action` says what to do: retry `POST /dense-execution/stop`. | | `round_trip_ns`, `link_lease_ns` | Measured round trip to `rt-control`, and the effective lease | **Endpoint: `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. **Endpoint: `GET /api/offline-programming/v1/dense-execution/telemetry/window`** The newest seconds of the machine's telemetry ring, thinned. | Query | Type | Default | Description | |---|---|---|---| | `seconds` | number, s | 3 | In (0, 30] | | `stride` | integer | 1 | Keep 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 **Endpoint: `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. | Field | Type | Required | Description | |---|---|---|---| | `axes` | integer array | no | Axis 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 **Endpoint: `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. | Field | Type | Required | Description | |---|---|---|---| | `armed` | boolean | yes | Arm or disarm | 200 OK: ```json {"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 **Endpoint: `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. request: ```json {"session_id": "ds-9b1e4f07a2c3"} ``` 200 OK: ```json {"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 **Endpoint: `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`. request: ```json { "trajectory_digest": "sha256:…", "plan_id": "bracket_fillet:3f1c0a9d2b7e", "program_id": "bracket_fillet", "program_digest": "sha256:…", "manifest_revision": 1, "plan_revision": 3, "dry_run": true } ``` | Field | Type | Required | Description | |---|---|---|---| | `trajectory_digest` | string | yes | `sha256:<64 hex>`. The server fetches `GET /api/motion/dense/{digest}` from the weld planner. | | `plan_id`, `program_id`, `program_digest` | string | yes | Must equal the `.rdt` header exactly | | `manifest_revision`, `plan_revision` | integer | yes | Must equal the header. JSON integers. | | `dry_run` | boolean | no | Strip 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](https://advancedmetalresearch.com/docs/reference/rdt-format#rt-control-admission). 5. The prepared identity, axis mask and process requirement match (`program_identity_or_process_mismatch`). 200 OK: ```json {"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`. **Endpoint: `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. **Endpoint: `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 OK: ```json {"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](https://advancedmetalresearch.com/docs/get-started/safety-model#software-stops). **Endpoint: `POST /api/offline-programming/v1/dense-execution/pause`** Always refused on `rt_core` with `capability_unimplemented`. Use Stop. **Endpoint: `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 **Endpoint: `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. | Field | Type | Required | Description | |---|---|---|---| | `target_id` | string | yes | The cell's pair ID, `capabilities.cell` | | `session_id` | string | yes | `status.session.id` | | `revision` | integer | yes | `jog/state.revision`. It advances when a jog ends or a move starts, so a late request from an old hold is refused. | | `axis` | integer | yes | Axis index, within the cell's axis mask | | `direction` | integer | yes | `1` or `-1` | | `fraction` | number | yes | (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/state: ```json {"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 **Endpoint: `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 zero: ```json {"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}]} ``` | Field | Type | Required | Description | |---|---|---|---| | `target_id`, `session_id`, `revision` | | yes | As for joint jog | | `axis` | integer | yes | The joint, for a single-joint move | | `degrees` | number, **degrees** | yes | Signed relative move for a single joint. Non-zero. | | `fraction` | number | yes | (0, 1] of each joint's velocity and acceleration | | `axes[]` | array | no | A 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 OK: ```json {"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 **Endpoint: `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. | Field | Type | Required | Description | |---|---|---|---| | `target_id`, `session_id`, `revision` | | yes | As for joint jog | | `frame` | string | yes | `base` or `tool` | | `twist` | 6 numbers | yes | X, Y, Z in m/s (each \|v\| ≤ 1), RX, RY, RZ in rad/s (each \|ω\| ≤ π). At least one non-zero. | | `fraction` | number | yes | (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 **Endpoint: `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. | Field | Type | Required | Description | |---|---|---|---| | `target_id`, `session_id`, `revision` | | yes | As for joint jog | | `frame` | string | yes | `base` or `tool` | | `axis` | integer | yes | 0–2 for X, Y, Z; 3–5 for RX, RY, RZ | | `distance_mm` | number, **mm** | for axes 0–2 | Non-zero for a linear axis, zero otherwise. At most 1,000 mm. | | `angle_degrees` | number, **degrees** | for axes 3–5 | Non-zero for a rotary axis, zero otherwise. At most 180°. | | `fraction` | number | yes | (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 path | Description | |---|---| | `GET /local-simulator/status` | Simulator phase and readiness | | `POST /local-simulator/start`, `POST /local-simulator/stop` | Start or stop it. No request body. | | `POST /local-simulator/plans` | Plan a saved program against the simulator | | `POST /local-simulator/jog/start`, `/jog/stop` | Start 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/state` | Joints in rad, axis names, running flag | | `POST /local-simulator/jog/home`, `/jog/halt` | Home 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](https://advancedmetalresearch.com/docs/reference/error-codes). | Code | HTTP | Meaning | |---|---|---| | `dense_target_unavailable` | 503, 409 | No cell selected, or the backend cannot do this | | `target_changed` | 409 | The fencing headers name an old selection | | `ui_heartbeat_lost` | 409 | OLP stopped the machine: no heartbeat for 5 s | | `rt_core_inhibited` | 409 | A previous Stop or Release was not confirmed. Retry Stop. | | `rt_core_identity_mismatch` | 409 | Describe no longer matches the cell's pinned identity | | `rt_core_request_slow` | 409 | `rt-control` did not answer within the call budget. The lease is kept; retry. | | `rt_core_connection_reopened` | 409 | The request was not sent; the client reconnected. Retry. | | `rt_core_transport_lost`, `rt_core_protocol_error`, `rt_core_closed`, `rt_core_cancelled`, `rt_core_failure` | 409 | Transport or protocol failures | | `rt_core_incarnation_changed`, `rt_core_event` | 409 | `rt-control` restarted, or an event (a fault, a lost grant, dropped events) ended the session | | `control_renewal_lost` | 409 | Lease renewal failed; OLP stopped | | `control_session_stale` | 409 | The session was stopped or replaced before this request ran | | `capability_unimplemented` | 409 | Pause, go-home, or a missing `rt-control` capability | | `dense_session_required`, `dense_session_mismatch` | 400, 409 | Missing or unknown `session_id` | | `dense_internal` | 500 | Unexpected server error | ## Related pages - [Connect to a cell and run a program](https://advancedmetalresearch.com/docs/guides/connect-a-cell) - [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning#olp) - [Program format](https://advancedmetalresearch.com/docs/reference/program-format) - [Weld planner HTTP API](https://advancedmetalresearch.com/docs/apis/weld-planner-http) - [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `offline-programming/v1/server.go:29-38,145-218,220-264,522-600,855-1100,1294-1330,1482-1502,1602-1614,1631-1696` - `offline-programming/v1/dense_execution.go:21-361` - `offline-programming/v1/dense_cells.go:1-57` - `offline-programming/v1/cell_targets.go:1-47` - `offline-programming/v1/cartesian_jog.go:1-84` - `offline-programming/v1/robots.go:26-127` - `offline-programming/v1/weld_plan.go:29-224,420-445,495-540` - `offline-programming/v1/main.go:257-320,896-940` - `offline-programming/v1/internal/denseexec/session.go:21-317` - `offline-programming/v1/internal/denseexec/rt_core.go:25-146,217-238,267-397,399-504,556-871,980-1031,1086-1193,1256-1406` - `offline-programming/v1/internal/denseexec/joint_jog_api.go:1-70` - `offline-programming/v1/internal/denseexec/joint_jog.go:17-31,196-420,515-524` - `offline-programming/v1/internal/denseexec/joint_move.go:17-305,437-520` - `offline-programming/v1/internal/denseexec/cartesian_jog_api.go:1-60` - `offline-programming/v1/internal/denseexec/cartesian_jog.go:52-215` - `offline-programming/v1/internal/denseexec/cartesian_move.go:17-60` - `offline-programming/v1/internal/denseexec/robot.go:12-104` - `offline-programming/v1/internal/denseexec/registry.go:1-57` - `offline-programming/v1/internal/denseexec/fleet.go:1-43` - `offline-programming/v1/internal/denseexec/rt_core_telemetry.go:49-80` - `offline-programming/v1/internal/targets/picker.go:16-260` - `offline-programming/v1/internal/seam/types.go:20-56` - `offline-programming/v1/internal/cad/types.go:17-20` - `offline-programming/v1/internal/simulator/contracts.go:23-26` - `offline-programming/v1/internal/simulator/jog_manager.go:10-17` - `offline-programming/v1/internal/simulator/jog_session.go:37-57` - `robot_description/go/description.go:298-323` - `motion-server/v1/src/cartesian_resolver.hpp:14-97` --- # Weld planner HTTP API > The weld planner's four routes on port 8796, for health, progress, planning a .weldplan and fetching the verified dense trajectory, with query parameters, units, the result document and error codes. URL: https://advancedmetalresearch.com/docs/apis/weld-planner-http Section: RosieOS docs / APIs Last updated: 2026-10-10 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](https://advancedmetalresearch.com/docs/concepts/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](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner#bind). > [!TIP] **Machine-readable.** The four routes, their query parameters and errors as an [OpenAPI 3.1 document](https://advancedmetalresearch.com/docs/openapi/weld-planner.json), generated from this page. ## Quick start Check the GPU, plan a committed example part, and fetch its trajectory: ```bash 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](https://advancedmetalresearch.com/docs/apis/olp-http#weld-plan). ## 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 **Endpoint: `GET /api/motion/health`** Reports whether the planner's Torch build can see a CUDA device. 200 OK: ```json {"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 **Endpoint: `GET /api/motion/progress`** The stage of the plan that is running now, for a client watching a long request. 200 OK: ```json {"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 **Endpoint: `POST /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 `.rdt` is 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: 200 OK: ```json { "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_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](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification#result) for every block, and [Verdicts](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification#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 `/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](/docs/reference/rdt-format#validation) | `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": ": "}` | 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 **Endpoint: `GET /api/motion/dense/{trajectory_digest}`** Returns the stored `.rdt` bytes for a digest. ```bash 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-.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](/docs/reference/rdt-format) for the file layout. ## Related pages - [Run the weld planner](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner) - [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification) - [Weld program and `.weldplan` container](/docs/reference/weld-program-format) - [Offline programming HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http) - [Ports, sockets and environment variables](https://advancedmetalresearch.com/docs/reference/ports-and-environment) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `weld_planner/v1/python/weld_motion_planner/server/motion_planner_server.py:57,78-82,85-185,196-317,379-553,556-571` - `weld_planner/v1/python/weld_motion_planner/planner/dp_seam_search/options.py:17-31` - `weld_planner/v1/python/weld_motion_planner/planner/planner_main.py:64-96,235,252` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/options.py:21-90` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/freespace.py:2035,2168,2435-2464` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/connecting_trajopt_main.py:79` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/weld_trajopt.py:245-294` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/taught.py:78-80` - `weld_planner/v1/python/weld_motion_planner/io/wire.py:69-121` - `weld_planner/v1/python/weld_motion_planner/io/native_result.py:17-164` - `weld_planner/v1/python/weld_motion_planner/io/dense_output.py:39-139` - `weld_planner/v1/python/weld_motion_planner/verifier/verify.py:552-606,1640-1687` - `weld_planner/v1/python/weldplan/native_admission.py:204-250` - `weld_planner/v1/python/weldplan/dense_joint_trajectory.py:255-365,655-797` - `offline-programming/v1/weld_plan.go:424-438` - `offline-programming/v1/internal/denseexec/session.go:236-262` --- # 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..…` or `robot/v4/robot..…`. `--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, `:`. 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 | `:`. 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//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` --- # Dense trajectory daemon > The joint_trajectory_daemon store-and-play service for .rdt programs, with its config file, command-line flags, TCP ingest protocol, NATS leader lease and play commands, status document and refusal codes. URL: https://advancedmetalresearch.com/docs/apis/dense-trajectory-daemon Section: RosieOS docs / APIs Last updated: 2026-10-10 `joint_trajectory_daemon` stores immutable [`.rdt` programs](/docs/reference/rdt-format) and plays them on the robot through `rt-control`. It has two planes that never overlap: - **Data plane: TCP**, on `127.0.0.1:8797` by default. Upload, validate, preload and status. Nothing on this socket can start motion. - **Control plane: NATS**, on the cell's robot command subject. A controller acquires the daemon's leader lease, then sends `play` with the exact plan identity. `stop` is always accepted. The daemon uses `rt_core` as its only backend. Retired backends and options exit with `backend_retired` or `option_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). > [!IMPORTANT] Programs played through this daemon are not re-verified. The daemon validates the `.rdt` format, and `rt-control` checks native position, velocity and continuity limits. Neither checks collisions or the plan's robot and cell identity. Only OLP's Load path requires a weld planner verifier PASS. See [What is verified before motion](https://advancedmetalresearch.com/docs/get-started/safety-model#what-is-verified). Also, **play runs native Home** on any axis whose Home is not valid. ## Quick start The dev stack runs the daemon against the simulated core like this: ```bash joint_trajectory_daemon --backend rt_core \ --config "$ROSIE_LOCAL_RT_BUILD/daemon-rt-core.json" \ --dense-ingest-listen 127.0.0.1:8797 \ --plan-store-dir "$ROSIE_LOCAL_RT_BUILD/dense-plans" \ --nats-url nats://127.0.0.1:14222 \ --robot-cell dev-cell \ --robot-command-subject robot/v4/robot.dev-cell.command \ --status-publish-subject robot/v4/robot.dev-cell.status ``` Build it with `make -C motion-server/joint-trajectory/v1 all`. The binary goes to `rt-core/build/dense` (`make ... print-bin-dir` prints the path). Then upload a program from Python. The weld planner package has a client for the TCP framing: upload.py: ```python from weldplan.dense_joint_trajectory import upload, request blob = open("hold.rdt", "rb").read() print(upload("127.0.0.1", 8797, blob)) # {'ok': True, 'kind': 'upload', 'trajectory_digest': 'sha256:…', # 'segment_count': 1, 'total_sample_count': 3, 'total_duration_s': 0.02} print(request("127.0.0.1", 8797, "status")) ``` To play it, a NATS client acquires the leader lease, preloads over TCP, and sends `play`. The sequence is in [Play a program](https://advancedmetalresearch.com/docs/apis/dense-trajectory-daemon#play-a-program). ## Command line Settings come from the JSON config file first. Command-line flags override it. | Flag | Default | Description | |---|---|---| | `--config PATH` | env `JOINT_TRAJECTORY_DAEMON_CONFIG` | The config file. Without one, compiled defaults apply and the native binding is empty, so startup fails with `native_binding_required`. | | `--backend rt_core` | `rt_core` (env `JOINT_TRAJECTORY_BACKEND`) | Anything else exits with `backend_retired` | | `--dense-ingest-listen HOST:PORT` | `127.0.0.1:8797` | TCP ingest address. The host must be an IPv4 literal. | | `--plan-store-dir DIR` | `$HOME/.rosie/motion-server/joint-trajectory/v1/dense-plans` | Where uploaded blobs are stored, as `.rdt` | | `--ready-file PATH` | none | Written after the listener binds: `{"schema":"robot-v4.dense-joint-trajectory-ingest-ready.v1","host":…,"port":…}` | | `--nats-url nats://HOST:PORT` | none | Without it the daemon is ingest-only and nothing can deliver a `play` | | `--robot-cell NAME` | none | Commands whose `robot` field differs are ignored | | `--robot-command-subject SUBJ` | none | The NATS subject the daemon subscribes to for commands | | `--status-publish-subject SUBJ` | none | Streams the full [status document](https://advancedmetalresearch.com/docs/apis/dense-trajectory-daemon#status) at `status.publish_hz`. Off when unset. | | `--help` | | Print usage | `--joint-target` and `--rtcore-status-subject` are retired and exit with `option_retired`. Exit code 2 means a configuration or usage error. ## Config file The canonical, annotated copy is `motion-server/joint-trajectory/v1/config/joint_trajectory_daemon.config.json`. Every `REPLACE_…` value must be replaced with the cell's approved binding before the daemon will start. joint_trajectory_daemon.config.json: ```json { "schema": "robot.v4.joint-trajectory-daemon.config.v1", "ingest": { "listen": "127.0.0.1:8797" }, "status": { "publish_hz": 20.0 }, "leader": { "ttl_min_ms": 500, "ttl_max_ms": 2000 }, "backend": "rt_core", "rt_core": { "socket": "/run/rosie-rt-core/control.sock", "controller": "motion-server", "pair_id": "REPLACE_WITH_APPROVED_PAIR", "pair_revision": 1, "machine_sha256": "REPLACE_WITH_APPROVED_MACHINE_SHA256", "deployment_sha256": "REPLACE_WITH_APPROVED_DEPLOYMENT_SHA256", "configuration_sha256": "REPLACE_WITH_APPROVED_CONFIGURATION_SHA256", "expected_backend": "simulation", "home_policy": "home", "status_subject": "motion-server.status", "control_timeout_ms": 10000 } } ``` | Key | Type | Default | Description | |---|---|---|---| | `schema` | string | required | `robot.v4.joint-trajectory-daemon.config.v1` | | `backend` | string | required | `rt_core` | | `ingest.listen` | string | `127.0.0.1:8797` | TCP ingest `HOST:PORT` | | `ingest.plan_store_dir` | string | see `--plan-store-dir` | Blob store directory | | `status.publish_hz` | number, Hz | `20` | Rate of the `--status-publish-subject` stream | | `leader.ttl_min_ms` | integer, ms | `500` | Shortest leader lease the daemon grants. Requested TTLs are clamped to this range. | | `leader.ttl_max_ms` | integer, ms | `2000` | Longest leader lease | | `rt_core.socket` | string | `/run/rosie-rt-core/control.sock` | The `rt-control` Unix socket | | `rt_core.controller` | string | `motion-server` | The controller name the daemon acquires `rt-control` with | | `rt_core.pair_id` | string | required | The pair binding, as `rt-control` was started with | | `rt_core.pair_revision` | integer | required, > 0 | The pair revision | | `rt_core.machine_sha256` | 64 hex | required | Must equal Describe's `machine_sha256` | | `rt_core.deployment_sha256` | 64 hex | required | Must equal Describe's `deployment_sha256` | | `rt_core.configuration_sha256` | 64 hex | required | Must equal Describe's `configuration_sha256` | | `rt_core.expected_backend` | string | `simulation` | Must equal Describe's `backend`: `simulation` or `ethercat` | | `rt_core.home_policy` | string | `home` | What play does for an axis without valid Home: `home` runs native Home, `restore_anchor` restores the saved anchor | | `rt_core.status_subject` | string | `.status` | Subject for the executor status, published every 200 ms | | `rt_core.control_timeout_ms` | integer, ms | `10000` | Receipt and observation budget for each `rt-control` call | Integers must be positive and strings non-empty. A missing file that was asked for, a wrong schema or a malformed value is a startup error; the daemon never falls back to compiled defaults. On `leader_acquire` the daemon also checks that `rt-control` implements `describe`, `acquire`, `renew`, `enable`, `arm`, `home`, `restore_anchor`, `recovery_status`, `prepare_program`, `start_program`, `status`, `subscribe_events`, `stop` and `release`, and that Describe lists exactly nine axes `J1`…`J9` in rad. A six-axis cell is refused with `native_axis_map_invalid`. ## TCP ingest One request and one response per connection, then the daemon closes it. Each exchange must finish within 240 s. ```text frame = [u64 BE payload_size][payload] payload_size ≤ 64 MiB payload = [u32 BE json_len][control JSON][optional binary body] ``` The response uses the same framing, with a JSON body and no binary part. A rejection is: ```json {"ok": false, "reason": "block_sha256_mismatch", "segment_index": 0, "detail": "stored sha256:… computed sha256:…"} ``` `segment_index` is present only when one segment is at fault. ### `upload` Validates the binary body as a `.rdt` and stores it. Upload never causes motion. request control JSON: ```json {"kind": "upload"} ``` response: ```json {"ok": true, "kind": "upload", "trajectory_digest": "sha256:…", "segment_count": 1, "total_sample_count": 3, "total_duration_s": 0.02} ``` Refusals: any [`.rdt` validation reason](/docs/reference/rdt-format#validation) (the daemon reports a wrong schema as `schema_mismatch`), and `store_write_failed`. ### `validate` The same checks as `upload`, without storing. Answers `"kind": "validate"`. ### `preload` Stages a stored blob by reference and prepares it on `rt-control` (`prepare_program`). It needs the leader lease to be held, because preparation happens under the daemon's `rt-control` grant. request control JSON: ```json { "kind": "preload", "trajectory_digest": "sha256:…", "plan_id": "demo:1", "program_id": "demo", "program_digest": "sha256:…", "manifest_revision": 1, "plan_revision": 1 } ``` | Field | Type | Required | Description | |---|---|---|---| | `trajectory_digest` | string | yes | `sha256:<64 hex>` of a stored blob | | `plan_id`, `program_id`, `program_digest` | string | yes | Must equal the stored header exactly | | `manifest_revision`, `plan_revision` | integer | yes | Must equal the stored header. JSON integers, not strings. | The daemon re-verifies the stored bytes in full, so a blob corrupted on disk fails here rather than at play. response: ```json {"ok": true, "kind": "preload", "trajectory_digest": "sha256:…", "total_sample_count": 3} ``` | Reason | Meaning | |---|---| | `request_invalid` | Missing `trajectory_digest`, bad framing or unknown `kind` | | `plan_not_found` | No stored blob with that digest | | `identity_mismatch` | A named identity field differs from the stored header | | `native_acquisition_required` | No `rt-control` grant: acquire the leader lease first | | `leader_lease_expired` | The leader lease is not current | | `native_commissioning_in_progress` | A play is still homing, enabling or arming | | `native_playback_in_progress` | A program is playing. Stop first. | | `native_torch_unsupported` | The program has torch samples. Remove them. | | `native_program_identity_mismatch` | `rt-control` prepared a program whose identity, axis mask or sample counts differ | | any `rt-control` reason | For example `native_limit_exceeded`. See [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes#reasons-dense). | A refused `native_limit_exceeded` leaves any previously prepared program in place. Other native refusals stop the executor and release the grant. ### `status` request control JSON: ```json {"kind": "status"} ``` Returns the [status document](https://advancedmetalresearch.com/docs/apis/dense-trajectory-daemon#status). ## NATS control plane Commands are JSON messages on `--robot-command-subject`. Send them as NATS requests: the daemon replies on the message's reply subject. It ignores messages with a different `robot`, a missing envelope field or a command it does not own. Every command carries this envelope: | Field | Type | Required | Description | |---|---|---|---| | `schema` | string | yes | `robot.v4.robot-command.v1` | | `command` | string | yes | `leader_acquire`, `leader_renew`, `leader_release`, `play`, `pause`, `stop` or `go_home` | | `robot` | string | yes | Must equal `--robot-cell` | | `command_id` | string | yes | Unique per command. A resend with the same ID gets the same reply without running again, except `stop`, which always runs. The daemon remembers the last 256 IDs. | | `sender_id` | string | yes | `offline-programming:`, where the instance is 1–31 characters from `A-Za-z0-9-`. No other role may hold the lease. | Replies have this shape: ```json {"schema": "robot.v4.command-reply.v1", "command": "play", "command_id": "c-42", "ok": false, "reason": "fresh_exact_leader_lease_required"} ``` ### Leader lease The daemon mints the lease itself. A client never supplies an epoch or lease ID to `leader_acquire`. leader_acquire: ```json {"schema": "robot.v4.robot-command.v1", "command": "leader_acquire", "robot": "dev-cell", "command_id": "c-1", "sender_id": "offline-programming:bench-1", "controller_boot_id": "boot-7f3a", "ttl_ms": 1000} ``` reply: ```json {"schema": "robot.v4.command-reply.v1", "command": "leader_acquire", "command_id": "c-1", "ok": true, "lease_id": "motion-server-…-0000000000000001", "leader_fence_epoch": 1, "expires_in_ms": 1000} ``` | Command | Extra fields | Effect | |---|---|---| | `leader_acquire` | `controller_boot_id`, `ttl_ms` | Stops any motion first, then grants a new lease with a new fence epoch. The daemon then acquires `rt-control` under its configured binding and checks the deployment identity. If that fails, no lease is granted and the reply carries the native reason. | | `leader_renew` | `controller_boot_id`, `lease_id`, `leader_fence_epoch`, `ttl_ms` | Extends the lease. Refused with `lease_not_held` unless every field matches the current holder. | | `leader_release` | `controller_boot_id`, `lease_id`, `leader_fence_epoch` | Stops motion and ends the lease. The daemon then stops and releases its `rt-control` grant. | `ttl_ms` is clamped to `leader.ttl_min_ms`…`leader.ttl_max_ms`. Renew well inside the TTL. If the lease expires, the executor aborts any playback with `leader_lease_expired`, then stops and releases its `rt-control` grant. Note the field names: lease commands use `lease_id`, motion commands use `leader_lease_id`. Lease refusals: `leader_identity_invalid`, `ttl_ms_required`, `client_fence_not_accepted` (an acquire that carried a lease ID or epoch), `lease_not_held`. ### Play a program 1. `leader_acquire` on NATS. Keep renewing. 2. `upload` the blob over TCP, then `preload` it with its identity. 3. `play` on NATS: play: ```json { "schema": "robot.v4.robot-command.v1", "command": "play", "robot": "dev-cell", "command_id": "c-3", "sender_id": "offline-programming:bench-1", "controller_boot_id": "boot-7f3a", "leader_lease_id": "motion-server-…-0000000000000001", "leader_fence_epoch": 1, "motion_intent_seq": 1, "plan_id": "demo:1", "program_id": "demo", "program_digest": "sha256:…", "trajectory_digest": "sha256:…", "manifest_revision": 1, "plan_revision": 1 } ``` | Field | Type | Description | |---|---|---| | `controller_boot_id`, `leader_lease_id`, `leader_fence_epoch` | string, string, integer | The current lease, exactly | | `motion_intent_seq` | integer | Greater than zero and strictly greater than the last one this sender used, so a delayed duplicate can never run | | `plan_id`, `program_id`, `program_digest`, `trajectory_digest` | string | Must equal the preloaded plan | | `manifest_revision`, `plan_revision` | integer | Must equal the preloaded plan | A play the daemon accepts runs these phases, reported in `commissioning_phase`: 1. `preparation`: status and the prepared identity are read back from `rt-control`. 2. `home` or `restore_anchor`: only for axes whose Home is not valid, as `home_policy` says. Refused with `native_home_unavailable` if an axis has no native Home. 3. `enable_arm`: recovery status must show no faults, then `enable` and `arm`. 4. `readiness`: waits until every program axis reports `ready`. 5. `start`: `start_program` with the full identity. The reply comes when `start_program` has been accepted, or when a phase fails. Progress is then visible in the status document. ### Other commands | Command | Fenced | Effect | |---|---|---| | `stop` | No | Always accepted and always executed, even on a resend. Aborts playback, then stops and releases the `rt-control` grant. | | `pause` | Yes | Refused with `pause_unsupported`; use `stop` | | `go_home` | Yes | Refused with `go_home_unsupported` on the `rt_core` backend | Fenced commands are refused with `fresh_exact_leader_lease_required`, `ordered_motion_intent_sequence_required`, `stale_motion_intent_sequence` or `exact_staged_plan_identity_required` before the executor sees them. `play`, `leader_acquire` and `leader_release` run on a queue; when 256 are already waiting, a new one is refused with `command_queue_full`. ## Status document The TCP `status` reply, the `--status-publish-subject` stream and the `rt_core.status_subject` stream carry the same document. ```json { "ok": true, "kind": "status", "schema": "robot.v4.dense-joint-trajectory.v1", "store_dir": "/srv/rosie/dense-plans", "staged": {"trajectory_digest": "sha256:…", "plan_id": "demo:1", "total_sample_count": 3}, "executor": { "state": "playing", "backend": "rt_core", "native_state": "owned", "execution_state": "executing", "commissioning_phase": "", "execution_generation": 4, "native_sequence": 118, "segment_index": 0, "sample_index": 1, "t_s": 0.01, "detail": "", "consumer_action": "", "native_result": null, "program_identity": {"plan_id": "demo:1", "…": "…"}, "authority_fresh": true, "leader_fresh": true }, "last_error": null } ``` `--status-publish-subject` carries this whole document at `status.publish_hz`. `rt_core.status_subject` carries only the `executor` object, every 200 ms. | Field | Description | |---|---| | `staged` | The preloaded plan, or `null` | | `last_error` | The last ingest refusal as `{reason, detail}`, or `null` | | `executor.state` | `idle`, `staged`, `playing`, `done` or `aborted` | | `executor.native_state` | The `rt-control` grant: `inactive`, `acquiring`, `owned`, `reconciling`, `cleanup_uncertain` or `grant_active` | | `executor.execution_state` | `rt-control`'s execution state, for example `prepared`, `executing`, `completed` | | `executor.commissioning_phase` | The current play phase, or empty | | `executor.segment_index`, `sample_index`, `t_s` | The playback cursor on the program's own segment clock | | `executor.execution_generation`, `native_sequence` | The native execution identity of the running program | | `executor.program_identity` | The prepared identity, including `source_digest` and `normalised_digest` | | `executor.abort_reason`, `detail` | Why the executor last stopped or refused | | `executor.native_result` | The native refusal result, when there was one | | `executor.consumer_action` | What the client should do next (see below) | | `executor.authority_fresh` | The grant is owned under the current leader epoch and the lease is fresh | | `executor.leader_fresh` | A leader lease is active and unexpired | The executor document also carries compatibility fields from an earlier backend: `armed`, `torch_on`, `feedback_age_ms`, `tracking_error_rad`, `tracking_error_axis`, `transient`, and the `authority` and `feedback` objects. The `rt_core` executor does not fill them; they keep their defaults (`armed` is `false`, `feedback_age_ms` is `-1`). Read `native_state`, `execution_state` and `rt-control`'s own status instead. ### `consumer_action` | Value | Meaning | |---|---| | `acquire_and_preload` | Idle and clean. Acquire the leader lease and preload. | | `remove_torch_samples` | The program has torch samples | | `use_stop` | `pause` is not supported | | `correct_request` | The request was refused but nothing was stopped. Fix it and retry. | | `observe_standstill_before_acquire` | Cleanup is in progress | | `reconcile_status_then_acquire` | Stop or Release could not be confirmed. Check `rt-control` status before acquiring again. | | `stop_reconcile_reacquire_upload` | The `rt-control` session was fenced or expired | | `invalidate_describe_bind_reacquire_upload` | `rt-control` or the core restarted | | `abort_stop_reconcile` | Any other native failure | After an abort the daemon stops the grant, releases it, and waits for two consecutive, newer status samples that show no grant, the axes disarmed and disabled, and unchanged positions before it reports `inactive`. ## Refusal codes Beyond the ingest and lease reasons above, `abort_reason` and play replies can carry: | Reason | Meaning | |---|---| | `native_binding_required` | Startup: the config does not name a complete binding | | `native_session_already_present` | `leader_acquire` while the daemon still holds a grant | | `native_contract_mismatch` | `rt-control`'s contract version or capabilities digest differs from the client's | | `native_deployment_identity_mismatch` | Describe's machine, deployment or configuration digest, or backend, differs from the config | | `native_capability_unavailable` | A required capability is not `implemented` | | `native_axis_map_invalid` | Describe does not list exactly nine rad axes `J1`…`J9` with valid limits | | `invalid_home_policy` | `home_policy` is not `home` or `restore_anchor` | | `native_binding_mismatch`, `native_grant_mismatch` | The grant returned by `rt-control` differs from the binding | | `native_grant_active` | Another controller holds `rt-control` | | `native_preparation_required` | `play` before a successful `preload` | | `native_preparation_retired` | The prepared program is no longer prepared on `rt-control` | | `native_home_unavailable`, `native_home_unconfirmed` | Home could not run, or could not be confirmed | | `native_fault_observed` | A safety fault or recovery fault is present | | `native_readiness_` | An axis reported `faulted`, `coordinate_invalid`, `home_required` or `mode_mismatch`, or another non-ready state | | `readiness_observation_expired` | Readiness did not arrive within `control_timeout_ms` | | `native_status_unavailable`, `native_status_stale` | No status, or a core sample older than 200 ms | | `native_incarnation_changed` | `rt-control` or the core restarted | | `native_execution_aborted`, `execution_identity_mismatch` | The running program faulted, was cancelled or changed identity | | `native_operation_cancelled` | Stop or a lease change cancelled the operation | | `native_standstill_unconfirmed`, `native_shutdown_expired`, `release_uncertain` | Cleanup could not be confirmed | | `leader_revoked`, `operator_stop`, `daemon_shutdown` | Why playback was aborted | `rt-control`'s own reasons pass through unchanged. See [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes). ## Related pages - [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning#dense) - [Dense trajectory (.rdt) format](https://advancedmetalresearch.com/docs/reference/rdt-format) - [NATS subjects and streams](https://advancedmetalresearch.com/docs/reference/nats-subjects) - [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `motion-server/joint-trajectory/v1/src/joint_trajectory_daemon.cpp:1-276` - `motion-server/joint-trajectory/v1/src/runtime_config.hpp:31-234` - `motion-server/joint-trajectory/v1/config/joint_trajectory_daemon.config.json` - `motion-server/joint-trajectory/v1/src/dense_joint_trajectory_ingest.hpp:41-586` - `motion-server/joint-trajectory/v1/src/command_dispatch.hpp:25-241` - `motion-server/joint-trajectory/v1/src/leader_authority.hpp:24-265` - `motion-server/joint-trajectory/v1/src/rt_control_executor.hpp:48-607` - `motion-server/joint-trajectory/v1/src/rt_control_program.hpp:11-55` - `motion-server/joint-trajectory/v1/src/execution_backend.hpp:46-164` - `motion-server/joint-trajectory/v1/src/executor_status_json.hpp:40-114` - `motion-server/joint-trajectory/v1/Makefile:7,16-17` - `weld_planner/v1/python/weldplan/dense_joint_trajectory.py:429-488` - `dev-stack.sh:280-283` - `motion-server/v1/local-rt-core.sh:122-127` --- # rtctl command reference > Every rtctl subcommand and flag, with defaults and exit codes, for compiling and validating machine configurations, checking a cell host, inspecting a running cell through rt-control, and converting telemetry dumps. URL: https://advancedmetalresearch.com/docs/reference/rtctl Section: RosieOS docs / Reference Last updated: 2026-10-10 `rtctl` is the operator CLI for rt-core. It compiles and validates machine configurations, launches the core, checks a cell host, and sends single requests to [rt-control](https://advancedmetalresearch.com/docs/apis/rt-control-http) for inspection and administration. ```bash cd rt-core make control # builds build/rtctl build/rtctl describe --socket /run/rosie-rt-core/control.sock build/rtctl status --json | head -c 400; echo ``` > [!IMPORTANT] **rtctl is not a motion client.** Each control command sends one request and exits. rtctl never renews the lease, and on the `lan` profile a lease lasts at most 500 ms, so it has lapsed before your next command runs and the core inhibits outputs. There is no `jog` or `renew` command. Use rtctl to observe, fetch resources, stop and recover. For anything that moves the robot, use the [Go SDK](https://advancedmetalresearch.com/docs/apis/go-sdk) or the [C++ client](https://advancedmetalresearch.com/docs/apis/cpp-client), which renew in the background. rtctl builds and runs on Linux only. There is no `rosie` command in this repository; READMEs that mention `rosie …` or `./rosie.sh …` refer to an internal tool. ## Commands at a glance | Group | Commands | |---|---| | [Configuration](https://advancedmetalresearch.com/docs/reference/rtctl#configuration-commands) | `compile`, `validate`, `run`, `control-env` | | [Host](https://advancedmetalresearch.com/docs/reference/rtctl#host-commands) | `hostcheck`, `inventory`, `recover-encoder` | | [Control API](https://advancedmetalresearch.com/docs/reference/rtctl#control-commands) | `describe`, `status`, `events`, `resources fetch`, `acquire`, `release`, `stop`, `halt`, `enable`, `arm`, `home`, `reset-fault`, `prepare`, `start`, `discard`, `program-prepare`, `program-start` | | [Telemetry](https://advancedmetalresearch.com/docs/reference/rtctl#telemetry) | `telemetry dump-to-jsonl` | Every command rejects positional arguments it does not expect. ## Exit codes | Code | Configuration commands | `hostcheck` | `inventory` | Control commands | |---|---|---|---|---| | 0 | Success | Every check passed | Every read matched | Success | | 1 | Compile or file error | — | Compile or file error | — | | 2 | Usage error | A check failed, or usage error | A slave mismatched, the machine is not `live`, or usage error | Rejected by rt-control, invalid options, or usage error | | 3 | — | Required evidence was unreadable, or the rt-core root was not found | A slave or the I/O terminal was unreadable | Transport, protocol or closed-connection failure | `validate` returns the daemon's own exit status. ## Where rtctl finds rt-core Configuration and host commands need the rt-core root, the directory that holds `config/drives/`. rtctl looks beside its own executable first (`/build/rtctl` or `/bin/rtctl`), then walks up from the working directory to a directory containing both `config/drives/` and `go.mod`. If neither works it prints `cannot locate rt-core root` and exits 1 (3 for `hostcheck`). ## Configuration commands ### `compile` Validates a machine config and writes the compiled configuration. It never contacts drives. ```bash build/rtctl compile --config config/machines/simulation/simulation-program.json --out build/config # /…/build/config/ ``` | Flag | Type | Default | Description | |---|---|---|---| | `--config` | path | — | Machine config JSON. Required. | | `--profiles` | dir | `/config/drives` | Drive config directory. | | `--out` | dir | — | Output directory. Required. The compiled directory is `//`. | It prints the compiled directory on stdout and any `warning:` lines on stderr. The files it writes and the three digests are described in [Configuration files](https://advancedmetalresearch.com/docs/reference/configuration#compiled-output). ### `validate` Compiles, then runs the daemon with `--validate-config`, so the daemon checks its own private format without starting a runtime session. ```bash build/rtctl validate --backend simulation --config config/machines/simulation/simulation-program.json # daemon exit status: 0 ``` | Flag | Type | Default | Description | |---|---|---|---| | `--config` | path | — | Machine config JSON. Required. | | `--backend` | `simulation` \| `ipc-only` \| `live` | — | Required. Selects the daemon binary. | | `--binary` | path | `/build/rosie-rt-core-sim`, `-ipc`, or `rosie-rt-core` | Daemon executable to run. | | `--profiles` | dir | `/config/drives` | Drive config directory. | | `--out` | dir | `/build/config` | Where the compiled configuration is written. | ### `run` Compiles, then replaces itself (`exec`) with the selected daemon and the compiled argument list. This is how you start the core by hand in simulation. ```bash build/rtctl run --backend simulation \ --config config/machines/simulation/simulation-program.json \ --out "$TMPDIR/config" --socket "$TMPDIR/ipc.sock" ``` | Flag | Type | Default | Description | |---|---|---|---| | `--config` | path | — | Machine config JSON. Required. | | `--backend` | `simulation` \| `ipc-only` \| `live` | — | Required. Runs `/build/rosie-rt-core-sim`, `-ipc` or `rosie-rt-core`. | | `--out` | dir | `/build/config` | Compiled output directory. | | `--socket` | path | `/run/rosie-rt-core/ipc.sock` | The core's private IPC socket, passed as `--socket-path`. Must not be empty. | Two guards keep simulation away from hardware: - `--backend live` requires the machine config itself to say `"backend": "live"`. - In an installed package (where `/bin/rtctl` exists), `run` accepts only `--backend live` and runs `/bin/rosie-rt-core`. It never resolves a simulation request to the hardware binary. On WSL, keep sockets on a Linux tmpfs such as `/dev/shm`; they cannot bind on `/mnt/c`. ### `control-env` Compiles a **live** machine config for installed systemd units and prints the environment file they read. `host/generate-control-env.sh` calls it during [host installation](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host). ```bash rtctl control-env --config machine.json --pair-id cell-a --pair-revision 1 \ --out /tmp/staging --installed-out /etc/rosie-rt-core/compiled ``` | Flag | Type | Default | Description | |---|---|---|---| | `--config` | path | — | Live machine config. Required. Must have `"backend": "live"`. | | `--pair-id` | token | — | Deployment pair id: letters, digits, `_`, `.`, `:`, `-`. | | `--pair-revision` | uint64 | — | Positive decimal, no leading zeros. | | `--out` | dir | — | Private staging directory for the compiled output. Required. | | `--installed-out` | dir | — | Where the compiled directory will live once installed. Required; used in the paths it prints. | It prints these variables on stdout: ```text ROSIE_RT_PAIR_ID=cell-a ROSIE_RT_PAIR_REVISION=1 ROSIE_RT_CONFIGURATION_SHA256=<64 hex> ROSIE_RT_CORE_ARGS="" ROSIE_RT_COMPILED_CONFIG=/etc/rosie-rt-core/compiled/<64 hex> ``` It refuses an argument list containing characters that would be unsafe unquoted in an environment file. ## Host commands ### `hostcheck` Read-only inspection of a cell host's real-time prerequisites. It never measures cycle latency, so passing it does not qualify timing: it always reports `timing_qualification` as `not_applicable`. ```bash sudo rtctl hostcheck --config /etc/rosie-rt-core/machine.json # pass rt_cpu: observed="2" expected="configured nonnegative runtime.cpu (or deployed --rt-cpu)" # … ``` | Flag | Type | Default | Description | |---|---|---|---| | `--config` | path | — | Machine config to read `runtime.cpu` from. Without it, hostcheck reads `--rt-cpu` from `ROSIE_RT_CORE_ARGS` in `/etc/rosie-rt-core/control.env`. | | `--json` | bool | `false` | Print a `rosie-rt-core.hostcheck.v1` JSON report instead of lines. | | `--expected-release` | path | — | A `rosie-rt-core.host-identity.v1` manifest. Also compares the installed services and loaded executables against it, read-only. | It checks, among others: the RT CPU; kernel version and build; PREEMPT_RT (`/sys/kernel/realtime` or `PREEMPT_RT` in `uname -v`); the loaded IgH `ec_master` version (1.6.9); `/dev/EtherCAT0` permissions for the device group; the RT CPU in `isolated` and `nohz_full`; the CPU frequency governor (`performance`); EtherCAT NIC IRQ affinity; clock source, timer slack and resolution; service users, groups and directories. Each line is ` : observed="…" expected="…"`, with status `pass`, `fail` or `not_applicable`. | Variable | Default | Description | |---|---|---| | `ROSIE_RT_ETHERCAT_GROUP` | `ethercat` | Group expected on `/dev/EtherCAT0`. | | `ROSIE_RT_HOSTCHECK_ROOT` | `/` | Alternate filesystem root. Used by tests. | The `--expected-release` manifest has `schema` and a `services` map for exactly `rosie-rt-core`, `rosie-rt-control` and `rosie-rt-natspublisher`. Each entry gives `template_sha256`, `unit_files[]` (`path`, `sha256`), `argv`, `executable_sha256`, `environment_files` and `environment`. No tool in the repository generates it; the cell owner supplies it. ### `inventory` Compiles a live machine config, then reads the EtherCAT bus and compares each slave with what the configuration expects. It only issues read commands (`ethercat slaves` and SDO `upload`). ```bash sudo rtctl inventory --config /etc/rosie-rt-core/machine.json ``` | Flag | Type | Default | Description | |---|---|---|---| | `--config` | path | — | Live machine config. Required. | | `--profiles` | dir | `/config/drives` | Drive config directory. | | `--ethercat` | path | `ethercat` | The IgH `ethercat` tool to run. | | `--json` | bool | `false` | Print the inventory JSON document. | Each axis reports `match`, `mismatch` or `unreadable`. A configured I/O terminal is reported too. ### `recover-encoder` A maintenance procedure that clears an absolute-encoder alarm on a supported servo drive. It talks to the drive with the `ethercat` tool while the EtherCAT master is idle. ```bash sudo systemctl stop rosie-rt-control rosie-rt-core sudo rtctl recover-encoder --backend ethercat --slave 3 ``` | Flag | Type | Default | Description | |---|---|---|---| | `--slave` | integer | — | Absolute slave position on master 0, 0 to 65535. Required. | | `--backend` | `ethercat` | — | Required. | It refuses to write unless master 0 is idle (stop the daemon first), the slave is identified as a supported drive, is in PREOP and is disabled, and shows the specific encoder alarm it can clear. It refuses on any unexpected alarm between steps. It never starts the daemon or Home. > [!CAUTION] An encoder reset invalidates the axis's saved Home anchor. Home the axis again before you enable it, and do not restore an old anchor. ## Control commands These send one request to rt-control through the Go SDK and print the result as indented JSON (compact with `--json`). `describe` and `status` print the description or status document itself; the other commands print the response envelope (`schema`, `operation`, `sequence`, `handle`, `data`, `error`). ### Common flags | Flag | Type | Default | Description | |---|---|---|---| | `--socket` | path | `/run/rosie-rt-core/control.sock` | Local control socket. | | `--remote` | URL | — | Mutual-TLS HTTPS origin, for example `https://rosie.local:8443`. Exclusive with `--socket`. | | `--ca`, `--cert`, `--key` | path | — | Server CA, client certificate and client key PEM files. Only with `--remote`. See [Remote access](https://advancedmetalresearch.com/docs/apis/remote-access-mtls). | | `--session` | string | — | Session token from `acquire`. | | `--generation` | uint64 | 0 | Control generation from `acquire`. | | `--timeout` | duration | `10s` | Deadline for the HTTP operation. Must be positive. | | `--request-id` | string | generated | Reuse only to retry an identical request. See [idempotent retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). | | `--json` | bool | `false` | Compact JSON output. | ### Observation | Command | Flags | Sends | |---|---|---| | `describe` | — | [`describe`](/docs/apis/rt-control-http#describe) | | `status` | — | [`status`](/docs/apis/rt-control-http#status) | | `events` | `--after N` (default 0), `--follow` | [`subscribe_events`](/docs/apis/rt-control-http#subscribe-events). With `--follow`, streams one JSON event per line until Ctrl-C, then exits 0. `--timeout` does not apply to the stream. | ```bash build/rtctl events --after 0 --follow --socket "$TMPDIR/control.sock" ``` ### `resources fetch` Downloads the cell's robot description files, verified by digest. ```bash sha=$(build/rtctl describe --json | jq -r '.robot.resources[0].sha256') build/rtctl resources fetch "$sha" --out ./cell-model # ./cell-model/resources.json ``` | Argument or flag | Description | |---|---| | `` | A 64-hex, lowercase digest that is a member of the cell's current resource set, as listed by Describe. | | `--out DIR` | Output directory. Required. | It writes every resource as `DIR/` and then the manifest `DIR/resources.json`, last, so an incomplete set never looks complete. It never overwrites an existing file. A digest the cell does not list is refused with `resource_unknown`. ### Authority and machine control | Command | Flags | Sends | |---|---|---| | `acquire` | `--pair-id`, `--pair-revision`, `--configuration-sha256`, `--controller` (default `rtctl`) | [`acquire`](/docs/apis/rt-control-http#acquire). Prints the response envelope; its `data` is the grant, whose `session` and `generation` are the fence. rtctl does not set a requested lease, so the cell's ceiling applies. | | `release` | fence | [`release`](/docs/apis/rt-control-http#release) | | `stop` | fence | [`stop`](/docs/apis/rt-control-http#stop). A known session still stops after its lease expired. | | `halt` | fence | [`halt`](/docs/apis/rt-control-http#halt) | | `enable` | fence, `--axis-mask` | [`enable`](/docs/apis/rt-control-http#enable) | | `arm` | fence | [`arm`](/docs/apis/rt-control-http#arm) | | `home` | fence, `--axis-mask` | [`home`](/docs/apis/rt-control-http#home) | | `reset-fault` | fence | [`reset_fault`](/docs/apis/rt-control-http#reset-fault) (interim capability) | "Fence" means `--session` and `--generation`. The axis mask is a bit per axis, 0 to 0xffffffff. ```bash # Take authority, then stop and give it back. Each call is one request. build/rtctl acquire --pair-id readme --pair-revision 1 --configuration-sha256 "$digest" build/rtctl stop --session "$session" --generation "$generation" build/rtctl release --session "$session" --generation "$generation" ``` Because rtctl does not renew, commands after `acquire` that need a live lease are usually refused with an authority reason once the lease lapses. See [authority reasons](https://advancedmetalresearch.com/docs/reference/error-codes#reasons-authority). ### Trajectories and programs > [!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). | Command | Flags | Sends | |---|---|---| | `prepare` | fence, `--axis-mask`, `--file points.json` | [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory). The file is a JSON array of [`Point`](/docs/apis/rt-control-http#type-point): `time_ns`, `position` (rad or m), optional `velocity`. | | `start` | fence, `--handle` | [`start_trajectory`](/docs/apis/rt-control-http#start-trajectory) | | `discard` | fence, `--handle` | [`discard_trajectory`](/docs/apis/rt-control-http#discard-trajectory) | | `program-prepare` | fence, `--file program.rdt` | [`POST /v1/program`](/docs/apis/rt-control-http#prepare-program) with the binary `.rdt`. See [Dense trajectory format](https://advancedmetalresearch.com/docs/reference/rdt-format). | | `program-start` | fence, `--file identity.json` | [`start_program`](/docs/apis/rt-control-http#start-program). The file is an [`Identity`](/docs/apis/rt-control-http#type-identity) object. | rtctl commands bypass the weld planner's verifier: the core admits them against joint limits, velocity and continuity only. And because the lease lapses between commands, a prepared trajectory or program usually cannot be started from a second rtctl call. Drive motion from an SDK client instead. ## Telemetry ### `telemetry dump-to-jsonl` Converts a raw telemetry dump written by the core (`runtime.telemetry.directory`) into JSON Lines, one record per cycle. ```bash build/rtctl telemetry dump-to-jsonl /dev/shm/rt-core/telemetry/.bin > cycles.jsonl ``` Each line has the cycle time `t_ns`, `seq`, `cycle`, `work_ns`, cycle-level state (`armed`, grant and jog generations, `safety_fault_mask`, `execution_fault_reasons`, I/O words) and an `ax` array per axis (position `p` and target `tp` in counts, statusword `sw`, error codes, velocity and following error in counts, readiness and diagnostic fields). Values are in drive counts and ns, not rad. Keep the `.bin` file with its `.json` index. Usage: `rtctl telemetry dump-to-jsonl `. Exit 2 on wrong arguments. ## Related pages - [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http): what each control command sends - [Configuration files](https://advancedmetalresearch.com/docs/reference/configuration) - [Install rt-core on a cell host](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host) - [Your first motion](https://advancedmetalresearch.com/docs/get-started/first-motion): why the SDK, not rtctl, moves the robot - [Ports, sockets and environment variables](https://advancedmetalresearch.com/docs/reference/ports-and-environment) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/tools/rtctl/cmd/rtctl/main.go:10-34` - `rt-core/tools/rtctl/command.go:16-138,141-172` - `rt-core/tools/rtctl/run.go:13-76` - `rt-core/tools/rtctl/control_env.go:22-90` - `rt-core/tools/rtctl/control.go:1-194` - `rt-core/tools/rtctl/resources_cli.go:1-98` - `rt-core/tools/rtctl/hostcheck.go:21-100,169-294` - `rt-core/tools/rtctl/hostidentity.go:17-60,161` - `rt-core/tools/rtctl/inventory.go:60-146,245-322` - `rt-core/tools/rtctl/recover_encoder.go:13-60,110-273` - `rt-core/tools/rtctl/telemetry.go:12-261` - `rt-core/sdk/control/client.go:210-228` - `rt-core/adapters/rosie/control/api_generated.go:426-434` - `rt-core/adapters/rosie/control/resources.go:51-56,358-375` - `rt-core/Makefile:296-301` - `rt-core/config/templates/machine.json (runtime.telemetry, control)` --- # Configuration files (drive, machine, cell) > Field reference for rt-core machine, drive and cell configuration, with units, allowed ranges and defaults, plus what rtctl compile writes, the three identity digests, and the warnings and refusals the compiler returns. URL: https://advancedmetalresearch.com/docs/reference/configuration Section: RosieOS docs / Reference Last updated: 2026-10-10 rt-core reads one compiled configuration. You write it as JSON in `rt-core/config/`, and [`rtctl compile`](/docs/reference/rtctl#compile) turns it into an immutable directory named after its digest. This page lists every field of the machine, drive and cell layers. Robot descriptions have their own page: [Robot description files](https://advancedmetalresearch.com/docs/reference/robot-description). Compile a shipped simulation machine and print its digest directory: ```bash cd rt-core make control build/rtctl compile --config config/machines/simulation/simulation-program.json --out build/config # /…/rt-core/build/config/ ``` How the layers fit together is explained in [Cells, machines and positioners](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners). ## Templates `rt-core/config/templates/` holds one annotated template per layer: `machine.json`, `drive.json`, `cell.json` and `robot.json`. Every field has a `_doc` sibling that states its unit, allowed values and default. The tables below are drawn from those templates and the compiler. The templates are class definitions, not deployable configs. They show mutually exclusive branches side by side (flat axes and robot-bound axes, for example), and `` marks a value the example cannot choose. To use one, pick a branch, remove the `_doc` siblings and fill the values. Tests in `rt-core/tools/rtctl` and `rt-core/config/cells` check the templates against the compiler's readers and compile their simulation branches. None of them touches hardware. ### Provenance siblings Numeric robot facts and safety exceptions must say where they came from. Add a `_source` ("cited: …") or `_unverified` ("unverified: …") string beside the field. Missing provenance is a compile error. Free-text reasons (for example `collision_watchdog.reason`) must be non-empty. ## What compile writes `rtctl compile --out DIR` writes `DIR//`: | File | Contents | |---|---| | `argv.json` | The core's command line, from the compiled configuration. | | `axes.conf` | Per-axis native profiles. | | `configuration.identity` | The canonical text the configuration digest is computed over. | | `configuration.base.identity` | Only for a machine with an `io` block: the identity before cell I/O was bound. The final digest covers that base digest plus the `io` block and its terminal profile. | | `machine.identity`, `deployment.identity` | Canonical texts for the machine and deployment digests. | | `coordinate.identity.json` | Per-axis coordinate identities and their SHA-256. | | `robot.json` | The robot binding, when the machine pins a robot description. | | `resources.json`, `resources/` | The robot description files (and cell calibration) as content-addressed resources. `rt-control` serves them through Describe and `GET /v1/resources/`. | `rt-control` loads this directory through `--compiled-config` or `ROSIE_RT_COMPILED_CONFIG`. ### Three digests The compiler produces three SHA-256 digests. Describe reports all three. | Digest | Covers | Changes when | |---|---|---| | `configuration_sha256` | The original machine JSON and the resolved drive configs (and, for a robot-bound machine, the pinned description) | Any byte of meaning in the machine or its drives changes. This is the pin in cell configs, `rt-control --configuration-sha256` and every `acquire` binding. | | `machine_sha256` | Axis list and order, names, slave positions, units, scaling, gearing, sign, wrap; drive PDO, SDO, DC, Home, absolute, brake and coordinate blocks; limits, tolerances, jog durations, motor cap; cycle and lateness timing; bus bring-up policies | Anything that changes motion or drive behaviour. | | `deployment_sha256` | Runtime CPU and priority, host, NIC, sockets, pair id, service users, backend and other non-motion metadata | Only where and how the core runs. | Canonical identity text uses sorted keys, one `key=value` per line, `%.17g` doubles and decimal integers. ## Machine config A machine config binds drives, and optionally a robot description, to bus positions, timing, limits, I/O and host policy. The shipped ones are in `config/machines/`, `config/machines/bench/` and `config/machines/simulation/`. A machine uses one of two axis forms: - **Flat axes** declare their own scaling and limits. Simulation fixtures and bare-motor benches use them. - **Robot-bound axes** name a `robot_joint` in a pinned robot description. They inherit travel, velocity, acceleration, gearing and Home policy from the description, and must not redeclare them. config/machines/simulation/simulation-rosie1400.json (excerpt): ```json { "schema_version": 1, "backend": "simulation", "cycle_ns": 1000000, "robot": { "robot_description": { "path": "robot_description/robots/rosie_1400_v3", "sha256": "sha256:", "source": "cited: …" } }, "axes": [ { "robot_joint": "J1", "slave_position": 0, "limits": { "max_target_lead": 0.02, "following_error": 0.04, "following_error_timeout_ms": 100, "completion_tolerance": 0.0002, "completion_timeout_ms": 500 } } ], "max_cycle_lateness_ns": 20000000, "runtime": { "cpu": 2, "priority": 90 } } ``` > [!NOTE] On the simulated bus, `simulation-rosie1400.json` takes the real drive profile from the Rosie 1400 definition, and Home is refused on it, so nothing arms or jogs. For a simulated cell that homes, use the dev-stack machine described in [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation). ### Top level | Field | Type | Unit | Allowed | Default | |---|---|---|---|---| | `schema_version` | integer | — | `1` | Required | | `backend` | string | — | `simulation` (synthetic drives) or `live` (hardware) | Live admission. `rtctl run --backend live`, `control-env` and `inventory` require `live`. | | `cycle_ns` | integer | ns | 250000 to 10000000, divisible by every nonzero drive DC quantum | Required. Shipped machines use 1000000 (1 kHz). | | `max_cycle_lateness_ns` | integer | ns | 1 to 249999999 | Required | | `axes` | array | — | 1 to 16 ordered axes | Required | | `drive_speed_limit_motor_rpm` | number | motor rpm | > 0; at most 6000 on the supported servo motor frame | 3000 for flat fixtures. Forbidden on robot-bound machines: their cap derives from the URDF velocity. | | `max_motor_rpm` | number | legacy wire rpm | > 0; exclusive with `drive_speed_limit_motor_rpm` | Omitted. Deprecated: compile warns. | | `runtime` | object | — | See [runtime](https://advancedmetalresearch.com/docs/reference/configuration#machine-runtime) | Required | | `robot` | object | — | See [robot](https://advancedmetalresearch.com/docs/reference/configuration#machine-robot) | Absent for flat axes | | `control` | object | — | See [control](https://advancedmetalresearch.com/docs/reference/configuration#machine-control) | `lan` profile | | `bus` | object | — | See [bus](https://advancedmetalresearch.com/docs/reference/configuration#machine-bus) | Unknown slaves refused | | `bus_bringup` | object | — | See [bus_bringup](https://advancedmetalresearch.com/docs/reference/configuration#machine-bus-bringup) | Defaults below | | `io` | object | — | See [io](https://advancedmetalresearch.com/docs/reference/configuration#machine-io) | Absent | ### `axes[]`, flat form | Field | Type | Unit | Allowed | Default | |---|---|---|---|---| | `name` | token | — | ASCII letters, digits, `_`; unique | Required | | `slave_position` | integer | EtherCAT position | 0 to 65535, unique across axes, I/O and unused slaves | Required | | `profile` | string | — | A drive config basename in `config/drives/`, without `.json` | Required | | `type` | string | — | `rotary` or `linear` | Required | | `wire_counts_per_rev` | integer | counts per wire revolution | 1 to 2147483647 | Required | | `motor_encoder_counts_per_rev` | integer | counts per motor revolution | 1 to 2147483647 | Required | | `wire_revs_per_axis_rev` | number | wire revolutions per axis revolution | > 0; `single_turn_absolute` requires 1 | Required | | `gear_ratio.numerator`, `.denominator` | integer | ratio | 1 to 2147483647 each | 1 for flat simulation | | `lead_m_per_rev` | number | m per revolution | > 0 for linear axes | 0 for rotary | | `sign` | integer | — | −1 or +1 | Required | | `coordinate_evidence_mode` | string | — | `multi_turn` or `single_turn_absolute` | Required on flat axes. A generic simulation drive uses `multi_turn`. | | `position_tracking_mode` | string | — | Identity token | `continuous` in simulation; required otherwise | | `require_home` | boolean | — | `true` requires the drive's `native_home` | Required on flat axes | | `max_acceleration` | number | rad/s² or m/s² | > 0 | Flat fixtures only | | `limits` | object | — | See [limits](https://advancedmetalresearch.com/docs/reference/configuration#machine-limits) | Required | | `startup` | object | — | Per-axis drive startup overrides, constrained by the drive's `startup_schema` | Drive `startup_defaults` | | `jog`, `brake`, `collision_watchdog`, `diagnostics` | object | — | See below | Omitted | ### `axes[]`, robot-bound form | Field | Type | Description | |---|---|---| | `robot_joint` | token | Exactly one joint name from the pinned robot definition. No joint twice. | | `slave_position` | integer | EtherCAT position, as above. | | `limits` | object | Only the tracking fields: `max_target_lead`, `following_error`, `following_error_timeout_ms`, `completion_tolerance`, `completion_timeout_ms`. | | `startup`, `jog`, `brake`, `collision_watchdog`, `diagnostics` | object | As for flat axes. | A robot-bound axis must omit `limits.min`, `limits.max`, `limits.max_velocity`, `limits.jog_acceleration`, `max_acceleration` and `coordinate_evidence_mode`. It inherits them: - travel and maximum velocity from `robot.urdf` - trajectory and jog acceleration from `config.json` → `planning.joints..acceleration_rad_s2` (required) - the per-axis drive speed cap, derived from the URDF velocity through the definition's gearing and encoder scale - Home policy and coordinate evidence mode from the definition Every joint in the definition needs an axis, unless you list it in `robot.absent_joints`. ### `limits` | Field | Type | Unit | Allowed | Default | |---|---|---|---|---| | `min`, `max` | number | rad (rotary) or m (linear) | Finite, `min` < `max` | Required on flat axes; inherited on robot-bound axes | | `max_velocity` | number | rad/s or m/s | > 0 | Flat fixtures only | | `jog_acceleration` | number | rad/s² or m/s² | > 0 | Flat fixtures only | | `max_target_lead` | number | rad or m | > 0 | Required | | `following_error` | number | rad or m | > 0 | Required | | `following_error_timeout_ms` | integer | ms | 1 to 1000 | Required | | `completion_tolerance` | number | rad or m | > 0 | Required | | `completion_timeout_ms` | integer | ms | 1 to 4294967295 | Required | ### `jog` The ramp when jog input stops. Also settable per drive; the axis value wins. | Field | Type | Unit | Allowed | Default | |---|---|---|---|---| | `arrest_ns` | integer | ns | 1 to 65535 × `cycle_ns` | 200000000 (200 ms) | | `quick_stop_ns` | integer | ns | 1 to 65535 × `cycle_ns` | 300000000 (300 ms) | ### `brake` and `brake_override` Brake fields are taken from the drive config first, then overridden per axis. | Field | Type | Unit | Allowed | Default | |---|---|---|---|---| | `brake.present` | boolean | — | `false` cannot keep `gravity_axis` or `released_signal` | `false` | | `brake.gravity_axis` | boolean | — | `true` only with `present` | `false` | | `brake.release_delay_ms` | integer | ms | 0 to 4294967295 | 100 | | `brake.hold_delay_ms` | integer | ms | 0 to 4294967295 | 100 | | `brake.hold_displacement_tolerance_counts` | integer | counts | 0 to 2147483647 | 1 | | `brake.released_signal.semantic` | string | — | An existing TX PDO semantic containing the bit | Required with `released_signal` | | `brake.released_signal.bit` | integer | bit | 0 to 31 for mapped brake feedback | Required with `released_signal` | | `brake_override.present` | boolean | — | `false` only | Required with `brake_override` | | `brake_override.reason` | string | — | Non-empty, with a validated bench hold policy | Required with `brake_override` | `brake_override` exists for benches whose brake wiring is not yet verified. It disables brake handling on that axis and must say why. ### `collision_watchdog` Faults the axis when torque or following error stays above a bound. It detects an impact after it happens; it does not avoid one. Also settable per drive. | Field | Type | Unit | Allowed | Default | |---|---|---|---|---| | `torque_abs_max_raw` | integer | raw drive torque units | 1 to 1000 | Required when enabled | | `following_error_counts_max` | integer | counts | 1 to 2147483647 | Required when enabled | | `sustained_cycles` | integer | cycles | 2 to 1000 | Required when enabled | | `disabled` | boolean | — | `true` needs a `reason` and no thresholds; `false` needs thresholds and a torque PDO | `false` | | `reason` | string | — | Non-empty | Required when disabled | Both branches need `_source` and `_unverified` siblings. An axis with no watchdog at all compiles with a warning: `axis : collision_watchdog missing; disabled`. ### `diagnostics` | Field | Type | Allowed | Default | |---|---|---|---| | `external_enable_input.bit_index` | integer | 0 to the mapped digital-input width minus 1, at most 31 | Required with the block | | `external_enable_input.polarity` | string | `active_high` or `active_low` | Required with the block | This lets rt-core report the state of an external enable, such as an auxiliary contact on the cell's stop chain. It is a diagnostic only. It never gates motion and is not a safety function. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model#hardware-e-stop). ### `runtime` | Field | Type | Unit | Allowed | Default | |---|---|---|---|---| | `cpu` | integer | logical CPU | 0 to 1023 | Required | | `priority` | integer | SCHED_FIFO priority | 1 to 99 | Required | | `telemetry.retention_ms` | integer | ms | 100 to 3600000, within a 2 GiB ring ceiling | 100000 | | `telemetry.dump_count` | integer | files | 1 to 100 | 20 | | `telemetry.dump_bytes` | integer | bytes | 529680 or more | 536870912 | | `telemetry.directory` | string | absolute path | Non-root, no NUL | The runtime chooses | The core writes telemetry dumps (a `.bin` file and its `.json` index) to `telemetry.directory`. Convert one with [`rtctl telemetry dump-to-jsonl`](/docs/reference/rtctl#telemetry). ### `robot` | Field | Type | Description | |---|---|---| | `robot_description.path` | path | Repository-relative `robot_description/robots/`. The manifest must register `rtcore_definition.json`. | | `robot_description.sha256` | `sha256:<64 hex>` | The description identity, as `go run ./cmd/identity` prints it. Compile refuses a mismatch with `robot_description_mismatch`. | | `robot_description.source` | string | Provenance. | | `absent_joints` | string[] | Definition joints this machine has no axis for. Default empty. | | `bench_measurement_profile` | string | Restricted to one bare-motor bench measurement profile. Omit otherwise. | | `machine_planning_calibration.path` | path | Component-relative `config/...` path to this machine's `machine_planning_calibration.json`. Every joint it corrects must exist and its frames must name known links. | | `machine_planning_calibration.sha256` | 64 hex | SHA-256 of the file's exact bytes. | | `machine_planning_calibration.source` | string | Provenance: who measured it, how and when. | Omit `machine_planning_calibration` and the cell serves the empty calibration document for its model. ### `control` The per-cell lease and jog-age ceilings. See [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority). | Field | Type | Unit | Allowed | Default | |---|---|---|---|---| | `link_profile` | string | — | `lan` or `internet` | `lan` | | `max_grant_lease_ns` | integer | ns | 1000000 to 10000000000, whole milliseconds | 500000000 on `lan`, 3000000000 on `internet` | | `max_jog_input_age_ns` | integer | ns | 1 to 2000000000 | 250000000 on `lan`, 750000000 on `internet` | > [!WARNING] A longer lease or jog age delays the unattended stop after a client or link failure by the same amount. Keep the `lan` defaults unless you have measured a reason not to. The `internet` values are marked unverified in the code. ### `bus` | Field | Type | Allowed | Default | |---|---|---|---| | `unknown_slaves` | string | `refuse`, or `hold` (needs `unknown_slaves_note`) | `refuse` | | `unknown_slaves_note` | string | Why extra slaves may stay on the bus | — | | `hold_on_robot_acknowledged` | boolean | `true` plus `hold_on_robot_note` for a robot machine using `hold` | `false` | | `unused_slaves[]` | array | 0 to 256 entries: `slave_position`, `vendor_id`, `product_code`, `revision`, `note` | Empty | `hold` leaves unknown slaves in PREOP without outputs. It is meant for test benches; an assembled robot cell must use `refuse`. ### `bus_bringup` | Field | Type | Unit | Default | |---|---|---|---| | `startup_passive_ms` | integer | ms | 0 | | `explicit_pdo_config` | boolean | — | `false`. `true` is incompatible with fixed drive PDO presets. | | `disable_output_watchdog` | boolean | — | `false`. Use `true` only with a declared machine failure policy. | | `no_dc` | boolean | — | `false`. `true` disables distributed-clock setup. | | `wait_before_safeop_ms` | integer | ms | 250 | | `preop_safeop_timeout_ms` | integer | ms | 5000 | | `safeop_op_timeout_ms` | integer | ms | 5000 | ### `io` Cell I/O through a digital I/O terminal. See [Process I/O and sensing](https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing). | Field | Type | Unit | Allowed | Default | |---|---|---|---|---| | `profile` | string | — | A terminal drive config in `config/drives/` | Required | | `slave_position` | integer | EtherCAT position | 0 to 65535, unique | Required | | `torch_qualified` | boolean | — | `false` only | Required | | `inputs[]` | array | — | 0 to 8 unique bits | Required | | `inputs[].name` | token | — | Unique | Required | | `inputs[].bit` | integer | bit | 0 to 7 | Required | | `inputs[].polarity` | string | — | `active_high` or `active_low` | Required | | `inputs[].class` | string | — | `fast` or `supervisory` | Required | | `outputs[]` | array | — | 0 to 8 unique bits | Required | | `outputs[].name`, `.bit` | token, integer | —, bit | As for inputs | Required | | `outputs[].safe_state` | boolean | — | `false` (OFF) only | Required | | `outputs[].expiry_ns` | integer | ns | 1 to 1000000000 | Required | | `outputs[].class` | string | — | `process` or `torch`. Torch outputs are always refused at runtime. | Required | | `outputs[].readback_bit` | integer | bit | 0 to 7, unique per output | Required | | `outputs[].readback_polarity` | string | — | `active_high` or `active_low` | Required | `config/cell-io/simulation.json` is a complete simulation machine with an `io` block, not a terminal descriptor. ## Drive config A drive config describes one drive or I/O terminal model: its EtherCAT identity, PDO layout, scaling semantics, startup parameters, Home transaction and protective defaults. A machine axis selects one with `profile`; a robot definition with `drive_profile`. Names resolve only in `config/drives/`. The template is `config/templates/drive.json`. | Field | Type | Description | |---|---|---| | `schema_version` | integer | `1`. | | `id`, `label` | string | Metadata only. `profile` selects the file, not `id`. | | `simulation_only` | boolean | `true` requires `backend: simulation`. Default `false`. | | `ethercat.vendor_id`, `.product_code`, `.revision_no` | integer | Expected slave identity. | | `ethercat.rx_pdo`, `.tx_pdo`, `.rx_sync`, `.tx_sync` | integer | PDO assignment object indices and sync managers. | | `ethercat.dc_quantum_ns` | integer | ns. A nonzero quantum must divide `cycle_ns`. | | `ethercat.dc_assign_activate` | integer | DC activation bit mask. | | `ethercat.rx_layout[]`, `.tx_layout[]` | array | Mapped PDO entries: `semantic`, `index`, `subindex`, `bits`. At most 32 entries in total. RX must map `cw`, `target_pos` and `mode`; TX must map `sw`, `pos` and `mode_disp`. | | `ethercat.restore_assignment_on_exit` | boolean | Default `false`. | | `home_truth_sign` | −1 \| +1 | Direction of the drive's Home reference. | | `native_home` | object | The Home transaction: `steady_state_mode` and `commissioning_mode` (CiA402 mode numbers), `truth_source`, and an ordered `transaction[]` of `set_mode`, `restore_mode`, `write_sdo`, `wait_sdo`, `write_sdo_wrap_fraction`, `controlword_sequence`, `wait_statusword`, `refresh_truth` and `release_service_override` steps. | | `feedback_counts_wrap`, `command_counts_wrap` | boolean | Whether position feedback and commands wrap. Linear axes require `false`. | | `startup_schema`, `startup_defaults` | object | Which startup parameters the drive accepts, their types and ranges, and the default written at startup. A machine axis overrides them under `startup`. | | `absolute_feedback[]`, `absolute_pair_field` | array, string | Absolute encoder readbacks used for coordinate evidence. | | `coordinate_evidence` | object | Policy for accepting absolute position evidence. | | `position_semantics.drive_native_ratio_enabled` | boolean | `true` when the drive scales to the output shaft; `false` when software scales from the motor shaft. | | `jog`, `brake`, `collision_watchdog` | object | Defaults for the axis blocks above. | | `max_acceleration` | number | Synthetic fixtures only. Never applies to a robot-bound axis. | The PDO semantics RX accepts are `cw`, `target_pos`, `target_vel`, `target_torque`, `mode`, `tp_func` and `max_profile_vel`. TX semantics include `sw`, `pos`, `mode_disp`, `err`, `manufacturer_err`, `velocity_actual`, `following_error`, `torque`, `di` and the drive's extended diagnostics. > [!NOTE] The shipped drive configs carry values measured on specific drive firmware. Treat them as the source of those values, and do not copy them into other documents. ## Cell config A cell config binds one deployed cell to a machine config and its compiled digest. The template is `config/templates/cell.json`. config/templates/cell.json (without _doc fields): ```json { "name": "simulation-cell", "nodes": [ { "runtime": "rt-core", "rt_core": { "machine_config": "rt-core/config/machines/simulation/simulation.json", "configuration_sha256": "<64 hex from rtctl compile>", "pair_id": "simulation-cell", "pair_revision": 1, "remote_listen": "127.0.0.1:8443" } } ] } ``` | Field | Type | Allowed | Default | |---|---|---|---| | `name` | string | Display text. Ignored by the validator. | Omitted | | `nodes[]` | array | Ordered nodes | Required | | `nodes[].runtime` | string | `rt-core` for a native node | — | | `nodes[].rt_core.machine_config` | path | Repository-relative path to an existing machine config, with no traversal or escaping symlink | Required | | `nodes[].rt_core.configuration_sha256` | 64 lowercase hex | The exact `rtctl compile` digest | Optional to the reader. Pin it on every deployed cell. | | `nodes[].rt_core.pair_id` | token | Letters, digits, `_`, `.`, `-` | Required | | `nodes[].rt_core.pair_revision` | integer | Positive uint64 | Required | | `nodes[].rt_core.remote_listen` | host:port | IP or DNS host, port 1 to 65535 | Required | The validator in Go package `rosieos/rt-core/config/cells` recompiles every node's machine config and refuses a mismatched digest, axis count, pair or listener: ```bash cd rt-core go test -count=1 ./config/cells -run TestRepositoryCompatibility ``` Cell configs in `config/cells/` also carry deployment fields for the deploy tooling. Those fields select and pin a configuration but never enter its digest. ## Compile warnings Warnings go to stderr, prefixed `warning:`. The compile still succeeds. | Warning | Meaning | |---|---| | `axis : collision_watchdog missing; disabled` | The axis has no collision watchdog. | | `max_motor_rpm is accepted for one release …` | Migrate to `drive_speed_limit_motor_rpm` (motor rpm). | | `legacy flat axes are accepted for one release …` | Migrate to a robot definition and `robot_joint` axes. | | `axis motor speed cap rpm exceeds … rated 3000 rpm …` | The axis speed cap is above the servo motor's rated speed but within its 6000 rpm maximum. Above the maximum, compile refuses. | ## Robot compile refusals When a machine pins a robot description, compile refuses with `: `: | Reason | Cause | |---|---| | `robot_description_unavailable` | The description directory or its files cannot be read. | | `robot_description_mismatch` | The pinned identity differs from the files, the definition's `robot_id` differs from the directory, or the directory has a `robot.urdf` its manifest does not register. The detail gives both hashes. | | `robot_definition_unavailable` | The manifest does not register `rtcore_definition.json`. | | `robot_definition_field` | An unexpected field, or a restricted field used where it is not allowed. | | `robot_duplicate_field` | A JSON key appears twice. | | `robot_reference_path` | A path escapes the repository root or is not a valid relative path. | | `robot_hash_invalid` | A pinned hash is not lowercase SHA-256. | | `robot_hash_mismatch` | A pinned file hashes to something else. | | `robot_joint_mapping` | An axis names an unknown, duplicate or absent `robot_joint`. | | `robot_joint_missing` | A definition joint has no axis. Declare it in `robot.absent_joints`. | | `robot_urdf_limits_required` | An axis or definition tries to redeclare a limit, velocity or gearing it must inherit. | | `robot_limit_widened` | A requested value exceeds the model's bound. | | `robot_acceleration_required` | `config.json` has no `planning.joints..acceleration_rad_s2` for a mapped axis. | | `robot_bench_inheritance` | A bench machine redeclares a field it must inherit from the robot definition. | | `machine_planning_calibration_invalid` | The pinned calibration fails its rules, or the description has no geometry to calibrate. | Other compile errors print a plain message and exit 1. Nothing is written until the whole configuration is valid. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/config/templates/machine.json:1-321` - `rt-core/config/templates/drive.json:1-629` - `rt-core/config/templates/cell.json:1-24` - `rt-core/config/templates/robot.json` - `rt-core/config/machines/simulation/simulation-rosie1400.json` - `rt-core/config/machines/simulation/simulation-program.json` - `rt-core/tools/rtctl/command.go:16-138` - `rt-core/tools/rtctl/compiler.go:171-200,265-280,650-745,988-1045` - `rt-core/tools/rtctl/compiled_resources.go:130-154` - `rt-core/tools/rtctl/collision_watchdog.go:9-45` - `rt-core/tools/rtctl/robot_definition.go:66-160,200-230,313-420,500-735` - `rt-core/tools/rtctl/render.go:170` - `rt-core/config/cells/compatibility.go` - `rt-core/cmd/rt-control/main.go:24-39` - `motion-server/v1/local-rt-core.sh:17-28` --- # Robot description files > Field reference for a RosieOS robot description: the manifest and its identity algorithm, config.json, rtcore_definition.json, spheres.json and a cell's machine_planning_calibration.json, with units, rules and the tools that check them. URL: https://advancedmetalresearch.com/docs/reference/robot-description Section: RosieOS docs / Reference Last updated: 2026-10-10 This page lists every file in a robot description directory, `robot_description/robots//`, with its fields, units and the rules the loaders enforce. For what a description is and how its identity is used, read [Robot description and coordinate frames](https://advancedmetalresearch.com/docs/concepts/robot-description-and-frames) first. Check every description in the repository: ```bash python3 robot_description/tools/manifest.py --check # manifest: rosie_1400_v3 ok, files (one line per model) ``` ## Loaders and tools | Tool | What it does | |---|---| | `python3 robot_description/tools/manifest.py --check [model ...]` | Reports a registered file that is missing, a needed file that is not registered, and a mesh the URDF references that is not in `meshes/`. It does not fail on unregistered extra files. Exit 1 on any problem. CI runs this. | | `python3 robot_description/tools/manifest.py ` | Rewrites that model's manifest and prints its identity. It imports `weldplan` from `weld_planner/v1/python` to compute the hash. | | `go run ./cmd/identity ...` (in `robot_description/go`) | Prints `sha256: ` for each directory. Exit 1 if any fails, 2 with no arguments. | | Go module `rosieos/robotdesc` | `DescriptionFiles`, `RobotDescriptionSHA256`, `HashFiles`, `HashEntries`, `MachinePlanningCalibrationSHA256`, `Store.Load`, `Resolve`. Used by `rtctl` and OLP. | | `rtctl compile` | Checks the machine's pin against the identity, reads `rtcore_definition.json`, and refuses with a [robot compile reason](https://advancedmetalresearch.com/docs/reference/configuration#robot-compile-refusals). | ## `robot_description_manifest.json` robot_description/robots/rosie_1420_v1/robot_description_manifest.json (shape): ```json { "schema": "rosie.robot-manifest.v1", "model_id": "rosie_1420_v1", "files": ["config.json", "meshes/link_1.dae", "robot.srdf", "robot.urdf", "rtcore_definition.json", "spheres.json"] } ``` | Field | Type | Required | Description | |---|---|---|---| | `schema` | string | Yes | Exactly `rosie.robot-manifest.v1`. | | `model_id` | string | Yes | Must equal the directory name. | | `files` | string[] | Yes | Non-empty. Paths relative to the description directory, with `/` separators. | Rules for `files`, applied by `DescriptionFiles`: - No empty, absolute or backslash path, no `.` or `..` segment, no duplicates. - The manifest cannot list itself. - It cannot list `machine_planning_calibration.json`. That name belongs to the cell's own file, which is served beside the description. - Every entry must be a regular file. `manifest.py` registers `robot.urdf`, `robot.srdf`, `config.json`, `spheres.json` and `rtcore_definition.json` when present, plus every file in `meshes/`. Everything else in the directory is provenance. ### Identity algorithm The identity is a domain-separated SHA-256 over the manifest and every registered file: 1. Build the entry list: the manifest itself, plus every file in `files`. Each entry is its path and the SHA-256 of its bytes. 2. Sort the entries by path (byte order). 3. Hash, in order: - the ASCII domain `rosie.robot-description.identity.v1` followed by one `0x00` byte - the entry count, as a big-endian uint64 - for each entry: the path length as a big-endian uint64, the path bytes, then the 32-byte file digest 4. Write the result as `sha256:` followed by 64 lowercase hex digits. Because the manifest is an entry, removing a name from it changes the identity even if the file stays on disk. ## `robot.urdf` and `robot.srdf` Standard URDF and SRDF, with these constraints: - The URDF `` must equal the model id. - Every movable joint needs bounded ``. Revolute limits are in rad and `velocity` in rad/s. - Mesh references use `package://.../meshes/`, and each named mesh must be registered. - The SRDF defines a `manipulator` group for Tesseract. On the Rosie models it also defines a `home` group state with every joint at 0. ## `config.json` Schema `rosie.robot-config.v1`. Unknown fields are rejected. A description with no `config.json` still loads, with zero correction caps (no cell correction can be accepted). | Field | Type | Unit | Description | |---|---|---|---| | `schema` | string | — | `rosie.robot-config.v1`. | | `model_id` | string | — | Must equal the directory name. | | `frames.base` | link name | — | Frame everything is measured in. | | `frames.tool` | link name | — | Tool centre point. | | `frames.work` | frame name | — | Frame a workpiece is fixtured to. | | `frames.work_surface.parent` | link name | — | Optional. The link the work frame is attached to. | | `frames.work_surface.xyz_m` | [3]number | m | Offset of the work frame from `parent`. | | `kinematic_correction_caps.xyz_m` | number | m | Largest per-component translation a cell calibration may apply. Not negative. | | `kinematic_correction_caps.rpy_rad` | number | rad | Largest per-component rotation a cell calibration may apply. Not negative. | | `planning.speed_ceiling_rad_s` | number | rad/s | Planning speed ceiling. Must not exceed the URDF's fastest revolute joint. | | `planning.joints..velocity_rad_s` | number | rad/s | Planning velocity. Positive, and not above the URDF `velocity` of that joint. | | `planning.joints..acceleration_rad_s2` | number | rad/s² | Planning acceleration. Positive. rt-core also uses it for the axis's trajectory and jog acceleration, and requires it for every robot-bound axis. | | `axes.driven` | string[] | — | Joints the planner moves. Each must be a revolute URDF joint. | | `axes.held` | map | rad | Joints the planner keeps fixed, with their value. Finite. | | `reset_pose` | map | rad | Named reset position per joint. | | `torch.tool_frame` | link name | — | The torch's frame, normally `tool0`. | | `torch.electrode_axis` | [3]number | unit vector | Wire direction in the tool frame. | Every joint named in `planning.joints`, `axes` and `reset_pose` must be a revolute joint in the URDF. Do not restate a number the URDF already holds: state only what the URDF cannot. ## `rtcore_definition.json` Schema `rosie.robot-definition.v1`. It tells rt-core how each joint maps to a drive. The annotated template is `rt-core/config/templates/robot.json`, where every field has a `_doc` sibling with its unit, range and default. > [!NOTE] This file carries each joint's gearing and encoder scale. These docs list the fields only. Take the values from the robot's own definition file. Every numeric robot fact needs a `_source` or `_unverified` sibling that says where the number came from. Duplicate JSON keys and unexpected fields are rejected. | Field | Type | Description | |---|---|---| | `schema` | string | `rosie.robot-definition.v1`. | | `robot_id` | token | Must equal the description directory name and the URDF robot name. | | `name` | string | Display name. Non-empty. | | `revision` | integer | Model revision, 1 to 2⁶⁴−1. | | `calibration.identity` | token | Must be `home_calibration`. Home offsets and encoder anchors live in a separate Home calibration record, never here. | | `calibration.note` | string | Non-empty. | | `defaults.drive_profile` | drive config name | Default drive config (a basename in `rt-core/config/drives/`). | | `defaults.coordinate_evidence_mode` | `multi_turn` \| `single_turn_absolute` | How absolute position evidence is judged. | | `defaults.home_policy` | `required` \| `optional` | Whether joints need Home before motion. Default `required`. | | `joints[]` | array | 1 to 16 uniquely named joints that together map every movable URDF joint. | | `joints[].name` | token | Joint name, for example `J1`. | | `joints[].kind` | `arm` \| `positioner` \| `external` | Joint role. | | `joints[].urdf_joint` | string \| null | The URDF joint this drive turns. `null` only for a non-Cartesian external or positioner joint. `arm` joints require a mapping. | | `joints[].cartesian_participates` | boolean | `true` only with a valid URDF mapping. | | `joints[].drive_profile` | string \| null | Per-joint drive config; `null` inherits `defaults.drive_profile`. | | `joints[].gear_ratio.numerator`, `.denominator` | integer | Mechanical ratio, each 1 to 4294967295. Requires a source. | | `joints[].motor_encoder_counts_per_rev` | integer | Encoder counts per motor revolution, 1 to 2147483647. | | `joints[].sign` | −1 \| +1 | Direction multiplier from logical to drive motion. | | `joints[].encoder_battery` | `present` \| `absent` | Whether the absolute encoder has battery backup. Requires `encoder_battery_source`. | | `joints[].mechanical_travel.min`, `.max` | number | rad or m. Only for an external joint with no URDF joint. URDF-mapped joints inherit their URDF limits. | ## `spheres.json` The arm's collision spheres, used by the weld planner's screen. The verifier re-checks results against the exact meshes. | Field | Type | Description | |---|---|---| | `schema` | string | `amr-weld-planner-v1.sphere-model.v1`. | | `cell_id` | string | The model id. | | `provenance.robot_urdf_sha256` | hex | SHA-256 of the URDF the spheres were fitted against. | | `provenance.meshes_sha256` | `sha256:` | Hash of the meshes they were fitted against. | | `note` | string | Free text. | | `links.[]` | array | Spheres in that link's own frame: `center_m` [3]number (m) and `radius_m` number (m). | | `coverage` | object | The fit's coverage report. | A URDF or mesh change that leaves `provenance` stale is caught by the planner's provenance test. Refit or restamp with `weld_planner/v1/tools/fit_spheres.py` (see [Add a robot model](https://advancedmetalresearch.com/docs/guides/add-a-robot-model#spheres)). ## `machine_planning_calibration.json` Per cell, not in the repository. Schema `rosie.machine-planning-calibration.v1`. A machine config pins it with `robot.machine_planning_calibration{path, sha256, source}`, and the compiled configuration serves it beside the description. Unknown fields are rejected. machine_planning_calibration.json (example shape, values illustrative): ```json { "schema": "rosie.machine-planning-calibration.v1", "model_id": "rosie_1420_v1", "limits": { "speed_ceiling_rad_s": 2.0, "joints": { "J1": { "lower_rad": -3.0, "upper_rad": 3.0, "velocity_rad_s": 1.5 } }, "held": {} }, "kinematics": { "J2": { "xyz_m": [0.0005, 0, 0], "rpy_rad": [0, 0.001, 0] } } } ``` | Field | Type | Unit | Rule | |---|---|---|---| | `schema` | string | — | `rosie.machine-planning-calibration.v1`. | | `model_id` | string | — | Must equal the description's model id. | | `limits.speed_ceiling_rad_s` | number | rad/s | Positive, and not above the URDF's fastest revolute joint. | | `limits.joints..lower_rad` | number | rad | Revolute URDF joint with stated limits. Must not be below the URDF lower limit. | | `limits.joints..upper_rad` | number | rad | Must not be above the URDF upper limit, and must stay above `lower_rad`. | | `limits.joints..velocity_rad_s` | number | rad/s | In (0, URDF velocity]. | | `limits.held.` | number | rad | Finite, and inside the joint's limits (narrowed ones if given). | | `kinematics..xyz_m` | [3]number | m | Each component's magnitude at most `kinematic_correction_caps.xyz_m`. | | `kinematics..rpy_rad` | [3]number | rad | Each component's magnitude at most `kinematic_correction_caps.rpy_rad`. | A correction composes onto the URDF joint origin as T' = T_origin · Trans(xyz) · R(rpy), where R follows URDF convention Rz(yaw)·Ry(pitch)·Rx(roll). ### The empty document A cell with no calibration serves this exact document for its model, and planners hash it the same way: ```text {"schema":"rosie.machine-planning-calibration.v1","model_id":""} ``` followed by one newline (`\n`). ### Calibration identity The identity is the SHA-256 of the file's exact bytes, never a re-serialisation. Planners and program headers write it `sha256:`. The machine config pin (`robot.machine_planning_calibration.sha256`) is the same digest as 64 bare lowercase hex digits. ## Program header identities A dense program's `robot_cell` header block carries these fields, which OLP Load compares with the cell: | Field | Compared at Load | Description | |---|---|---| | `model_id` | Yes | The model the plan was made for. | | `robot_description_sha256` | Yes | The description identity. | | `machine_planning_calibration_sha256` | Yes | The calibration identity, or the empty document's. | | `machine_planning_calibration_source` | No | `target` if the bytes came from the cell, `empty` if the plan used the empty document. | | `machine_configuration_sha256` | No | `sha256:` plus the machine's configuration digest, when known. A difference only adds a note. | See [Load refusals](https://advancedmetalresearch.com/docs/concepts/robot-description-and-frames#load-refusals) and [Dense trajectory format](https://advancedmetalresearch.com/docs/reference/rdt-format). ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `robot_description/go/identity.go:32-167` - `robot_description/go/cmd/identity/main.go:1-32` - `robot_description/go/description.go:20-285` - `robot_description/go/cell.go:26-300` - `robot_description/tools/manifest.py:1-131` - `robot_description/robots/rosie_1400_v3/robot.srdf` - `robot_description/robots/rosie_1400_v3/spheres.json:1-8` - `robot_description/robots/rosie_1420_v1/spheres.json` - `robot_description/robots/rosie_1400_v3/rtcore_definition.json (keys only)` - `rt-core/config/templates/robot.json:1-109` - `rt-core/tools/rtctl/robot_definition.go:66-160,200-230,313-420,426-500,650-735` - `rt-core/config/templates/machine.json (robot.machine_planning_calibration)` - `offline-programming/v1/internal/denseexec/robot.go:20-110` - `weld_planner/v1/tools/fit_spheres.py:28-33,168-226` --- # Mechanical interfaces > How to mount Rosie 1400 and attach tooling. Base-plate anchor pattern, tool-flange hole pattern, interface frames and dimensions measured from the CAD, with STEP, PDF and JSON downloads. URL: https://advancedmetalresearch.com/docs/reference/mechanical-interfaces Section: RosieOS docs / Reference Last updated: 2026-10-10 Rosie 1400 has two mechanical interfaces: the base plate that anchors it to the floor or a pedestal, and the J6 tool flange that carries the end-effector. This page gives their dimensions and frames, and links the files you need to design a pedestal or a tool plate. The dimensions are measured from the robot CAD. The interface solids are extracted from the robot STEP unchanged and moved into the frames described below. They are nominal, with no tolerances or fits. Where the CAD does not define a thread or a fit, this page gives the plain hole size and marks it **verify**. Confirm with AMR before machining. ## Downloads | File | Contents | |---|---| | [`rosie-1400-base.step`](/assets/cad/rosie-1400-base.step) | Base plate solid, AP214, mm, in the base frame | | [`rosie-1400-base.pdf`](/assets/cad/rosie-1400-base.pdf) | A3 drawing sheet: plan, section A–A, key dimensions, notes | | [`rosie-1400-tool-flange.step`](/assets/cad/rosie-1400-tool-flange.step) | Tool flange unit solid, AP214, mm, in the flange frame | | [`rosie-1400-tool-flange.pdf`](/assets/cad/rosie-1400-tool-flange.pdf) | A3 drawing sheet: face view, section A–A, key dimensions, notes | | [`rosie-1400-interfaces.json`](/assets/cad/rosie-1400-interfaces.json) | Every dimension on this page, the frame definitions and the file links, machine-readable. `null` means not determinable from the CAD | This page holds the dimensioned drawings. Full STEP models of the whole robot, with URDF and MuJoCo files, are in the kits under [CAD and simulation](https://advancedmetalresearch.com/rosie#cad) on the Rosie page. ## Robot base ![Plan of the 300 by 300 mm Rosie 1400 base plate: four Ø26 mm anchor holes on a 250 mm square, R25 corners, and the base frame on the J1 axis at the centre.](https://advancedmetalresearch.com/assets/cad/rosie-1400-base-plan.svg) *Figure: Base plate from above. The base frame is in red.* ![Diagonal section A-A through two anchor holes: the plate is 28.4 mm thick at the edges and anchors, and the flat underside is the mounting face at Z = 0.](https://advancedmetalresearch.com/assets/cad/rosie-1400-base-section.svg) *Figure: Section A–A, on the diagonal through two anchor holes.* | Feature | Value | |---|---| | Footprint | 300 × 300 mm, R25 corners | | Anchor holes | 4 × Ø26 mm through, on a 250 × 250 mm square | | Plate thickness | 28.4 mm at the edges and anchors (the clamp length) | | Mounting face | Flat underside with no spigot and no dowel holes | The holes and recesses inside the anchor pattern take the robot's own fasteners. They are not part of the mounting interface, and their geometry is in the STEP file. **Mounting the robot.** Mount on a flat, rigid surface through the four Ø26 holes. Ø26 is the ISO 273 medium clearance for M24; confirm the anchor size, grade and tightening torque with AMR. The robot's own fasteners are recessed into the plate, so the pedestal face can be plain and flat. The plate has no dowel holes. If the robot must be re-mounted repeatably, agree a locating method with AMR. ## Tool flange The J6 output is the flange of a bought-in hollow-shaft reducer unit (HSS-17-100-I-D8). Tooling sits on its Ø38 mounting face and bolts on with six M5 screws. ![View on the J6 flange face: the Ø38 mm mounting face, raised 0.5 mm, with six M5 holes on PCD 27 and 11.7 mm of thread, the first hole 30 degrees from the flange X axis, and the Ø79 mm outside diameter.](https://advancedmetalresearch.com/assets/cad/rosie-1400-tool-flange-face.svg) *Figure: View on the flange face. The flange frame is in red.* ![Section A-A on the J6 axis: the flange unit is Ø79 mm outside and 39 mm long from the flange face.](https://advancedmetalresearch.com/assets/cad/rosie-1400-tool-flange-section.svg) *Figure: Section A–A on the J6 axis.* | Feature | Value | |---|---| | Mounting face | Ø38 mm, raised 0.5 mm | | Mounting holes | 6 × M5 on PCD 27 | | Thread depth | 11.7 mm, through the 12.5 mm output plate. Screws must not protrude | **Mounting a tool.** The M5 holes are tapped through the 12.5 mm output plate, which has a 0.4 mm chamfer at each end, so 11.7 mm of thread is available. The back of the plate opens into the reducer unit, so screws must not protrude past it. The first hole is 30° from the flange X axis, then every 60°. The Ø38 face stands 0.5 mm proud and has no specified fit, so do not use it as a register without AMR confirmation. The outside envelope of the flange unit is Ø79 × 39 mm from the face. Tooling larger than the Ø49.5 output ring must clear the stationary housing behind it, including screw heads that are not modelled. The flange is not an ISO 9409-1 flange; standard tooling mounts on the ISO adapter, which is quoted separately. ## Frames Both STEP files are written in the frame of their interface, in millimetres. | Frame | Origin | Axes | |---|---|---| | Base | Centre of the base-plate underside, on the J1 axis | Z up along J1. X robot forward. Y = Z × X | | Tool flange | Centre of the flange face, on the J6 axis | Z out of the face along J6. X 30° before the first mounting hole. Y = Z × X | The base frame is parallel to the robot CAD frame. In the RosieOS description `rosie_1400_v3` it matches the J1 joint frame (the `link_1` origin at J1 = 0) in orientation and in X and Y. The flange frame is not the URDF `tool0`. In `rosie_1400_v3`, `tool0` is the torch contact-tip frame (see [Robot description and coordinate frames](https://advancedmetalresearch.com/docs/concepts/robot-description-and-frames)). The `link_6` origin lies on the J6 axis at the flange face with +Z pointing into the wrist, so the flange frame's Z is `link_6` −Z. At the CAD home pose, flange X is 3° from straight down. Confirm the hole-pattern angle at J6 = 0 with AMR before you rely on it. ## To verify with AMR - Anchor bolt size, grade and torque, and a locating method if you need repeatable re-mounting. - The hole-pattern clock angle at J6 = 0. Write to [generalcontact@advancedmetalresearch.com](mailto:generalcontact@advancedmetalresearch.com?subject=Rosie%201400%20mechanical%20interfaces) with the part you are designing. --- # Dense trajectory (.rdt) format > The robot.v4.dense-joint-trajectory.v1 binary format for immutable joint programs, with its header fields, sample records, content digest, validation rules and the extra checks rt-control applies at admission. URL: https://advancedmetalresearch.com/docs/reference/rdt-format Section: RosieOS docs / Reference Last updated: 2026-10-10 A `.rdt` file is an immutable joint program: a JSON header followed by fixed-size binary samples, one block per motion segment. The weld planner writes it, OLP and the dense trajectory daemon carry it, and `rt-control` admits it with `prepare_program`. The schema name is `robot.v4.dense-joint-trajectory.v1`. Three codecs implement the format and are tested against one reference fixture, byte for byte: | Language | Role | Source | |---|---|---| | Python | Encoder (the weld planner) | `weld_planner/v1/python/weldplan/dense_joint_trajectory.py` | | Go | Decoder and encoder (OLP, `rt-control`) | `offline-programming/v1/internal/densejointtraj/`, `rt-core/adapters/rosie/densejointtraj/` | | C++ | Decoder (the dense trajectory daemon) | `motion-server/joint-trajectory/v1/src/dense_joint_trajectory_format.hpp` | ## Write a file The Python encoder validates the plan with the same rules the decoders apply, then packs it. Run this inside the weld planner's pixi environment (`pixi shell` in `weld_planner/v1`): make_hold.py: ```python from weldplan.dense_joint_trajectory import ( DensePlan, DensePlanIdentity, DenseSegment, encode, ) rest = [0.0] * 9 # J1..J9, rad hold = DenseSegment( kind="dwell", source_id="hold-1", t_s=[0.0, 0.01, 0.02], # segment-local clock, s q_rad=[rest] * 3, qd_rad_s=[rest] * 3, torch_on=[False] * 3, ) plan = DensePlan( identity=DensePlanIdentity( plan_id="demo:1", program_id="demo", program_digest="sha256:" + "0" * 64, manifest_revision=1, plan_revision=1, created_at="2026-09-29T00:00:00Z", ), segments=[hold], ) blob = encode(plan) # raises DenseJointTrajectoryError(reason, detail, segment_index) open("hold.rdt", "wb").write(blob) ``` `dt_s` (0.01 s), `axis_mask` (511) and the per-axis velocity ceiling (3.0 rad/s) default to the values in `weld_planner/v1/data/dense_joint_trajectory.params.json`. Pass `dt_s`, `axis_mask` and `max_abs_qd_rad_s` to `DensePlan` to override them. ## Read a file read_header.py: ```python import json, struct blob = open("hold.rdt", "rb").read() (header_len,) = struct.unpack(">I", blob[:4]) header = json.loads(blob[4:4 + header_len]) body = blob[4 + header_len:] for seg in header["segments"]: block = body[seg["block"]["byte_offset"]:][:seg["block"]["byte_length"]] t, *rest = struct.unpack(">19dB", block[:153]) # first record q, qd, flags = rest[:9], rest[9:18], rest[18] print(seg["index"], seg["kind"], seg["sample_count"], t, q[0], flags) ``` ## Blob layout ```text [u32 BE header_len][UTF-8 JSON header, header_len bytes][block 0][block 1]...[block S-1] ``` - All binary fields are big-endian. - There is one block per segment, contiguous and in segment order. `byte_offset` counts from the end of the JSON header. - The blocks must cover the body exactly: no gap, no trailing bytes. ## Sample record Every block is an array of 153-byte records (encoding `f64-be-aos.v1`): | Offset | Field | Type | Unit | Notes | |---|---|---|---|---| | 0 | `t_s` | f64 | s | Segment-local. Starts at 0.0 in every segment. | | 8 | `q_rad[9]` | 9 × f64 | rad | J1..J9 positions | | 80 | `qd_rad_s[9]` | 9 × f64 | rad/s | J1..J9 velocities. Mandatory in v1. | | 152 | `flags` | u8 | — | Bit 0 = `torch_on`. Bits 1–7 are reserved and must be 0. | The nine columns are J1–J6 (arm), J7 and J8 (positioner tables) and J9 (the H-frame turn), in that order. A six-axis robot still uses nine columns and says which ones it commands in `axes.axis_mask`: `0x1ff` for `rosie_1400_v3`, `0x3f` for `rosie_1420_v1`. At 0.01 s per sample, 18,000 samples (3 minutes) is about 2.75 MB. The 250,000-sample ceiling is about 38 MB. ## Header ```json { "schema": "robot.v4.dense-joint-trajectory.v1", "plan_id": "demo:1", "program_id": "demo", "program_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", "trajectory_digest": "sha256:…", "manifest_revision": 1, "plan_revision": 1, "created_at": "2026-09-29T00:00:00Z", "axes": {"axis_count": 9, "axis_mask": 511, "position_unit": "rad", "velocity_unit": "rad_s"}, "sampling": {"dt_s": 0.01, "total_sample_count": 3, "total_duration_s": 0.02}, "limits": {"max_abs_qd_rad_s": [3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0]}, "segments": [ {"index": 0, "kind": "dwell", "source_id": "hold-1", "sample_count": 3, "duration_s": 0.02, "block": {"byte_offset": 0, "byte_length": 459, "sha256": "sha256:…"}} ], "sample_encoding": {"encoding": "f64-be-aos.v1", "record_bytes": 153} } ``` | Field | Type | Required | Description | |---|---|---|---| | `schema` | string | yes | Exactly `robot.v4.dense-joint-trajectory.v1` | | `plan_id` | string | yes | Plan identity. The weld planner writes `:`. Must be non-empty for `rt-control`. | | `program_id` | string | yes | Program identity. Must be non-empty for `rt-control`. | | `program_digest` | string | yes | `sha256:<64 hex>`. The weld planner writes the SHA-256 of the `.weldplan` request the plan was made from. Must be non-empty for `rt-control`. | | `trajectory_digest` | string | yes | The [content digest](https://advancedmetalresearch.com/docs/reference/rdt-format#content-digest), `sha256:<64 hex>`. Recomputed and compared on every decode. | | `manifest_revision` | integer (u64) | yes | Must be a JSON integer, not a string | | `plan_revision` | integer (u64) | yes | Must be a JSON integer, not a string | | `created_at` | string | yes | RFC 3339 timestamp | | `axes.axis_count` | integer | yes | Must be 9 | | `axes.axis_mask` | integer | yes | One bit per commanded column, J1 = bit 0 | | `axes.position_unit` | string | yes | `rad` | | `axes.velocity_unit` | string | yes | `rad_s` | | `sampling.dt_s` | number | yes | The sample period in s. Must be > 0. The planner uses 0.01. | | `sampling.total_sample_count` | integer | yes | Sum of every segment's `sample_count`. At most 250,000. | | `sampling.total_duration_s` | number | yes | Sum of the segment durations, s | | `limits.max_abs_qd_rad_s` | 9 numbers | yes | Per-axis velocity ceiling in rad/s. Every sample's \|qd\| must be at or below it. Nine finite positive values. | | `segments[]` | array | yes | At least one segment; see below | | `sample_encoding.encoding` | string | yes | `f64-be-aos.v1` | | `sample_encoding.record_bytes` | integer | yes | 153 | | `robot_cell` | object | no | What the plan was made against; see [Robot and cell identity](https://advancedmetalresearch.com/docs/reference/rdt-format#robot-cell) | | `process_markers` | array | no | Output intents attached to exact samples; read by `rt-control` only. See [Admission by rt-control](https://advancedmetalresearch.com/docs/reference/rdt-format#rt-control-admission). | Decoders ignore unknown keys. That is the forward-compatibility rule. The identity fields are not part of the content digest; they are matched separately when a program is loaded and started. ### Segments | Field | Type | Description | |---|---|---| | `index` | integer | Must equal the segment's position in the list | | `kind` | string | `freespace`, `weld` or `dwell` | | `source_id` | string | The planner object this segment came from, for tracing | | `sample_count` | integer | At least 2 | | `duration_s` | number | Must equal the segment's last `t_s`, within 1e-9 s | | `block.byte_offset` | integer | Offset from the end of the JSON header. Must be the running sum of the earlier blocks. | | `block.byte_length` | integer | `sample_count × 153` | | `block.sha256` | string | `sha256:<64 hex>` over this block's bytes exactly. Checked before any sample is parsed. | Each segment has its own clock. The executor runs a segment to its end, holds the last position with zero velocity, then starts the next. It never interpolates across a boundary. Holds between segments are explicit `dwell` segments, not gaps. For that reason: - the first and last sample of every segment must be at rest: \|qd\| ≤ 1e-6 rad/s on every axis - each segment must start within 1e-3 rad of where the previous one ended, on every axis ### Robot and cell identity The weld planner writes a `robot_cell` block naming the robot and cell the plan was made for: | Field | Description | |---|---| | `model_id` | Robot model, for example `rosie_1400_v3` | | `robot_description_sha256` | Identity of the robot description, `sha256:<64 hex>` | | `machine_planning_calibration_sha256` | Identity of the cell's `machine_planning_calibration.json` | | `machine_planning_calibration_source` | `target` (read from the cell) or `empty` (the model's empty calibration) | | `machine_configuration_sha256` | Optional. `sha256:` + the rt-core configuration digest the plan was made for. Recorded only. | OLP's Load compares the first three with the selected cell before it acquires anything. A mismatch is refused with `robot_cell_mismatch`, a plan without the block with `robot_cell_missing`, and a cell that cannot say which robot it is with `robot_cell_unavailable`. A different `machine_configuration_sha256` does not refuse the load; OLP returns a note instead. The dense trajectory daemon and `rt-control` do not read this block. ## Content digest `trajectory_digest` is SHA-256 over a versioned layout, written `"sha256:" + lowercase hex`. Integers are u64 big-endian, an f64 is the u64 big-endian of its IEEE-754 bits, and a string is its u64 length followed by its UTF-8 bytes. ```text "robot.v4.dense-joint-trajectory.v1" 0x00 u64 axis_count (9) u64 axis_mask f64 dt_s u64 segment_count for each segment, in order: str kind str source_id u64 sample_count the segment's block bytes, exactly as stored u64 9 f64 max_abs_qd_rad_s[0..8] ``` The digest covers what the trajectory is, not what it is called: the identity strings (`plan_id`, `program_id` and so on) and `robot_cell` are not in it. v1 defines no optional sections. The layout reserves a tagged section after each block (u8 tag, u64 length, content) so that a later weld-process table can be added without changing existing digests. ## Validation Every decoder refuses a blob that breaks any of these rules, and names the first rule it hits. The `segment_index` is given when one segment is at fault. | Reason | Rule | |---|---| | `header_truncated` | The blob is shorter than 4 bytes, or `header_len` runs past its end | | `header_json_invalid` | The header is not valid JSON. The C++ daemon also uses it for a missing or mistyped required field. | | `schema_mismatch` or `dense_schema_mismatch` | `schema` is not `robot.v4.dense-joint-trajectory.v1`. `rt-control` reports `dense_schema_mismatch`; the OLP and daemon codecs report `schema_mismatch`. | | `axis_count_mismatch` | `axes.axis_count` is not 9 | | `sample_encoding_mismatch` | The encoding is not `f64-be-aos.v1` with 153-byte records | | `limits_invalid` | `max_abs_qd_rad_s` is not nine finite positive numbers | | `time_grid_invalid` | `dt_s` ≤ 0; a segment's first `t_s` is not 0; or a step differs from `dt_s` by more than 1e-9 s | | `segments_empty` | No segments | | `segment_index_out_of_order` | An `index` differs from the segment's position | | `kind_invalid` | `kind` is not `freespace`, `weld` or `dwell` | | `segment_too_short` | Fewer than 2 samples | | `block_layout_invalid` | Offsets are not contiguous, or a length is not `sample_count × 153` | | `blob_length_mismatch` | The blocks do not exactly cover the body | | `block_sha256_mismatch` | A block's bytes do not match its `sha256`. Checked before its samples are parsed. | | `reserved_flags_set` | A flag bit other than bit 0 is set | | `total_sample_count_mismatch` | `total_sample_count` differs from the sum of the segments | | `sample_count_overflow` | More than 250,000 samples | | `nonfinite_sample` | A NaN or infinity in `t_s`, `q_rad` or `qd_rad_s` | | `qd_limit_exceeded` | A sample's \|qd\| is above `max_abs_qd_rad_s` for its axis | | `torch_outside_weld` | The torch bit is set in a segment that is not `weld` | | `q_step_exceeded` | A position step is larger than 1.25 × limit × `dt_s` on some axis | | `boundary_qd_nonzero` | A segment's first or last sample has \|qd\| above 1e-6 rad/s | | `boundary_q_discontinuity` | A segment starts more than 1e-3 rad from where the previous one ended | | `duration_mismatch` | `duration_s` differs from the segment's last `t_s` by more than 1e-9 s | | `trajectory_digest_mismatch` | The recomputed content digest differs from `trajectory_digest` | The C++ daemon checks the content rules before the digest, and `rt-control` checks the digest first. When a blob breaks several rules, different components can name different ones. Don't rely on the order. Format validity is not permission to move. It is necessary, not sufficient. ## Admission by rt-control `prepare_program` (`POST /v1/program`) decodes the blob with the rules above, then adds its own checks against the live machine. See [`prepare_program`](/docs/apis/rt-control-http#prepare-program). - **Size.** The JSON header may be at most 1,048,576 bytes, and the whole body at most 1,048,580 + 250,000 × 153 bytes. Larger requests are refused as too large. - **Axis map.** `rt-control` maps the wire columns, in order, to the axes in its own description whose position unit is rad. A six-axis cell maps J1–J6, a nine-axis cell all nine. More mapped axes than nine, or none, is refused with `dense_axis_map_requires_nine_ids`. A column past the map must not be set in `axis_mask`, must hold one position throughout and must have zero velocity. Otherwise the program is refused with `unmapped_dense_axis`. - **Stationary seams.** Where one segment ends at rest and the next starts at rest, the two boundary samples become one shared sample, so the executed program has `samples − segments + 1` points. The adapter returns both identities: `source_digest` (the file's content digest) and `normalised_digest` (the executed points). `start_program` must present both. - **Native limits.** Every sample must be inside the axis's position limits and below its described velocity. Each interval, interpolated as a cubic Hermite from `q` and `qd`, must stay inside the limits and below the lower of the axis ceiling and the header's `max_abs_qd_rad_s`. If the axis describes a maximum acceleration, the finite-difference acceleration is checked too. These checks use no interpolation slack. Refusals include `native_limit_exceeded`, `native_segment_rate_exceeded`, `outside_limits_outward` and `segment_boundary_discontinuous`. - **Identity.** `plan_id`, `program_id` and `program_digest` must be non-empty (`program_identity_missing`). - **Process outputs.** A set torch bit, or any `process_markers` entry, marks the program as requiring process I/O. Torch output is refused everywhere in this release. OLP's dry-run Load strips the torch bits before upload. See [Process I/O and sensing](https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing). The full list of reasons is in [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes#reasons-dense). ## Tools - `rt-core/tools/rdtcheck` decodes `.rdt` files against a single-axis bench description and prints a boundary report: per-boundary position gaps, stationarity and the adapter's verdict. It never sends motion. Run `go run ./tools/rdtcheck [--root DIR] [--json OUT] FILE.rdt...` from `rt-core` on Linux. - The weld planner's motion server serves each `.rdt` it wrote at `GET /api/motion/dense/{trajectory_digest}`. See the [Weld planner HTTP API](https://advancedmetalresearch.com/docs/apis/weld-planner-http). - The dense trajectory daemon's `validate` request runs every rule above without storing the blob. See [TCP ingest](https://advancedmetalresearch.com/docs/apis/dense-trajectory-daemon#tcp-ingest). ## Related pages - [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning) - [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification) - [Dense trajectory daemon](https://advancedmetalresearch.com/docs/apis/dense-trajectory-daemon) - [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http#prepare-program) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `motion-server/joint-trajectory/v1/src/dense_joint_trajectory_format.hpp:35-67,394-445,470-531,556-665,676-914` - `rt-core/adapters/rosie/densejointtraj/densejointtraj.go:24-139,220-384,386-459` - `rt-core/adapters/rosie/densejointtraj/normalise.go:1-96` - `rt-core/adapters/rosie/control/program.go:20-284` - `rt-core/adapters/rosie/control/admission.go:20-33` - `rt-core/ipcclient/protocol_generated.go:14` - `rt-core/tools/rdtcheck/main.go:1-231` - `offline-programming/v1/internal/densejointtraj/densejointtraj.go:101-111,318` - `offline-programming/v1/internal/densejointtraj/dry_run.go:10-21` - `offline-programming/v1/internal/denseexec/robot.go:12-104` - `offline-programming/v1/internal/denseexec/rt_core.go:756-806` - `weld_planner/v1/python/weldplan/dense_joint_trajectory.py:42-192,366-426` - `weld_planner/v1/python/weld_motion_planner/io/dense_output.py:140-164` - `weld_planner/v1/data/dense_joint_trajectory.params.json` --- # Program format (robot.v4.program.v2) > The robot.v4.program.v2 document that OLP, the pendant and the weld planner share, with its top-level fields, node types, the single-line ordering rule, weld geometry, taught moves, units and digests. URL: https://advancedmetalresearch.com/docs/reference/program-format Section: RosieOS docs / Reference Last updated: 2026-10-10 A `robot.v4.program.v2` document is an authored robot program: an ordered list of nodes that say what the robot should do, not how. Welds carry their seam geometry, freespace moves carry constraints, taught moves carry a destination, and events (I/O and dwell) do not move. It never contains a solved trajectory. The weld planner turns it into joint trajectories and a [`.rdt`](/docs/reference/rdt-format) file. OLP, the Steam Deck v5 pendant and the weld planner all read and write this format. The TypeScript validator is `offline-programming/v1/ui/src/contracts/robot-v4-program-v2.ts`. Its twins are the Go test `steamdeck/real/v4/contracts/programs/program_v2_test.go`, the Python module `weld_planner/v1/python/weldplan/program_v2.py` and the JSON Schema `steamdeck/real/v4/contracts/programs/robot.v4.program.v2.schema.json`. ## Example A valid program with one weld: Home, approach, weld, retract, a dwell, Home. bracket_fillet.program.json: ```json { "schema": "robot.v4.program.v2", "program_id": "bracket_fillet", "program_name": "Bracket fillet, one pass", "program_revision": 1, "base_program_revision": 0, "program_state": "saved_unplanned", "execution_mode": "simulation", "created_utc": "2026-09-29T09:00:00Z", "units": {"length": "m", "angle": "rad", "time": "s", "joint_order": ["J1", "J2", "J3", "J4", "J5", "J6", "J7", "J8", "J9"]}, "producer": { "component": "offline-programming-v1", "source_revision": "local", "inputs": [{"kind": "project", "ref": "bracket", "sha256": "4f6c2d7a0b3e9f18c5a6d2e7b1f0c3a9d8e4b7f2a1c6d5e0f9b8a7c6d5e4f3a2"}] }, "workpiece": {"frame_id": "bracket_origin"}, "placement": {"cell_id": "rosie_1400_v3", "anchor_xyz_m": [0, 0, 0], "offset_xyz_m": [0.1, 0, 0.1], "rpy_rad": [0, 0, 0]}, "robot": {"model_id": "rosie_1400_v3", "robot_description_sha256": "sha256:b2103adffd53cb9a66db1edf842ae1cb34173f1c8ff83c3b30ff7c8dacee9b0a"}, "defaults": {"clearance_min_mm": 20, "speed_scale": 0.5, "torch_policy": "free", "positioner_policy": "hold"}, "nodes": [ {"id": "home-start", "op": "home", "enabled": true, "label": "Home", "comment": "", "row_hash": "sha256:…"}, {"id": "approach-1", "op": "approach", "enabled": true, "label": "Approach", "comment": "", "row_hash": "sha256:…", "arrive_standoff": {"direction": "torch_axis", "distance_mm": 30}}, {"id": "weld-1", "op": "weld", "enabled": true, "label": "Fillet A", "comment": "", "row_hash": "sha256:…", "geometry": {"kind": "manual_pose_wp", "frame_id": "bracket_origin", "points": [ {"xyz_m": [0.0, 0.0, 0.01], "rpy_rad": [3.14159, 0.0, 0.0], "continuity": "C0"}, {"xyz_m": [0.1, 0.0, 0.01], "rpy_rad": [3.14159, 0.0, 0.0], "continuity": "C0"}]}, "travel_speed_mm_s": {"kind": "constant", "value": 8.0}, "standoff_mm": {"kind": "constant", "value": 15.0}, "weave": {"shape": "none"}, "weld_parameters": {}}, {"id": "retract-1", "op": "retract", "enabled": true, "label": "Retract", "comment": "", "row_hash": "sha256:…", "depart_standoff": {"direction": "torch_axis", "distance_mm": 30}}, {"id": "settle", "op": "dwell", "enabled": true, "label": "Settle", "comment": "", "row_hash": "sha256:…", "duration_s": 0.5}, {"id": "home-end", "op": "home", "enabled": true, "label": "Home", "comment": "", "row_hash": "sha256:…"} ], "planner_owns": [ "the joint-space solution of every freespace node -- this document states constraints, never a path", "time parametrization and dynamics" ] } ``` `row_hash` is shown elided. A real one is the node's digest; see [Digests](https://advancedmetalresearch.com/docs/reference/program-format#digests). ## Validate a document The TypeScript module exports the validator. With Node 22 or later, from `offline-programming/v1/ui`: check-program.mjs: ```js import { readFileSync } from "node:fs"; import { programV2Problems } from "./src/contracts/robot-v4-program-v2.ts"; const doc = JSON.parse(readFileSync(process.argv[2], "utf8")); console.log(programV2Problems(doc)); // [] when the document is accepted ``` ```bash node --experimental-strip-types check-program.mjs bracket_fillet.program.json ``` The validator stops at the first problem and states it as a sentence, for example `weld "weld-1" is not preceded by an approach or transit (an approach is missing)`. `programV2LineProblems` returns every ordering problem at once. ## Units `units` is fixed: lengths in m, angles in rad, time in s, and `joint_order` is `J1`…`J9`. Fields that use another unit say so in their name: `_mm`, `_mm_s`, `_deg` and `_s`. The UI shows poses in mm and degrees, but the document stores m and rad. ## Top-level fields | Field | Type | Required | Description | |---|---|---|---| | `schema` | string | yes | `robot.v4.program.v2` | | `program_id` | string | yes | Stable program identity | | `program_name` | string | yes | Display name | | `program_revision` | integer ≥ 1 | yes | This revision | | `base_program_revision` | integer ≥ 0 | yes | The revision this one was edited from | | `program_digest` | string | no | `sha256:<64 hex>`; see [Digests](https://advancedmetalresearch.com/docs/reference/program-format#digests) | | `program_state` | string | yes | `draft`, `saved_unplanned`, `needs_plan`, `planning`, `planned`, `accepted_simulation`, `accepted_physical`, `execution_ready`, `running`, `completed`, `stopped` or `faulted` | | `execution_mode` | string | yes | `simulation`, `dry_run` or `production` | | `created_utc` | string | yes | RFC 3339 timestamp | | `units` | object | yes | See [Units](https://advancedmetalresearch.com/docs/reference/program-format#units) | | `producer.component` | string | yes | `offline-programming-v1` or `weld-planner-v1` | | `producer.source_revision` | string | yes | Revision of the producing code | | `producer.inputs[]` | array | yes | At least one `{kind, ref, sha256}`. `kind` is `project`, `weld_program`, `cad`, `cell` or `tool`; `sha256` is 64 hex characters without a prefix. | | `source` | object | no | Where the geometry came from, for example a STEP file and its hash | | `workpiece.frame_id` | string | yes | The workpiece frame. Seams and hand-placed weld poses are in this frame. | | `workpiece.solids[]` | array | no | Solids from the CAD import | | `workpiece.weld_joints[]` | array | no | Weld joints that a seam's `weld_joint_ref` resolves against. IDs must be unique. | | `placement.cell_id` | string | yes | Must equal `robot.model_id` | | `placement.anchor_xyz_m`, `offset_xyz_m` | 3 numbers, m | yes | Where the workpiece sits in the description's work frame | | `placement.rpy_rad` | 3 numbers, rad | yes | Workpiece orientation | | `robot.model_id` | string | yes | Robot model, for example `rosie_1400_v3` | | `robot.robot_description_sha256` | string | yes | `sha256:<64 hex>` identity of the robot description. The all-zero value means the producer had no description to pin to. | | `tool` | object | no | `{id, sha256}` of the torch asset | | `speed_scale` | number | no | Whole-program speed multiplier applied after planning, 0.01–1. Default 1. | | `defaults` | object | yes | The freespace policy; see below. All four keys are required. | | `nodes[]` | array | yes | 1–4,096 nodes | | `planner_owns[]` | array of strings | yes | At least one sentence naming something this document deliberately leaves to the planner | | `extensions` | object | no | Producer-specific data | ### Freespace policy `defaults` sets the policy for every freespace node. A node's `overrides` replaces any subset of it. | Key | Type | Description | |---|---|---| | `clearance_min_mm` | number ≥ 0, mm | Minimum clearance for freespace motion | | `speed_scale` | number in (0, 1] | Freespace speed fraction | | `torch_policy` | string | `free`, `hold_last` or `torch_down` | | `positioner_policy` | string | `free` (the planner may move the positioner) or `hold` | ## Nodes Every node has these fields, and only the fields its `op` allows. An unknown key is refused. | Field | Type | Description | |---|---|---| | `id` | string | Unique within the program | | `op` | string | `weld`, `approach`, `transit`, `retract`, `home`, `move`, `dwell` or `io` | | `enabled` | boolean | | | `label`, `comment` | string | May be empty | | `row_hash` | string | Non-empty; the node digest | | `extensions` | object | Optional | | `op` | Kind | Extra fields | |---|---|---| | `weld` | Motion | `geometry`, `travel_speed_mm_s`, `standoff_mm`, `weave`, `weld_parameters`, optional `weld_preset_id` | | `approach`, `transit`, `retract`, `home` | Freespace motion | Optional `depart_standoff`, `arrive_standoff`, `overrides` | | `move` | Taught motion | `motion`, `target`, `speed_scale`, `speed_mm_s`, `capture`; optional `target_space`, `via`, `acceleration_scale`, `constant_tcp_speed` | | `dwell` | Event | `duration_s` (≥ 0, s) | | `io` | Event | `channel` (non-empty string), `state` (boolean) | ### The single line The motion nodes, ignoring `dwell` and `io`, must form one line: ```text home? approach weld (transit weld)* retract home? ``` - A `weld` is preceded by an `approach` or `transit` and followed by a `retract` or `transit`. - A `transit` sits between two welds. - An `approach` is followed by a weld, and may follow nothing, a `home`, a `move` or a `retract`. - A `retract` follows a weld, and may be followed by nothing, a `home`, a `move` or an `approach`. - A `home` may only start or end the program. - `move` nodes may appear before, after or between complete weld blocks. ### Weld nodes | Field | Type | Description | |---|---|---| | `geometry` | object | `seam`, `posed_polyline` or `manual_pose_wp`; see below | | `travel_speed_mm_s` | scalar function, mm/s | Travel speed along the weld | | `standoff_mm` | scalar function, mm | Contact-tip standoff. Required, never defaulted, and positive everywhere. | | `weave` | object | `{shape, …}`; `shape` is required, for example `none` | | `weld_preset_id` | string | Optional label of the preset the values started from. The values are the authority. | | `weld_parameters` | object | Reserved. Must be present and empty in v2. | A **scalar function** is a value over normalised arc length `s` in [0, 1]: | `kind` | Fields | |---|---| | `constant` | `value` | | `piecewise_linear` | `knots`: `[s, value]` pairs from `s = 0` to `s = 1`, strictly increasing | | `bspline` | `degree`, `knots_u`, `control_values`; `knots_u` has `control_values + degree + 1` entries | | `samples` | `s` and `values`, the same length, at least two | #### Geometry kinds **`seam`** is the weld planner's seam. It needs `id`, `reference` and `orientation` (`work_angle_deg` and `travel_angle_deg` as scalar functions, in degrees). Its line is either an inline `curve` or the `weld_line` of the joint named by `weld_joint_ref`. An inline curve is a list of B-spline `segments`, each with `degree`, `control_points_xyz_m` and clamped, non-decreasing `knots_u` (poles + degree + 1 entries), and optional `weights`. Optional fields include `span` (`start_s`, `end_s`; wrapping through 0 only on a closed curve), `direction` (`forward` or `reverse`), `lead` (`lead_in_mm`, `lead_out_mm`) and `search_provenance`, the record of the angle search. See the [weld program format](https://advancedmetalresearch.com/docs/reference/weld-program-format) for the seam model. **`posed_polyline`** is the classic flow's weld: `frame_id` and at least two `points`, each `{xyz_m, rpy_rad}`. The RPY on each point is the authority. **`manual_pose_wp`** is a weld placed by hand as torch poses. `frame_id` must be the workpiece frame. Each point has `xyz_m` (the work point, m), `rpy_rad` (torch orientation, URDF fixed-axis; tool +Z points from the gun into the work) and `continuity` (`C0`, `C1` or `C2`; ignored on the first and last point). Consecutive points must be more than 1 µm apart. `shape` is `spline` (the default: chord-length cubic Hermite segments) or `arc` (exactly three non-collinear points on one circle). ### Freespace nodes `approach`, `transit`, `retract` and `home` state constraints only. A standoff is `{direction, distance_mm}`, where `direction` is `torch_axis`, `workpiece_z` or a 3-vector, and `distance_mm` ≥ 0. - `depart_standoff` is not allowed on `approach` or `home`, since nothing is welded before them. - `arrive_standoff` is not allowed on `retract` or `home`, since nothing is welded after them. - A freespace node may not carry path vocabulary: `knots`, `knots_u`, `times_s`, `control_points_xyz_m`, `segments`, `samples`, `points`, `poses`, `rpy_rad` or `path`. ### Move nodes A taught destination, with the observation it was taught from. | Field | Type | Description | |---|---|---| | `motion` | string | `joint` (MoveJ), `linear` (MoveL) or `circular` (MoveC) | | `target` | TeachPose | The destination | | `via` | TeachPose | Optional; the through point of a circular move | | `target_space` | string | Optional. `cartesian` re-solves IK from the pose at planning; `joint` or absent keeps the recorded joint values. | | `speed_scale` | number in (0, 1] | Joint speed fraction | | `speed_mm_s` | number > 0, mm/s | TCP speed for linear and circular moves | | `acceleration_scale` | number in (0, 1] | Optional joint acceleration fraction. Absent, it follows `speed_scale`. | | `constant_tcp_speed` | boolean | Optional, default `true`: linear and circular moves cruise at TCP speed, with ramps | | `capture.source` | string | `machine` (taught on a connected cell) or `preview` (taught in the viewer) | | `capture.observed_at` | string | RFC 3339 timestamp | | `capture.model_id`, `capture.robot_description_sha256` | string | The robot it was taught on | | `capture.machine_planning_calibration_sha256` | string | Optional, `sha256:<64 hex>` | | `capture.cell_id` | string | Required when `source` is `machine` | | `capture.pose` | TeachPose | The pose as observed | A **TeachPose** is `{frame_id: "world", xyz_m, rpy_rad, joint_names, joint_values_rad}`. `xyz_m` and `rpy_rad` are the TCP pose in m and rad. `joint_names` lists 1–9 unique names and `joint_values_rad` the matching values in rad. No other keys are allowed. See [Teach waypoints and moves](https://advancedmetalresearch.com/docs/guides/teach-waypoints) for how the UI records them. ## Forbidden keys The document states intent, never a solution. These keys are refused anywhere in it, at any depth: `joints`, `joint_positions_rad`, `target_joints_rad`, `q_rad`, `qd_rad_s`, `target_tcp_m`, `tcp_pose_m`, `trajectory`, `planned_path`, `accepted_plan`, `preloaded_plan`, `waypoints` and `rows`. Freespace nodes and `defaults` also refuse the path vocabulary listed under [Freespace nodes](https://advancedmetalresearch.com/docs/reference/program-format#freespace-nodes). ## Digests Both digests are `sha256:<64 hex>` over canonical JSON: keys sorted at every level, no whitespace, non-finite numbers refused. - **`row_hash`**: the node with `row_hash` removed. - **`program_digest`**: the whole document with `program_digest` removed. The validator runs first. The TypeScript module computes them with `computeProgramV2NodeDigest(node)` and `computeProgramV2Digest(program)`. OLP sends the program digest with a plan request and echoes it in the plan response. It is not the `program_digest` in the resulting `.rdt` header: there the planner writes `sha256:` + the SHA-256 of the `.weldplan` request it was given, and `plan_id` is `:`. So replanning the same program gives a new plan identity, and resending the same request gives the same one. ## Related pages - [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning) - [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification) - [Offline programming HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http#weld-plan) - [Dense trajectory (.rdt) format](https://advancedmetalresearch.com/docs/reference/rdt-format) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `offline-programming/v1/ui/src/contracts/robot-v4-program-v2.ts:26-58,63-332,352-525,527-563,565-605,607-691,714-873,882-902` - `offline-programming/v1/ui/src/contracts/robot-v4-program.ts:15-46,112-124,653-664` - `steamdeck/real/v4/contracts/programs/robot.v4.program.v2.schema.json` - `steamdeck/real/v4/contracts/programs/fixtures/complete-program-v2.json` - `weld_planner/v1/python/weldplan/program_v2.py:1-62` - `offline-programming/v1/weld_plan.go:47-77` --- # Weld program and `.weldplan` container > The seam model that weld nodes use (curves, arc length, the seam frame, work and travel angles, bands and tolerances), the .weldplan plan request container and its checks, and the seam worker's stdin protocol. URL: https://advancedmetalresearch.com/docs/reference/weld-program-format Section: RosieOS docs / Reference Last updated: 2026-10-10 A weld is described in three layers: 1. **The seam model**: how one weld's line and torch angles are written. Weld nodes in a [`robot.v4.program.v2`](/docs/reference/program-format) document use it for their `geometry` when `kind` is `seam`. 2. **The `.weldplan` container**: the program plus everything a planner needs to plan it, packed into one file with digests. This is what the [weld planner](https://advancedmetalresearch.com/docs/apis/weld-planner-http) accepts. 3. **The seam worker protocol**: the JSON operations that detect seams in a STEP file, search torch angles and build the container. A weld program says **what weld is required**, never how a robot gets there. It holds no joint values, no poses the robot must reach and no robot state. Rotation of the torch about its own electrode axis is left free by default, for the planner to use. ## The weld-program.v1 schema `weld_planner/v1/schema/amr-weld-planner-v1.weld-program.v1.schema.json` is the original standalone weld program schema. It is now the **source of the seam vocabulary**, not a format the planner reads: - program.v2 copies its definitions of `identifier`, `vec3`, `unit_vec3`, `scalar_function`, `analytic`, `curve_segment`, `curve`, `orientation_limit_axis`, `seam`, `weld_joint`, `groove_face`, `weave`, `source` and `workpiece`. Tests keep the copies identical. - Two differences, both deliberate: in program.v2 a seam has no `process` block (travel speed, standoff and weave are fields of the weld node), and `weld_joints` sits inside `workpiece` rather than at the top level. - A `.weldplan` whose `program.json` is a `weld-program.v1` document is refused. It has no weld nodes, so it would otherwise open and plan nothing. ## Conventions - **Units are in field names**: `_m`, `_mm`, `_deg`, `_rad`, `_mm_s`, `_l_min`. Geometry is in metres, process quantities in millimetres, angles authored by people in degrees. - **Workpiece frame only.** All seam geometry is in the frame named by `workpiece.frame_id`, which must be `workpiece` in a program with welds. The program's `placement` says where that frame sits on the cell. - **No baked sampling.** Anything that varies along a seam is a function of normalised arc length, never a per-sample array. The planner chooses the sampling. - **Identifiers** match `^[A-Za-z_][A-Za-z0-9_:.-]*$`. ## Curves Every curve is a list of clamped, optionally rational B-spline segments joined end to end. | Field | Type | Required | Description | |---|---|---|---| | `segments[]` | array | yes | The segments, in order | | `closed` | boolean | no | The last segment's end meets the first segment's start | | `length_m` | number, m | no | Cached total length. Derived and not authoritative. | Each segment: | Field | Type | Required | Description | |---|---|---|---| | `degree` | integer, 1–7 | yes | 1 with two control points is a line; 2 rational is an exact arc or conic | | `control_points_xyz_m` | array of 3-vectors, m | yes | At least two | | `knots_u` | array of numbers | yes | Non-decreasing and clamped. Length is control points + degree + 1. | | `weights` | array of numbers > 0 | no | Omit for a non-rational spline. One per control point. | | `continuity_to_next` | string | no | `C0`, `G1`, `C1` or `C2`. Advisory: `C0` marks a corner. | | `analytic` | object | no | The exact line, circle or arc, for readability. The B-spline wins if they disagree. | | `length_m` | number, m | no | Cached, derived | | `id`, `source_edge_id` | string | no | Identity and the CAD edge it came from | A line is degree 1, two control points, `knots_u` `[0, 0, 1, 1]`. A quarter arc is degree 2, three control points, `weights` `[1, 0.7071, 1]`, `knots_u` `[0, 0, 0, 1, 1, 1]`. ### Arc length The B-spline parameter `u` is not a distance. Everything along a seam is defined on **normalised arc length** `s` in [0, 1] over the whole curve: `s = 0.5` is halfway along the weld, whatever the segments look like. Travel speed is mm/s of arc length. ### Scalar functions A quantity that varies along the seam, such as an angle or a speed, is one of: | `kind` | Fields | |---|---| | `constant` | `value` | | `piecewise_linear` | `knots`: `[s, value]` pairs, `s` strictly increasing from 0.0 to 1.0 | | `bspline` | `degree` (≥ 1), `knots_u`, `control_values` | | `samples` | `s` and `values` (at least two each), optional `interpolation`: `linear` (default), `cubic` or `step` | ## Seams A seam is one pass of one weld. | Field | Type | Required | Description | |---|---|---|---| | `id` | identifier | yes | Unique | | `reference` | object | yes | Where the 0° work-angle direction comes from; see [The seam frame](https://advancedmetalresearch.com/docs/reference/weld-program-format#seam-frame) | | `orientation` | object | yes | `work_angle_deg`, `travel_angle_deg`, optional `spin` and `limits` | | `curve` | curve or reference | no | The line the electrode tip follows: inline, or the `weld_line` of the joint in `weld_joint_ref` | | `weld_joint_ref` | identifier or null | no | The weld joint this seam is on. Null means hand-authored. | | `span` | object | no | `start_s`, `end_s`: weld only part of the curve. `start_s > end_s` wraps through 0, on a closed curve only. | | `direction` | string | no | `forward` or `reverse` along the curve | | `pass` | object | no | `index`, `role` (`single`, `root`, `fill`, `cap`, `tack`), `offset_in_frame_rb_m` | | `weld_geometry` | object | no | ISO 2553 sizing: `leg_length_mm`, `throat_mm`, `root_gap_mm`, `bevel_angle_deg`, `intermittent` | | `lead` | object | no | `lead_in_mm`, `lead_out_mm` along the tangent | | `tolerance` | object | no | `position_mm`, `work_angle_deg`, `travel_angle_deg`, each > 0 | | `sampling_hint` | object | no | Advisory only: `max_chord_deviation_mm`, `max_segment_mm`, `min_segment_mm` | | `origin` | object | no | Why this seam is a separate piece: `group_id`, `index_in_group`, `group_size`, `split_reason` (`none`, `reachability`, `continuity`, `manual`), `boundary_continuity` | | `label` | string | no | Display name | `tolerance.position_mm` is the tolerance of the verifier's tracking certificate for this seam. It defaults to 0.5 mm. See [The tracking certificate](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification#the-tracking-certificate-welds-only). ### The seam frame At each `s` the seam has a right-handed frame: ```text P(s) point on the seam curve t(s) unit tangent, in the travel direction r(s) reference direction, orthogonalised against t b(s) = t × r ``` `reference.mode` says where `r` comes from: | `mode` | Needs | Reference direction | |---|---|---| | `rail_curve` | `rail` (a curve) | From the seam point toward the matching point on the rail. Points match by normalised arc length, per segment when the two curves have the same number of segments. | | `fixed_vector` | `fixed_direction_xyz` | A fixed direction, orthogonalised against the tangent | | `rotation_minimizing` | `seed_direction_xyz` | A seed direction carried along the curve without twisting | `reference.semantics` records how the reference was made: `bisector`, `member_face` (with `member_ref`), `gravity_projected` or `custom`. ### Work and travel angles ```text r' = R(t, work_angle) · r roll in the plane across the seam b' = t × r' u = R(b', −travel_angle) · r' tilt along travel ``` `u` points from the weld point toward the torch body. The electrode axis is `−u`. `R` is a right-handed rotation. - **Work angle** > 0 rotates `r` toward `b`. 0° puts the torch on the reference direction. - **Travel angle** > 0 is drag (backhand): the torch leans back over the finished weld. - **Spin**, the rotation about the electrode axis, is `free` by default. `preferred` and `locked` take an `angle_deg` function and a `reference` direction. ### Orientation bands `orientation.limits.work_angle_deg` and `orientation.limits.travel_angle_deg` bound how far a planner may move each angle. A profile outside its band is a different weld, so a consumer must refuse it rather than clamp it. | Field | Type | Description | |---|---|---| | `min`, `max` | scalar function, deg | Where the angle may be at all, as a function of `s` | | `max_deviation_deg` | number ≥ 0, deg | How far the angle may move from the authored profile, either way | | `max_deviation_plus_deg`, `max_deviation_minus_deg` | number ≥ 0, deg | The same, split by direction | | `max_rate_deg_per_mm` | number ≥ 0, deg/mm | The largest rate of change per millimetre of seam arc | The shipped weld preset `gmaw_steel_fillet` ("GMAW · mild steel · 6 mm fillet") authors 0° work and 12° travel, with bands of −15° to 15° work and 0° to 20° travel, each at most 0.5°/mm. ## Weld joints A weld joint is an objective fact from the CAD: where two parts meet. Seams refer to joints by `weld_joint_ref`. In program.v2 the list is `workpiece.weld_joints`. | Field | Type | Required | Description | |---|---|---|---| | `id` | identifier | yes | Unique | | `type` | string | yes | `butt`, `tee`, `lap`, `corner`, `edge`, `cruciform`, `plug` or `unknown` | | `members[]` | array | yes | At least two: `solid_id`, optional contact `face_ids` and `role` (`base`, `branch`, `unspecified`) | | `weld_line` | curve | yes | The line where the parts meet | | `weld_symbol` | string | no | ISO 2553, for example `fillet` or `v_groove` | | `contact` | object | no | `kind` (`coincident`, `gap`, `overlap`, `interference`), `gap_mm`, `overlap_area_mm2`, `max_face_deviation_mm` | | `groove_faces[]` | array | no | Per weld-line segment, the two exposed faces: `solid_id`, `face_id`, `surface_type`, `outward_normal_xyz` | | `dihedral_angle_deg` | scalar function, deg | no | The angle between the groove faces, through the open side | | `bisector_rail` | object | no | A rail along the dihedral bisector: `curve`, `offset_m`, `continuous`, `approximate`, `max_deviation_mm`, `within_tolerance` | | `accessibility` | object | no | Which stretches no torch direction can reach, found by casting rays: `blocked_spans`, `reachable_runs`, `open_fraction`, `suggested_start_s` and how they were measured | | `accessible_sides`, `confidence`, `evidence`, `label` | | no | Advisory data from detection | ## The `.weldplan` container A `.weldplan` is a zip file that carries one plan request. It is built by one implementation, `weldplan/plan_request.py`, through the seam worker's `build_plan_request` operation, so the bytes are reproducible: the same inputs give the same bytes and the same digests. | Member | Required | Contents | |---|---|---| | `mimetype` | yes | First, stored uncompressed: a fixed media type string, so the format can be identified without parsing JSON | | `manifest.json` | yes | What is inside, with digests; see below | | `program.json` | yes | The `robot.v4.program.v2` document, including its `placement` | | `cell.json` | yes | The cell descriptor: joints, axes, limits, frames. Mesh references are removed and `meshes_omitted` is `true`; the planner uses its own copy of the robot's meshes. | | `tooling.json` | when the program names a `tool` | The fitted torch | | `source/.step` | when the program has welds | The workpiece geometry, as bytes | | `fixtures.json` | no | Workcell bodies in the cell's world frame. Absent means the cell's own placeholder stands; an empty list means there are none. | | `context/…` | no | The planning context OLP compiled: the corrected URDF, the merged planning numbers, the sphere model and the meshes. The planner reads the robot from here rather than from its own disk. | JSON members are written with sorted keys and 2-space indentation, and every zip entry carries the fixed timestamp 1980-01-01 00:00:00. ### Manifest | Field | Description | |---|---| | `schema` | `amr-weld-planner-v1.plan-request.v1` | | `request_id`, `label`, `created_utc` | Request identity | | `app` | `{module: "amr-weld-planner/v1", ui_version}` | | `program` | `{path, program_id, schema, node_count, weld_count}` | | `cell` | `{path: "cell.json", id}` | | `planning_context` | `{root: "context/", files: [...]}`, when a context is packed | | `source` | `{kind: "none"}`, or `{kind: "step", path, filename, sha256, bytes}` | | `fixtures` | `{path: "fixtures.json", count}` or `null` | | `planner_owns[]` | Sentences naming what the request leaves to the planner: positioner joint values, the cell's collision meshes, and the torch orientation within each seam's bands | | `contents[]` | `{path, sha256, bytes}` for every member except `mimetype` and `manifest.json` | ### Checks when packing Packing refuses the request, with a sentence per problem, when: - the program fails its own program.v2 validation - the program has welds and `workpiece.frame_id` is not `workpiece` - a weld is still a hand-placed `manual_pose_wp`. OLP lowers those to seams before packing. - there is no `placement`, or its `anchor_xyz_m`, `offset_xyz_m` or `rpy_rad` is not three finite numbers, or it names no `cell_id` - the placement's `cell_id`, or the program's `robot.model_id`, differs from the packed cell's id - the cell has no `work` frame - the program has welds and no STEP was given - the program names a `tool` and no tooling was given, or pins one whose digest differs Packing also stamps two pins into `program.json`. A tool reference gets the SHA-256 of the packed `tooling.json`. A robot pin of all zeros takes the packed robot description's identity. A robot pin that names a **different** description is refused: the description changed since the program was written, so review and accept the new robot settings in OLP, then plan again. ### Checks when opening The planner refuses a `.weldplan` unless: - it is a zip file whose `mimetype` entry is the expected one - `manifest.json` exists with the expected `schema` - every member in `contents` exists and matches its SHA-256 - `program.json` is `robot.v4.program.v2` - the program's `robot.robot_description_sha256` equals the packed cell's - a program `tool` has a `tooling.json` whose SHA-256 matches its pin - every declared planning-context file exists The planner's answer names the request by the SHA-256 of the whole file. That digest becomes the plan's `program_digest` and the root of its `plan_id`. ## Seam worker protocol The seam worker is `python -m seam_worker.workers --stdin`, run in `weld_planner/v1`. It reads **one** JSON object from stdin and writes one JSON object to stdout, then exits. OLP starts one per request and forwards the operations through [`POST /api/offline-programming/v1/seam`](/docs/apis/olp-http#authoring-and-planning). ```bash cd weld_planner/v1 echo '{"operation": "sample_seam_frames", "curve": {"segments": [{"degree": 1, "control_points_xyz_m": [[0,0,0],[0.1,0,0]], "knots_u": [0,0,1,1]}]}, "reference": {"mode": "fixed_vector", "fixed_direction_xyz": [0,0,1]}, "samples": 3}' \ | pixi run -e default python -m seam_worker.workers --stdin ``` | `operation` | Request fields | Response | |---|---|---| | `detect_joints` | `step_base64`; optional `topology`, `rail_offset_m`, `intersector`, `seam_options` (`min_length_m`, `max_corner_angle_deg`) | `{topology, joints}`: the B-rep topology, and the weld joints annotated with accessibility, plus one default seam per reachable run | | `check_torch_fits` | `step_base64`, `joint`, `seam`, `torch`; optional `options` | `{clearance}`: how much of the seam the torch barrel can reach | | `plan_torch_path` | `step_base64`, `joint`, `seam`, `torch`, `default_standoff_mm`, `search` (`work_deviation_deg`, `travel_deviation_deg`; optional bin sizes, `knot_spacing_mm`, `sample_step_mm`, `ray_count`, `standoff_cost_deg_per_mm`); optional `direction` (`forward` or `reverse`), `intersector` | `{direction, plan, rays_cast, samples, warnings}`: the work and travel profile that welds the most arc, closest to what was authored | | `sample_seam_frames` | `curve`, `reference`; optional `samples`, `max_chord_m`, `max_angle_rad` | `{frames}`: the `{P, t, r, b}` frame at each sample | | `build_plan_request` | `program`, `planning_context` (`{files: {path: base64}}`), and `step_base64` or `workpiece_absent: true`; optional `step_filename`, `tooling`, `fixtures`, `request_id`, `label`, `created_utc` | `{weldplan_base64, bytes, sha256}` | `plan_torch_path` searches one direction per call. A `reverse` search answers with the reversed weld in the welder's own terms, and sets `direction_changed` in the plan. A failure is written as `{"error": {"code": "…", "detail": "…"}}`. The worker's own codes are `invalid_request` (exit status 2) and `worker_failed` (exit status 1). OLP adds `payload_too_large`, `busy`, `timeout`, `canceled`, `unavailable` and `invalid_response`; see the [OLP seam route](https://advancedmetalresearch.com/docs/apis/olp-http#authoring-and-planning). ## Related pages - [Program format (`robot.v4.program.v2`)](/docs/reference/program-format) - [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification) - [Program a weld from CAD](https://advancedmetalresearch.com/docs/guides/offline-programming) - [Weld planner HTTP API](https://advancedmetalresearch.com/docs/apis/weld-planner-http) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `weld_planner/v1/schema/amr-weld-planner-v1.weld-program.v1.schema.json ($defs identifier, curve_segment, curve, scalar_function, seam, orientation_limit_axis, weld_joint, groove_face)` - `weld_planner/v1/tests/shared/test_program_v2.py:31-46,126-140` - `weld_planner/v1/python/weldplan/plan_request.py:88-525` - `weld_planner/v1/python/seam_worker/seam/frames.py:55-84` - `weld_planner/v1/python/seam_worker/workers.py:249-701` - `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/weld_trajopt.py:494-498` - `weld_planner/v1/data/presets/weld/gmaw-steel-fillet.json` - `steamdeck/real/v4/contracts/programs/robot.v4.program.v2.schema.json ($defs seam)` - `offline-programming/v1/internal/seam/types.go:33-56` - `offline-programming/v1/internal/seam/worker.go:483-495` --- # Error codes and fault states > Every rt-control reason code (all 153), the numeric native command, jog and grant reasons, execution fault bits with their recovery classes, and axis readiness states. URL: https://advancedmetalresearch.com/docs/reference/error-codes Section: RosieOS docs / Reference Last updated: 2026-10-10 When rt-control refuses a request it returns HTTP 409 (413 for an oversized body, 404 for an unknown resource) with a reason code in `error`. This page lists every reason in the contract, grouped by area, with the operations that can return it. It also covers the numeric reasons inside native receipts, the execution fault bits and the per-axis readiness states in Status. The reason list is generated from `reasons` in `rt-core/protocol/application-v1.schema.json`, which the contract calls the closed catalogue of adapter-owned labels. The same names are exported as constants by all three clients: `control.Reason…` in Go, `rosie::rt_control::Reason…` in C++ and `Reason…` in TypeScript. > [!TIP] **Machine-readable.** Every reason on this page, with its meaning, group, HTTP status and the operations that return it, as [JSON](https://advancedmetalresearch.com/docs/data/error-codes.json). ## Reading an error 409 Conflict: ```json { "schema": "rosie.rt-control.response.v1", "operation": "start_trajectory", "error": "not_ready", "native_result": { "Sequence": 31, "Handle": 7, "Generation": 3, "Operation": 292, "Result": 1, "Reason": 2, "AxisMask": 511 } } ``` - `error` is a catalogue label, or a diagnostic that starts with one (`native_limit_exceeded: segment=2 sample=118 axis=J3`). Some errors are open diagnostics with no label: JSON decoding, I/O, context and native text such as `RTCore rejected operation 0x124: reason 2`. Match on the leading label. - `native_result` is present when the core refused a command. Read its numeric `Reason` in [native command reasons](https://advancedmetalresearch.com/docs/reference/error-codes#native-command-reasons). - `native_jog_result` is present when the jog lane refused. Read `StateReason`, `ControlReason` or `UpdateReason` in [native jog reasons](https://advancedmetalresearch.com/docs/reference/error-codes#native-jog-reasons). - `data.limit_violation` accompanies `native_limit_exceeded` and `native_segment_rate_exceeded` from a program upload. It gives `kind` (`position` or `velocity`), `segment`, `sample`, `axis`, `value`, `limit` and `unit` (`rad` or `rad/s`). Treat an unknown label, a malformed reply or a transport failure as an unknown outcome. Stop producing motion, send an authenticated `stop` if you can, and reconcile Status before acquiring again. ## rt-control reason codes All 153 reasons, in 13 groups. **Returned by** links to the operation on the [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http) page. The meaning is the contract's own text where it gives one, and otherwise a description of the code that raises the reason. ### Request envelope and transport | Reason | Meaning | Returned by | |---|---|---| | `body_too_large` | The request body exceeds the operation's size cap (HTTP 413). | Every `POST /v1/control` operation and `prepare_program` (HTTP 413) | | `invalid_request_envelope` | The body is not a JSON object, or an envelope key is not a string. | Every `POST /v1/control` operation | | `duplicate_request_field` | A field appears twice in the request envelope. | Every `POST /v1/control` operation | | `schema_mismatch` | `schema` is not `rosie.rt-control.request.v1`. | Every `POST /v1/control` operation | | `unknown_operation` | `operation` is not a POST `/v1/control` operation. | Every `POST /v1/control` operation | | `unknown_endpoint` | No route for this method and path. | Any unknown method or path. | | `trailing_request_data` | Data follows the JSON object. | Every `POST /v1/control` operation | | `request_envelope_changed` | The fully decoded request differs from the admitted envelope prefix. | Every `POST /v1/control` operation | | `invalid_request_id` | `request_id` is null, not a string, or not 1..64 printable ASCII bytes. | Every `POST /v1/control` operation | | `request_id_conflict` | The `request_id` was already used in this session with a different payload. | Every `POST /v1/control` operation | | `capability_unimplemented` | The named target capability is unavailable and performs no operation. | [`halt`](/docs/apis/rt-control-http#halt), [`io_arm`](/docs/apis/rt-control-http#io-arm), [`io_disarm`](/docs/apis/rt-control-http#io-disarm), [`mark_telemetry`](/docs/apis/rt-control-http#mark-telemetry), [`abort`](/docs/apis/rt-control-http#abort), [`readiness`](/docs/apis/rt-control-http#readiness). Also `abort`, `readiness`. | | `invalid_control_generation` | `X-Control-Generation` is missing or not a decimal uint64. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | ### Authority and session | Reason | Meaning | Returned by | |---|---|---| | `no_grant` | Halt requires a current valid application grant. | [`halt`](/docs/apis/rt-control-http#halt), [`io_arm`](/docs/apis/rt-control-http#io-arm), [`io_disarm`](/docs/apis/rt-control-http#io-disarm), [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `wrong_generation` | Halt carries a different application or native generation. | [`halt`](/docs/apis/rt-control-http#halt), [`io_arm`](/docs/apis/rt-control-http#io-arm), [`io_disarm`](/docs/apis/rt-control-http#io-disarm), [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `inhibited` | Halt requires an armed, enabled machine with current readiness; Stop or a fault takes precedence. | [`halt`](/docs/apis/rt-control-http#halt), [`io_arm`](/docs/apis/rt-control-http#io-arm), [`io_disarm`](/docs/apis/rt-control-http#io-disarm), [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `control_already_owned` | Another session holds authority, or a Stop is still draining. | [`acquire`](/docs/apis/rt-control-http#acquire) | | `control_session_stale` | The session or generation is not the current one, the lease has expired, a Stop is in flight, or the lease cannot cover the measured round trip. | Every `POST /v1/control` operation that carries a fence | | `session_principal_mismatch` | The session belongs to a different TLS principal/pair or local transport authority. | Every `POST /v1/control` operation that carries a fence | | `daemon_restarted` | The session or snapshot belongs to a previous core incarnation. | Every `POST /v1/control` operation that carries a fence | | `expired` | The lease deadline has passed. | [`renew`](/docs/apis/rt-control-http#renew), [`release`](/docs/apis/rt-control-http#release), [`stop`](/docs/apis/rt-control-http#stop) | | `fence` | A well-formed session this adapter never issued, or authority revoked while a Start was in progress. | Every `POST /v1/control` operation that carries a fence | | `authority_binding_mismatch` | `controller` is empty or longer than 63 bytes, or the binding's pair, revision or digest does not match the deployment. | [`acquire`](/docs/apis/rt-control-http#acquire) | | `application_generation_exhausted` | The uint64 grant generation is exhausted; no further Acquire is possible without a restart. | [`acquire`](/docs/apis/rt-control-http#acquire) | | `invalid_requested_lease` | Acquire requested lease is negative or exceeds the unverified absolute 10000 ms bound. | [`acquire`](/docs/apis/rt-control-http#acquire) | ### Handles | Reason | Meaning | Returned by | |---|---|---| | `unknown_handle` | No such handle in this adapter incarnation. | [`start_trajectory`](/docs/apis/rt-control-http#start-trajectory), [`discard_trajectory`](/docs/apis/rt-control-http#discard-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `trajectory_identity_mismatch` | Raw trajectory Start identity differs from its immutable preparation. | [`start_trajectory`](/docs/apis/rt-control-http#start-trajectory) | | `handle_active` | The handle has started, so it cannot be discarded. | [`discard_trajectory`](/docs/apis/rt-control-http#discard-trajectory) | | `handle_started` | This handle has already started. | [`start_trajectory`](/docs/apis/rt-control-http#start-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `handle_consumed` | This handle completed or was replaced by a later execution. | [`start_trajectory`](/docs/apis/rt-control-http#start-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `handle_discarded` | This handle was explicitly discarded. | [`start_trajectory`](/docs/apis/rt-control-http#start-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `handle_superseded` | A committed replacement superseded this handle. | [`start_trajectory`](/docs/apis/rt-control-http#start-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `handle_retired` | Cancellation or uncertain ownership permanently retired this handle. | [`start_trajectory`](/docs/apis/rt-control-http#start-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | ### Native admission | Reason | Meaning | Returned by | |---|---|---| | `native_rejected` | Native command admission refused; inspect native_result. | Every `POST /v1/control` operation | | `not_ready` | The controlled axes do not satisfy native movement readiness. | [`start_trajectory`](/docs/apis/rt-control-http#start-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program). Also native command reason 2 inside `native_result`. | | `mode_conflict` | Another active motion mode prevents this operation. | [`restore_anchor`](/docs/apis/rt-control-http#restore-anchor), [`begin_jog`](/docs/apis/rt-control-http#begin-jog), [`jog`](/docs/apis/rt-control-http#jog), [`start_trajectory`](/docs/apis/rt-control-http#start-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `outside_limits_outward` | A motion request increases an exterior limit excursion or crosses the opposite boundary; native command reason 8 and jog reason 15. | Every `POST /v1/control` operation | | `pdo_mapping_mismatch` | Assigned PDO mapping differs from the registered layout; corrected bring-up is required. | Native command reason 7 in `native_result`, and fault bit 11 (`restart_required`) in `RecoveryStatus`. | ### Program admission and execution | Reason | Meaning | Returned by | |---|---|---| | `busy` | An upload of the same kind is already in progress, the lifecycle lock is busy, or the core has no free plan slot. | [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory), [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `program_identity_missing` | The `.rdt` header lacks a required identity field. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `program_identity_mismatch` | No program is prepared, or the supplied identity differs from it. | [`start_program`](/docs/apis/rt-control-http#start-program) | | `program_already_executing` | A trajectory or program is executing. | [`reset_fault`](/docs/apis/rt-control-http#reset-fault), [`recovery_status`](/docs/apis/rt-control-http#recovery-status), [`prepare_program`](/docs/apis/rt-control-http#prepare-program), [`start_program`](/docs/apis/rt-control-http#start-program) | | `manifest_revision_mismatch` | The program's `manifest_revision` differs from the adapter's pair revision. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `process_io_executor_not_qualified` | The program needs torch output, which is not qualified and always refused. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program), [`start_program`](/docs/apis/rt-control-http#start-program) | | `dense_axis_map_requires_nine_ids` | The cell describes more rotary axes than the nine-column dense format carries. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `invalid_dense_axis_map` | The cell's rotary axes cannot be mapped onto the dense columns. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `unmapped_dense_axis` | The configured dense axis ID has no native axis mapping. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `native_limit_exceeded` | A dense sample exceeds a native position, velocity or declared sampled acceleration limit. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `native_segment_rate_exceeded` | An interpolated program segment exceeds a velocity limit. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `segment_boundary_discontinuous` | Rejected moving segment seam; boundary is the zero-based join index and gap_counts reports second minus first in source-axis counts. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `native_execution_failed` | Native execution ended without successful completion; motion completion is unverified. Inspect status and fault details, correct the cause, and explicitly prepare and start a new execution. | `execution.error` in `GET /v1/status` when a started program ends without completing. | ### Dense program format (.rdt) | Reason | Meaning | Returned by | |---|---|---| | `dense_schema_mismatch` | Dense header schema does not name the supported dense trajectory format. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `header_truncated` | Dense header length or header bytes are incomplete. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `header_json_invalid` | Dense header cannot be decoded or encoded as JSON. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `sample_encoding_mismatch` | Dense record encoding or record byte count is unsupported. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `axis_count_mismatch` | Dense axis count differs from the nine-axis format. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `limits_invalid` | Dense velocity limits must contain nine positive finite values. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `segments_empty` | Dense program contains no motion segments. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `segment_index_out_of_order` | Dense segment indices do not follow payload order. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `kind_invalid` | Dense segment kind is not freespace, weld or dwell. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `segment_too_short` | Dense segment contains fewer than two samples. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `block_layout_invalid` | Dense block offsets, record lengths or segment metadata counts are inconsistent. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `blob_length_mismatch` | Dense payload length differs from the declared block extents. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `block_sha256_mismatch` | Dense segment bytes do not match their declared SHA-256 digest. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `reserved_flags_set` | Dense sample sets a reserved process flag bit. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `total_sample_count_mismatch` | Dense total sample count differs from its segment counts. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `sample_count_overflow` | Dense source sample count exceeds the format capacity. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `trajectory_digest_mismatch` | Dense content digest differs from the declared trajectory digest. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `time_grid_invalid` | Dense sample clock is not on its declared positive time grid. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `nonfinite_sample` | Dense time, position or velocity sample is not finite. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `qd_limit_exceeded` | Dense supplied velocity exceeds its declared format limit. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `torch_outside_weld` | Dense torch flag is set outside a weld segment. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `q_step_exceeded` | Dense position step exceeds the format velocity allowance. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `boundary_qd_nonzero` | Rejected dense segment endpoint velocity above the stationary boundary tolerance. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `boundary_q_discontinuity` | Rejected dense segment position gap or accumulated normalisation correction above the boundary tolerance. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | | `duration_mismatch` | Dense segment duration differs from its final sample clock. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | ### Recovery and anchors | Reason | Meaning | Returned by | |---|---|---| | `reset_requires_inhibited` | The machine is armed, jogging, commissioning or homing, or no fresh observation arrived within 1 s. | [`reset_fault`](/docs/apis/rt-control-http#reset-fault), [`recovery_status`](/docs/apis/rt-control-http#recovery-status) | | `fault_persists` | At least one fault condition still prevents clearing. | [`reset_fault`](/docs/apis/rt-control-http#reset-fault) | | `recovery_observation_unavailable` | No matching native recovery observation arrived within 1 s. | [`restore_anchor`](/docs/apis/rt-control-http#restore-anchor), [`reset_fault`](/docs/apis/rt-control-http#reset-fault), [`recovery_status`](/docs/apis/rt-control-http#recovery-status) | | `invalid_axis_mask` | The requested axis mask is empty or names an axis outside the configured group. | [`restore_anchor`](/docs/apis/rt-control-http#restore-anchor) | | `anchor_missing` | No persisted anchor exists for the axis. | [`restore_anchor`](/docs/apis/rt-control-http#restore-anchor) | | `anchor_identity_mismatch` | The persisted anchor was recorded under a different configuration digest, drive identity or home epoch. | [`home`](/docs/apis/rt-control-http#home), [`restore_anchor`](/docs/apis/rt-control-http#restore-anchor) | | `anchor_source_invalid` | The drive reports no valid absolute source for the axis. | [`home`](/docs/apis/rt-control-http#home), [`restore_anchor`](/docs/apis/rt-control-http#restore-anchor) | | `anchor_disagrees` | The current absolute source disagrees with the persisted anchor beyond the profile tolerance. | [`restore_anchor`](/docs/apis/rt-control-http#restore-anchor) | | `anchor_store_io` | Anchor storage could not be read or persisted; unverified stored evidence cannot authorize restoration. Filesystem details are logged locally and never returned in public feedback. Retry after checking the anchor directory and disk. On restore refusal, the anchor store is not modified. | [`home`](/docs/apis/rt-control-http#home), [`restore_anchor`](/docs/apis/rt-control-http#restore-anchor) | ### Cell I/O | Reason | Meaning | Returned by | |---|---|---| | `io_not_configured` | named cell I/O is not configured. | [`io_arm`](/docs/apis/rt-control-http#io-arm), [`io_disarm`](/docs/apis/rt-control-http#io-disarm), [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory), [`prepare_program`](/docs/apis/rt-control-http#prepare-program), [`start_program`](/docs/apis/rt-control-http#start-program) | | `io_not_armed` | Explicit io_arm is required before an ON intent. | [`io_arm`](/docs/apis/rt-control-http#io-arm), [`io_disarm`](/docs/apis/rt-control-http#io-disarm), [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `io_fast_input_unsatisfied` | A cyclic fast contact is invalid or not satisfied. | [`io_arm`](/docs/apis/rt-control-http#io-arm), [`io_disarm`](/docs/apis/rt-control-http#io-disarm), [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `io_readback_disagreement` | Independent physical feedback disagrees with commanded output. | [`io_arm`](/docs/apis/rt-control-http#io-arm), [`io_disarm`](/docs/apis/rt-control-http#io-disarm), [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `io_torch_unqualified` | torch-class markers remain refused pending hardware qualification. | [`io_arm`](/docs/apis/rt-control-http#io-arm), [`io_disarm`](/docs/apis/rt-control-http#io-disarm), [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory), [`prepare_program`](/docs/apis/rt-control-http#prepare-program), [`start_program`](/docs/apis/rt-control-http#start-program) | | `io_marker_late` | A process marker missed its one-cycle delivery bound. | [`io_arm`](/docs/apis/rt-control-http#io-arm), [`io_disarm`](/docs/apis/rt-control-http#io-disarm), [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `io_exchange_lost` | Cell I/O has no current complete exchange. | [`io_arm`](/docs/apis/rt-control-http#io-arm), [`io_disarm`](/docs/apis/rt-control-http#io-disarm), [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory), [`start_program`](/docs/apis/rt-control-http#start-program) | | `io_marker_invalid` | A marker names an unknown output or has an invalid sample association. | [`prepare_program`](/docs/apis/rt-control-http#prepare-program) | ### Jog lane and jog clock | Reason | Meaning | Returned by | |---|---|---| | `independent_jog_unavailable` | The native client offers no independent jog lane, or the remote listener is closing. | [`jog_clock`](/docs/apis/rt-control-http#jog-clock), [`jog_status`](/docs/apis/rt-control-http#jog-status), [`begin_jog`](/docs/apis/rt-control-http#begin-jog), [`update_jog`](/docs/apis/rt-control-http#update-jog), [`end_jog`](/docs/apis/rt-control-http#end-jog) | | `jog_session_stale` | The jog session or its generation is not the current one. | [`update_jog`](/docs/apis/rt-control-http#update-jog), [`end_jog`](/docs/apis/rt-control-http#end-jog) | | `jog_session_or_sequence_stale` | No open jog session for this generation, or the source sequence did not increase. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_publisher_busy` | Another producer is publishing jog input at the same moment. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_invalid_frame` | A binary jog frame could not be decoded. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_stream_idle` | Rejected: no complete jog stream frame within the cell's jog input-age ceiling (250 ms on LAN). | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_clock_unqualified` | Remote clock qualification is disabled (the `--remote-jog-*` flags are 0) or calibration is incomplete. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_clock_invalid_budget_or_exchange` | A malformed calibration or Begin message, or an invalid timing budget. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_clock_incarnation_mismatch` | The clock incarnation is empty or differs from the negotiated one. | [`begin_jog`](/docs/apis/rt-control-http#begin-jog), [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_clock_mapping_generation_mismatch` | The input was mapped with an out-of-date clock mapping. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_clock_moved_backwards` | A source timestamp went backwards, or input predates the Begin sample. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_clock_arithmetic_range` | Converting a jog timestamp would overflow. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_clock_uncertainty_exceeded` | The calibrated offset interval is wider than `--remote-jog-max-uncertainty-ns`. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_clock_calibration_expired` | The remote clock calibration is older than `--remote-jog-calibration-max-age-ns`. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_clock_exchange_inconsistent` | The calibration timestamps are not causally consistent. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_input_too_old` | The conservatively mapped input age exceeds the cell's input-age ceiling. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_input_entirely_future` | The whole input interval lies in the host's future. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | | `jog_input_deadline_expired` | The input's deadline has already passed. | [`update_jog`](/docs/apis/rt-control-http#update-jog) | ### Events, telemetry and resources | Reason | Meaning | Returned by | |---|---|---| | `invalid_event_cursor` | Event cursor must be a single unsigned decimal uint64. | [`subscribe_events`](/docs/apis/rt-control-http#subscribe-events) | | `event_cursor_ahead` | Cursor exceeds this adapter incarnation's newest event; obtain current status/incarnation before resuming. | [`subscribe_events`](/docs/apis/rt-control-http#subscribe-events) | | `event_cursor_lost` | Event history was lost; reconcile status before resuming from a new cursor. | [`subscribe_events`](/docs/apis/rt-control-http#subscribe-events). Client side: the C++ `EventCursorLost` exception. | | `invalid_telemetry_cursor` | Telemetry after must be one unsigned decimal uint64 cursor. | [`telemetry`](/docs/apis/rt-control-http#telemetry) | | `telemetry_cursor_ahead` | Telemetry cursor exceeds the current ring sequence; reconcile incarnation before restarting. | [`telemetry`](/docs/apis/rt-control-http#telemetry) | | `telemetry_busy` | Ring header remained torn after the bounded 16 samples; retry the same cursor. Established streams continue with an empty batch and unchanged cursor. | [`telemetry`](/docs/apis/rt-control-http#telemetry) | | `telemetry_unavailable` | A compatible live telemetry ring could not be observed. | [`telemetry`](/docs/apis/rt-control-http#telemetry) | | `telemetry_label_invalid` | Telemetry label must contain 1..128 valid UTF-8 bytes. | [`mark_telemetry`](/docs/apis/rt-control-http#mark-telemetry) | | `resource_unknown` | Digest is malformed or absent from the loaded compiled resource set; HTTP 404. | [`resource`](/docs/apis/rt-control-http#resource) | ### WebSocket transport (remote jog) | Reason | Meaning | Returned by | |---|---|---| | `invalid websocket upgrade` | Missing or wrong upgrade headers, or a `Sec-WebSocket-Key` that is not 16 bytes. | WSS upgrade: HTTP 400, text/plain. | | `hijacking unavailable` | The server could not take over the connection for the WebSocket. | WSS upgrade: internal; the upgrade is abandoned. | | `fragmentation rejected` | A fragmented or continuation WebSocket frame was received. | WSS: connection closed with code 1009. | | `invalid frame flags` | Reserved WebSocket bits are set, or a client frame is not masked. | WSS: reserved bits set or unmasked frame; closed with code 1002. | | `noncanonical length` | A WebSocket payload length is not in its shortest encoding. | WSS: closed with code 1002. | | `payload too large` | A WebSocket frame payload exceeds 4096 bytes. | WSS: frame over 4096 bytes; closed with code 1009. | | `invalid opcode` | An unknown WebSocket opcode. | WSS: closed with code 1002. | | `control payload too large` | A WebSocket control frame carries more than 125 bytes. | WSS: control frame over 125 bytes; closed with code 1002. | | `invalid UTF-8` | A WebSocket text frame is not valid UTF-8. | WSS: text frame; closed with code 1007. | | `invalid close payload` | A WebSocket close frame has a one-byte payload. | WSS: closed with code 1002. | | `invalid close code` | A WebSocket close frame carries a reserved or invalid code. | WSS: closed with code 1002. | | `invalid close reason` | A WebSocket close reason is not valid UTF-8. | WSS: closed with code 1007. | ### Remote TLS (startup and handshake) | Reason | Meaning | Returned by | |---|---|---| | `remote pair-id required` | `--pair-id` is missing or not a valid token while the remote listener is enabled. | rt-control startup; exits with status 2. | | `remote CA, certificate, key and CRL are required` | `--remote-listen` is set but a CA, certificate, key or CRL path is missing. | rt-control startup; exits with status 2. | | `remote CA must contain one CA certificate` | The CA file is not exactly one PEM certificate. | rt-control startup; exits with status 2. | | `remote CA must be a self-signed component CA` | The CA certificate is not a CA, or is not self-signed. | rt-control startup; exits with status 2. | | `invalid remote CRL PEM` | The CRL file is not a PEM `X509 CRL`. | rt-control startup; exits with status 2. | | `unsupported critical CRL extension` | The CRL has a critical extension that rt-control does not support. | rt-control startup; exits with status 2. | | `remote CRL not current` | The current time is outside the CRL's validity window. | Startup (exit 2), and every later TLS handshake. | | `remote client must be issued directly by component CA` | The client chain is not exactly the leaf plus the component CA. | TLS handshake: the client is refused before any HTTP request. | | `remote client revoked` | The client certificate's serial number is on the CRL. | TLS handshake: the client is refused before any HTTP request. | | `remote identity SAN must be an opaque token` | A `rosie-principal` or `rosie-pair` URI has a host, user, path, query or fragment. | TLS handshake: the client is refused before any HTTP request. | | `invalid remote principal SAN` | The client certificate has more than one `rosie-principal` URI, or its token is invalid. | TLS handshake: the client is refused before any HTTP request. | | `invalid remote pair SAN` | The client certificate has more than one `rosie-pair` URI, or its token is invalid. | TLS handshake: the client is refused before any HTTP request. | | `remote principal/pair binding mismatch` | The certificate lacks a principal or pair, or its pair is not this cell's `--pair-id`. | TLS handshake: the client is refused before any HTTP request. | ### Startup and socket ownership | Reason | Meaning | Returned by | |---|---|---| | `invalid_pair_configuration_binding` | At startup: `--pair-id` is empty, `--pair-revision` is 0, or `--configuration-sha256` does not match the core. | rt-control startup; exits. | | `resource_artifact_invalid` | Startup refuses an incomplete, mismatched, malformed or over-bound compiled resource artefact. | rt-control startup; exits. | | `socket owned by a live process` | Another live process holds the socket's lock. rt-control exits with status 3. | rt-control startup; exits with status 3. | | `socket ownership cannot be verified` | The existing socket path is not a socket owned by this user. | rt-control startup; exits. | | `socket lock ownership cannot be verified` | The `.lock` file beside the socket is not a private regular file owned by this user. | rt-control startup; exits. | | `socket lock path changed` | The `.lock` file was replaced while rt-control was claiming it. | rt-control startup; exits. | | `bound path is not a socket` | After binding, the socket path is not the expected socket. | rt-control startup; exits. | | `process start time unavailable` | rt-control could not read its own start time to record socket ownership. | rt-control startup; exits. | | `unsupported socket ownership transport` | An internal socket type other than stream or datagram was requested. | rt-control startup; exits. | Four error texts carry detail after a fixed prefix: `unmapped_dense_axis: `, `native_limit_exceeded: segment= sample= axis=`, `discard preparation : ` and `jog_native_rejected_` (a WebSocket jog refusal carrying a [native jog reason](https://advancedmetalresearch.com/docs/reference/error-codes#native-jog-reasons)). At startup, `remote CRL signature: ` reports a CRL that the CA did not sign. ## Native command reasons The numeric `native_result.Reason` in a refused command, from `reasons` in `rt-core/protocol/control.json`. `native_result.Result` is 0 accepted, 1 rejected or 2 prepared. | Code | Name | |---|---| | 0 | `none` | | 1 | `invalid_command` | | 2 | `not_ready` | | 3 | `mode_conflict` | | 4 | `unknown_handle` | | 5 | `capacity` | | 6 | `invalid_trajectory` | | 7 | `pdo_mapping_mismatch` | | 8 | `outside_limits_outward` | | 9 | `no_grant` | | 10 | `wrong_generation` | | 11 | `inhibited` | | 12 | `io_not_configured` | | 13 | `io_not_armed` | | 14 | `io_fast_input_unsatisfied` | | 15 | `io_readback_disagreement` | | 16 | `io_torch_unqualified` | | 17 | `io_marker_late` | | 18 | `io_exchange_lost` | rt-control translates some of these into labels: 8 becomes `outside_limits_outward`, the cell I/O reasons 9..18 become their labels for I/O-carrying calls, and 2 or 3 during a Start become `not_ready` or `mode_conflict`. Anything else arrives as `native_rejected`. Reason 5 (capacity) during a program upload arrives as `busy`, and reason 6 (invalid trajectory) retires every prepared handle. ## Native jog reasons The numeric reasons of the jog lane, from `jog_reasons` in `control.json`. They appear in `JogObservation` (`StateReason`, `ControlReason`, `UpdateReason`), in `/v1/jog/ingress` counters and in WebSocket `jog_native_rejected_` refusals. Reason 14 (`limited`) is an accepted, governed state, not a failure. | Code | Name | |---|---| | 0 | `none` | | 1 | `closed` | | 2 | `mode_conflict` | | 3 | `not_ready` | | 4 | `wrong_identity` | | 5 | `invalid_generation` | | 6 | `stale_sequence` | | 7 | `invalid_vector` | | 8 | `invalid_timing` | | 9 | `expired` | | 10 | `authority_lost` | | 11 | `clock_reset` | | 12 | `generation_exhausted` | | 13 | `ramping` | | 14 | `limited` | | 15 | `outside_limits_outward` | ## Native grant reasons The numeric `GrantReason` in `GrantObservation`, from `grant_reasons` in `control.json`. | Code | Name | |---|---| | 0 | `none` | | 1 | `invalid` | | 2 | `connection_mismatch` | | 3 | `native_generation_mismatch` | | 4 | `disconnected` | | 5 | `busy` | | 6 | `not_inhibited` | | 7 | `stale_generation` | | 8 | `identity_mismatch` | | 9 | `expired` | | 10 | `clock_regression` | | 11 | `inactive` | | 12 | `axis_mask_mismatch` | ## Execution fault bits `core.execution_fault_reasons` in Status is a bit mask of latched execution faults. `RecoveryStatus.faults[]` names each latched bit with its affected axes and recovery class. This table is generated from `rt-core/include/fault_recovery.hpp`, whose own comments call the policy unverified. | Bit | Name | Invalidates Home | Needs drive reset | Needs reverification | Recovery class | |---|---|---|---|---|---| | 0 | `start_discontinuity` | No | No | No | `reset_clears` | | 1 | `position_rate` | Yes | No | No | `rehome_required` | | 2 | `cycle_deadline` | No | No | No | `reset_clears` | | 3 | `cycle_sleep` | No | No | No | `reset_clears` | | 4 | `target_lead` | Yes | No | No | `rehome_required` | | 5 | `following_error` | No | No | No | `reset_after_condition_clears` | | 6 | `coordinate_reference` | Yes | No | No | `rehome_required` | | 7 | `drive_readiness` | No | Yes | Yes | `reset_after_condition_clears` | | 8 | `bus_transport` | No | No | Yes | `reset_after_condition_clears` | | 9 | `completion_timeout` | No | No | No | `reset_clears` | | 10 | `brake_hold` | No | No | Yes | `restart_required` | | 11 | `pdo_mapping_mismatch` | Yes | No | Yes | `restart_required` | | 12 | `collision_watchdog` | No | No | No | `reset_after_condition_clears` | | 13 | `cell_io` | No | No | No | `restart_required` | Loss of trusted feedback can also require Home even when the table says No. Unknown bits persist. | Recovery class | What to do | |---|---| | `reset_clears` | `reset_fault` clears it. | | `reset_after_condition_clears` | Remove the cause first (fresh feedback below the bound, a healthy bus, drives fault-free), then `reset_fault`. | | `rehome_required` | `reset_fault`, then Home (or a qualified `restore_anchor`) on the axes in `rehome_axis_mask` before any motion. | | `restart_required` | Reset cannot clear it. Investigate, then restart the core. | `reset_fault` outcomes per bit are `cleared`, `persists` or `rehome_required`. Any persistent selected condition refuses the whole reset with `fault_persists`. ## Axis readiness Each entry of `axes[]` in Status has a `readiness` string: the first failing gate for that axis, in this order. It is an observation, not authority: the group gates (lease, Arm, bus, faults, configuration) still apply at every Start. | Order | Value | What to do | |---|---|---| | 1 | `faulted` | A drive alarm or latched safety fault. Diagnose it and follow its recovery class. | | 2 | `mode_mismatch` | The drive is not in CSP mode (8). Wait for an expected commissioning transition, or restore the mode. | | 3 | `home_required` | Home, or a qualified `restore_anchor`, before Start. | | 4 | `coordinate_invalid` | Coordinates are not trustworthy. Restore feedback, configuration or Home evidence. | | 5 | `not_enabled` | Enable the axis under a live grant. | | 6 | `not_operation_enabled` | Wait for the CiA402 transition to Operation Enabled. | | 7 | `brake_wait` | Wait for the brake release delay. | | 8 | `ready` | This axis passes. Check the whole selected group and the grant before Start. | The code also defines `warning_tolerated`: one tolerated drive warning on an axis without an encoder battery. It grants nothing, and Home and Enable keep their normal gates. ## Handle states `HandleRecord.state`, from `handle_states` in the schema. Only `prepared` can start, and terminal states never regain permission. | State | Meaning | |---|---| | `prepared` | Native preparation acknowledged. Start may be attempted under current authority. | | `started` | Native Start accepted. Observe completion. | | `consumed` | Completed, or a later execution replaced it. | | `discarded` | Discarded. | | `superseded` | A newer preparation replaced it. | | `retired` | Stop, grant or connection loss, or an uncertain outcome. Never startable again. | ## Other components Components that call rt-control add their own refusal codes. They are documented with each component: | Component | Codes | Page | |---|---|---| | Dense trajectory daemon | `native_grant_active`, `native_acquisition_required`, `native_home_unavailable`, `native_torch_unsupported`, `pause_unsupported`, `cleanup_uncertain`, `home_required` | [Dense trajectory daemon](https://advancedmetalresearch.com/docs/apis/dense-trajectory-daemon) | | Cartesian motion server | `rt_core_position_input_unresolved`, `rt_core_run_not_admitted`, `rt_core_home_abandoned` | [Cartesian motion server](https://advancedmetalresearch.com/docs/apis/cartesian-motion-server) | | Offline programming server | `target_changed`, `cell_configuration_mismatch`, `backend_retired`, `motion_plan_seam_refused`, `motion_plan_unjoined`, `motion_plan_no_trajectory` | [Offline programming HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http) | | Weld planner admission | `motion_join_failed`, `seam_not_planned`, `motion_not_verified`, `motion_result_invalid`, `candidate_only_requires_m4` | [Weld planner HTTP API](https://advancedmetalresearch.com/docs/apis/weld-planner-http) | ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/protocol/application-v1.schema.json:421 (reasons)` - `rt-core/protocol/application-v1.schema.json:413 (handle_states)` - `rt-core/protocol/control.json (reasons, jog_reasons, grant_reasons, results)` - `rt-core/include/fault_recovery.hpp:26-64` - `rt-core/include/motion_readiness.hpp:14-21` - `rt-core/adapters/rosie/control/http.go:247-283` - `rt-core/adapters/rosie/control/handles.go:5-19` - `rt-core/adapters/rosie/control/cell_io_linux.go:77-88` - `rt-core/adapters/rosie/control/websocket.go:80-148` - `rt-core/adapters/rosie/control/remote_tls.go:22-160` - `rt-core/adapters/rosie/control/socket_owner.go:19-149` - `rt-core/cmd/rt-control/main.go:199-213` - `rt-core/engine/status_publisher.hpp:1065` - `motion-server/joint-trajectory/v1/src/rt_control_executor.hpp` - `motion-server/v1/src/rt_core_cartesian_runtime.hpp` - `offline-programming/v1/weld_plan.go:502-530` - `offline-programming/v1/internal/targets/picker.go` - `weld_planner/v1/python/weldplan/native_admission.py:204-250` --- # NATS subjects and streams > Every NATS subject and JetStream stream RosieOS publishes or subscribes to, with payload schemas, the processes on each side, and which subjects carry command authority. URL: https://advancedmetalresearch.com/docs/reference/nats-subjects Section: RosieOS docs / Reference Last updated: 2026-10-10 RosieOS uses NATS for two different jobs: - **Observation.** `rt-natspublisher` copies `rt-control` status and telemetry onto NATS for displays and archives. These subjects carry no command authority. Nothing that reads them can move the robot. - **Motion-server commands.** The Cartesian motion server and the dense trajectory daemon take their leader lease and their commands from a per-cell robot command subject. `rt-control` itself never reads NATS. Its authority is the lease on its own socket. See [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority). > [!WARNING] The command subjects have no authentication. The motion servers' leader lease is cooperative fencing between well-behaved controllers on a trusted network; anyone who can publish on the subject can send a `stop`, or try to take the lease. Keep the cell's NATS server on a private network. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model). > [!TIP] **Machine-readable.** The subject map, streams and command tables below as [JSON](https://advancedmetalresearch.com/docs/data/nats-subjects.json). ## Subject map ``, `` and `` are deployment names, not fixed strings. `` is passed through a token filter: every run of characters outside `A-Z a-z 0-9 - _` becomes one `-`. | Subject | Publisher | Subscriber | Payload | Authority | |---|---|---|---|---| | `robot/v4/rtcore..status` | `rt-natspublisher` | Displays, archives | JSON `robot.v4.rtcore.status.v1` | None | | `robot/v4/rtcore..telemetry.batch` | `rt-natspublisher` | Displays, archives | JSON `robot.v4.rtcore.telemetry.batch.v1` | None | | `rosie/rt-core..telemetry.v1` | `rt-natspublisher` | Archives | Binary `TelemetryBatch` | None | | `robot/v4/robot..command` | Controllers | Cartesian motion server, dense trajectory daemon | JSON `robot.v4.robot-command.v1` | Leader lease commands and motion commands | | NATS reply subject of each command | Motion servers | The sender | JSON `robot.v4.command-reply.v1` | None | | `robot/v4/motion-server..status` | Cartesian motion server | Controllers, displays | JSON `robot_v4_motion_server_cartesian_live_status_v1` | None | | `robot/v4/motion-server..plan` | Cartesian motion server (self-test plan paths) | — | JSON | None | | `rt_core.status_subject`, default `.status` | Dense trajectory daemon | Controllers | JSON executor status | None | | `--status-publish-subject` | Dense trajectory daemon | Controllers | JSON ingest and executor status | None | The exact command and status subjects come from command-line flags or from a deployment manifest (`robot_command_subject` and each node's `cold_path_subjects`). The Cartesian motion server only accepts subjects that name its concrete peer: `robot/v4/motion-server..…` or `robot/v4/robot..…`. The dev stack uses `robot/v4/robot.dev-cell.command` and `robot/v4/robot.dev-cell.status` for the dense daemon. ## Observation: rt-natspublisher `rt-natspublisher` reads the public `rt-control` socket, never the core's private IPC, and holds no grant. ```bash rt-natspublisher --socket /run/rosie-rt-core/public/control.sock \ --nats-url nats://127.0.0.1:4222 --host cell-a --cadence 500ms ``` | Flag | Environment | Default | Description | |---|---|---|---| | `--socket PATH` | — | `/run/rosie-rt-core/public/control.sock` | The public `rt-control` socket | | `--nats-url URL` | `ROSIE_RT_NATS_URL` | none | `nats://host:port`. Credentials, TLS and paths in the URL are refused. | | `--host NAME` | `ROSIE_RT_NATS_HOST` | none | The deployment's identity in the subject. Must be concrete: not empty, not `unknown`, no `@ / ? # = , \ " ' * >` or whitespace. | | `--cadence D` | `ROSIE_RT_NATS_CADENCE` | `500ms` | Status and telemetry-batch interval, whole milliseconds, 1 ms to 5 s | | `--telemetry-cadence D` | `ROSIE_RT_NATS_TELEMETRY_CADENCE` | the status cadence | Binary telemetry polling interval | | `--telemetry-max-bytes N` | `ROSIE_RT_NATS_TELEMETRY_MAX_BYTES` | `536870912` (512 MiB) | Byte retention of the binary telemetry stream | | `--print-env` | — | — | Validate the settings and print them as a systemd environment file | The installed unit `rosie-rt-natspublisher.service` reads `/etc/rosie-rt-core/natspublisher.env`. Before publishing, the publisher subscribes to its own status subject for one cadence. If another publisher is already live on that identity, it refuses to start (`another publisher is active on the deployment status subject`). ### Streams The publisher creates or checks both JetStream streams on connect: | Stream | Subjects | Retention | Limits | |---|---|---|---| | `ROBOT_V4_RTCORE` | `robot/v4/rtcore.>` | limits, file storage, discard old | 250,000 messages, 256 MiB, 10 minutes | | `ROSIE_RT_CORE_TELEMETRY` | `rosie/rt-core.*.telemetry.v1` | limits, file storage, discard old | no message or age limit; `--telemetry-max-bytes` | At a 1 kHz cycle, one 100 s telemetry ring window is about 539 MB, so the default 512 MiB cap can evict records before a full window is kept. ### Status payload ```json { "schema": "robot.v4.rtcore.status.v1", "stream": "ROBOT_V4_RTCORE", "subject": "robot/v4/rtcore.cell-a.status", "host": "cell-a", "component": "rtcore", "published_at": "2026-09-29T12:00:00.000Z", "publisher_state": "publishing", "last_error": "", "reconnects": 0, "drops": 0, "dropped": 0, "status": { "name": "robot-v4-rtcore", "state": "running", "rtcore_mode": "sim", "simulation_mode": true, "ethercat_live": false, "servos_armed_requested": false, "home_valid_axis_mask": 511, "axis_enable_mask": 0, "configured_axis_count": 9, "safe_min_rad": [ … ], "safe_max_rad": [ … ], "rt_core": { "core": { … }, "motion": { … }, "execution": { … }, "axes": [ … ], "configuration_sha256": "…", "machine_sha256": "…" } } } ``` `status` is `null` until the publisher has a valid core sample. `rt_core` holds the public `rt-control` status and the deployment digests; the flat fields beside it keep the shape an earlier consumer expected, and fields that `rt-control` cannot supply are `null`. For the meaning of the `rt_core` fields, see the [rt-control status](https://advancedmetalresearch.com/docs/apis/rt-control-http#status). The telemetry batch message (`robot.v4.rtcore.telemetry.batch.v1`) carries one sampled record per cadence in `records`, with `sequence`, `source_monotonic_ms` and `interval_ms`. For every cycle, use the binary stream instead. ### Binary telemetry `rosie/rt-core..telemetry.v1` carries `rt-control`'s binary `TelemetryBatch` exactly: a 312-byte header and 5,392-byte cycle records, split to fit the server's `max_payload`. The publisher keeps its own cursor and advances it only past acknowledged records. If `rt-control` or the core restarts, it stops with an error rather than join two incarnations. See [Events and telemetry streams](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry). ## Commands: the robot command subject Both motion servers subscribe to the cell's robot command subject and share its envelope, `robot.v4.robot-command.v1`, with `schema`, `command`, `robot`, `command_id` and `sender_id`. Each ignores the commands the other owns. Their lease fields differ: | | Cartesian motion server | Dense trajectory daemon | |---|---|---| | Lease commands | `leader_acquire`, `leader_renew`, `leader_release`, `leader_cancel` | `leader_acquire`, `leader_renew`, `leader_release` | | Lease request fields | `controller_boot_id`, `request_id`, `campaign_generation`, `ttl_ms`, `lease_id`, `fence_epoch` | `controller_boot_id`, `ttl_ms`, `lease_id`, `leader_fence_epoch` | | Motion credential fields | `controller_boot_id`, `leader_lease_id`, `leader_fence_epoch` | the same, plus `motion_intent_seq` | | Allowed senders | Roles in the manifest's `leader_controller_roles` (default `steamdeck`) | `offline-programming:` only | | TTL | 500–2000 ms | `leader.ttl_min_ms`–`leader.ttl_max_ms` (500–2000 ms by default) | | Commands | `arm`, `disarm`, `home`, `go_home`, `position`, `stop`, `end_run` | `play`, `pause`, `go_home`, `stop` | `stop` needs no lease on either server. Full field tables are on each server's page: [Cartesian motion server](https://advancedmetalresearch.com/docs/apis/cartesian-motion-server#nats-commands) and [Dense trajectory daemon](https://advancedmetalresearch.com/docs/apis/dense-trajectory-daemon#nats-control-plane). ## Related pages - [Architecture](https://advancedmetalresearch.com/docs/concepts/architecture) - [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning) - [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): - `rt-core/tools/natspublisher/main.go:44-101` - `rt-core/tools/natspublisher/publisher/publisher.go:26,44,60-110,140-160,219-224,316-385` - `rt-core/tools/natspublisher/publisher/nats.go:21-49,188-203` - `rt-core/tools/natspublisher/publisher/telemetry.go:16-183` - `rt-core/tools/natspublisher/publisher/payload.go:43-175` - `rt-core/host/rosie-rt-natspublisher.service:22-24` - `motion-server/v1/src/robot_v4_cartesian_cli.hpp:25-126,190-208` - `motion-server/v1/src/robot_v4_cartesian_nats_protocol.hpp:1008-1073,1152,1370-1406` - `motion-server/v1/src/robot_v4_cartesian_leader_authority.hpp:495-530` - `motion-server/v1/src/robot_v4_cartesian_daemon.cpp:1166-1199,3508-3545` - `motion-server/joint-trajectory/v1/src/joint_trajectory_daemon.cpp:171-248` - `motion-server/joint-trajectory/v1/src/command_dispatch.hpp:25-207` - `motion-server/joint-trajectory/v1/src/leader_authority.hpp:24-89` - `dev-stack.sh:282` --- # Pendant controls > Every control of the v5 Steam Deck teach pendant, including the hold-to-enable trigger, joint and Cartesian jog, teaching, arming, stop, navigation, touch and keyboard, and the Steam Input layout. URL: https://advancedmetalresearch.com/docs/reference/pendant-controls Section: RosieOS docs / Reference Last updated: 2026-10-10 This is the control map of the v5 teach pendant on a Steam Deck, as the app implements it. Press **Menu** on the pendant to see the same guide on screen. For how to use them, see [Program from the Steam Deck pendant](https://advancedmetalresearch.com/docs/guides/program-from-the-pendant). > [!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). The v5 pendant is not qualified for real motion, and its stick, trigger and rear-button handling has not been qualified on hardware. **R2** is a software deadman, not a safety-rated enabling device. ## Jog Jogging works on the **JOG** page only, with a cell connected, and only while **R2** is held. | Input | Action | |---|---| | **R2** | Hold to enable jog. Release stops. | | **R2** + left stick ▲▼ | Joints: jog the selected joint. Up is the positive direction. | | **R2** + left stick | Cartesian: move X and Y. Up is X+, left is Y+. | | **R2** + right stick ▲▼ | Cartesian: move Z | | **R2** + right stick ◀▶ | Cartesian: turn about Z | | **L2** + **R2** + left stick | Cartesian: tilt about X and Y | | **D-pad** ▲▼ | Select the joint | | **D-pad** ◀▶ | Jog speed: 5, 10, 25, 50, 75 or 100 % | | **L3** | Switch between Joints and Cartesian | Rules the app enforces: - **One axis at a time.** Cartesian jog moves only the axis with the largest stick deflection. - **Commit threshold.** A stick direction counts once it is past 0.35 of full travel. The sticks have a 12 % dead zone. **R2** and **L2** count as held past a quarter of their travel. - **Neutral first.** After a page change, a Joints or Cartesian switch, a new direction, loss of window focus (for example the Steam overlay), or a lost controller, the hold ends. No new jog starts until the controls have returned to neutral. - **Step mode.** With **STEP** chosen on the Cartesian panel, each push makes one bounded move of the linear or angular step size. Centre the stick before the next. - **Lost controller.** The pendant reports `Controller disconnected; jogging stopped` and looks for the pad again every second. ## Teach, arm and stop | Input | Action | |---|---| | **A** or **L4** | Record a waypoint at the measured pose. In a dialog: OK. | | **X** or **L5** | Reteach the selected waypoint | | **Y** or **R4** | Record a via pose for the selected waypoint | | **R2** + **A** | Arm or disarm. A real machine asks for a second press: **CONFIRM ARM · A**. | | **B** or **R5** | Stop the robot, from every page and dialog. **B** also cancels the open dialog. | | **Esc**, header **■ STOP** | Stop the robot | Stop also disarms. It is a software stop, not the hardware E-stop. ## Navigate | Input | Action | |---|---| | **L1** / **R1** | Previous / next page | | **View** | CELL page | | **Menu** | Open the controls guide. **A** closes it; **B** stops and closes it. | | **D-pad** ▲▼ | Joint on JOG, row on PROGRAM | | Right stick, without **R2** | Orbit the 3D view. With **L2**: zoom. | | **R3** | Reset the 3D view | | Right trackpad | Pointer and click | | Left trackpad | Scroll | | **Steam** + **X** | On-screen keyboard, for text fields | The RUN, TELEMETRY and CELL pages are touch pages. In the 3D view: drag orbits, pinch zooms, two fingers pan and a double tap resets the view; the **+**, **−** and **FIT** buttons zoom by touch. ## Keyboard The rear buttons arrive as function keys through the Steam Input layout. A keyboard gives the same keys: | Key | Action | |---|---| | **F1** | Record a waypoint | | **F2** | Reteach | | **F3** | Record a via pose | | **F4**, **Esc** | Stop | ## Steam Input layout `steamdeck/real/v5/controller.vdf` is the pendant's Steam Input layout, titled "Rosie Pendant v5". Install it as the layout of the pendant's Steam shortcut. See [Build and install the pendant](https://advancedmetalresearch.com/docs/guides/install-the-pendant#add-it-to-steam). | Deck control | Sends | |---|---| | A, B, X, Y | Gamepad A, B, X, Y | | L1, R1 | Left and right shoulder | | View, Menu | Gamepad select and start | | L2, R2 | Analogue triggers | | Left and right sticks | Joysticks, with their clicks as L3 and R3 | | D-pad | D-pad | | L4 (upper left rear) | F1 | | L5 (lower left rear) | F2 | | R4 (upper right rear) | F3 | | R5 (lower right rear) | F4 | | Right trackpad | Absolute mouse, click on press | | Left trackpad | Scroll wheel | The Steam and Quick Access buttons stay system controls. ## Related pages - [Program from the Steam Deck pendant](https://advancedmetalresearch.com/docs/guides/program-from-the-pendant) - [Teach waypoints and moves](https://advancedmetalresearch.com/docs/guides/teach-waypoints) - [Safety model](https://advancedmetalresearch.com/docs/get-started/safety-model) ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `steamdeck/real/v5/src/window.cpp:55-58,98-101,176-187,378-523,794-873` - `steamdeck/real/v5/src/input.hpp:6-49` - `steamdeck/real/v5/src/controller.hpp:9-30` - `steamdeck/real/v5/src/controller.cpp:14-57` - `steamdeck/real/v5/src/pages.hpp:44-66` - `steamdeck/real/v5/src/jog_page.cpp:18-22,245-300` - `steamdeck/real/v5/controller.vdf` --- # Ports, sockets and environment variables > Every TCP port, Unix socket and environment variable used by the RosieOS services, the dev stack and the offline programming launcher, with defaults. URL: https://advancedmetalresearch.com/docs/reference/ports-and-environment Section: RosieOS docs / Reference Last updated: 2026-10-10 This page lists the defaults from the code. Hosts are shown as they are bound. Replace `rosie.local` with your cell host where a remote address is meant. ## Unix sockets | Socket | Default path | Owner | Public? | |---|---|---|---| | Native core IPC | `/run/rosie-rt-core/ipc.sock` (`rt-control --core-socket`, `rtctl run --socket`). Installed units use `/run/rosie-rt-core/native/ipc.sock`. | `rosie-rt-core` | **No.** Private between the core and `rt-control`. | | Control API | `/run/rosie-rt-core/control.sock` (`rt-control --socket`, `rtctl --socket`). Installed units create it at `/run/rosie-rt-core/public/control.sock`, with a symlink at the default path. | `rt-control` | Yes. HTTP/JSON. | | Jog lane | `jog.sock` in the same directory as `control.sock` | `rt-control` | Yes. 224-byte datagrams. | | Dev-stack sockets | `$ROSIE_LOCAL_RT_DIR/{ipc,control,jog}.sock`, default `${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/rosie-dev-rt-$UID/` | `dev-stack.sh` | Local only | Unix sockets must be on a Linux filesystem. Under WSL, `/mnt/c` does not work. ## TCP ports | Port | Service | Default bind | Notes | |---|---|---|---| | 8443 | `rt-control` mutual-TLS listener | Off unless `--remote-listen` is set. Cell configs pin `127.0.0.1:8443`. | Remote HTTP API and WebSocket jog. Requires a CA, server certificate and key, and a CRL. | | 8794 | OLP server | `127.0.0.1:8794` (`serve --listen`, launcher `OLP_LISTEN`) | API under `/api/offline-programming/v1/` | | 5189 | OLP UI (Vite) | `127.0.0.1`, or `0.0.0.0` under dev-stack when a Tailscale address is found | App at `/offline-programming/v1/ui/`. It proxies `/api` to the OLP server. | | 8796 | Weld planner motion server | **`0.0.0.0:8796`** (`--host`, `--port`) | Listens on all interfaces by default. Pass `--host 127.0.0.1` on an untrusted network. | | 8797 | Dense trajectory daemon ingest | `127.0.0.1:8797` | Length-framed TCP, one request per connection | | 51711 | Virtual pendant UI (Vite) | `127.0.0.1` | Page at `/steamdeck/virtual/v1/` | | 51712 | Virtual pendant bridge | `127.0.0.1` (must be loopback) | `/healthz`, `/api/v1/*` | | 14222, 18222 | NATS server (dev stack only) | `127.0.0.1` | Client and monitoring ports | | 8787 | Program catalog and daemon state (optional) | — | Not part of this repository's public path. The dev-stack `catalog` service is skipped unless you supply its script. | The Cartesian motion server listens for UDP intent packets only on the address you pass with `--udp-listen HOST:PORT`. It has no default port. Its status file defaults to `/run/robot-v4-cartesian/status.json`. ## Dev stack (`dev-stack.sh`) | Variable | Default | Effect | |---|---|---| | `ROSIE_DEV_STACK_DIR` | `$RUN_BASE/rosie-stack-$UID` | Pid files, logs and readiness files. `RUN_BASE` is `$XDG_RUNTIME_DIR`, else `$TMPDIR`, else `/tmp`. | | `ROSIE_LOCAL_RT_DIR` | `$RUN_BASE/rosie-dev-rt-$UID` | Simulator and `rt-control` sockets | | `ROSIE_LOCAL_RT_BUILD` | `rt-core/build/dev-stack` | Compiled config and consumer bindings | | `DEV_STACK_BACKEND` | `rt_core` | The only accepted value. Anything else exits with `backend_retired`. | | `DEV_STACK_MACHINE_CONFIG` | `rt-core/config/machines/simulation/simulation-program.json` | The simulated machine, read by `local-rt-core.sh prepare` | | `DEV_STACK_UI_HOST` | `127.0.0.1`; `0.0.0.0` if Tailscale is up | UI bind host | | `DEV_STACK_UI_MODE` | `dev`; `built` if Tailscale is up | `dev` (hot reload) or `built` (compressed bundle) | | `DEV_STACK_FIREWALL` | `1` | `0` skips adding a `ufw` rule for remote UI access | | `DEV_STACK_PENDANT_PORT` | `51712` | Virtual pendant bridge port | | `DEV_STACK_DENSE_INGEST` | `127.0.0.1:8797` | Dense daemon listen address | | `DEV_STACK_DENSE_NATS_URL` | `nats://127.0.0.1:14222` | Dense daemon NATS URL | | `DEV_STACK_DENSE_BIN_DIR` | `make -C motion-server/joint-trajectory/v1 print-bin-dir` (`rt-core/build/dense`) | Where the dense daemon binary is | | `DEV_STACK_NATS_BIN` | `~/.rosie/bin/nats-server` | NATS server binary | | `DEV_STACK_OLP_CELLS` | unset | A cell catalogue file (`offline-programming.cell-catalogue.v1`). Its cells are added after the local simulation. | | `DEV_STACK_OLP_REMOTE` | unset | A remote rt-core binding file for OLP. Its address must be `https://`. | | `CATALOG_DAEMON_SCRIPT` | `~/.cache/rosieos-olp/catalog-daemon/start-catalog-daemon.sh` | Start script for the optional `catalog` service | | `STACK_WAIT_SECS` | `300` | Health wait for `start` | `local-rt-core.sh bind` writes `binding.env` into `ROSIE_LOCAL_RT_BUILD`. It exports these variables: - `ROSIE_RT_CONTROL_SOCKET` and `ROSIE_RT_JOG_SOCKET` - `ROSIE_RT_PAIR_ID` (`local-dev`) and `ROSIE_RT_PAIR_REVISION` (`1`) - `ROSIE_RT_CONFIGURATION_SHA256` - `ROSIE_RT_URDF` and `ROSIE_RT_URDF_SHA256` - `OFFLINE_PROGRAMMING_RT_CORE_CONFIG` ## rt-control and installed units | Variable or file | Used by | Meaning | |---|---|---| | `ROSIE_RT_COMPILED_CONFIG` | `rt-control` | Default for `--compiled-config`, the compiled digest directory containing `resources.json` | | `/etc/rosie-rt-core/control.env` | `rosie-rt-core.service`, `rosie-rt-control.service` | Sets `ROSIE_RT_CONFIGURATION_SHA256`, `ROSIE_RT_PAIR_ID`, `ROSIE_RT_PAIR_REVISION`, `ROSIE_RT_REMOTE_ARGS` (extra `rt-control` flags) and `ROSIE_RT_CORE_ARGS` (core arguments) | | `/etc/rosie-rt-core/natspublisher.env` | `rosie-rt-natspublisher.service` | NATS publisher settings | `rt-control` flags are in [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http), and `rtctl` flags in [rtctl](https://advancedmetalresearch.com/docs/reference/rtctl). ## Offline programming server | Variable | Flag it defaults | Meaning | |---|---|---| | `OFFLINE_PROGRAMMING_WELD_PLANNER_MOTION_ORIGIN` | `--weld-planner-motion-origin` | Weld planner origin, usually `http://127.0.0.1:8796`. It is used by the server only, to plan and to fetch `.rdt` files. | | `OFFLINE_PROGRAMMING_EXECUTION_BACKEND` | `--execution-backend` | `rt_core`, the only backend | | `OFFLINE_PROGRAMMING_JOG_BACKEND` | — | Must be `rt_core` | | `OFFLINE_PROGRAMMING_RT_CORE_CONFIG` | `--rt-core-config` | JSON binding for the local rt-core: socket, pair, digests, axis mask | | `OFFLINE_PROGRAMMING_CELLS` | — | Cell catalogue for the machine list | | `OFFLINE_PROGRAMMING_PROGRAM_CATALOG_ORIGIN` | `--program-catalog-origin` | Optional program catalog origin | | `OFFLINE_PROGRAMMING_DAEMON_STATE_URL` | `--daemon-state-url` | Optional daemon state URL | | `OFFLINE_PROGRAMMING_EXECUTION_STATE_DIR` | `--execution-state-dir` | Directory for execution audit state | | `OFFLINE_PROGRAMMING_JOG_BINARY` | — | Path to `robot-v4-cartesiand` | | `OLP_CARTESIAN_RESOLVER`, `OLP_CARTESIAN_RESOLVER_REPO` | — | Override the Cartesian resolver binary and the repository it loads robots from | | `SEAM_WORKER_PYTHON`, `SEAM_WORKER_SPARES` | — | Python for the seam worker subprocess, and how many spare workers to keep warm | | `CADQUERY_TOPOLOGY_PYTHON` | — | Python for the CadQuery tessellation subprocess | | `ROSIE_REPO` | — | Repository root, if the server cannot find it | | `ROSIE_RT_CONTROL_SOCKET`, `ROSIE_RT_PAIR_ID`, `ROSIE_RT_PAIR_REVISION` | — | Local rt-control binding | Launcher only (`start-offline-programming.sh`): | Variable | Default | Meaning | |---|---|---| | `OLP_LISTEN` | `127.0.0.1:8794` | Server listen address | | `OLP_UI_HOST`, `OLP_UI_PORT` | `127.0.0.1`, `5189` | The UI URL it prints | | `OLP_SIM_WAIT_SECS` | `270` | Wait for the local simulator to become ready | | `OLP_RESTART_SERVE` | unset | `1` restarts a server that is already running | | `ROSIEOS_OLP_CACHE` | `~/.cache/rosieos-olp` | Build cache for the server and motion-server binaries | | `OFFLINE_PROGRAMMING_RT_CORE_REMOTE` | unset | A remote rt-core binding file for dense execution | OLP UI: `OFFLINE_PROGRAMMING_API_URL` sets where Vite proxies `/api`. It defaults to `http://127.0.0.1:8794`. ## Weld planner | Variable or flag | Default | Meaning | |---|---|---| | `--host`, `--port` | `0.0.0.0`, `8796` | Listen address | | `--dense-store-dir`, `WELD_PLANNER_DENSE_STORE_DIR` | `~/.cache/rosieos-olp/weld-planner-dense` | Where dense `.rdt` files are written for OLP to fetch | | `AMR_WELD_PLANNER_SOURCE_REVISION` | Set by `pixi run -e motion motion-serve` to `git rev-parse HEAD` | Stamped into results so you can tell which code planned them | ## Virtual pendant | Variable | Default | Meaning | |---|---|---| | `STEAMDECK_VIRTUAL_BRIDGE_URL` | `http://127.0.0.1:51712` | Where the UI's Vite server proxies `/healthz` and `/api/v1` | ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/cmd/rt-control/main.go:24-38,56-57` - `rt-core/tools/rtctl/control.go:103-106` - `rt-core/tools/rtctl/run.go:17-21` - `rt-core/host/rosie-rt-core.service:20-28` - `rt-core/host/rosie-rt-control.service:19-23` - `rt-core/host/rosie-rt-natspublisher.service:26-27` - `rt-core/config/templates/cell.json (remote_listen)` - `dev-stack.sh:23-80,269-297,299-310,442-450,555-575` - `motion-server/v1/local-rt-core.sh:6-7,24,93-110` - `motion-server/joint-trajectory/v1/Makefile:7,16-17` - `motion-server/joint-trajectory/v1/config/joint_trajectory_daemon.config.json` - `motion-server/v1/src/robot_v4_cartesian_daemon.cpp:53-55,1185` - `motion-server/v1/src/robot_v4_cartesian_cli.hpp:236-339` - `offline-programming/v1/main.go:257-320,379,714` - `offline-programming/v1/cartesian_jog.go:21` - `offline-programming/v1/internal/seam/worker.go:155-170` - `offline-programming/v1/internal/cad/worker.go:194` - `offline-programming/v1/internal/simulator/canonical_binary.go:35-50` - `offline-programming/v1/start-offline-programming.sh:16-18,32,35-36,48-58,115-121` - `offline-programming/v1/ui/vite.config.ts:3-14` - `weld_planner/v1/python/weld_motion_planner/server/motion_planner_server.py:57,78,343-344,555-565` - `weld_planner/v1/pixi.toml:156` - `steamdeck/virtual/v1/ui/vite.config.ts:12,108-131` - `steamdeck/virtual/bridge/cli.go:20-45` - `steamdeck/virtual/bridge/server.go:731-739` --- # Repository layout > Where each RosieOS component lives in the repository, which versioned folders are current and which are legacy, and which files are generated from contracts rather than edited by hand. URL: https://advancedmetalresearch.com/docs/contributing/repo-layout Section: RosieOS docs / Contributing Last updated: 2026-10-10 RosieOS is one repository with a folder per component. There is no root build: each component builds and tests on its own with its own tool (`make`, `go`, `npm`, `pixi`). Many folders are versioned (`v1`, `v4`, `v5`). Only some versions are current. This page tells you which. ```bash git clone https://github.com/advanced-metal-research/RosieOS.git cd RosieOS ``` ## Current components | Folder | Language | What it is | Docs | |---|---|---|---| | `rt-core/` | C++17, Go | The real-time core (`rosie-rt-core`, `rosie-rt-core-sim`), the public control API `rt-control`, the Go SDK, C++ and TypeScript clients, `rtctl`, host install scripts and all rt-core configuration | [Real-time core](https://advancedmetalresearch.com/docs/concepts/real-time-core), [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http) | | `robot_description/` | data, Go, Python | One directory per robot model, the Go module `rosieos/robotdesc` and the manifest tool | [Robot description](https://advancedmetalresearch.com/docs/concepts/robot-description-and-frames) | | `motion-server/v1/` | C++17 | The Cartesian motion server `robot-v4-cartesiand` | [Cartesian motion server](https://advancedmetalresearch.com/docs/apis/cartesian-motion-server) | | `motion-server/joint-trajectory/v1/` | C++17 | The dense trajectory daemon | [Dense trajectory daemon](https://advancedmetalresearch.com/docs/apis/dense-trajectory-daemon) | | `offline-programming/v1/` | Go; `ui/` TypeScript | The offline programming (OLP) server and browser app | [OLP HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http) | | `weld_planner/v1/` | Python (pixi) | Seam worker, CUDA weld motion planner and verifier, `weldplan` contracts | [Weld planner HTTP API](https://advancedmetalresearch.com/docs/apis/weld-planner-http) | | `tesseract/v1/`, `cadquery/v1/` | Python (pixi) | Planning and CAD environments used by the motion server and OLP | [Install the toolchain](https://advancedmetalresearch.com/docs/get-started/installation) | | `steamdeck/real/v5/` | C++/Qt | The Steam Deck teach pendant. Not qualified for real motion. | [Program from the pendant](https://advancedmetalresearch.com/docs/guides/program-from-the-pendant) | | `steamdeck/real/v4/` | Go, C++ | The native deploy module (`go run .`) and the v4 rollback pendant | — | | `steamdeck/virtual/` | TypeScript, Go | The browser pendant and its bridge, for simulation only | [Virtual pendant](https://advancedmetalresearch.com/docs/guides/virtual-pendant) | | `urdf/v1/` | TypeScript | URDF tooling; `sphere_tool/` authors collision spheres | [Add a robot model](https://advancedmetalresearch.com/docs/guides/add-a-robot-model#spheres) | | `dev-stack.sh` | Bash | Starts the local simulated stack | [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation) | | `quality/` | Node | Lint, format and report runners | [Code style and quality](https://advancedmetalresearch.com/docs/contributing/code-style) | | `tools/robot-stack-release/`, `releases/` | Python, JSON | Release bundling and release records | [Releasing](https://advancedmetalresearch.com/docs/contributing/releasing) | | `.github/workflows/` | YAML | CI | [Building and testing](https://advancedmetalresearch.com/docs/contributing/building-and-testing#ci) | Also in the tree and installed alongside a cell, but not documented here: `daemon/v1` (the mesh daemon and program catalogue), `nats/v1`, `mujoco-sim/v1` (an experimental physics model), `preflight/` and `coordination/v1` (internal tooling). ### Inside `rt-core/` | Path | Holds | |---|---| | `src/main.cpp`, `engine/`, `include/` | The cyclic core. Header-only engine. | | `cmd/rt-control/`, `adapters/rosie/control/` | `rt-control` and the public API implementation. | | `ipcclient/` | The Go side of the private core IPC. | | `sdk/control/` | The public Go SDK. | | `clients/cpp/`, `clients/ts/` | Header-only C++ client; TypeScript types and telemetry decoders. | | `protocol/` | The two contracts: `control.json` (private IPC) and `application-v1.schema.json` (public API). | | `config/` | `drives/`, `machines/`, `cells/`, `cell-io/` and `templates/`. | | `host/` | Install scripts and systemd units. | | `tools/` | `rtctl`, `benchdrive`, `natspublisher`, the generators, packaging and `rdtcheck`. | | `tests/`, `simfixture/` | C++ policy tests and the Go simulation fixture. | ## Generated code Some files are generated from a contract. Edit the contract and regenerate; never edit the output. `make check-protocol` and `make check-api` fail on drift, and both run in `make test`. | Contract | Generator | Outputs | |---|---|---| | `rt-core/protocol/control.json` | `make -C rt-core generate-protocol` (`tools/protocolgen`) | `ipcclient/protocol_generated.go`, `clients/cpp/include/rosie/protocol_generated.hpp` (and the `include/protocol_generated.hpp` shim), `clients/ts/rt_protocol_generated.ts` | | `rt-core/protocol/application-v1.schema.json` | `make -C rt-core generate-api` (`tools/apigen`) | `adapters/rosie/control/api_generated.go`, `clients/cpp/include/rosie/rt_control_api_generated.hpp`, `clients/ts/rt_control_api_generated.ts` | The generators compare the TypeScript output against goldens using Node 22.13.1. They look for it at `~/.rosie/node-22.13.1/bin/node` or in `NODE`. ## Legacy folders These are earlier generations. No current code references them. Do not build on them. | Folder | What it was | |---|---| | `robot/v1` to `robot/v10` | Earlier robot CLIs, stacks and third-party arm scaffolds | | `daemon/v2` | An earlier mesh daemon | | `urdf/v2` to `urdf/v5` | Earlier CAD-to-URDF experiments | | `steamdeck/real/v1` to `v3` | Earlier pendant demos | | `rt-core/docs/history/`, `rt-core/docs/evidence/` | Dated design and run records | The retired v4 real-time core, its NATS command bridge and the OLP's connected-execution path over NATS are also legacy. OLP keeps the last behind flags that are off by default. ## Things that are not in this repository - **The `rosie` CLI.** Many READMEs show `./rosie.sh …`, `.\rosie.ps1 …` or `rosie … v1 …`. That tool lives in an internal repository and those commands do not work from a clone. Use the per-component commands in [Building and testing](https://advancedmetalresearch.com/docs/contributing/building-and-testing). - **A root `go.mod`, `go.work` or build dispatcher.** Go modules are per component: `rt-core/go.mod` (`rosieos/rt-core`), `robot_description/go/go.mod` (`rosieos/robotdesc`), and one each in `offline-programming/v1`, `steamdeck/real/v4` and `steamdeck/virtual`. `rt-core` reaches `robot_description/go` through a `replace` directive. - **Toolchain management.** Install Go, Node, pixi, uv and a C++17 compiler yourself. See [Install the toolchain](https://advancedmetalresearch.com/docs/get-started/installation). ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/go.mod:1-13` - `robot_description/go/go.mod` - `rt-core/Makefile:224,296-315` - `rt-core/tools/protocolgen/generate.go:380-396` - `rt-core/tools/apigen/main.go:405-452` - `rt-core/cmd/rt-control/main.go:24-39` - `dev-stack.sh:76` - `offline-programming/v1/main.go:35,80-96,272-281` - `steamdeck/real/v4/cli.go:128-178` - `quality/quality.mjs:1-110` - `.github/workflows/rt-core.yml:29-37` - `urdf/v1/sphere_tool/package.json` --- # Building and testing > Build and test commands for each RosieOS component, the environment the rt-core tests expect, and what each CI workflow runs and gates. URL: https://advancedmetalresearch.com/docs/contributing/building-and-testing Section: RosieOS docs / Contributing Last updated: 2026-10-10 Each component builds and tests on its own. Run the commands on this page from the repository root unless a step says otherwise. Install the toolchains first: see [Install the toolchain](https://advancedmetalresearch.com/docs/get-started/installation). A quick check of the controller and two of its consumers, on Linux or WSL: ```bash export TMPDIR=/dev/shm/rosie-tests && mkdir -p "$TMPDIR" make -C rt-core control sim test make -C motion-server/joint-trajectory/v1 all test BIN_DIR="$PWD/rt-core/build/dense" (cd offline-programming/v1 && go test -race -count=1 ./...) ``` ## Test environment The rt-core tests start real processes and bind Unix sockets, and some assert real locked-memory behaviour. - **Keep `TMPDIR` on a Linux tmpfs**, such as `/dev/shm/...`. On WSL, sockets cannot bind under `/mnt/c`. - **Allow unlimited locked memory** for the test shell. CI runs `sudo prlimit --pid $$ --memlock=unlimited:unlimited` and checks `ulimit -l` prints `unlimited`. Do the same, or raise `memlock` in `/etc/security/limits.conf`. - **Node 22.13.1** is used by the generator golden tests. They look for it at `~/.rosie/node-22.13.1/bin/node` or in `NODE`. CI symlinks its Node there. - To keep Go caches inside the checkout, set `GOCACHE`, `GOMODCACHE` and `GOTMPDIR` under `rt-core/build/`, and `GOFLAGS=-mod=mod`. ```bash cd rt-core export GOCACHE=$PWD/build/go-cache GOMODCACHE=$PWD/build/go-mod-cache GOTMPDIR=$PWD/build/go-tmp GOFLAGS=-mod=mod mkdir -p "$GOCACHE" "$GOMODCACHE" "$GOTMPDIR" ``` ## rt-core ```bash cd rt-core make control sim # rt-control, rtctl, benchdrive, rt-natspublisher, rosie-rt-core-sim make test ``` | Target | What it does | |---|---| | `make control` | Builds `build/rt-control`, `build/rtctl`, `build/benchdrive` and `build/rt-natspublisher`. | | `make sim` | Builds `build/rosie-rt-core-sim`, the core with a simulated bus. | | `make ipc-only` | Builds `build/rosie-rt-core-ipc`, for IPC-only validation. | | `make live` (default `all`) | Builds `build/rosie-rt-core` against IgH `libethercat`. Refuses missing or mismatched IgH metadata. | | `make clients` | Builds the C++ client examples. | | `make test` | C++ policy tests, Go oracle parity, and `check-protocol` and `check-api`. | | `make test-go` | Every Go package, with `-race -count=1 -p=1`. | | `make test-faults` | The IPC fault matrix. | | `make test-sanitize` | ASan/UBSan builds of the core and cell I/O tests, with the native oracle. | | `make test-tsan` | ThreadSanitizer builds of the policy tests and core. | | `make check-protocol`, `make check-api` | Fail if generated code differs from the contracts. | | `make generate-protocol`, `make generate-api` | Regenerate from the contracts. See [generated code](https://advancedmetalresearch.com/docs/contributing/repo-layout#generated). | | `make soak` | 50 simulation soak iterations; writes a JSON record under `build/`. | | `make ci` | Runs the local CI driver. | To build the live daemon without hardware, build the pinned IgH userspace library first: ```bash cd rt-core bash tools/build-igh-userlib.sh # IgH 1.6.9 by default; 1.6.10 to 1.6.12 are also pinned export PKG_CONFIG_PATH="$PWD/build/deps/igh-prefix/lib/pkgconfig" make live control build/rtctl validate --backend live --config config/machines/a6ec-bench.example.json ``` The script downloads the IgH source archive and checks its SHA-256 before building. ## Robot descriptions ```bash python3 robot_description/tools/manifest.py --check (cd robot_description/go && go test ./...) ``` ## Motion servers Both build against rt-core's C++ client and test against the simulated core, so build `rt-core` first. ```bash make -C rt-core control sim make -C motion-server/v1 all test test-composed \ BIN_DIR="$PWD/rt-core/build/cartesian" OBJECT_CACHE_DIR="$PWD/rt-core/build/cartesian-cache" make -C motion-server/joint-trajectory/v1 all test \ BIN_DIR="$PWD/rt-core/build/dense" TEST_BUILD_DIR="$PWD/rt-core/build/dense-tests" ``` The dense daemon also has `test-go` and `test-sanitize`. `make -C motion-server/joint-trajectory/v1 print-bin-dir` prints its default output directory. The Cartesian server's default `BIN_DIR` is outside the repository, which is why these commands set it. ## Offline programming ```bash make -C rt-core control sim (cd offline-programming/v1 && go test -race -count=1 ./...) (cd offline-programming/v1/ui && npm ci && npm run lint && npm test) ``` ## Simulation stack and virtual pendant ```bash make -C rt-core control sim (cd steamdeck/virtual && go test -race -count=1 ./bridge ./stack ./v1) DEV_STACK_BACKEND=rt_core DEV_STACK_FIREWALL=0 DEV_STACK_UI_HOST=127.0.0.1 \ bash motion-server/v1/tests/dev-stack-smoke.sh (cd steamdeck/virtual/v1/ui && npm ci && npm test && npm run build) ``` `DEV_STACK_FIREWALL=0` stops the dev stack from changing firewall rules. See [Ports, sockets and environment variables](https://advancedmetalresearch.com/docs/reference/ports-and-environment#dev-stack-dev-stacksh). ## Weld planner ```bash cd weld_planner/v1 pixi install -e default pixi run -e default pytest -q tests/programmer/test_dense_joint_trajectory.py pixi install -e motion # Linux, NVIDIA GPU with CUDA 12 pixi run -e motion pytest -q tests/motion_planner ``` Always pass `-e`. Several tasks exist in more than one environment, and the motion tests need the CUDA `motion` environment. [Run the weld planner](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner) covers the environments. ## Pendants and deploy module ```bash (cd steamdeck/real/v4 && go build ./... && go vet ./... && go test -race -count=1 -p=1 ./...) make -C steamdeck/real/v4/steamdeck all (cd offline-programming/v1/ui && npm ci) && make -C steamdeck/real/v5 -j4 all check ``` The v5 pendant needs Qt 5.15 and Assimp. No CI workflow builds it. See [Build and install the pendant](https://advancedmetalresearch.com/docs/guides/install-the-pendant). ## Quality checks Formatting and lint runners live in `quality/`. See [Code style and quality](https://advancedmetalresearch.com/docs/contributing/code-style). ## CI All workflows run on GitHub-hosted Ubuntu runners, on pull requests and pushes that touch their paths. | Workflow | Runs | Paths | |---|---|---| | `rt-core.yml` | On a sparse checkout of only `rt-core/` and `robot_description/`: `make test` (native lane), `make test-sanitize` (sanitize lane), and an IgH lane that builds the userspace library, `make live control`, `rtctl validate --backend live`, and a runtime package | `rt-core/**`, `robot_description/**`, the dense daemon, `steamdeck/real/v4/**` | | `rt-core-consumers.yml` | Cartesian and dense motion server builds and tests; the virtual bridge and dev-stack smoke test; the v4 pendant; OLP `go test` and UI lint and tests; deploy-module package tests | rt-core, motion servers, OLP, `steamdeck/**`, `dev-stack.sh` | | `virtual-deck-ui.yml` | Virtual pendant UI tests, build, strict `tsc`, trailing-whitespace check | `steamdeck/virtual/**` | | `weld-planner-verifier.yml` | The dense trajectory contract test (default env), the M6 verifier contract with CPU PyTorch, and the sphere tool build | weld planner, sphere tool | | `steamdeck-client-contracts.yml` | Pendant client authority contracts | `steamdeck/real/v4/**` | | `robot-runtime-contracts.yml` | Mesh-daemon restart-ordering tests | `daemon/v1/**` | | `quality-pilot.yml` | `node quality/quality.mjs check` and `quality.mjs workflows` (actionlint) | `quality/**`, `.github/workflows/**` | | `deployment-surface.yml` | Advisory deployment-surface report | deployment manifests, units | | `triage-labels.yml` | Issue and pull request labelling | — | What CI does **not** cover: the v5 pendant, `mujoco-sim`, the Tesseract environment's self-test, the full weld planner suite (only the contract tests above run, on CPU), and anything on real hardware. A green CI run says nothing about powered motion. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/Makefile:1-50,138,224,276-350` - `rt-core/tools/build-igh-userlib.sh:9-40` - `.github/workflows/rt-core.yml:1-72` - `.github/workflows/rt-core-consumers.yml:1-135` - `.github/workflows/virtual-deck-ui.yml:1-45` - `.github/workflows/weld-planner-verifier.yml:1-112` - `.github/workflows/robot-runtime-contracts.yml:36-60` - `.github/workflows/steamdeck-client-contracts.yml:38-70` - `.github/workflows/quality-pilot.yml:1-33` - `.github/workflows/deployment-surface.yml:1-25` - `.github/workflows/triage-labels.yml:1-40` - `motion-server/v1/Makefile:26-60` - `motion-server/joint-trajectory/v1/Makefile:16-70` - `weld_planner/v1/pixi.toml:9-43,60-128` - `robot_description/tools/manifest.py:13-18` - `offline-programming/v1/ui/package.json` --- # Code style and quality checks > The quality runner's gate and report commands, what each one checks, and the conventions RosieOS code follows for constants, units, provenance, errors and tests. URL: https://advancedmetalresearch.com/docs/contributing/code-style Section: RosieOS docs / Contributing Last updated: 2026-10-10 RosieOS has one quality runner, `quality/quality.mjs`, plus conventions that the code follows everywhere. Run the gate before you open a pull request: ```bash npm ci --prefix coordination/v1 # installs the pinned npm tools the runner uses node quality/quality.mjs check ``` Each step prints a status line: `passed`, `rejected`, `capability_unavailable` (the tool is missing) or `report_completed_not_qualified`. The runner exits 1 if any step was rejected or unavailable. ## Gate commands These are the checks CI runs (`quality-pilot.yml`). They cover the TypeScript sources the runner owns, in `coordination/v1`, and they use the tools pinned in that directory's lockfile: Oxfmt, Oxlint, ast-grep and tsc. | Command | What it does | |---|---| | `node quality/quality.mjs check` | The CPU-only CI gate: the runner's own tests, then `format-check`, `lint` and `typecheck`, then the unit and schema tests. The default command. | | `node quality/quality.mjs format` | Rewrites formatting with Oxfmt. The only command that edits files. | | `node quality/quality.mjs format-check` | Oxfmt in check mode. | | `node quality/quality.mjs lint` | Oxlint, plus the tested ast-grep rules in `quality/sgconfig.yml`. | | `node quality/quality.mjs typecheck` | Generates Worker types, then runs tsc. | The runner uses explicit allowlists. It never formats evidence, CAD, generated files, vendor code or legacy trees. ## Report commands These are not CI gates. Run them on the component you change. A missing tool or a nonzero tool exit still fails the run, and none of them applies fixes. | Command | Scope | Tool | |---|---|---| | `node quality/quality.mjs python` | `weld_planner/v1` `python/`, `tests/`, `tools/` | Ruff 0.15.6 through `uvx` (format check and lint), with the project's Ruff settings | | `node quality/quality.mjs go` | `gofmt` over tracked `steamdeck/real/v4` sources, then golangci-lint 2.13.2 with `quality/golangci.yml` | Go toolchain of that module | | `node quality/quality.mjs rust` | `daemon/v1`: `cargo fmt --check`, then Clippy on all targets | Rust 1.95.0 through `rustup` | | `node quality/quality.mjs workflows` | Every workflow | actionlint 1.7.12 | | `node quality/quality.mjs shell` | `dev-stack.sh` | ShellCheck, installed by you | | `node quality/quality.mjs cpp ` | One `.cpp` file under `motion-server/joint-trajectory/v1/src`, with a real `compile_commands.json` | clang-format and clang-tidy, with `quality/clang-format.yml` and `quality/clang-tidy.yml` | Clippy can exit 0 with warnings, so the Rust step reports `report_completed_not_qualified`, never `passed`. Read its output. The C++ configuration is opt-in and is not a repository-wide style. ## Conventions These are visible throughout the code. Follow them in new code. ### Every constant carries its evidence A tunable number states, in the comment above it, whether it was measured and where, or that it is unverified: rt-core/tools/rtctl/hostcheck.go: ```go // Measured repository dependency: host/ethercat-foundation.sh pins IgH 1.6.9. const hostcheckIgHVersion = "1.6.9" // Unverified inspection ceiling: 1000 ns CLOCK_MONOTONIC resolution. const hostcheckTimerResolutionNS = 1000 ``` If you change a constant, update its measurement or mark it unverified. Configuration follows the same rule. Every numeric robot fact and every safety exception in a config file needs a `_source` ("cited: …") or `_unverified` ("unverified: …") sibling, and the compiler refuses one without it. See [provenance siblings](https://advancedmetalresearch.com/docs/reference/configuration#templates). ### Units are in the name Fields and variables carry their unit: `cycle_ns`, `lease_ms`, `velocity_rad_s`, `acceleration_rad_s2`, `xyz_m`, `rpy_rad`, `following_error_counts_max`. The public rt-core API uses rad or m, rad/s or m/s, and ns on the host's `CLOCK_MONOTONIC`. When a unit changes at a boundary, name both sides. ### Docstrings state the contract A doc comment says what the code guarantees and the failure it exists to prevent, not what the next line does: rt-core/tools/rtctl/command.go: ```go // DefaultRoot finds config/drives beside the build directory or above the // working directory, preventing unrelated launch directories from selecting a // different drive config tree. ``` ### Fail closed, with a named reason - Reject unknown fields, duplicate keys, trailing data and malformed values. Do not fall back to a default when an input is invalid. - Refuse with a stable, named reason (`robot_description_mismatch`, `resource_unknown`) and a detail that says what to change. - Never hide an error by dropping failure handling or weakening a test. A suppression comment must name the rule and the reason. ### Tests assert absolute units Assert the contract in fixed units, such as "within 0.5 mm" or "under 250 ms". Do not compute a threshold from the thing under test (for example from a trajectory's own time step): a threshold that moves with the behaviour still passes when the behaviour collapses. ### Generated code is never edited Change the contract in `rt-core/protocol/` and regenerate. See [generated code](https://advancedmetalresearch.com/docs/contributing/repo-layout#generated). ### Keep formatting separate Use one formatter per language, and keep formatting-only commits apart from behaviour changes. > [!NOTE] Linters and formatters prove none of these: installation, controller admission, collision freedom or physical execution. Do not describe a clean check as qualification. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `quality/quality.mjs:1-110` - `quality/sgconfig.yml` - `quality/golangci.yml` - `quality/clang-format.yml` - `quality/clang-tidy.yml` - `.github/workflows/quality-pilot.yml:1-33` - `rt-core/tools/rtctl/hostcheck.go:21-27` - `rt-core/tools/rtctl/command.go:141-144` - `rt-core/tools/rtctl/robot_definition.go:66-160` - `rt-core/tools/rtctl/recover_encoder.go:13-19` - `rt-core/adapters/rosie/control/http.go:139-148` - `rt-core/config/templates/machine.json; policy wording from CONTRIBUTING.md (README-type source, narrative only)` --- # Releasing > How rt-core runtime packages are built, identified and verified, how robot-stack bundles are assembled offline, and what a release does and does not claim. Provisional. URL: https://advancedmetalresearch.com/docs/contributing/releasing Section: RosieOS docs / Contributing Last updated: 2026-10-10 > [!NOTE] **Provisional.** RosieOS has no published public release process yet. This page describes the packaging and bundling tools that exist in the repository today. Expect it to change. There are two layers: an **rt-core runtime package**, which is what a cell host installs, and a **robot-stack bundle**, which pins a set of component artifacts together. Neither contacts a device, and neither qualifies hardware. ## rt-core runtime package Build a package from a clean, built tree: ```bash cd rt-core bash tools/build-igh-userlib.sh export PKG_CONFIG_PATH="$PWD/build/deps/igh-prefix/lib/pkgconfig" make live control SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) bash tools/package-runtime.sh /tmp/rt-package # package status=staged backend=live # component status=staged output=/tmp/rt-package.components bash tools/verify-package.sh /tmp/rt-package ``` `package-runtime.sh DESTINATION` requires an empty or absent destination. It refuses a daemon whose built source identity differs from `git describe --always --dirty` ("daemon source identity is stale … rebuild it"), so rebuild after every commit. ### Package contents | Path | Contents | |---|---| | `bin/` | `rosie-rt-core`, `rt-control`, `rtctl`, `rt-package`, and `rt-natspublisher` if built | | `lib/libethercat.so.1`, `licenses/igh/` | The IgH userspace library the core links against, with its licence files | | `host/` | `install.sh`, `generate-control-env.sh`, `remote-pki.sh`, `ethercat-foundation.sh`, `run-core`, and the three unit files, rewritten for the slot root | | `tools/verify-package.sh` | The verifier | | `config/` | The rt-core configuration tree | | `robot_description/robots/…` | Every robot description's manifest and registered files, so a cell can compile its machine config from the package | | `rt_package.json` | The package manifest | ### The manifest `rt_package.json` has schema `rosie.rt-core.package.v1`. It records the backend (`live` or `simulation`), the component version, every file with its mode, install mode and SHA-256 (and, for ELF files, the machine, needed libraries, symbol versions and interpreter), and the build inputs: | Input | Description | |---|---| | `git_sha`, `dirty` | Source commit, and whether the tree had uncommitted or untracked changes | | `daemon_version` | The core's own `--version` | | `compiler`, `go` | Toolchain versions | | `igh_version`, `igh_archive_sha256` | The pinned IgH release | | `architecture`, `build_kernel`, `kernel_requirement` | Target and build host facts | | `source_date_epoch` | `SOURCE_DATE_EPOCH`, if set, for reproducible timestamps | A cell host stores each installed release under its 40-hex git SHA and refuses to replace one with different bytes under the same identity. See [Install rt-core on a cell host](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host#install). ### Component archive Beside the package, `DESTINATION.components/` holds what a release publishes: | File | Contents | |---|---| | `rosie-rt-core--linux-.tar.gz` | The runtime archive (`` is the ELF machine: x86-64 or aarch64) | | `rosie-rt-core.json` | The component descriptor: name, role, version, runtime requirements, receipts | | `rt_package.json` | The manifest | | `build.log`, `verify-package.log` | Receipts. Pass the producer's build log with `ROSIE_RT_PACKAGE_BUILD_LOG`. | | `SHA256SUMS` | Checksums of the above | The component version comes from the packaging tool (currently `0.1.0`). ### Options | Variable | Default | Description | |---|---|---| | `ROSIE_RT_PACKAGE_SLOT_ROOT` | `/opt/rosie-rt-core/current` | Absolute path the unit files point at. Not under `/home`, `/root` or `/run/user`. | | `ROSIE_RT_PACKAGE_BUILD_LOG` | — | A non-empty build log to include as a receipt. | | `ROSIE_RT_PACKAGE_CORE` | `build/rosie-rt-core` | Set to `build/rosie-rt-core-sim` to build a `simulation` package. No other substitution is allowed. | | `SOURCE_DATE_EPOCH` | — | Integer seconds; stamps every file's time. | | `ECRT_LICENSE_DIR` | `build/deps/ethercat-` | Where the IgH `COPYING` files are. | | `GO` | `go` | Go command used to build `rt-package`. | ### Verify ```bash bash tools/verify-package.sh /tmp/rt-package # exit 0 on success, 3 if the inspector is missing build/rt-package git-sha /tmp/rt-package build/rt-package backend /tmp/rt-package ``` `verify-package.sh` runs `rt-package verify`, which checks every payload hash and the definition-bound robot resources without executing any packaged code. `rt-package` also has `manifest` and `component`, which `package-runtime.sh` calls. ## Robot-stack bundles A robot-stack release pins several components by exact commit and artifact hash. The specification is a `rosie.robot-stack.release.v1` JSON file under `releases/robot-stack/`. `releases/robot-stack/next/` is a draft covering the native rt-core package only; `v0.1.0/` is the historical record of an earlier release. `tools/robot-stack-release/bundle.py` works entirely offline: ```bash uv run --no-project python tools/robot-stack-release/bundle.py package \ --spec releases/robot-stack//release.json --repo . --inputs --bundle uv run --no-project python tools/robot-stack-release/bundle.py verify \ --spec releases/robot-stack//release.json --bundle uv run --no-project python tools/robot-stack-release/bundle.py archives \ --spec releases/robot-stack//release.json --bundle --output ``` | Command | Does | |---|---| | `package` | Reads source by the specification's exact commits (never by a moving tag) and the retained artifacts, and writes a bundle. It never fetches. | | `verify` | Checks a bundle's integrity and completeness against the specification. | | `archives` | Writes release archives and a `SHA256SUMS` to `--output`. | On success it prints `passed: bundle integrity/completeness only; no deployment or physical claim`, and on failure `rejected: ` with exit 1. `tools/robot-stack-release/prepare.py` prepares an offline install packet from a verified bundle (`--spec`, `--bundle`, `--manifest`, `--fixture`, `--selection`, `--cell`, `--home`, `--output`). It refuses retired components and never contacts or restarts a device. Run the tools' tests with: ```bash uv run --no-project python -m unittest discover -s tools/robot-stack-release -v ``` ## What a release claims A package or bundle that verifies proves that its bytes are the ones recorded. It does not prove installation on a host, controller admission, or anything about powered motion. Hardware qualification of a cell is separate, manual work by the cell owner. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model#simulation). ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `rt-core/tools/package-runtime.sh:1-101` - `rt-core/tools/verify-package.sh:1-9` - `rt-core/tools/packageinfo/main.go:18-70,280-310,620-653` - `rt-core/tools/packageinfo/component.go:17,50-110,150-245` - `rt-core/host/install.sh:48-97` - `rt-core/Makefile:7-16` - `tools/robot-stack-release/bundle.py:1-215` - `tools/robot-stack-release/prepare.py:1-125` - `releases/robot-stack//release.json:1-10` --- # Licence > RosieOS is open source under the Apache License, Version 2.0. URL: https://advancedmetalresearch.com/docs/contributing/license Section: RosieOS docs / Contributing Last updated: 2026-10-10 RosieOS is licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). The source is on GitHub at [advanced-metal-research/RosieOS](https://github.com/advanced-metal-research/RosieOS). The licence lets you use, modify and redistribute RosieOS, including commercially, provided you keep its licence and notices with it. Third-party components keep their own licences, in the files that ship with them. ## Sources Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS): - `owner decision (Apache-2.0)`