[{"title":"RosieOS","section":"Overview","url":"/docs/","markdown":"/docs/index.md","description":"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.","headings":[{"id":"what-is-in-rosieos","text":"What is in RosieOS"},{"id":"safety-first","text":"Safety first"},{"id":"for-ai-agents","text":"For AI agents"},{"id":"where-to-start","text":"Where to start"}],"text":"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. 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 sets out exactly what is enforced, and where. For AI agents These docs are also published for machines. /docs/llms.txt indexes every page, and /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 documents, and the search index is at /assets/docs/search.json. For AI agents lists them all, with the rules an agent must follow. Where to start Try it. Install the toolchain, then run the Quickstart to bring up a simulated cell and jog it from your browser. It takes no hardware. Program a cell. Read the safety model before you move hardware. Then read Run everything in simulation and the Guides section. Integrate RosieOS. Start with Your first motion, which drives the simulated core from a short Go program. Then read Architecture and the APIs. Contribute. The Contributing section covers the repository layout, building and testing."},{"title":"Install the toolchain","section":"Get started","url":"/docs/get-started/installation","markdown":"/docs/get-started/installation.md","description":"The host, compilers and package managers RosieOS needs, which component needs which tool, and how to clone the repository.","headings":[{"id":"host","text":"Host"},{"id":"tools","text":"Tools"},{"id":"clone","text":"Clone"},{"id":"check-the-install","text":"Check the install"},{"id":"which-component-needs-what","text":"Which component needs what"},{"id":"next","text":"Next"}],"text":"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, 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 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: 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. Clone Install Git LFS before you clone. Sample STEP parts and some mesh sources are stored in LFS. 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: 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: (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: 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."},{"title":"Quickstart: simulated cell","section":"Get started","url":"/docs/get-started/quickstart","markdown":"/docs/get-started/quickstart.md","description":"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.","headings":[{"id":"1-clone-and-build-the-core","text":"1. Clone and build the core"},{"id":"2-start-the-simulated-core","text":"2. Start the simulated core"},{"id":"3-ask-the-core-what-it-is","text":"3. Ask the core what it is"},{"id":"4-start-the-offline-programming-app","text":"4. Start the offline programming app"},{"id":"5-home-arm-and-jog","text":"5. Home, arm and jog"},{"id":"6-stop-the-stack","text":"6. Stop the stack"},{"id":"if-something-is-off","text":"If something is off"},{"id":"next-steps","text":"Next steps"}],"text":"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. The simulator also needs at least two CPUs that the process is allowed to use. 1. Clone and build the core 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: 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: 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: 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 (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. 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. The Machine panel walks you through five steps: Choose, Connect, Home, Arm, Drive. Choose a machine: select Local simulation. This is the simulated core you started in step 2. Connect: OLP describes the machine and checks its identity. This does not take control. Home: press Home. The dev-stack simulation requires Home on every axis, just like a real cell. Arm: press ARM, then Confirm: energise motors. OLP acquires the lease, enables the axes and arms them. 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 ./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 moves a joint from your own Go program. Run everything in simulation covers every dev-stack service, including the weld planner and the virtual pendant. Architecture explains what you just started."},{"title":"Your first motion (simulation)","section":"Get started","url":"/docs/get-started/first-motion","markdown":"/docs/get-started/first-motion.md","description":"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.","headings":[{"id":"1-build-and-set-up-a-scratch-directory","text":"1. Build and set up a scratch directory"},{"id":"2-start-the-simulated-core","text":"2. Start the simulated core"},{"id":"3-write-the-program","text":"3. Write the program"},{"id":"4-run-it","text":"4. Run it"},{"id":"5-look-at-what-the-core-recorded","text":"5. Look at what the core recorded"},{"id":"why-not-rtctl","text":"Why not rtctl?"},{"id":"next-steps","text":"Next steps"}],"text":"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: Acquire control. Enable and arm the axes. Jog J1 at 0.01 rad/s for one second. 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. You need the toolchain and a clone of the repository. Run every command from rt-core/. 1. Build and set up a scratch directory 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: 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: (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: 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.gogo 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 go run build/first-motion.go The output looks like this. The count delta depends on timing: 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: 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: 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, step 5. Learn the lease, fence and jog-lane rules: Control authority. The full SDK: Go SDK. Every operation: rt-control HTTP API."},{"title":"Safety model","section":"Get started","url":"/docs/get-started/safety-model","markdown":"/docs/get-started/safety-model.md","description":"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.","headings":[{"id":"at-a-glance","text":"At a glance"},{"id":"hardware-e-stop","text":"The hardware E-stop is the only emergency stop"},{"id":"software-stops","text":"Software stops"},{"id":"motion-start-gate","text":"The motion start gate"},{"id":"what-is-verified","text":"What is verified before motion"},{"id":"planned-weld-programs-are-verified","text":"Planned weld programs are verified"},{"id":"jog-moves-and-home-are-not-verified","text":"Jog, moves and Home are not verified"},{"id":"what-you-may-claim","text":"What you may claim"},{"id":"one-controller","text":"One controller at a time"},{"id":"process-outputs","text":"Process outputs are refused"},{"id":"simulation","text":"Simulation does not qualify hardware"},{"id":"checklist","text":"Before you move hardware"},{"id":"standard-warning","text":"Standard warning for motion pages"}],"text":"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 Removes drive power through the cell's safety chain Cell wiring, independent of software Part of RosieOS 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 Refuses to start motion unless authority, readiness and limits all hold The 1 kHz core, every start and every cycle Collision avoidance Program verification 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 Only one client can command the robot at a time rt-control lease and fence Authentication of people 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: No other motion source is active. A running trajectory, jog or commissioning step refuses a new start with mode_conflict. The request carries the current generation, and the lease is valid. The machine is armed, the EtherCAT bus is ready and no safety fault is set. The configuration epoch is verified and every requested axis is configured and verified. Every requested axis that requires Home has a valid Home. Every requested axis has valid limits, is in cyclic synchronous position (CSP) mode, is enabled, and reports Operation Enabled with fresh feedback. No axis is outside its limits and moving further out (outside_limits_outward). 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 and The 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 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. 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. 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 Confirm the hardware E-stop removes drive power, and test it before each session. Clear the cell. Stay outside the robot's reach whenever the drives are armed. Check that the client is bound to the cell you expect. Describe shows the backend (simulation on the simulator) and the configuration digest. Home, then arm, at low speed. For programs, plan, verify and replay in simulation first, and use dry run until process outputs are qualified. 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."},{"title":"For AI agents","section":"Get started","url":"/docs/get-started/for-ai-agents","markdown":"/docs/get-started/for-ai-agents.md","description":"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.","headings":[{"id":"robot-data-and-downloads","text":"Robot data and downloads"},{"id":"read-the-docs-as-data","text":"Read the docs as data"},{"id":"the-system-in-brief","text":"The system in brief"},{"id":"safety-rules-you-must-respect","text":"Safety rules you must respect"},{"id":"rules-of-thumb-for-code","text":"Rules of thumb for code"},{"id":"where-to-find-things","text":"Where to find things"}],"text":"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 is the step-by-step guide to fetching them, with copy-paste curl, Python and JavaScript and a link to every file. /agents.json is the same list as JSON, and /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. 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 Every page with a one-line description, in the llmstxt.org format RosieOS docs index /docs/llms.txt The docs section on its own Full text /docs/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.json How to fetch the robot data, and every data file, API spec and guide with its URL Search index /assets/docs/search.json Title, section, URL, Markdown URL, description, headings and text of every page rt-control API OpenAPI 3.1, original contract Generated from rt-core/protocol/application-v1.schema.json 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, weld planner OpenAPI 3.1, generated from their reference pages Error codes /docs/data/error-codes.json All 153 rt-control reasons, with meaning, group, HTTP status and callers NATS /docs/data/nats-subjects.json Subjects, streams and the command tables Robot models /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. The system in brief One public control API. Every client drives the robot through rt-control. 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. Behind it, the 1 kHz real-time core owns the EtherCAT drives. See 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. 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, then start_program). The motion paths page says which process owns each path. Clients. The Go SDK, the header-only C++ client, TypeScript contracts (types only, no HTTP client) and the rtctl CLI. Other services. The OLP server on 127.0.0.1:8794 and the weld planner on port 8796. The planner listens on all interfaces by default. Neither has authentication. Every port and socket is listed in Ports and environment. Formats. robot.v4.program.v2 programs, the .weldplan plan request, .rdt dense trajectories, and robot description and configuration files. Safety rules you must respect Read the 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. 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. 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. A 200 reply for motion acknowledges admission, not physical completion. Follow events and 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 Load a Rosie robot into PyBullet, MuJoCo, ROS 2 or CAD Simulate a Rosie robot First program against the core Your first motion Every rt-control operation, type and reason rt-control HTTP API Plan and run a weld from CAD Program a weld from CAD, Connect to a cell Mount a tool or the robot Mechanical interfaces Repository layout and generated files Repository layout"},{"title":"Architecture","section":"Concepts","url":"/docs/concepts/architecture","markdown":"/docs/concepts/architecture.md","description":"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.","headings":[{"id":"processes","text":"Processes"},{"id":"public-and-private-interfaces","text":"Public and private interfaces"},{"id":"how-motion-reaches-the-drives","text":"How motion reaches the drives"},{"id":"jog","text":"Jog"},{"id":"point-list-moves","text":"Point-list moves"},{"id":"dense-weld-programs","text":"Dense weld programs"},{"id":"identity-ties-it-together","text":"Identity ties it together"},{"id":"observation","text":"Observation"},{"id":"maturity","text":"Maturity"}],"text":"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. OPERATOR PC CELL HOST · LINUX PREEMPT_RT STEAM DECK OLP UIBrowser, served on :5189 same-origin HTTP OLP server:8794 · programs, sim, machine HTTP .weldplan in, .rdt out rt-core Go SDK Unix socket, or mTLS :8443 Weld planner :8796 · CUDA verifier PASS, then .rdt OLP subprocesses seam_worker, CadQuery, resolver Cartesian serverUDP intent · NATS lease Dense daemon.rdt on TCP :8797 C++ client C++ client rt-control Public API · one lease · control.sock, jog.sock private IPC observe only rosie-rt-core1 kHz · or -sim rt-natspublisherto NATS, no commands EtherCAT, CiA402 CSP Pendant v5Qt app, runs OLP code loopback HTTP Headless OLP127.0.0.1:8794 mTLS :8443 Pendant v4rollback · mTLS, WSS jog DRIVES AND I/O Servo drives J1–J9 · DIO terminal Cell I/O; torch outputs refused Hardware E-stop Removes drive power observed as a DI, diagnostic only 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. 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. 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. 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. The OLP server sends a .weldplan to the weld planner. The planner plans each seam and each connecting move, runs the verifier, and writes a .rdt only if every segment passes. 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). 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 and 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/<model>/) 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 and Robot description and coordinate 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.<host>.status and robot/v4/rtcore.<host>.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"},{"title":"Robot description and coordinate frames","section":"Concepts","url":"/docs/concepts/robot-description-and-frames","markdown":"/docs/concepts/robot-description-and-frames.md","description":"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.","headings":[{"id":"what-is-in-a-description","text":"What is in a description"},{"id":"bench-descriptions","text":"Bench descriptions"},{"id":"identity","text":"Identity"},{"id":"how-the-identity-travels","text":"How the identity travels"},{"id":"load-refusals","text":"Load refusals"},{"id":"cell-calibration","text":"Cell calibration"},{"id":"frames","text":"Coordinate frames"},{"id":"units","text":"Units"},{"id":"rosie-1400","text":"Rosie 1400"},{"id":"rosie-1420","text":"Rosie 1420"},{"id":"limits","text":"Which limit applies where"},{"id":"related-pages","text":"Related pages"}],"text":"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. To create a new model see Add a robot model. What is in a description A description lives at robot_description/robots/<model_id>/. 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: 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. How the identity travels How a robot description's identity travels: a machine config pins the description, rtctl compile snapshots it, rt-control serves it, OLP plans against it, the .rdt header records it, and OLP Load compares the header with what the cell describes. IN THE REPOSITORY PLANNING Robot description robot_description/robots/<model>/ manifest + registered files → sha256:… Machine config robot.robot_description{path, sha256} pinned by path and identity ON THE CELL HOST Compiled configuration rtctl compile snapshots every registered file, plus the cell's machine_planning_calibration.json rt-control Describe robot.resources · /v1/resources/<sha256> OLP robot config Opens the description and the cell's calibration Seam worker and weld planner Receive settled numbers, never the files .rdt header: robot_cell robot_description_sha256 machine_planning_calibration_sha256 OLP Load compares the identities Describes the cell fresh, then checks the plan's model, description and calibration identities. Any difference refuses the program before a byte reaches the robot: robot_cell_missing · robot_cell_unavailable · robot_cell_mismatch 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. 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. 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/<sha256>, or with rtctl resources fetch. 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. 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. 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. 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. Rosie 1420 Model rosie_1420_v1: a six-axis arm on a pedestal. 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. Related pages Robot description files: every field and the identity algorithm Add a robot model Cells, machines and positioners Configuration files: how a machine config pins a description Dense trajectory format: the robot_cell header block"},{"title":"Control authority: leases, fences and the jog lane","section":"Concepts","url":"/docs/concepts/control-authority","markdown":"/docs/concepts/control-authority.md","description":"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.","headings":[{"id":"the-lifecycle-in-one-example","text":"The lifecycle in one example"},{"id":"the-grant","text":"The grant"},{"id":"the-fence","text":"The fence"},{"id":"lease-timing","text":"Lease timing"},{"id":"what-ends-authority","text":"What ends authority"},{"id":"idempotent-retries","text":"Idempotent retries"},{"id":"the-jog-lane","text":"The jog lane"},{"id":"one-controller-many-clients","text":"One controller, many clients"},{"id":"related-pages","text":"Related pages"}],"text":"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. 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 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.gogo 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)<<len(d.Axes)-1); err != nil { log.Print(err) } if _, err := c.Arm(ctx); err != nil { log.Print(err) } // ... motion ... if _, err := c.Stop(ctx); err != nil { // inhibits and bumps the generation log.Print(err) } if _, err := c.Release(ctx); err != nil { // stops renewal, then releases log.Print(err) } } The Go SDK page covers these calls in detail. The C++ client has the same shape. The grant acquire returns a Grant: Field Meaning session A fresh, unpredictable token of 64 lowercase hex characters. Empty after release or expiry. generation A uint64 that increases on every Acquire and every Stop. lease_ms The effective lease: min(requested_lease_ms, cell ceiling). It stays the same for the life of the session. deadline_host_ns When the lease runs out, in the cell host's CLOCK_MONOTONIC ns. stopping true while a Stop is draining. Renewal still succeeds, but grants no motion. controller, binding Your label and the binding you acquired with. acquire succeeds only when nobody else holds authority and your binding matches the deployment: the pair ID and revision rt-control was started with, and the machine digest from Describe. If you send machine_sha256 it must match exactly, and there is no fallback to configuration_sha256. A mismatch is authority_binding_mismatch. If another session holds authority you get control_already_owned. The connection itself is authenticated by the transport: file permissions on the local socket, or the client certificate on the remote listener. A session belongs to the principal that acquired it. Using it from another principal, or from the local socket when it was acquired remotely, returns session_principal_mismatch. The fence Every command after acquire carries session and generation. rt-control checks them on every call. Motion needs the exact current generation. Enable, Arm, Home, jog, Prepare and Start are refused with control_session_stale if the generation is not the current one, if the lease has expired, or while a Stop is in flight. Stop increments the generation. A live session gets its session back with the new generation, so any command still in flight under the old one is fenced off. You need a fresh Enable and Arm before moving again. Renew and Stop accept older generations. A non-zero generation that is not newer than the current one, from the same session, may renew (and learns the current fence from the reply) or stop. It can never move. Stop works after expiry. A session that has expired or been revoked can still send Stop, so a client that lost its lease can still inhibit. It cannot regain motion permission. Restarts invalidate sessions. A session from a previous core incarnation gets daemon_restarted. A well-formed session this rt-control process never issued (for example, one from before an adapter restart) gets fence. Handles follow the same rule. A trajectory handle belongs to the generation that prepared it, and Stop, expiry and connection loss retire every handle. A retired handle can never start again. Lease timing The cell's compiled configuration sets two ceilings, and Describe publishes both: Profile (control.link_profile) Lease ceiling max_grant_lease_ns Jog input age max_jog_input_age_ns lan (the default) 500 ms 250 ms internet (starting values, unverified) 3 s 750 ms A cell can override either value explicitly, up to 10 s for the lease and 2 s for the input age, and leases are whole milliseconds. The effective lease is the smaller of what you request and the ceiling. If you request nothing, you get 500 ms, still capped by the cell. Renew at most every third of the effective lease, on a connection of its own, so Status polls and uploads cannot delay it. The Go and C++ clients measure the Describe round trip and refuse to start when the lease cannot cover at least three round trips plus the renewal interval; that refusal is typed control_session_stale (LeaseTimingRejected). 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. 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. GET /v1/jog/clock returns the host clock's incarnation. 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. 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. 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. 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: acquire, renew, release, stop, halt and the jog calls. The real-time core: what the core checks before any motion starts. Error codes: the authority reasons."},{"title":"The real-time core","section":"Concepts","url":"/docs/concepts/real-time-core","markdown":"/docs/concepts/real-time-core.md","description":"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.","headings":[{"id":"three-builds-of-one-controller","text":"Three builds of one controller"},{"id":"drives-cia402-in-cyclic-synchronous-position","text":"Drives: CiA402 in cyclic synchronous position"},{"id":"bringing-an-axis-up","text":"Bringing an axis up"},{"id":"the-motion-start-gate","text":"The motion start gate"},{"id":"the-final-output-permit","text":"The final output permit"},{"id":"stop-halt-and-expiry","text":"Stop, Halt and expiry"},{"id":"limits-and-interpolation","text":"Limits and interpolation"},{"id":"faults-and-recovery","text":"Faults and recovery"},{"id":"home-and-anchors","text":"Home and anchors"},{"id":"brakes","text":"Brakes"},{"id":"collision-watchdog","text":"Collision watchdog"},{"id":"telemetry","text":"Telemetry"},{"id":"what-the-core-does-not-do","text":"What the core does not do"},{"id":"related-pages","text":"Related pages"}],"text":"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, 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. rt-control and the real-time core: clients reach the core only through rt-control; the core's cycle ends in a final output permit; the hardware E-stop chain acts on the drives directly. CLIENTS OLP server, motion servers, pendant, rtctl, your code Go SDK, C++ client, or plain HTTP HTTP/JSON, jog frames (local socket or mutual TLS) PUBLIC CONTROL API rt-control Lease and fence, handles, jog lane, events, telemetry private IPC: Unix socket and shared memory (internal) REAL-TIME CORE rosie-rt-core (rosie-rt-core-sim in simulation) Read feedbackPDOs, faults Readiness andstart gate Trajectory orjog target Final outputpermit Controlword is zeroed when the lease is invalid or a safety fault is latched. Motion is permitted only while permitted, armed and fault-free. EtherCAT (IgH), CiA402 cyclic synchronous position DRIVES AND I/O Servo drives, one per axis; cell I/O terminal Torch-class outputs are always refused Hardware E-stop and safety chain acts on drive power directly; not software observed as a drive input: diagnostic only 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. 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 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). 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. Enable the axes with enable and an axis mask. The drives move through the DS402 states to Operation enabled. Arm the core with arm. Wait for readiness. Poll Status until every axis you will move reports readiness: \"ready\". 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 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. 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. 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: leases, fences and the jog lane. rt-control HTTP API: the calls that drive the core. Error codes and fault states. Architecture: where the core sits among the other processes."},{"title":"Motion paths and planning","section":"Concepts","url":"/docs/concepts/motion-and-planning","markdown":"/docs/concepts/motion-and-planning.md","description":"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.","headings":[{"id":"the-three-kinds-of-motion-request","text":"The three kinds of motion request"},{"id":"the-four-motion-owners","text":"The four motion owners"},{"id":"olp","text":"Offline programming"},{"id":"cartesian","text":"Cartesian motion server"},{"id":"dense","text":"Dense trajectory daemon"},{"id":"direct","text":"Your own client"},{"id":"what-is-checked-and-where","text":"What is checked, and where"},{"id":"starting-from-rest","text":"Starting from rest"},{"id":"stopping","text":"Stopping"},{"id":"related-pages","text":"Related pages"}],"text":"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. SOURCE MOTION OWNER PUBLIC API CORE, 1 kHz OLP UI or pendant v5 HTTP to :8794 OLP server dense-execution API programs need a verifier PASS UDP intent source plus NATS commands Cartesian motion server robot-v4-cartesiand NATS leader lease .rdt producer TCP :8797, NATS play Dense trajectory daemon joint_trajectory_daemon NATS leader lease Your code Go or C++ client rt-control one lease, one fence jog lane trajectory program rosie-rt-core motion start gate and output permit all three jog, trajectory program direct, any operation Every path acquires the same rt-control lease, so only one of them can command the robot at a time. 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. 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 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. Their lease and fence rules are in Control authority. The four motion owners Path Owner process How a client reaches it Extra authority layer rt-control lanes Offline programming OLP server HTTP on 127.0.0.1:8794 Selected-target generation headers Jog, trajectory, program, Home Cartesian motion server robot-v4-cartesiand UDP intent packets and NATS commands NATS leader lease Jog, trajectory, Home Dense trajectory daemon joint_trajectory_daemon TCP on 127.0.0.1:8797 for bytes, NATS for play NATS leader lease Program, Home Your own client 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. 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. 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 and the guide Connect to a cell and run a program. 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. 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. Note 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. Your own client Your code can call rt-control directly through the Go SDK or the C++ 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 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. 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. Related pages Safety model Architecture Control authority Weld planning and verification Ports, sockets and environment"},{"title":"Weld planning, verification and evidence","section":"Concepts","url":"/docs/concepts/weld-planning-and-verification","markdown":"/docs/concepts/weld-planning-and-verification.md","description":"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.","headings":[{"id":"the-pipeline","text":"The pipeline"},{"id":"what-the-verifier-checks","text":"What the verifier checks"},{"id":"collisions","text":"Collisions"},{"id":"limits","text":"Limits"},{"id":"the-tracking-certificate-welds-only","text":"The tracking certificate, welds only"},{"id":"verdicts","text":"Verdicts"},{"id":"admission","text":"What admission requires"},{"id":"not-verified","text":"What is not verified"},{"id":"result","text":"The plan result"},{"id":"related-pages","text":"Related pages"}],"text":"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 in the safety model first. This page explains what that check covers. 1 · AUTHOR (OLP + SEAM WORKER) STEP part imported, placed on the cell Seam worker weld joints, seams, torch angle search, trim or accept Program robot.v4.program.v2 weld nodes, taught moves .weldplan program, cell, tooling, STEP, fixtures, digests 2 · PLAN (WELD PLANNER :8796, CUDA) M4 seam search sampled poses, sphere model M5 + M6 trajopt weld curves, moves between Verifier exact meshes, limits, tracking certificate Admission + .rdt every segment PASS, or no trajectory is written 3 · LOAD AND RUN Simulate replays the exact bytes optional operator step OLP Load fetch by digest, identity, Home, no process outputs rt-control admission positions, velocities, steps, digest Play motion start gate in rosie-rt-core Not on this path, and not verified: jog, joint and Cartesian moves, Home, Go to plan start, direct rtctl or SDK commands. 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 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. 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. 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. Verify. Every M5 curve and every M6 move goes to the verifier, which judges it against the exact cell meshes. 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. 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. 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, 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. 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: The connecting-move stage did not fail. Otherwise: motion_join_failed. seams and connecting_trajectories are lists of objects, and every seam has a non-empty, unique id. Otherwise: motion_result_invalid. 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). Every seam has a continuous trajectory. Otherwise: motion_not_verified. 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). 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. 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. 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. 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. 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. When a plan comes back without a .rdt, the planner keeps the request and its full result under <dense store>/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 <program_id>:<first 12 hex characters of that digest>. OLP's Load refuses a plan made for another robot description or cell calibration. See Robot description and coordinate frames. Related pages Run the weld planner Weld planner HTTP API Weld program and .weldplan container Motion paths and planning Error codes"},{"title":"Cells, machines and positioners","section":"Concepts","url":"/docs/concepts/cells-and-positioners","markdown":"/docs/concepts/cells-and-positioners.md","description":"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.","headings":[{"id":"four-layers","text":"Four layers"},{"id":"compiling-a-machine-into-an-identity","text":"Compiling a machine into an identity"},{"id":"robot-bound-limits","text":"Robot-bound limits"},{"id":"cell-config","text":"Cell config"},{"id":"robot-models","text":"Robot models"},{"id":"the-h-frame-positioner","text":"The H-frame positioner"},{"id":"simulation-machines","text":"Simulation machines"}],"text":"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/<name>.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/<model_id>/ 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/<cell>.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. 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: cd rt-core build/rtctl compile --config config/machines/simulation/simulation.json --out build/config # build/config/<configuration_sha256> 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.<joint>.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: 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. 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. 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."},{"title":"Process I/O and sensing: current status","section":"Concepts","url":"/docs/concepts/process-io-and-sensing","markdown":"/docs/concepts/process-io-and-sensing.md","description":"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.","headings":[{"id":"cell-io-in-rt-core","text":"Cell I/O in rt-core"},{"id":"torch-outputs-are-refused","text":"Torch outputs are refused"},{"id":"no-seam-tracking-or-sensing","text":"No seam tracking or sensing"},{"id":"weld-parameters-are-data","text":"Weld parameters are data"}],"text":"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: 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. A trajectory point sets the output's bit in io_mask and io_values, both 8-bit fields in configured output order. 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. 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. 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. 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."},{"title":"Run everything in simulation","section":"Guides","url":"/docs/guides/run-in-simulation","markdown":"/docs/guides/run-in-simulation.md","description":"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.","headings":[{"id":"services","text":"Services"},{"id":"the-simulated-machine","text":"The simulated machine"},{"id":"where-files-go","text":"Where files go"},{"id":"useful-overrides","text":"Useful overrides"},{"id":"run-the-smoke-test","text":"Run the smoke test"},{"id":"without-the-dev-stack","text":"Without the dev stack"}],"text":"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. 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. 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: 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, <service>.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/<sha256>/, 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: 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. 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 and the 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. 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 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."},{"title":"Simulate a Rosie robot","section":"Guides","url":"/docs/guides/simulate-a-rosie-robot","markdown":"/docs/guides/simulate-a-rosie-robot.md","description":"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.","headings":[{"id":"downloads","text":"Downloads"},{"id":"what-a-kit-contains","text":"What a kit contains"},{"id":"frames-and-conventions","text":"Frames and conventions"},{"id":"quickstart","text":"Quickstart"},{"id":"robotjson","text":"robot.json"},{"id":"accelerations-and-motion-profiles","text":"Accelerations and motion profiles"},{"id":"benchmark-cycle","text":"Benchmark cycle"},{"id":"ik-reference-cases","text":"IK reference cases"},{"id":"glb","text":"GLB"},{"id":"simulation-parameters","text":"Simulation parameters"},{"id":"geometry","text":"Geometry"},{"id":"licence","text":"Licence"}],"text":"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. 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 robot.json rosie_600.urdf rosie_600.xml rosie_600.step rosie_600.glb ik_cases.json Rosie 1000 rosie-1000-sim-kit.zip robot.json rosie_1000.urdf rosie_1000.xml rosie_1000.step rosie_1000.glb ik_cases.json Rosie 1400 rosie-1400-sim-kit.zip robot.json rosie_1400.urdf rosie_1400.xml rosie_1400.step rosie_1400.glb ik_cases.json Every file of every kit also has its own URL under /assets/sim/<model>/, with the same layout as the zip. /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: TerminalBash 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. MuJoCoPyBulletROS 2Kinematics mujoco_hold.pyPython 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.pyPython 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 workspaceBash 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 onlyPython 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) 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_<model>_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/<model>/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.pyPython 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_<model>.glb is the visual geometry as one glTF 2.0 binary, for three.js, Babylon.js, <model-viewer>, 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."},{"title":"Program a weld from CAD","section":"Guides","url":"/docs/guides/offline-programming","markdown":"/docs/guides/offline-programming.md","description":"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.","headings":[{"id":"before-you-start","text":"Before you start"},{"id":"1-create-a-program","text":"1. Create a program"},{"id":"2-import-the-part","text":"2. Import the part"},{"id":"3-place-the-part","text":"3. Place the part"},{"id":"4-choose-presets","text":"4. Choose presets"},{"id":"5-define-the-welds","text":"5. Define the welds"},{"id":"6-plan","text":"6. Plan"},{"id":"7-simulate","text":"7. Simulate"},{"id":"when-planning-is-refused","text":"When planning is refused"},{"id":"export-the-request","text":"Export the request"},{"id":"next-run-it-on-a-cell","text":"Next: run it on a cell"},{"id":"related-pages","text":"Related pages"}],"text":"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. Before you start You need three things running: The OLP server and UI. The 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. 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 and the seam model. 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. 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: Packing the program for the weld planner Planning: seam search, weld and connecting trajectory optimisation, verify Keeping the plan and its trajectory 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> samples, <seconds> 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. 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. 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 and the OLP HTTP API. 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, or to keep with the plan result as evidence. The file format is in the weld program reference. 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. 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. Related pages Weld planning and verification Teach waypoints and moves Weld program and .weldplan container Offline programming HTTP API Process I/O and sensing"},{"title":"Teach waypoints and moves","section":"Guides","url":"/docs/guides/teach-waypoints","markdown":"/docs/guides/teach-waypoints.md","description":"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.","headings":[{"id":"move-types","text":"Move types"},{"id":"where-the-pose-comes-from","text":"Where the pose comes from"},{"id":"record-a-waypoint","text":"Record a waypoint"},{"id":"reteach-and-via","text":"Reteach and via"},{"id":"edit-a-move","text":"Edit a move"},{"id":"add-events-and-home","text":"Add events and Home"},{"id":"plan-and-simulate","text":"Plan and simulate"},{"id":"related-pages","text":"Related pages"}],"text":"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. 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. 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. 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. The Jog panel (toolbar Jog) has joint and Cartesian jog for the selected, armed cell. Record a waypoint Jog the robot to the pose you want. Let go of the jog control and wait for the robot to be still. 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. 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. 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. 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. 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 Weld planning and verification Connect to a cell and run a program Pendant controls"},{"title":"Program from the Steam Deck pendant","section":"Guides","url":"/docs/guides/program-from-the-pendant","markdown":"/docs/guides/program-from-the-pendant.md","description":"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.","headings":[{"id":"how-it-fits-together","text":"How it fits together"},{"id":"the-screen","text":"The screen"},{"id":"1-connect-a-cell","text":"1. Connect a cell"},{"id":"2-arm","text":"2. Arm"},{"id":"3-jog","text":"3. Jog"},{"id":"4-record-waypoints","text":"4. Record waypoints"},{"id":"5-edit-the-program","text":"5. Edit the program"},{"id":"6-plan-preview-and-run","text":"6. Plan, preview and run"},{"id":"telemetry","text":"Telemetry"},{"id":"related-pages","text":"Related pages"}],"text":"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. Danger 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 until the cell owner has qualified it. To install it on a Deck, see Build and install the pendant. Every control is listed in 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. 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 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. 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: 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> samples, <seconds> s · Preview, or Load to robot, and the pendant keeps the trajectory bytes in its own data directory. ▶ PREVIEW PLAN replays the kept trajectory in the 3D view, with a scrubber. ■ END PREVIEW stops it. Choose DRY RUN (process outputs off). Torch outputs are refused in this release, and there is no seam tracking. 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. 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. ▶ 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. 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 Build and install the pendant Teach waypoints and moves Use the virtual pendant Offline programming HTTP API"},{"title":"Connect to a cell and run a program","section":"Guides","url":"/docs/guides/connect-a-cell","markdown":"/docs/guides/connect-a-cell.md","description":"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.","headings":[{"id":"before-you-start","text":"Before you start"},{"id":"1-commission-the-cell-in-the-repository","text":"1. Commission the cell in the repository"},{"id":"2-write-the-cell-catalogue","text":"2. Write the cell catalogue"},{"id":"3-start-olp-with-the-catalogue","text":"3. Start OLP with the catalogue"},{"id":"4-select-the-cell","text":"4. Select the cell"},{"id":"5-home","text":"5. Home"},{"id":"6-arm","text":"6. Arm"},{"id":"7-plan-and-load","text":"7. Plan and Load"},{"id":"8-play","text":"8. Play"},{"id":"9-stop-and-disconnect","text":"9. Stop and disconnect"},{"id":"when-something-is-refused","text":"When something is refused"},{"id":"related-pages","text":"Related pages"}],"text":"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. 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. 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 and Remote access (mTLS). 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:<pair_id> URI, and OLP refuses the connection if that pair differs from the catalogue's binding (session_principal_mismatch). See 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/<cell_id>.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 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: cd rt-core go test -count=1 ./config/cells -run TestRepositoryCompatibility Note 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.jsonJSON { \"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): {\"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 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: 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: 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: 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: <target.selection_generation> and X-RT-Target-Cell: <target.cell_id>. If anyone selects another cell in between, the request is refused with target_changed. The UI does this for you. See Target fencing. For the commands below: 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: 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 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\": \"<status.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. 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: 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: the .rdt exists in the planner's store, which it only does if every segment passed the verifier its header matches the identity you sent the plan's robot description and cell calibration match the machine 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. 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: 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. 9. Stop and disconnect Press STOP in the UI, or: 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 and Error codes. Related pages Safety model Motion paths and planning Offline programming HTTP API Cells, machines and positioners Dense trajectory (.rdt) format"},{"title":"Install rt-core on a cell host","section":"Guides","url":"/docs/guides/install-on-a-cell-host","markdown":"/docs/guides/install-on-a-cell-host.md","description":"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.","headings":[{"id":"what-you-need","text":"What you need"},{"id":"foundation","text":"1. Provision the EtherCAT foundation"},{"id":"package","text":"2. Build a runtime package"},{"id":"install","text":"3. Install the package, inactive"},{"id":"remote-pki","text":"4. Set up remote access (optional)"},{"id":"bind","text":"5. Bind the deployment"},{"id":"check","text":"6. Check the host"},{"id":"start","text":"7. Start the services"},{"id":"rollback","text":"Update and roll back"},{"id":"source-tree","text":"Installing from a source tree"},{"id":"related-pages","text":"Related pages"}],"text":"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. 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 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. 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, 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. export NO_REAL_MOVEMENT=1 \\ ROSIE_RT_FOUNDATION_ETHERCAT_INTERFACE=eth1 \\ ROSIE_RT_FOUNDATION_ETHERCAT_MAC=<mac> ROSIE_RT_FOUNDATION_ETHERCAT_PERMANENT_MAC=<mac> \\ ROSIE_RT_FOUNDATION_UPLINK_INTERFACE=eth0 \\ ROSIE_RT_FOUNDATION_UPLINK_MAC=<mac> ROSIE_RT_FOUNDATION_UPLINK_PERMANENT_MAC=<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: 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 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 sudo bash /tmp/rt-package/host/install.sh --from-package /tmp/rt-package --live # Installed. No units were started. This: verifies the package, and with --live refuses a simulation package 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 copies the package to /opt/rosie-rt-core/<git-sha>/ and points /opt/rosie-rt-core/current at it, keeping the old target as previous 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. 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/<principal>.pem and clients/<principal>-key.pem; hand those and ca.pem to that client. Certificate details, SAN identity and revocation are in 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: 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: 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, 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/<digest>/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. Danger 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 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 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 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 <file> 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: 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: 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: sudo bash /opt/rosie-rt-core/current/host/install.sh --rollback # Rolled back current to <git-sha>. 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 Configuration files Remote access (mTLS) Ports, sockets and environment variables Releasing"},{"title":"Build and install the pendant","section":"Guides","url":"/docs/guides/install-the-pendant","markdown":"/docs/guides/install-the-pendant.md","description":"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.","headings":[{"id":"before-you-start","text":"Before you start"},{"id":"qt5qml-without-root","text":"Qt5Qml without root"},{"id":"build","text":"Build"},{"id":"test","text":"Test"},{"id":"run-it-on-the-workstation","text":"Run it on the workstation"},{"id":"package","text":"Package"},{"id":"deploy","text":"Deploy"},{"id":"add-it-to-steam","text":"Add it to Steam"},{"id":"site-files","text":"Site files"},{"id":"plannerenv","text":"planner.env"},{"id":"related-pages","text":"Related pages"}],"text":"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. Danger 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. Before you start On the build workstation, Ubuntu 22.04 or WSL2: 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. Qt5Qml without root The Makefile looks for a sysroot at ~/.rosie/qt5qml-sysroot, or wherever QML_SYSROOT points: 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: (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 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 does), then: 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 <origin> http://127.0.0.1:8794 The OLP server the pendant fronts --bundle <path> <app dir>/olp-core.js The OLP logic bundle --robots <dir> <app dir>/../olp/robot_description/robots Robot descriptions, for the 3D view --programs <dir> ~/Rosie programs The folder for OPEN and SAVE AS --windowed full screen Run in a desktop window --snapshot <prefix> Render every page at 1280×800 to <prefix>-<n>.png after 3 s, then exit Package 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: 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: refuses while the pendant or its rosie-v5-olp unit is running, so nothing that holds a machine grant is replaced underneath it removes the files of the retired browser kiosk from the app directory copies the package over ssh 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 Add run.sh as a non-Steam game (or devkit) shortcut. 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. 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. 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). 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.envBash 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. 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 Pendant controls Run the weld planner Use the virtual pendant"},{"title":"Run the weld planner","section":"Guides","url":"/docs/guides/run-the-weld-planner","markdown":"/docs/guides/run-the-weld-planner.md","description":"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.","headings":[{"id":"before-you-start","text":"Before you start"},{"id":"install","text":"Install"},{"id":"environments","text":"Environments"},{"id":"check-the-gpu","text":"Check the GPU"},{"id":"serve-the-motion-planner","text":"Serve the motion planner"},{"id":"bind","text":"Bind it safely"},{"id":"server-options","text":"Server options"},{"id":"connect-olp-to-it","text":"Connect OLP to it"},{"id":"plan-from-the-command-line","text":"Plan from the command line"},{"id":"inspect-inputs","text":"Inspect inputs"},{"id":"run-the-tests","text":"Run the tests"},{"id":"when-something-goes-wrong","text":"When something goes wrong"},{"id":"related-pages","text":"Related pages"}],"text":"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. Before you start Linux on x86-64 for the motion environment. For an NVIDIA Jetson Thor, see 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. 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 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 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 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: 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: 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: sudo ufw allow from <deck-address> 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. 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: 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. 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: 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 <file.weldplan> A .weldplan as the planner sees it pixi run -e default plan-request --read <file.weldplan> The container's manifest, with every digest verified pixi run -e default program-v2 <program.json> 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 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 <dense store>/refused/. See 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 Program a weld from CAD Weld program and .weldplan container Install the toolchain"},{"title":"Add a robot model","section":"Guides","url":"/docs/guides/add-a-robot-model","markdown":"/docs/guides/add-a-robot-model.md","description":"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.","headings":[{"id":"before-you-start","text":"Before you start"},{"id":"1-create-the-directory","text":"1. Create the directory"},{"id":"urdf","text":"2. Write robot.urdf and robot.srdf"},{"id":"config","text":"3. Write config.json"},{"id":"rtcore-definition","text":"4. Write rtcore_definition.json"},{"id":"spheres","text":"5. Fit the collision spheres"},{"id":"register","text":"6. Register the files"},{"id":"pin","text":"7. Pin it from a machine config"},{"id":"plan","text":"8. Plan against it"},{"id":"when-the-robot-changes","text":"When the robot changes"}],"text":"This guide creates a new robot description in robot_description/robots/<model_id>/, 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 first; every field is in Robot description files. The checks along the way: python3 robot_description/tools/manifest.py --check # manifest and meshes (cd robot_description/go && go run ./cmd/identity ../robots/<model_id>) (cd rt-core && build/rtctl compile --config config/machines/<your-machine>.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). 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 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 <robot name=\"rosie_1600_v1\">, equal to the directory name. Give every movable joint a bounded <limit lower upper velocity>, 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://<anything>/meshes/<file>. 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. 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 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: (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 python3 robot_description/tools/manifest.py rosie_1600_v1 # manifest: rosie_1600_v1 <n> 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: (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:<identity from step 6>\", \"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: 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. Then deploy it as in Install rt-core 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. When the robot changes Edit the files, rerun manifest.py <model_id>, 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."},{"title":"Use the virtual pendant","section":"Guides","url":"/docs/guides/virtual-pendant","markdown":"/docs/guides/virtual-pendant.md","description":"Run the browser Steam Deck pendant against the simulated core, take control, arm, home and jog joints, and know what it cannot do.","headings":[{"id":"start-it","text":"Start it"},{"id":"drive-the-simulated-robot","text":"Drive the simulated robot"},{"id":"keyboard-and-mouse","text":"Keyboard and mouse"},{"id":"what-it-cannot-do","text":"What it cannot do"},{"id":"run-the-bridge-by-hand","text":"Run the bridge by hand"}],"text":"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: 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: 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. Home. This sends a public home for all axes. The dev-stack simulation requires Home before anything moves. Arm. This sends enable for all axes, then arm. 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%. 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 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: (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."},{"title":"rt-control HTTP API","section":"APIs","url":"/docs/apis/rt-control-http","markdown":"/docs/apis/rt-control-http.md","description":"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.","headings":[{"id":"quick-start","text":"Quick start"},{"id":"operations-at-a-glance","text":"Operations at a glance"},{"id":"connecting","text":"Connecting"},{"id":"request-envelope","text":"Request envelope"},{"id":"idempotent-retries","text":"Idempotent retries"},{"id":"responses-and-errors","text":"Responses and errors"},{"id":"common-reasons","text":"Common reasons"},{"id":"observation","text":"Observation"},{"id":"describe","text":"describe"},{"id":"status","text":"status"},{"id":"jog-clock","text":"jog_clock"},{"id":"jog-status","text":"jog_status"},{"id":"jog-ingress","text":"jog_ingress"},{"id":"subscribe-events","text":"subscribe_events"},{"id":"telemetry","text":"telemetry"},{"id":"resource","text":"resource"},{"id":"authority","text":"Authority"},{"id":"acquire","text":"acquire"},{"id":"renew","text":"renew"},{"id":"release","text":"release"},{"id":"stop","text":"stop"},{"id":"machine-control","text":"Machine control"},{"id":"enable","text":"enable"},{"id":"arm","text":"arm"},{"id":"home","text":"home"},{"id":"halt","text":"halt"},{"id":"restore-anchor","text":"restore_anchor"},{"id":"reset-fault","text":"reset_fault"},{"id":"recovery-status","text":"recovery_status"},{"id":"io-arm","text":"io_arm"},{"id":"io-disarm","text":"io_disarm"},{"id":"mark-telemetry","text":"mark_telemetry"},{"id":"jog-lane","text":"Jog"},{"id":"begin-jog","text":"begin_jog"},{"id":"update-jog","text":"update_jog"},{"id":"end-jog","text":"end_jog"},{"id":"jog","text":"jog"},{"id":"trajectories","text":"Trajectories"},{"id":"prepare-trajectory","text":"prepare_trajectory"},{"id":"start-trajectory","text":"start_trajectory"},{"id":"discard-trajectory","text":"discard_trajectory"},{"id":"programs","text":"Programs"},{"id":"prepare-program","text":"prepare_program"},{"id":"start-program","text":"start_program"},{"id":"reserved-operations","text":"Reserved operations"},{"id":"abort","text":"abort"},{"id":"readiness","text":"readiness"},{"id":"types","text":"Types"},{"id":"types-authority","text":"Authority"},{"id":"types-requests-and-responses","text":"Requests and responses"},{"id":"types-motion","text":"Motion"},{"id":"types-jog","text":"Jog"},{"id":"types-description","text":"Description"},{"id":"types-status","text":"Status"},{"id":"types-recovery","text":"Recovery"},{"id":"types-events","text":"Events"},{"id":"types-telemetry","text":"Telemetry"},{"id":"types-cell-i-o","text":"Cell I/O"},{"id":"related-pages","text":"Related pages"}],"text":"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. 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 before you arm a real cell. Tip Machine-readable. The same contract as an OpenAPI 3.1 document, the contract file itself, and every reason code as 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 curlBash 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 or the C++ client. rtctl wraps the same calls for one-off commands; see 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 implemented GET /v1/telemetry?after=<sequence> GET /v1/telemetry/stream?after=<sequence> none TelemetryBatch mark_telemetry implemented POST /v1/control session and generation sequence acquire implemented POST /v1/control no fence; checks binding Grant renew implemented POST /v1/control session and generation Grant release implemented POST /v1/control session and generation Grant stop implemented POST /v1/control session and generation Grant enable implemented POST /v1/control session and generation sequence arm implemented POST /v1/control session and generation sequence home implemented POST /v1/control session and generation sequence reset_fault interim POST /v1/control session and generation RecoveryStatus recovery_status implemented POST /v1/control session and generation RecoveryStatus jog test_only POST /v1/control session and generation sequence begin_jog implemented POST /v1/control session and generation handle end_jog implemented POST /v1/control session and generation handle prepare_trajectory implemented POST /v1/control session and generation handle start_trajectory implemented POST /v1/control session and generation sequence discard_trajectory implemented POST /v1/control session and generation sequence start_program implemented POST /v1/control session and generation sequence describe implemented GET /v1/describe none Description status implemented GET /v1/status none ProcessStatus jog_clock implemented GET /v1/jog/clock none LocalJogClock jog_status implemented GET /v1/jog none JogObservation jog_ingress implemented GET /v1/jog/ingress none JogIngressObservation prepare_program implemented POST /v1/program session and generation headers Program update_jog implemented jog.sock datagram or WSS /v1/jog protocol/control.json local_jog_update session and generation JogObservation halt implemented POST /v1/control session and generation Response.sequence/native_result abort unimplemented none none none subscribe_events implemented GET /v1/events?after=<sequence> GET /v1/events/stream?after=<sequence> (SSE) none EventBatch readiness unimplemented none none none restore_anchor implemented POST /v1/control session and generation RecoveryStatus io_arm implemented POST /v1/control session and generation sequence io_disarm implemented POST /v1/control session and generation sequence resource implemented GET /v1/resources/<sha256> 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. 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. 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 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 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[] No 2..250000 Point records; declared axis order, native limits and continuity apply. Large bodies require the envelope before this array. identity 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 No The core's JogObservation when the jog lane refused. native_result 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/<sha256> 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=<index> sample=<index> axis=<id>. 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 The body is larger than the operation's cap. HTTP 413. invalid_request_envelope The body is not a JSON object, or a key is not a string. duplicate_request_field A field appears twice in the envelope prefix. schema_mismatch schema is not rosie.rt-control.request.v1. unknown_operation operation is not a POST /v1/control operation. trailing_request_data Data follows the JSON object. request_envelope_changed The fully decoded envelope differs from the admitted prefix. invalid_request_id request_id is null, not a string, or not 1..64 printable ASCII bytes. request_id_conflict The request_id was already used in this session with a different payload. session_principal_mismatch The session belongs to another TLS principal or to the local transport. Authority reasons (every fenced call) Reason When control_session_stale Wrong or stale session or generation, lease expired, or a Stop is in flight. daemon_restarted The session belongs to a previous native daemon incarnation. fence 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 The core refused the command. Read the numeric native reason in native_result.Reason. outside_limits_outward 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 Native cell I/O refusal: no current grant. wrong_generation Native cell I/O refusal: generation mismatch. inhibited Native cell I/O refusal: outputs are inhibited. io_not_configured No cell I/O is configured. io_not_armed An ON intent needs io_arm first. io_fast_input_unsatisfied A cyclic fast input contact is invalid or not satisfied. io_readback_disagreement Physical feedback disagrees with the commanded output. io_torch_unqualified A torch-class output was requested. Always refused. io_marker_late A process marker missed its one-cycle delivery bound. io_exchange_lost 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 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. Name Type Required Description (embedded) 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[] 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 No Detached prepared program metadata including both identity digests; absent when no program is prepared. robot RobotDescription No Null when no robot definition is bound or no compiled artefact is loaded; never guessed from axis count. drives DriveDescription[] Yes Per-axis compiled drive configuration and current verified bring-up identity; empty without a compiled artefact. Example GET /v1/describe HTTP/1.1 Host: localhost 200 OKJSON { \"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 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. Name Type Required Description daemon_incarnation string Yes Core process identity for this snapshot. adapter_incarnation string Yes rt-control process identity. grant GrantObservation Yes Native grant observation. jog JogObservation Yes Native jog observation. jog_ingress JogIngressObservation Yes Adapter-lifetime ingress outcomes across datagram, WSS and UpdateJog; counters and latest refusal match GET /v1/jog/ingress. core NativeStatus Yes Native core status. motion MotionState Yes Native motion state. execution Execution Yes Program and handle lifecycle. time_ns uint64 Yes Native publication time, ns. axes LogicalAxisStatus[] Yes Per-axis logical status, in Describe order. generations StatusGenerations Yes Current generations and epochs. plan_cursor PlanCursor Yes Native plan cursor. buffer_health BufferHealth Yes Native buffer health. adapter AdapterStatus Yes Adapter runtime observations; not native motion state. Example GET /v1/status HTTP/1.1 Host: localhost 200 OKJSON { \"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 The native daemon was replaced while the snap"},{"title":"Events and telemetry streams","section":"APIs","url":"/docs/apis/rt-control-events-telemetry","markdown":"/docs/apis/rt-control-events-telemetry.md","description":"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.","headings":[{"id":"follow-the-event-stream","text":"Follow the event stream"},{"id":"events","text":"Events"},{"id":"cursors-and-loss","text":"Cursors and loss"},{"id":"event-types","text":"Event types"},{"id":"telemetry","text":"Telemetry"},{"id":"cursors-and-loss-1","text":"Cursors and loss"},{"id":"batch-header","text":"Batch header"},{"id":"cycle-record","text":"Cycle record"},{"id":"axis-entry","text":"Axis entry"},{"id":"resources","text":"Resources"},{"id":"related-pages","text":"Related pages"}],"text":"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 curlGo # 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' 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 outputText : 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 GET /v1/events?after=<sequence> One batch of up to 64 events after the cursor, in the normal Response envelope with an EventBatch in data. SSE /v1/events/stream?after=<sequence> 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 acquire succeeded. grant_renewed grant GrantEvent renew succeeded. stopping: true while a Stop drains. grant_released grant GrantEvent release. grant_expired grant GrantEvent The lease ran out. grant_revoked grant GrantEvent stop, or authority lost to a native or transport failure. enable_changed enable EnableEvent axis_enable_mask changed. arm_changed enable EnableEvent armed changed. handle_transition handle HandleTransitionEvent A trajectory handle changed state (from → to). execution_started execution ExecutionEvent A handle started. execution_completed execution ExecutionEvent A started handle was consumed: it completed, or a later execution replaced it. execution_aborted execution ExecutionEvent A started handle was retired with no fault bits set. execution_faulted execution ExecutionEvent A started handle was retired with fault bits set; fault_bits and recovery say which. jog_begin jog JogEvent A jog session opened. jog_end jog JogEvent A jog session closed for a reason other than expiry. jog_expired jog JogEvent Jog input expired. jog_ramping jog JogEvent The jog is ramping down. jog_limited jog JogEvent The jog is being slowed at a position limit (an accepted state, not a failure). fault_latched fault FaultEvent An execution fault bit was set. fault_cleared fault FaultEvent An execution fault bit was cleared. home_epoch_changed epoch EpochEvent Home evidence changed. daemon_incarnation_changed incarnation IncarnationEvent The core process changed (and once at adapter start). adapter_incarnation_changed incarnation IncarnationEvent Emitted once when rt-control starts. publisher_overflow overflow PublisherOverflowEvent Native observations were dropped before they reached the event log. events_dropped events_dropped EventsDropped Your cursor fell behind the 256-event ring; the lost range is inclusive. telemetry_mark mark 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. Events never contain the session token. The field tables for every payload type are in the types section of the HTTP reference. Telemetry GET /v1/telemetry?after=<sequence> One binary batch after the cursor. Sends gzip when you ask for it with Accept-Encoding: gzip. GET /v1/telemetry/stream?after=<sequence> 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 telemetrygo 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. 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/<sha256>. The bytes are immutable, and an unknown digest returns 404 resource_unknown. Related pages rt-control HTTP API: subscribe_events, telemetry, mark_telemetry and resource. The real-time core: where the telemetry ring comes from. TypeScript contracts: decoding telemetry in the browser or Node."},{"title":"Remote access (mTLS) and remote jog","section":"APIs","url":"/docs/apis/remote-access-mtls","markdown":"/docs/apis/remote-access-mtls.md","description":"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.","headings":[{"id":"how-the-listener-authenticates","text":"How the listener authenticates"},{"id":"enable-the-listener","text":"Enable the listener"},{"id":"provision-certificates","text":"Provision certificates"},{"id":"connect-a-client","text":"Connect a client"},{"id":"remote-jog-over-websocket","text":"Remote jog over WebSocket"},{"id":"the-exchange","text":"The exchange"},{"id":"with-the-client-libraries","text":"With the client libraries"},{"id":"related-pages","text":"Related pages"}],"text":"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 as the local socket, plus a WebSocket lane for jogging. Read Describe over mutual TLSBash 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. 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:<id> and exactly one rosie-pair:<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 <directory> 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. 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. # 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. Subcommand What it creates init <dir> <pair> 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 <dir> <principal> clients/<principal>.pem and clients/<principal>-key.pem, valid for 365 days, with URI:rosie-principal:<principal> and URI:rosie-pair:<pair>. Each principal can be issued once. revoke <dir> <principal> 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 GoC++curl 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}, }) #include <rosie/rt_control_client.hpp> 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<HttpStream> { return open_my_mtls_stream(\"rosie.local\", 8443, d); // your TLS implementation }; RtControlClient client(tls_factory); auto description = client.describe(); 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. 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 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. GET /v1/jog?session=<session>&generation=<generation>&jog_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: 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 → <binary local_jog_update frames, no replies> server ← {\"type\":\"rejected\",\"seq\":N,\"reason\":\"…\"} (only on refusal) 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. 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. 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 list them. Jog refusals, which keep the connection open, are listed under update_jog. With the client libraries GoC++ // 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 #include <rosie/rt_jog_remote_producer.hpp> // 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<double> 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 Control authority: the jog lane's deadlines and generations. Error codes: remote TLS and WebSocket reasons."},{"title":"Go SDK (rosieos/rt-core/sdk/control)","section":"APIs","url":"/docs/apis/go-sdk","markdown":"/docs/apis/go-sdk.md","description":"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.","headings":[{"id":"example-acquire-jog-stop-release","text":"Example: acquire, jog, stop, release"},{"id":"add-the-module","text":"Add the module"},{"id":"connect","text":"Connect"},{"id":"authority","text":"Authority"},{"id":"commands","text":"Commands"},{"id":"jog","text":"Jog"},{"id":"observe","text":"Observe"},{"id":"errors","text":"Errors"},{"id":"types","text":"Types"},{"id":"related-pages","text":"Related pages"}],"text":"Package rosieos/rt-core/sdk/control is the Go client for rt-control. 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. 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), then set TMPDIR to the directory holding control.sock and SHA to the compiled configuration digest. jog.gogo 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 READMEText 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.modText 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. (*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. Commands Each method sends one rt-control operation 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: 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. 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 for cursor handling. Errors 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. 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. 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 Control authority C++ client"},{"title":"C++ client","section":"APIs","url":"/docs/apis/cpp-client","markdown":"/docs/apis/cpp-client.md","description":"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.","headings":[{"id":"example-run-one-program","text":"Example: run one program"},{"id":"build","text":"Build"},{"id":"connect","text":"Connect"},{"id":"methods","text":"Methods"},{"id":"retries-and-request-ids","text":"Retries and request IDs"},{"id":"renewal","text":"Renewal"},{"id":"exceptions","text":"Exceptions"},{"id":"jog","text":"Jog"},{"id":"remote-transport","text":"Remote transport"},{"id":"related-pages","text":"Related pages"}],"text":"rosie::rt_control::RtControlClient is a header-only C++17 client for rt-control. 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. 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)C++ #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: 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. 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; 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: #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<double> 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. Remote transport The header-only client does no TLS itself. To reach the remote listener, construct the client with a StreamFactory: a function that takes a Deadline and returns a new std::unique_ptr<HttpStream> 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 Go SDK Control authority"},{"title":"TypeScript contracts","section":"APIs","url":"/docs/apis/typescript-types","markdown":"/docs/apis/typescript-types.md","description":"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.","headings":[{"id":"example-typed-calls-from-node","text":"Example: typed calls from Node"},{"id":"uint64-precision","text":"uint64 precision"},{"id":"telemetry-decoders","text":"Telemetry decoders"},{"id":"what-is-exported","text":"What is exported"},{"id":"related-pages","text":"Related pages"}],"text":"rt-core/clients/ts/ holds two generated TypeScript files for applications that talk to rt-control 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.tsTypeScript 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<Response> { 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. 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.tsTypeScript import { decodeTelemetryBatchHeaderV1, decodeCycleCaptureRecordV2, TelemetryBatchHeaderV1Bytes, CycleCaptureRecordV2Bytes, } from \"./rt_protocol_generated.ts\"; // Feed raw chunks from GET /v1/telemetry/stream?after=<cursor>. 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. 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. 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. 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 Events and telemetry"},{"title":"Offline programming HTTP API","section":"APIs","url":"/docs/apis/olp-http","markdown":"/docs/apis/olp-http.md","description":"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.","headings":[{"id":"quick-start","text":"Quick start"},{"id":"conventions","text":"Conventions"},{"id":"errors","text":"Errors"},{"id":"target-fencing","text":"Target fencing"},{"id":"routes-at-a-glance","text":"Routes at a glance"},{"id":"service","text":"Service"},{"id":"authoring-and-planning","text":"Authoring and planning"},{"id":"weld-plan","text":"Plan a program"},{"id":"cells-and-targets","text":"Cells and targets"},{"id":"machine-control","text":"Machine control"},{"id":"observe","text":"Observe"},{"id":"home","text":"Home"},{"id":"arm-and-disarm","text":"Arm and disarm"},{"id":"heartbeat","text":"Heartbeat"},{"id":"load-play-stop","text":"Load, Play, Stop"},{"id":"joint-jog","text":"Joint jog"},{"id":"joint-move","text":"Joint move"},{"id":"cartesian-jog","text":"Cartesian jog"},{"id":"cartesian-move","text":"Cartesian move"},{"id":"local-simulator","text":"Local simulator"},{"id":"program-catalog","text":"Program catalog"},{"id":"legacy-and-disabled-routes","text":"Legacy and disabled routes"},{"id":"error-codes","text":"Error codes"},{"id":"related-pages","text":"Related pages"}],"text":"The offline programming (OLP) server is the backend of the OLP web app and of the Steam Deck v5 pendant. It serves CAD import, seam authoring and weld planning, a local simulator, and machine control: Home, Arm, joint and Cartesian jog, joint and Cartesian moves, Load, Play and Stop on a selected cell, through rt-control. All routes are under /api/offline-programming/v1 on 127.0.0.1:8794 by default (serve --listen). The server has no authentication. It is meant to be reached from the same host, by the UI's dev proxy or by a pendant's local process. Don't expose it on a network. Warning Energised motion. This moves the robot. RosieOS has no software E-stop: keep the hardware E-stop within reach and clear the cell before you arm. Jog, moves and Home are checked against joint limits only, not against collisions. See the safety model. Tip Machine-readable. This page's routes, request fields and error codes as an OpenAPI 3.1 document, generated from this page. Quick start This selects the local simulated cell that the dev stack provides, homes it, arms it, jogs J1 for a moment, and stops. OLP=http://127.0.0.1:8794/api/offline-programming/v1 # 1. Pick a cell. The dev stack lists \"local-simulation\" first. curl -s $OLP/targets curl -s -X POST $OLP/targets/select -d '{\"cell_id\":\"local-simulation\",\"model_id\":\"rosie_1400_v3\"}' # 2. Every mutating dense-execution call carries the selection it was made against. STATUS=$(curl -s $OLP/dense-execution/status) GEN=$(echo \"$STATUS\" | jq -r .target.selection_generation) CELL=$(echo \"$STATUS\" | jq -r .target.cell_id) FENCE=(-H \"X-RT-Target-Generation: $GEN\" -H \"X-RT-Target-Cell: $CELL\") # 3. Home, then arm. Arm acquires the rt-control lease. curl -s -X POST $OLP/dense-execution/home \"${FENCE[@]}\" curl -s -X POST $OLP/dense-execution/arm \"${FENCE[@]}\" -d '{\"armed\":true}' # 4. Jog J1 at 10 % for one hold. A real client repeats \"update\" every 50 ms. TARGET=$(curl -s $OLP/dense-execution/capabilities | jq -r .cell) SESSION=$(curl -s $OLP/dense-execution/status | jq -r .session.id) REV=$(curl -s $OLP/dense-execution/jog/state | jq -r .revision) JOG=\"{\\\"target_id\\\":\\\"$TARGET\\\",\\\"session_id\\\":\\\"$SESSION\\\",\\\"revision\\\":$REV,\\\"axis\\\":0,\\\"direction\\\":1,\\\"fraction\\\":0.1}\" curl -s -X POST $OLP/dense-execution/jog/begin \"${FENCE[@]}\" -d \"$JOG\" curl -s -X POST $OLP/dense-execution/jog/end -d \"$JOG\" # 5. Stop always works and needs no fence. It also releases the lease. curl -s -X POST $OLP/dense-execution/stop While OLP holds the lease, send a heartbeat at least every 5 s, or OLP stops the machine with ui_heartbeat_lost. Conventions Request and response bodies are JSON unless a route says otherwise. Unknown request fields are ignored; trailing data after the JSON value is refused. Units are in the field names: _rad, _deg, _mm, _m, _s, _ms, _ns. Where a name has no unit, the table says. Request body limits: 64 KiB for dense-execution routes, 12 MiB for /cadquery/topology and /seam, 24 MiB for /weld-plan. Responses are gzip-compressed when the client accepts it. Errors Two error shapes are in use. Machine-control routes (/dense-execution/*, /targets/select): {\"error\": \"home_required\", \"detail\": \"home_required: establish the current joint position reference in Home before loading the program\"} error is a stable code. A refusal from rt-control keeps rt-control's reason as the code. A limit refusal can add limit_violation: {kind, segment, sample, axis, value, limit, unit}. Authoring and service routes: {\"ok\": false, \"code\": \"seam_worker_unavailable\", \"error\": \"the seam worker is unavailable, so no plan request can be packed\"} Target fencing When the server runs with a cell catalogue (OFFLINE_PROGRAMMING_CELLS), every POST under /dense-execution/ must name the selection it was issued against: 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 GET /api/offline-programming/v1/health Server status. Always 200. {\"ok\": true, \"schema\": \"offline-programming.server-status.v1\", \"mode\": \"offline_preview\", \"network_required\": false, \"connected\": false, \"connected_targets\": 0, \"local_simulator\": { … }, \"local_ready\": true, \"execution_enabled\": false, \"target_planning\": false, \"teleop_enabled\": false, \"program_catalog\": {\"available\": false, \"owner\": \"motion-server\"}} execution_enabled and connected_targets describe the legacy connected-execution path, not dense execution. GET /api/offline-programming/v1/capabilities Feature flags, schema offline-programming.capabilities.v1: whether the seam worker is wired (weld_planner.enabled), the local simulator's availability, and the planning flags. GET /api/offline-programming/v1/robots The robot descriptions this server can plan against: the workstation's own, plus any fetched from cells, by identity. {\"robots\": [{\"model_id\": \"rosie_1400_v3\", \"robot_description_sha256\": \"sha256:…\", \"frames\": {\"base\": \"world\", \"tool\": \"tool0\", \"work\": \"positioner_table_a_top\", \"work_world_m\": [0, -0.622, 0.1], \"arm_base_world_m\": [ … ]}, \"axes\": {\"driven\": [\"J1\", \"J2\", \"J3\", \"J4\", \"J5\", \"J6\", \"J7\"], \"held\": {\"J8\": 0, \"J9\": 0}}, \"reset_pose\": { … }}], \"problems\": []} problems names each description that failed to load. A 500 with robot_store_unreadable means the description directory itself could not be read. POST /api/offline-programming/v1/robots/capture-model Compiles the URDF for a model at a given description and cell calibration, for recording a pose. 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\": \"<xml>\", \"identity\": …}. A failure returns 400 {\"error\": \"…\"}. If the description is neither local, cached nor advertised by the selected cell: adopt the matching robot description before recording. Authoring and planning POST /api/offline-programming/v1/cadquery/topology Forwards the body to the bounded CadQuery worker (one subprocess per request) and returns its JSON. The worker operations are extract_topology, build_seam, build_sequence and tessellate. Error codes are the same set as the seam worker's, below. POST /api/offline-programming/v1/seam Forwards the body to the seam worker, python -m seam_worker.workers --stdin, and returns its JSON. The body names the operation: {\"operation\": \"detect_joints\", …}. Operations: detect_joints, check_torch_fits, plan_torch_path, sample_seam_frames, build_plan_request. The response header X-Offline-Seam-Part identifies the part the answer is about. 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 for the operations' payloads. Plan a program POST /api/offline-programming/v1/weld-plan Packs a .weldplan, sends it to the weld planner, and brings back the planner's result and the dense trajectory the robot will play. One plan at a time; up to 30 minutes. Field Type Required Description program object yes A robot.v4.program.v2 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 OKJSON {\"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. 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 POST /api/offline-programming/v1/weld-plan/export Packs the same .weldplan without planning it. Same body as /weld-plan. Returns the container bytes as an attachment named <program_id>.weldplan, with its hash in X-Weldplan-SHA256. Cells and targets A cell is a commissioned machine in the server's catalogue. Selecting one creates the controller for it and bumps the selection generation used for target fencing. Selection grants no authority; Arm or Load does. See Connect to a cell for the catalogue file. GET /api/offline-programming/v1/targets Lists the catalogue's cells with what each one reports about itself. {\"targets\": [{\"cell_id\": \"local-simulation\", \"label\": \"Local simulation\", \"role\": \"LOCAL SIMULATION\", \"models\": [\"rosie_1400_v3\"], \"requested_mode\": \"simulation\", \"backend\": \"simulation\", \"simulation\": true, \"host\": \"localhost\"}], \"selected\": null} A cell that cannot be used has backend: \"unreachable\" and a reason, for example cell_configuration_missing, cell_mode_mismatch or cell_unreachable. POST /api/offline-programming/v1/targets/select Selects a cell. Stops and releases any session on the previous cell first. 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. POST /api/offline-programming/v1/dense-execution/target The Cells dialog's selection. Body {\"cell\": \"<id>\"}; an empty string clears the selection. Returns the dense-execution capabilities. Not fenced. GET /api/offline-programming/v1/dense-execution/cells The registered cells, with reachability: {\"cells\": [{id, label, address, host, credential_ref, model_id, backend, configuration_digest, source, reachable, error}], \"active_cell\": \"…\"}. POST /dense-execution/cells only accepts a cell that matches a commissioned server-side binding (otherwise cell_not_commissioned or cell_binding_pinned). DELETE /dense-execution/cells/{id} always refuses with cell_commissioning_required, or dense_cell_active for the selected cell: commissioned cells are removed from the catalogue file, not from the UI. Machine control Every route below answers 503 dense_target_unavailable when no cell is selected. Every mutating route except the exempt ones needs the fencing headers. Observe GET /api/offline-programming/v1/dense-execution/capabilities What the selected controller is and whether it is reachable. {\"backend\": \"rt_core\", \"available\": true, \"cell\": \"local-dev\", \"controller_id\": \"offline-programming:server-3f9a1c\", \"daemon_reachable\": true, \"rt_core\": {\"description\": { … }, \"address_host\": \"\"}, \"active_cell\": \"local-simulation\", \"cells\": [ … ]} cell is the cell's pair ID. Joint and Cartesian jog and moves send it as target_id. rt_core.description is rt-control's Describe. reason explains an unreachable controller, for example rt_core_identity_mismatch. GET /api/offline-programming/v1/dense-execution/status The machine and session state. Takes no authority. Poll it. { \"backend\": \"rt_core\", \"available\": true, \"target\": {\"selection_generation\": \"4\", \"cell_id\": \"local-simulation\", \"model_id\": \"rosie_1400_v3\", \"label\": \"Local simulation\", \"backend\": \"simulation\", \"simulation\": true, \"host\": \"localhost\"}, \"session\": {\"id\": \"ds-9b1e4f07a2c3\", \"kind\": \"trajectory\", \"state\": \"loaded\", \"dry_run\": true, \"trajectory_digest\": \"sha256:…\", \"plan_id\": \"bracket_fillet:3f1c0a9d2b7e\", \"lease\": {\"held\": true, \"expires_in_ms\": 0}, \"heartbeat_age_ms\": 820, \"heartbeat_deadline_ms\": 5000, \"stop_reason\": \"\", \"last_error\": \"\"}, \"armed\": true, \"robot_cell\": {\"configured\": true, \"valid\": true, \"model_id\": \"rosie_1400_v3\", \"robot_description_sha256\": \"sha256:…\", \"machine_planning_calibration_sha256\": \"sha256:…\", \"machine_planning_calibration_present\": false, \"error\": \"\"}, \"rt_core\": {\"inhibited\": false, \"stop_uncertain\": false, \"recovery_action\": \"\", \"refusal\": \"\", \"refusal_kind\": \"\", \"reason\": \"\", \"status\": { … }, \"events\": [ … ], \"description\": { … }}, \"round_trip_ns\": 180000, \"link_lease_ns\": 500000000 } Field Description target The selection this status belongs to. Use it for the fencing headers. 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. rt_core.status rt-control's full status. See 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 GET /api/offline-programming/v1/dense-execution/cell The selected machine's robot identity, its machine_planning_calibration.json as base64, and its execution limits. This is what a plan request for this cell carries. GET /api/offline-programming/v1/dense-execution/telemetry/window The newest seconds of the machine's telemetry ring, thinned. 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 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 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 OKJSON {\"ok\": true, \"accepted\": true, \"status\": { … }} If a latched execution fault can be cleared by a reset (recovery class reset_clears or reset_after_condition_clears), Arm resets it first, then re-acquires and arms, so one Arm recovers the machine. Faults that need a re-Home or a restart are left to their refusal. Errors: 400 dense_arm_invalid, 409 rt_core_arm_refused, 409 rt_core_arm_timeout, and rt-control reasons such as control_already_owned or not_ready. Heartbeat POST /api/offline-programming/v1/dense-execution/heartbeat Tells OLP a supervising client is still there. Send it at least every 5 s while OLP holds the lease. Not fenced. requestJSON {\"session_id\": \"ds-9b1e4f07a2c3\"} 200 OKJSON {\"ok\": true, \"deadline_ms\": 5000, \"age_ms\": 1004} age_ms is how long the previous beat had stood. If no beat arrives for 5 s, OLP stops the machine with ui_heartbeat_lost, except while an accepted Cartesian move is finishing. Errors: 400 dense_session_required, 409 dense_session_mismatch. Load, Play, Stop POST /api/offline-programming/v1/dense-execution/load Fetches a verified .rdt from the weld planner by digest, checks it, and prepares it on rt-control. requestJSON { \"trajectory_digest\": \"sha256:…\", \"plan_id\": \"bracket_fillet:3f1c0a9d2b7e\", \"program_id\": \"bracket_fillet\", \"program_digest\": \"sha256:…\", \"manifest_revision\": 1, \"plan_revision\": 3, \"dry_run\": true } 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"},{"title":"Weld planner HTTP API","section":"APIs","url":"/docs/apis/weld-planner-http","markdown":"/docs/apis/weld-planner-http.md","description":"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.","headings":[{"id":"quick-start","text":"Quick start"},{"id":"routes","text":"Routes"},{"id":"health","text":"Health"},{"id":"progress","text":"Progress"},{"id":"plan","text":"Plan"},{"id":"query-parameters","text":"Query parameters"},{"id":"how-a-request-runs","text":"How a request runs"},{"id":"response","text":"Response"},{"id":"a-plan-without-a-trajectory","text":"A plan without a trajectory"},{"id":"errors","text":"Errors"},{"id":"fetch-a-trajectory","text":"Fetch a trajectory"},{"id":"related-pages","text":"Related pages"}],"text":"The weld planner's motion server is a small FastAPI app. You POST a .weldplan container and get back a plan result, and, if every segment passed the verifier, the digest of a dense trajectory (.rdt) you can then fetch. The offline programming (OLP) server is its usual client. See Weld planning and verification for what the planner does. The server listens on 0.0.0.0:8796 by default. It has no authentication, and there is no OpenAPI page (/docs is disabled). Warning Bind the weld planner to localhost, or firewall it. By default it listens on every network interface, and anyone who can reach port 8796 can queue GPU plans and download every stored trajectory. Start it with --host 127.0.0.1 when OLP runs on the same machine. When a Steam Deck or another host must reach it, allow only those hosts through a firewall. See Run the weld planner. Tip Machine-readable. The four routes, their query parameters and errors as an OpenAPI 3.1 document, generated from this page. Quick start Check the GPU, plan a committed example part, and fetch its trajectory: PLANNER=http://localhost:8796 curl -s $PLANNER/api/motion/health # {\"cuda\": true, \"device\": \"NVIDIA RTX A4000\"} # Plan with verification and a dense trajectory. This takes minutes. curl -s -X POST --data-binary @weld_planner/v1/data/motion/bracket_a_2x.weldplan \\ -H \"Content-Type: application/octet-stream\" \\ \"$PLANNER/api/motion/plan?run_weld_trajopt=true&run_connecting_trajopt=true&verify=true&dense=true&program_id=bracket_a_2x\" \\ -o result.json jq '.dense.trajectory_digest // .dense_error' result.json # Fetch the verified trajectory by its digest. DIGEST=$(jq -r .dense.trajectory_digest result.json) curl -s -o plan.rdt \"$PLANNER/api/motion/dense/$DIGEST\" The device name in the health answer is whatever your GPU reports. To make your own .weldplan, use Export .weldplan… in OLP, or POST /api/offline-programming/v1/weld-plan/export on the OLP server. Routes Method and path Purpose GET /api/motion/health Whether CUDA is available GET /api/motion/progress The running plan's current stage POST /api/motion/plan Plan a .weldplan GET /api/motion/dense/{trajectory_digest} Fetch a stored .rdt Health GET /api/motion/health Reports whether the planner's Torch build can see a CUDA device. 200 OKJSON {\"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 GET /api/motion/progress The stage of the plan that is running now, for a client watching a long request. 200 OKJSON {\"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 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 OKJSON { \"schema\": \"amr-weld-planner-v1.motion-plan-result.v1\", \"cell_id\": \"rosie_1400_v3\", \"screened\": true, \"k_best\": 5, \"request\": {\"request_sha256\": \"3f1c0a9d2b7e…\", \"program_id\": \"bracket_a_2x\", \"cell_id\": \"rosie_1400_v3\", \"…\": \"…\"}, \"producer\": {\"module\": \"amr-weld-planner/v1\", \"source_revision\": \"0123456789abcdef0123456789abcdef01234567\", \"source_revision_state\": \"bound\"}, \"coverage\": {\"candidate_collision_screening\": \"sampled_lattice_nodes\", \"weld_trajectory_continuous_verification\": \"evaluated_for_all_emitted_trajectories\", \"connecting_trajectory_continuous_verification\": \"evaluated_for_all_emitted_trajectories\", \"canonical_motion_server\": \"not_evaluated\", \"physical_motion\": \"not_evaluated\", \"execution_authority\": \"none\", \"candidate_limit_checks\": \"reported_per_candidate\"}, \"seams\": [{ \"id\": \"seam_0001\", \"length_mm\": 118.0, \"crossed\": true, \"axis_names\": [\"J1\", \"J2\", \"J3\", \"J4\", \"J5\", \"J6\", \"J7\"], \"held\": {\"J8\": 0.0, \"J9\": 0.0}, \"candidates\": [{\"cost_deg\": 41.8, \"joints_deg\": [[…]], \"times_s\": […]}], \"trajectory\": { \"knots_deg\": [[…]], \"knot_velocity_deg_s\": [[…]], \"times_s\": […], \"sampled_tracking_mm\": {\"knots\": 0.02, \"midpoints\": 0.07}, \"tracking\": {\"verdict\": \"PASS\", \"tolerance_mm\": 0.5, \"bound_mm\": 0.21, \"worst_seen_mm\": 0.08, \"segments_out\": 0, \"undecided\": 0, \"unverifiable\": []}, \"verdict\": {\"verdict\": \"PASS\", \"collisions\": 0, \"penetrating\": 0, \"limit_violations\": 0, \"unverifiable\": [], \"worst_clearance_mm\": 6.412} } }], \"connecting_trajectories\": [{ \"kind\": \"approach\", \"from\": null, \"to\": \"seam_0001\", \"duration_s\": 4.2, \"knots_deg\": [[…]], \"knot_velocity_deg_s\": [[…]], \"times_s\": […], \"clearance_mm\": {\"env\": 38.5, \"self\": 61.2}, \"verdict\": {\"verdict\": \"PASS\", \"collisions\": 0, \"penetrating\": 0, \"limit_violations\": 0, \"unverifiable\": []} }], \"dense\": { \"trajectory_digest\": \"sha256:9b1e4f07a2c3…\", \"plan_id\": \"bracket_a_2x:3f1c0a9d2b7e\", \"program_digest\": \"sha256:3f1c0a9d2b7e…\", \"dt_s\": 0.01, \"total_sample_count\": 5230, \"total_duration_s\": 52.29, \"robot_cell\": {\"model_id\": \"rosie_1400_v3\", \"…\": \"…\"}, \"segments\": [{\"index\": 0, \"kind\": \"freespace\", \"source_id\": \"home->seam_0001\", \"sample_count\": 421, \"duration_s\": 4.2}] }, \"options\": {\"k_best\": 5, \"verify\": true, \"verify_margin_mm\": 2.0, \"…\": \"…\"}, \"timings_ms\": {\"…\": 0} } The values above are illustrative. The fields that matter most: Field Description seams[].crossed Whether the M4 search found a path across the seam. When false, frontier and frontier_causes say where and why it stopped. seams[].trajectory The M5 curve: knots_deg, knot_velocity_deg_s and times_s form a cubic Hermite, with its verdict and tracking reports connecting_trajectories[] The M6 moves, in execution order: kind (approach, transit or retract), from and to seam ids (null at the home end), and a node_id for moves tied to a program node connecting_trajectories_error Present when M6 failed. The welds may still be good. program_order Present when the program has taught moves: the enabled nodes in order speed_scale Present when the program's plan speed is below 1. times_s are already stretched and knot_velocity_deg_s already scaled. dense With dense=true: the .rdt summary, including trajectory_digest, plan_id (<program_id>:<first 12 hex of the request digest>), program_digest (sha256: plus the request digest), dt_s, total_sample_count, total_duration_s, max_abs_qd_rad_s (rad/s), robot_cell, bytes and segments[] dense_error With dense=true, when no .rdt was written: {reason, detail, segment_index} See The plan result for every block, and Verdicts for what PASS, REFUSED and FAIL mean. A plan without a trajectory A plan can answer 200 OK and still have no .rdt. Then dense_error says why, and the server keeps the request and result under <dense store>/refused/ (newest 5) for diagnosis. The reasons are admission's and the encoder's: reason Meaning result_empty dense=true without both trajectory stages, or nothing playable in the result identity_invalid program_id is empty or not a plain identifier seam_not_planned A seam was not crossed by the search motion_not_verified A seam or move has no PASS, a non-zero collision, penetration or limit count, an unverifiable check, or a tracking certificate out of tolerance motion_join_failed The connecting moves could not be planned motion_result_invalid The result is malformed, for example duplicate seam ids or an unknown move kind motion_not_planned A program node has no planned motion qd_limit_exceeded A sample is faster than the cell's per-axis velocity ceiling q_step_exceeded, boundary_qd_nonzero, boundary_q_discontinuity, time_grid_invalid, sample_count_overflow, … The resampled trajectory broke an .rdt format rule; see the .rdt format segment_index names the failing segment when there is one. Errors Status Body When 400 {\"error\": \"an empty body is not a .weldplan\"} Empty body 422 {\"detail\": \"unknown free_space_backend …\"} Unknown free_space_backend 422 {\"detail\": [ … ]} A query parameter is out of range or the wrong type 422 {\"error\": \"<type>: <message>\"} Planning failed, including a .weldplan the planner refused to open (bad zip, digest mismatch, wrong program schema) or missing cell meshes 499 {\"error\": \"Planning cancelled\"} The client disconnected Fetch a trajectory GET /api/motion/dense/{trajectory_digest} Returns the stored .rdt bytes for a digest. curl -s -o plan.rdt http://localhost:8796/api/motion/dense/sha256:9b1e4f07a2c3… Name In Type Description trajectory_digest path string sha256: and 64 lowercase hex characters, from dense.trajectory_digest The response is application/octet-stream with Cache-Control: no-store. A malformed digest returns 422 (expected sha256:<64 hex>). An unknown digest returns 404. The store only holds trajectories that passed admission. It lives in --dense-store-dir, else WELD_PLANNER_DENSE_STORE_DIR, else ~/.cache/rosieos-olp/weld-planner-dense, as sha256-<hex>.rdt. It is a cache: if a file is gone, plan again. OLP's Load fetches from this route and answers dense_blob_not_found when the planner no longer has the file. See the .rdt format for the file layout. Related pages Run the weld planner Weld planning and verification Weld program and .weldplan container Offline programming HTTP API Ports, sockets and environment variables"},{"title":"Cartesian motion server","section":"APIs","url":"/docs/apis/cartesian-motion-server","markdown":"/docs/apis/cartesian-motion-server.md","description":"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.","headings":[{"id":"run-it","text":"Run it"},{"id":"command-line","text":"Command line"},{"id":"bindings","text":"Bindings"},{"id":"network-and-status","text":"Network and status"},{"id":"offline-modes","text":"Offline modes"},{"id":"udp-intent","text":"UDP intent packet"},{"id":"scaling","text":"Scaling"},{"id":"frames","text":"Frames"},{"id":"what-halts-the-jog","text":"What halts the jog"},{"id":"nats-commands","text":"NATS commands"},{"id":"leader-lease","text":"Leader lease"},{"id":"robot-commands","text":"Robot commands"},{"id":"status","text":"Status document"},{"id":"resolve-only","text":"Resolve-only protocol"},{"id":"refusal-and-halt-reasons","text":"Refusal and halt reasons"},{"id":"related-pages","text":"Related pages"}],"text":"robot-v4-cartesiand turns a stream of UDP intent packets into Cartesian jog on the robot. It resolves each Cartesian twist into joint velocities with a damped Jacobian, applies joint-limit and singularity scaling, and streams the result on the rt-control jog lane. NATS carries its leader lease and its lifecycle commands: arm, disarm, Home and stop. The same binary has a second, motion-free mode, --resolve-only, which OLP runs as a subprocess to turn a twist or a displacement into joint velocities or waypoints. rt_core is the only backend. The retired selections exit with backend_retired. Warning Energised motion. This moves the robot. RosieOS has no software E-stop: keep the hardware E-stop within reach and clear the cell before you arm. Jog, moves and Home are checked against joint limits only, not against collisions. See the safety model. Run it Every binding is required. The server refuses to start without the control and jog sockets, the pair binding, six axis IDs, a URDF with its SHA-256, a UDP listen address, a NATS URL, a robot command subject and --trusted-lan-leader-authority. robot-v4-cartesiand --backend rt_core \\ --rt-control-socket /run/rosie-rt-core/control.sock \\ --rt-jog-socket /run/rosie-rt-core/jog.sock \\ --rt-pair-id \"$ROSIE_RT_PAIR_ID\" --rt-pair-revision \"$ROSIE_RT_PAIR_REVISION\" \\ --rt-configuration-sha256 \"$ROSIE_RT_CONFIGURATION_SHA256\" \\ --rt-axis-ids J1,J2,J3,J4,J5,J6 \\ --rt-urdf robot_description/robots/rosie_1400_v3/robot.urdf \\ --rt-urdf-sha256 \"$URDF_SHA256\" \\ --udp-listen 127.0.0.1:9000 \\ --nats-url nats://127.0.0.1:14222 \\ --robot-cell cell-a \\ --robot-command-subject robot/v4/robot.cell-a.command \\ --nats-status-subject robot/v4/motion-server.cell-a.status \\ --trusted-lan-leader-authority Build it with make -C motion-server/v1 all. The binary goes to $(ROSIE_HOME)/motion-server/v1/bin; set BIN_DIR to change it. On an installed host, motion-server/v1/start-motion-server.sh supplies the --rt-* bindings from the ROSIE_RT_* environment variables (ROSIE_RT_CONTROL_SOCKET, ROSIE_RT_JOG_SOCKET, ROSIE_RT_PAIR_ID, ROSIE_RT_PAIR_REVISION, ROSIE_RT_CONFIGURATION_SHA256, ROSIE_RT_AXIS_IDS, ROSIE_RT_URDF, ROSIE_RT_URDF_SHA256), and MOTION_SERVER_BINARY names the binary. On startup the server acquires rt-control with its pair binding and renews the grant every 100 ms. It enables and arms only when a controller asks. Home never arms. On shutdown it ends the jog, stops and releases. Command line Bindings Flag Required Description --backend rt_core no The only backend. Default from MOTION_SERVER_BACKEND, else rt_core. --rt-control-socket PATH yes The rt-control Unix socket --rt-jog-socket PATH yes The jog datagram socket, jog.sock beside control.sock --rt-pair-id ID yes The pair binding rt-control was started with --rt-pair-revision N yes Positive integer --rt-configuration-sha256 HASH yes The compiled configuration digest --rt-axis-ids J1,…,J6 yes Six Describe axis IDs, in URDF J1..J6 order. Each must be in rad. Other axes stay unselected. --rt-urdf PATH yes The URDF the resolver loads --rt-urdf-sha256 HASH yes SHA-256 of that file, lowercase hex. If the file changes on disk, jog stops. Network and status Flag Default Description --udp-listen HOST:PORT none (required) UDP intent listener. Must name a concrete host. --udp-ready-file PATH none Written with the bound address once the socket is open --status-output PATH /run/robot-v4-cartesian/status.json The status document, rewritten atomically --latency-report-output PATH none Latency report output --nats-url URL none (required) nats://HOST:PORT, or a daemon-v1://PEER/ROLE/NAME reference resolved through --daemon-state-url --daemon-state-url URL http://127.0.0.1:8787/api/state Only used to resolve a daemon-v1:// NATS reference --robot-command-subject SUBJ from the manifest The subject the server subscribes to for commands (required) --robot-cell NAME the manifest name Commands whose robot differs are ignored --nats-status-subject SUBJ from the manifest Where the status document is published, at most every 250 ms --nats-plan-subject SUBJ from the manifest Plan subject for the self-test plan paths --rtcore-status-subject SUBJ env ROBOT_V4_RTCORE_STATUS_SUBJECT, or the manifest Validated against the manifest if one is given --manifest PATH env ROBOT_V4_MANIFEST A deployment manifest that supplies the subjects, the cell name and leader_controller_roles --trusted-lan-leader-authority off (required) Enables the NATS leader lease. This is cooperative fencing on a trusted network, not authentication: anyone who can publish on the subject can send commands. --control-frequency-hz N 100 Control loop rate for the smoother --diagnostic-echo-source off Diagnostic latency echo A subject must name the concrete motion-server peer: robot/v4/motion-server.<peer>.… or robot/v4/robot.<peer>.…. --nats-arm-subject, --nats-go-home-subject, --nats-io-subject and --rtcore-target are retired and exit with backend_retired. Offline modes Invocation Description --resolve-only REPO_ROOT MODEL The resolve-only protocol on stdin and stdout. No sockets, no grant. --self-test NAME Offline solver and contract tests: cartesian-io, spreadsheet-tesseract-plan, spreadsheet-tesseract-plan-server, accepted-plan-contract, solver-speed, kinematics-authority, table-calibration-fit, table-calibration-apply. They take --input, --output, --manifest, --samples (5), --period-ms (10), --iterations (1000) and --oneshot as each test needs. UDP intent packet One datagram per intent, big-endian throughout. Version 2 adds the leader lease, and motion on the rt_core backend needs it. Offset Field Type Description 0 magic u16 0x4A49 (\"JI\") 2 version u8 1 or 2 3 sample_timestamp_ns u64 When the source sampled the input 11 sequence u32 Must increase for each packet under one lease 15 frame u8 See frames 16 tool_id_len u8 0–31 17 axes[6] 6 × f32 Normalised X, Y, Z, RX, RY, RZ, each clamped to [-1, 1] 41 speed_scale f32 Clamped to [0, 1] 45 deadman u8 Nonzero while the operator holds the enabling control 46 tool_tcp[6] 6 × f32 For an ARM frame, tool_tcp[0] > 0.5 means arm and ≤ 0.5 disarm 70 tool_id tool_id_len bytes The sender identity, <role>:<instance>. A bare value such as steamdeck-1 is read as steamdeck:steamdeck-1. 70 + n leader_fence_epoch u64 Version 2 only. Nonzero. 78 + n lease_id_len u8 Version 2 only. 1–63. 79 + n lease_id bytes Version 2 only A version 1 packet is exactly 70 + tool_id_len bytes. A version 2 packet is exactly 79 + tool_id_len + lease_id_len bytes. The maximum is 173 bytes. Anything else, or any non-finite float, is rejected. Scaling Each linear axis maps to axes[i] × speed_scale × 0.2 m/s and each angular axis to axes[i] × speed_scale × π rad/s. The command is slew-limited at 0.75 m/s² linear and 540°/s² angular, resolved into six joint velocities, and scaled as one vector so no joint exceeds 100 rpm and the Jacobian's singularity gate. The rt_core backend then scales the whole vector again to Describe's per-axis velocity caps. One common scale is applied, so the direction of a Cartesian jog never bends. Each jog output's deadline is at most 250 ms after the packet arrived, shortened by the time it waited in the socket queue. Frames Code Name On the rt_core backend 0 BASE Cartesian jog 1 TOOL Cartesian jog. The rt_core runtime resolves it exactly like BASE; it does not rotate the twist into the tool frame. 2 JOINT Refused: halts with rt_core_command_unavailable 3 HOME Native Home on the selected axes 4 TELEMETRY Refused 5 ARM Arm or disarm, from tool_tcp[0] 6 GO_HOME Refused 7 WELD_IO Refused What halts the jog The server reads every waiting datagram (up to 64 per loop) and acts only on the newest. It halts, which ends the jog and runs Stop, when any packet in the batch: fails to decode, or has the deadman released is a neutral hold (a TOOL or JOINT packet with every axis within 0.02 of zero) is a disarm arrived without a kernel receive timestamp, or waited in the socket queue for 250 ms or more It also halts when a packet's lease ID, fence epoch or sender does not match the current leader, or its sequence does not increase (leader_fence_or_sequence_rejected). After a halt, fresh input cannot resume motion. The controller must arm again. NATS commands Commands arrive on --robot-command-subject as robot.v4.robot-command.v1 JSON. The server ignores messages for another robot and commands it does not own. { \"schema\": \"robot.v4.robot-command.v1\", \"command\": \"arm\", \"robot\": \"cell-a\", \"command_id\": \"c-17\", \"sender_id\": \"steamdeck:deck-1\", \"controller_boot_id\": \"boot-5c1e\", \"leader_lease_id\": \"…\", \"leader_fence_epoch\": 3, \"armed\": true } Field Type Required Description schema string yes robot.v4.robot-command.v1 command string yes See the table below robot string yes Must equal --robot-cell command_id string yes A resend with the same ID is acknowledged as a duplicate and not run again sender_id string yes <role>:<instance>. The role must be in the manifest's leader_controller_roles, which defaults to [\"steamdeck\"]. controller_boot_id, leader_lease_id, leader_fence_epoch string, string, integer for owned commands The sender's current leader lease Replies use robot.v4.command-reply.v1: {\"schema\": \"robot.v4.command-reply.v1\", \"component\": \"motion-server\", \"command\": \"arm\", \"command_id\": \"c-17\", \"accepted\": true, \"duplicate\": false, \"state\": \"completed\"} A reply that the command was queued is not proof it was applied. Read latest_processed_cold_command_id, latest_processed_cold_accepted and latest_processed_cold_result in the status document. Leader lease The server grants one leader lease at a time. It is process-local and empty after every restart. A new grant is revoke-first: motion stops before the new fence epoch exists. Command Fields Description leader_acquire sender_id, controller_boot_id, request_id, campaign_generation (> 0), ttl_ms (> 0) Request the lease. Must not carry lease_id or fence_epoch. leader_renew the above plus lease_id, fence_epoch Extend the lease leader_release sender_id, controller_boot_id, request_id, campaign_generation, lease_id, fence_epoch End the lease. No ttl_ms. leader_cancel sender_id, controller_boot_id, request_id, campaign_generation Withdraw a pending acquire. No ttl_ms, lease_id or fence_epoch. ttl_ms is clamped to 500–2000 ms. The grant shows in the status document: leader_id, leader_lease_id, leader_fence_epoch, leader_expires_in_ms and leader_lease_fresh. latest_leader_request_id and latest_leader_result report the outcome of your request, for example acquired, renewed, released, rejected or expired. Lease commands spell the fence fence_epoch. Motion commands and UDP packets spell it leader_fence_epoch. Robot commands Command Lease Effect stop, disarm not needed Halt: end the jog, run Stop, cancel any pending Home or position run end_run (or end-run) not needed Revoke the current source run and halt arm needed \"armed\": true enables and arms. \"armed\": false halts. home (alias hm35, hm35_home) needed Native Home on the selected axes. Completes when a fresh status shows a new Home epoch with Home valid on every selected axis. go_home (alias go-home) needed Needs a source run admitted through position; otherwise halts with rt_core_run_not_admitted position needed One joint to a target: axis and exactly one of target_rad, target_deg or relative_jog_rad, with optional min_rad/max_rad, max_speed_rad_s and timeout_ms. Anything else is refused with rt_core_position_input_unresolved. Note A position command first needs a source-run admission: the server sends position_execution_admit on the command subject and waits for a robot.v4.bridge-position-permit.v1 reply from the run's source bridge. No component in this repository sends that reply outside its tests, so position and go-home runs need an external bridge. A command that needs the lease and arrives without a matching one halts the server with rt_core_command_unowned, which stops any motion in progress. Any other command halts with rt_core_command_unavailable_or_unowned. Status document Written to --status-output and published on --nats-status-subject. The schema name is robot_v4_motion_server_cartesian_live_status_v1. Field Description backend rt_core state running, or inhibited after a halt refusal, typed_refusal The last halt or refusal reason, and the structured native refusal servos_armed_requested Arm was requested and accepted last_stop_confirmed true when the last Stop was acknowledged, false when its outcome is uncertain latest_input_fresh A jog is running on fresh input latest_applied_qd_rad_s The joint velocities last sent, rad/s latest_joint_limit_scale, latest_singularity_scale, latest_singularity_class The scaling applied to the last jog; the class is clear, warning, hard_stop or unavailable datagrams_received, rejected_input_count, rtcore_outputs_sent, latest_sequence Input counters latest_command_kind rt_core_intent_applied, or the last refusal latest_processed_cold_command_id, _kind, _accepted, _result The last NATS command and whether it was applied leader_*, latest_leader_* The leader lease, as above rt_core_status The complete rt-control status snapshot axes The per-axis logical status from rt-control, including readiness, Home and statusword fault_table The recovery faults from rt-control, or null v4_fields_available Always false on this backend. The earlier backend's mode and activation fields are present and null. Resolve-only protocol robot-v4-cartesiand --resolve-only /path/to/RosieOS rosie_1400_v3 The server loads REPO_ROOT/robot_description/robots/<MODEL>/robot.urdf once, then answers one JSON line on stdout for each JSON line on stdin. MODEL is rosie_1400_v3 or rosie_1420_v1. No sockets are opened and no grant is taken. Exit code 2 means a bad invocation, an unavailable model or a request line over 16,384 bytes; end of input exits 0. request: twistJSON {\"model\": \"rosie_1400_v3\", \"frame\": \"base\", \"twist\": [0.05, 0, 0, 0, 0, 0], \"fraction\": 0.5, \"input_age_ns\": 250000000, \"pose\": [0, -0.4, 0.8, 0, 0.6, 0], \"lower\": [-3.14, -1.9, -1.57, -3.14, -3.37, -2.09], \"upper\": [3.14, 1.9, 1.53, 3.14, 1.3, 3.14]} response (values illustrative)JSON {\"accepted\": true, \"reason\": \"\", \"velocities\": [0.0, 0.07, -0.05, 0.0, -0.02, 0.0], \"joint_limit_scale\": 1, \"singularity_scale\": 1, \"waypoints\": []} Field Type Required Description model string yes Must equal the MODEL argument frame string yes base or tool. Here, unlike the UDP path, a tool twist is rotated into the base frame. operation string no Empty for a twist, move for a displacement twist 6 numbers yes m/s and rad/s, multiplied by fraction. Required even for move. delta 6 numbers for move Exactly one nonzero component: up to 1 m linear or π rad angular pose 6 numbers, rad yes Measured J1–J6 positions lower, upper 6 numbers, rad yes Joint limits. Intersected with the URDF limits. fraction number yes (0, 1] input_age_ns integer, ns yes The input lifetime. A twist is refused if pose + velocity × lifetime would leave the limits. For a twist, velocities are J1–J6 in rad/s. For a move, waypoints are J1–J6 positions in rad at 1 mm or 0.25° spacing along the straight line. Reason Meaning cartesian_input_invalid Malformed request, wrong model, zero twist, or a move with not exactly one component joint_limit The pose is outside the limits, or the result would leave them jacobian_gate Too close to a singularity ik_no_solution No joint solution, or no motion results cartesian_reach The displacement is too long, or the path crosses the singularity gate Refusal and halt reasons Reason Meaning input_safety_barrier An unsafe or untimed packet was in the batch deadman_released, operator_disarm, operator_stop The operator ended motion leader_fence_or_sequence_rejected A packet without the current lease or with an old sequence leader_transition The leader lease changed hands invalid_cartesian_input A packet failed to decode rt_core_urdf_changed The URDF on disk no longer matches --rt-urdf-sha256 rt_core_command_unowned, rt_core_command_unavailable, rt_core_command_unavailable_or_unowned See Robot commands rt_core_position_input_unresolved A position command without exactly one resolved joint target rt_core_run_not_admitted go_home or a follow-up command without an admitted source run rt_core_home_abandoned, rt_core_home_failed, rt_core_home_requires_idle_run Home was replaced, failed, or asked for during a run home_preempted_by_arm, home_replaced, home_replaced_by_udp A new request replaced a pending Home rt_core_position_timeout, rt_core_admission_timeout, rt_core_source_permit_rejected A position run expired or its permit was refused rt-control's own reasons pass through in refusal and typed_refusal. See Error codes. Related pages Motion paths and planning NATS subjects and streams Control authority Ports, sockets and environment"},{"title":"Dense trajectory daemon","section":"APIs","url":"/docs/apis/dense-trajectory-daemon","markdown":"/docs/apis/dense-trajectory-daemon.md","description":"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.","headings":[{"id":"quick-start","text":"Quick start"},{"id":"command-line","text":"Command line"},{"id":"config-file","text":"Config file"},{"id":"tcp-ingest","text":"TCP ingest"},{"id":"upload","text":"upload"},{"id":"validate","text":"validate"},{"id":"preload","text":"preload"},{"id":"status","text":"status"},{"id":"nats-control-plane","text":"NATS control plane"},{"id":"leader-lease","text":"Leader lease"},{"id":"play-a-program","text":"Play a program"},{"id":"other-commands","text":"Other commands"},{"id":"status-1","text":"Status document"},{"id":"consumer_action","text":"consumer_action"},{"id":"refusal-codes","text":"Refusal codes"},{"id":"related-pages","text":"Related pages"}],"text":"joint_trajectory_daemon stores immutable .rdt programs 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. Note 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. 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: 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.pyPython 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. 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 <digest-hex>.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 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.jsonJSON { \"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 <robot-command-subject>.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. 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: {\"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 JSONJSON {\"kind\": \"upload\"} responseJSON {\"ok\": true, \"kind\": \"upload\", \"trajectory_digest\": \"sha256:…\", \"segment_count\": 1, \"total_sample_count\": 3, \"total_duration_s\": 0.02} Refusals: any .rdt validation reason (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 JSONJSON { \"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. responseJSON {\"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. 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 JSONJSON {\"kind\": \"status\"} Returns the status document. 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:<instance>, where the instance is 1–31 characters from A-Za-z0-9-. No other role may hold the lease. Replies have this shape: {\"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_acquireJSON {\"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} replyJSON {\"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 leader_acquire on NATS. Keep renewing. upload the blob over TCP, then preload it with its identity. play on NATS: playJSON { \"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: preparation: status and the prepared identity are read back from rt-control. 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. enable_arm: recovery status must show no faults, then enable and arm. readiness: waits until every program axis reports ready. 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. { \"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_<reason> 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. Related pages Motion paths and planning Dense trajectory (.rdt) format NATS subjects and streams Control authority"},{"title":"rtctl command reference","section":"Reference","url":"/docs/reference/rtctl","markdown":"/docs/reference/rtctl.md","description":"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.","headings":[{"id":"commands-at-a-glance","text":"Commands at a glance"},{"id":"exit-codes","text":"Exit codes"},{"id":"root","text":"Where rtctl finds rt-core"},{"id":"configuration-commands","text":"Configuration commands"},{"id":"compile","text":"compile"},{"id":"validate","text":"validate"},{"id":"run","text":"run"},{"id":"control-env","text":"control-env"},{"id":"host-commands","text":"Host commands"},{"id":"hostcheck","text":"hostcheck"},{"id":"inventory","text":"inventory"},{"id":"recover-encoder","text":"recover-encoder"},{"id":"control-commands","text":"Control commands"},{"id":"control-flags","text":"Common flags"},{"id":"observation","text":"Observation"},{"id":"resources-fetch","text":"resources fetch"},{"id":"authority-commands","text":"Authority and machine control"},{"id":"motion-commands","text":"Trajectories and programs"},{"id":"telemetry","text":"Telemetry"},{"id":"telemetry-dump-to-jsonl","text":"telemetry dump-to-jsonl"},{"id":"related-pages","text":"Related pages"}],"text":"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 for inspection and administration. 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 Note 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 or the C++ 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 compile, validate, run, control-env Host hostcheck, inventory, recover-encoder Control API describe, status, events, resources fetch, acquire, release, stop, halt, enable, arm, home, reset-fault, prepare, start, discard, program-prepare, program-start 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 (<root>/build/rtctl or <root>/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. build/rtctl compile --config config/machines/simulation/simulation-program.json --out build/config # /…/build/config/<configuration_sha256> Flag Type Default Description --config path — Machine config JSON. Required. --profiles dir <root>/config/drives Drive config directory. --out dir — Output directory. Required. The compiled directory is <out>/<configuration_sha256>/. 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. validate Compiles, then runs the daemon with --validate-config, so the daemon checks its own private format without starting a runtime session. 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 <root>/build/rosie-rt-core-sim, -ipc, or rosie-rt-core Daemon executable to run. --profiles dir <root>/config/drives Drive config directory. --out dir <root>/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. 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 <root>/build/rosie-rt-core-sim, -ipc or rosie-rt-core. --out dir <root>/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 <root>/bin/rtctl exists), run accepts only --backend live and runs <root>/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. 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: ROSIE_RT_PAIR_ID=cell-a ROSIE_RT_PAIR_REVISION=1 ROSIE_RT_CONFIGURATION_SHA256=<64 hex> ROSIE_RT_CORE_ARGS=\"<compiled argv>\" 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. 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 <status> <id>: 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). sudo rtctl inventory --config /etc/rosie-rt-core/machine.json Flag Type Default Description --config path — Live machine config. Required. --profiles dir <root>/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. 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. Danger 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. --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. --json bool false Compact JSON output. Observation Command Flags Sends describe — describe status — status events --after N (default 0), --follow subscribe_events. With --follow, streams one JSON event per line until Ctrl-C, then exits 0. --timeout does not apply to the stream. build/rtctl events --after 0 --follow --socket \"$TMPDIR/control.sock\" resources fetch Downloads the cell's robot description files, verified by digest. 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 <definition-sha256> 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/<sha256> 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. 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 stop fence stop. A known session still stops after its lease expired. halt fence halt enable fence, --axis-mask enable arm fence arm home fence, --axis-mask home reset-fault fence reset_fault (interim capability) \"Fence\" means --session and --generation. The axis mask is a bit per axis, 0 to 0xffffffff. # 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. 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. Command Flags Sends prepare fence, --axis-mask, --file points.json prepare_trajectory. The file is a JSON array of Point: time_ns, position (rad or m), optional velocity. start fence, --handle start_trajectory discard fence, --handle discard_trajectory program-prepare fence, --file program.rdt POST /v1/program with the binary .rdt. See Dense trajectory format. program-start fence, --file identity.json start_program. The file is an 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. build/rtctl telemetry dump-to-jsonl /dev/shm/rt-core/telemetry/<dump>.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 <file>. Exit 2 on wrong arguments. Related pages rt-control HTTP API: what each control command sends Configuration files Install rt-core on a cell host Your first motion: why the SDK, not rtctl, moves the robot Ports, sockets and environment variables"},{"title":"Configuration files (drive, machine, cell)","section":"Reference","url":"/docs/reference/configuration","markdown":"/docs/reference/configuration.md","description":"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.","headings":[{"id":"templates","text":"Templates"},{"id":"provenance-siblings","text":"Provenance siblings"},{"id":"compiled-output","text":"What compile writes"},{"id":"digests","text":"Three digests"},{"id":"machine","text":"Machine config"},{"id":"top-level","text":"Top level"},{"id":"machine-axes-flat","text":"axes[], flat form"},{"id":"machine-axes-robot","text":"axes[], robot-bound form"},{"id":"machine-limits","text":"limits"},{"id":"machine-jog","text":"jog"},{"id":"machine-brake","text":"brake and brake_override"},{"id":"machine-collision-watchdog","text":"collision_watchdog"},{"id":"machine-diagnostics","text":"diagnostics"},{"id":"machine-runtime","text":"runtime"},{"id":"machine-robot","text":"robot"},{"id":"machine-control","text":"control"},{"id":"machine-bus","text":"bus"},{"id":"machine-bus-bringup","text":"bus_bringup"},{"id":"machine-io","text":"io"},{"id":"drive","text":"Drive config"},{"id":"cell","text":"Cell config"},{"id":"compile-warnings","text":"Compile warnings"},{"id":"robot-compile-refusals","text":"Robot compile refusals"}],"text":"rt-core reads one compiled configuration. You write it as JSON in rt-core/config/, and 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. Compile a shipped simulation machine and print its digest directory: cd rt-core make control build/rtctl compile --config config/machines/simulation/simulation-program.json --out build/config # /…/rt-core/build/config/<configuration_sha256> How the layers fit together is explained in Cells, machines 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 <field>_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 <required> 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 <field>_source (\"cited: …\") or <field>_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/<configuration_sha256>/: 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/<sha256> The robot description files (and cell calibration) as content-addressed resources. rt-control serves them through Describe and GET /v1/resources/<sha256>. 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:<identity>\", \"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. 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 Required robot object — See robot Absent for flat axes control object — See control lan profile bus object — See bus Unknown slaves refused bus_bringup object — See bus_bringup Defaults below io object — See 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 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.<joint>.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 <name>: 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. 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. robot Field Type Description robot_description.path path Repository-relative robot_description/robots/<model_id>. 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. 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. 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: 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 <name>: collision_watchdog missing; disabled The ax"},{"title":"Robot description files","section":"Reference","url":"/docs/reference/robot-description","markdown":"/docs/reference/robot-description.md","description":"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.","headings":[{"id":"loaders-and-tools","text":"Loaders and tools"},{"id":"manifest","text":"robot_description_manifest.json"},{"id":"identity-algorithm","text":"Identity algorithm"},{"id":"urdf","text":"robot.urdf and robot.srdf"},{"id":"config-json","text":"config.json"},{"id":"rtcore-definition","text":"rtcore_definition.json"},{"id":"spheres","text":"spheres.json"},{"id":"machine-planning-calibration","text":"machine_planning_calibration.json"},{"id":"the-empty-document","text":"The empty document"},{"id":"calibration-identity","text":"Calibration identity"},{"id":"header-identity","text":"Program header identities"}],"text":"This page lists every file in a robot description directory, robot_description/robots/<model_id>/, 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 first. Check every description in the repository: python3 robot_description/tools/manifest.py --check # manifest: rosie_1400_v3 ok, <n> 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 <model> 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 <dir> ... (in robot_description/go) Prints sha256:<hex> <dir> 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. 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: Build the entry list: the manifest itself, plus every file in files. Each entry is its path and the SHA-256 of its bytes. Sort the entries by path (byte order). 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 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 <robot name> must equal the model id. Every movable joint needs bounded <limit lower upper>. Revolute limits are in rad and velocity in rad/s. Mesh references use package://.../meshes/<file>, 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.<J>.velocity_rad_s number rad/s Planning velocity. Positive, and not above the URDF velocity of that joint. planning.joints.<J>.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 <field>_source or <field>_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:<hex> Hash of the meshes they were fitted against. note string Free text. links.<link>[] 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). 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.<J>.lower_rad number rad Revolute URDF joint with stated limits. Must not be below the URDF lower limit. limits.joints.<J>.upper_rad number rad Must not be above the URDF upper limit, and must stay above lower_rad. limits.joints.<J>.velocity_rad_s number rad/s In (0, URDF velocity]. limits.held.<J> number rad Finite, and inside the joint's limits (narrowed ones if given). kinematics.<J>.xyz_m [3]number m Each component's magnitude at most kinematic_correction_caps.xyz_m. kinematics.<J>.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: {\"schema\":\"rosie.machine-planning-calibration.v1\",\"model_id\":\"<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:<hex>. 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 and Dense trajectory format."},{"title":"Mechanical interfaces (base and tool flange)","section":"Reference","url":"/docs/reference/mechanical-interfaces","markdown":"/docs/reference/mechanical-interfaces.md","description":"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.","headings":[{"id":"downloads","text":"Downloads"},{"id":"base","text":"Robot base"},{"id":"tool-flange","text":"Tool flange"},{"id":"frames","text":"Frames"},{"id":"verify","text":"To verify with AMR"}],"text":"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 Base plate solid, AP214, mm, in the base frame rosie-1400-base.pdf A3 drawing sheet: plan, section A–A, key dimensions, notes rosie-1400-tool-flange.step Tool flange unit solid, AP214, mm, in the flange frame rosie-1400-tool-flange.pdf A3 drawing sheet: face view, section A–A, key dimensions, notes 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 on the Rosie page. Robot base Rosie 1400 base plate, plan view Plan of the 300 by 300 mm base plate from above: four Ø26 mm through holes on a 250 mm square for floor anchors, R25 corners, and the base frame axes on the J1 axis at the centre. 300 250 300 250 4× Ø26 THRU FLOOR ANCHORS R25 4 CORNERS A A X Y Z Base plate from above. The base frame is in red. Rosie 1400 base plate, section A-A Diagonal section through two anchor holes: the plate is 28.4 mm thick at the edges and anchors, the underside is the flat mounting face at Z = 0, and the base frame origin is on it. 28.4 MOUNTING FACE Z = 0 FLAT, NO SPIGOT OR DOWELS Z 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. Rosie 1400 tool flange, face view View on the J6 flange face: the Ø38 mm mounting face, raised 0.5 mm, with six M5 holes equally spaced on PCD 27, 11.7 mm of thread, the first 30 degrees from the flange X axis, Ø79 mm outside diameter, and the flange frame axes. 6× M5 THRU EQ SP ON PCD 27 THREAD DEPTH 11.7 Ø38 MOUNTING FACE RAISED 0.5 30° Ø79 A A X Y Z View on the flange face. The flange frame is in red. Rosie 1400 tool flange, section A-A Section through the flange unit on the J6 axis: Ø79 mm outside diameter and 39 mm long from the flange face. 39 Ø79 Z Y 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). 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 with the part you are designing."},{"title":"Dense trajectory (.rdt) format","section":"Reference","url":"/docs/reference/rdt-format","markdown":"/docs/reference/rdt-format.md","description":"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.","headings":[{"id":"write-a-file","text":"Write a file"},{"id":"read-a-file","text":"Read a file"},{"id":"blob-layout","text":"Blob layout"},{"id":"sample-record","text":"Sample record"},{"id":"header","text":"Header"},{"id":"segments","text":"Segments"},{"id":"robot-cell","text":"Robot and cell identity"},{"id":"content-digest","text":"Content digest"},{"id":"validation","text":"Validation"},{"id":"rt-control-admission","text":"Admission by rt-control"},{"id":"tools","text":"Tools"},{"id":"related-pages","text":"Related pages"}],"text":"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.pyPython 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.pyPython 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 [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 { \"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 <program_id>:<first 12 hex of the request digest>. 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, 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 process_markers array no Output intents attached to exact samples; read by rt-control only. See Admission by rt-control. 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. \"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. 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. The full list of reasons is in Error codes. 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. The dense trajectory daemon's validate request runs every rule above without storing the blob. See TCP ingest. Related pages Motion paths and planning Weld planning and verification Dense trajectory daemon rt-control HTTP API"},{"title":"Program format (robot.v4.program.v2)","section":"Reference","url":"/docs/reference/program-format","markdown":"/docs/reference/program-format.md","description":"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.","headings":[{"id":"example","text":"Example"},{"id":"validate-a-document","text":"Validate a document"},{"id":"units","text":"Units"},{"id":"top-level-fields","text":"Top-level fields"},{"id":"freespace-policy","text":"Freespace policy"},{"id":"nodes","text":"Nodes"},{"id":"the-single-line","text":"The single line"},{"id":"weld-nodes","text":"Weld nodes"},{"id":"freespace-nodes","text":"Freespace nodes"},{"id":"move-nodes","text":"Move nodes"},{"id":"forbidden-keys","text":"Forbidden keys"},{"id":"digests","text":"Digests"},{"id":"related-pages","text":"Related pages"}],"text":"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 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.jsonJSON { \"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. Validate a document The TypeScript module exports the validator. With Node 22 or later, from offline-programming/v1/ui: check-program.mjsJavaScript 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 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 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 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: 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 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 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. 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 <program_id>:<first 12 hex of that digest>. 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 Weld planning and verification Offline programming HTTP API Dense trajectory (.rdt) format"},{"title":"Weld program and .weldplan container","section":"Reference","url":"/docs/reference/weld-program-format","markdown":"/docs/reference/weld-program-format.md","description":"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.","headings":[{"id":"weld-program-v1","text":"The weld-program.v1 schema"},{"id":"conventions","text":"Conventions"},{"id":"curves","text":"Curves"},{"id":"arc-length","text":"Arc length"},{"id":"scalar-functions","text":"Scalar functions"},{"id":"seams","text":"Seams"},{"id":"seam-frame","text":"The seam frame"},{"id":"work-and-travel-angles","text":"Work and travel angles"},{"id":"orientation-bands","text":"Orientation bands"},{"id":"weld-joints","text":"Weld joints"},{"id":"weldplan","text":"The .weldplan container"},{"id":"manifest","text":"Manifest"},{"id":"checks-when-packing","text":"Checks when packing"},{"id":"checks-when-opening","text":"Checks when opening"},{"id":"seam-worker","text":"Seam worker protocol"},{"id":"related-pages","text":"Related pages"}],"text":"A weld is described in three layers: The seam model: how one weld's line and torch angles are written. Weld nodes in a robot.v4.program.v2 document use it for their geometry when kind is seam. 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 accepts. 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 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. The seam frame At each s the seam has a right-handed frame: 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 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/<name>.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. 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. Related pages Program format (robot.v4.program.v2) Weld planning and verification Program a weld from CAD Weld planner HTTP API"},{"title":"Error codes and fault states","section":"Reference","url":"/docs/reference/error-codes","markdown":"/docs/reference/error-codes.md","description":"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.","headings":[{"id":"reading-an-error","text":"Reading an error"},{"id":"reason-codes","text":"rt-control reason codes"},{"id":"reasons-envelope","text":"Request envelope and transport"},{"id":"reasons-authority","text":"Authority and session"},{"id":"reasons-handles","text":"Handles"},{"id":"reasons-native","text":"Native admission"},{"id":"reasons-program","text":"Program admission and execution"},{"id":"reasons-dense","text":"Dense program format (.rdt)"},{"id":"reasons-recovery","text":"Recovery and anchors"},{"id":"reasons-io","text":"Cell I/O"},{"id":"reasons-jog","text":"Jog lane and jog clock"},{"id":"reasons-observation","text":"Events, telemetry and resources"},{"id":"reasons-websocket","text":"WebSocket transport (remote jog)"},{"id":"reasons-tls","text":"Remote TLS (startup and handshake)"},{"id":"reasons-startup","text":"Startup and socket ownership"},{"id":"native-command-reasons","text":"Native command reasons"},{"id":"native-jog-reasons","text":"Native jog reasons"},{"id":"native-grant-reasons","text":"Native grant reasons"},{"id":"fault-bits","text":"Execution fault bits"},{"id":"axis-readiness","text":"Axis readiness"},{"id":"handle-states","text":"Handle states"},{"id":"other-components","text":"Other components"}],"text":"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. Reading an error 409 ConflictJSON { \"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. native_jog_result is present when the jog lane refused. Read StateReason, ControlReason or UpdateReason in 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 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, io_arm, io_disarm, mark_telemetry, abort, readiness. Also abort, readiness. invalid_control_generation X-Control-Generation is missing or not a decimal uint64. prepare_program Authority and session Reason Meaning Returned by no_grant Halt requires a current valid application grant. halt, io_arm, io_disarm, prepare_trajectory, start_program wrong_generation Halt carries a different application or native generation. halt, io_arm, io_disarm, prepare_trajectory, start_program inhibited Halt requires an armed, enabled machine with current readiness; Stop or a fault takes precedence. halt, io_arm, io_disarm, prepare_trajectory, start_program control_already_owned Another session holds authority, or a Stop is still draining. 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, release, 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 application_generation_exhausted The uint64 grant generation is exhausted; no further Acquire is possible without a restart. acquire invalid_requested_lease Acquire requested lease is negative or exceeds the unverified absolute 10000 ms bound. acquire Handles Reason Meaning Returned by unknown_handle No such handle in this adapter incarnation. start_trajectory, discard_trajectory, start_program trajectory_identity_mismatch Raw trajectory Start identity differs from its immutable preparation. start_trajectory handle_active The handle has started, so it cannot be discarded. discard_trajectory handle_started This handle has already started. start_trajectory, start_program handle_consumed This handle completed or was replaced by a later execution. start_trajectory, start_program handle_discarded This handle was explicitly discarded. start_trajectory, start_program handle_superseded A committed replacement superseded this handle. start_trajectory, start_program handle_retired Cancellation or uncertain ownership permanently retired this handle. start_trajectory, 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, start_program. Also native command reason 2 inside native_result. mode_conflict Another active motion mode prevents this operation. restore_anchor, begin_jog, jog, start_trajectory, 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, prepare_program program_identity_missing The .rdt header lacks a required identity field. prepare_program program_identity_mismatch No program is prepared, or the supplied identity differs from it. start_program program_already_executing A trajectory or program is executing. reset_fault, recovery_status, prepare_program, start_program manifest_revision_mismatch The program's manifest_revision differs from the adapter's pair revision. prepare_program process_io_executor_not_qualified The program needs torch output, which is not qualified and always refused. prepare_program, start_program dense_axis_map_requires_nine_ids The cell describes more rotary axes than the nine-column dense format carries. prepare_program invalid_dense_axis_map The cell's rotary axes cannot be mapped onto the dense columns. prepare_program unmapped_dense_axis The configured dense axis ID has no native axis mapping. prepare_program native_limit_exceeded A dense sample exceeds a native position, velocity or declared sampled acceleration limit. prepare_program native_segment_rate_exceeded An interpolated program segment exceeds a velocity limit. 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 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 header_truncated Dense header length or header bytes are incomplete. prepare_program header_json_invalid Dense header cannot be decoded or encoded as JSON. prepare_program sample_encoding_mismatch Dense record encoding or record byte count is unsupported. prepare_program axis_count_mismatch Dense axis count differs from the nine-axis format. prepare_program limits_invalid Dense velocity limits must contain nine positive finite values. prepare_program segments_empty Dense program contains no motion segments. prepare_program segment_index_out_of_order Dense segment indices do not follow payload order. prepare_program kind_invalid Dense segment kind is not freespace, weld or dwell. prepare_program segment_too_short Dense segment contains fewer than two samples. prepare_program block_layout_invalid Dense block offsets, record lengths or segment metadata counts are inconsistent. prepare_program blob_length_mismatch Dense payload length differs from the declared block extents. prepare_program block_sha256_mismatch Dense segment bytes do not match their declared SHA-256 digest. prepare_program reserved_flags_set Dense sample sets a reserved process flag bit. prepare_program total_sample_count_mismatch Dense total sample count differs from its segment counts. prepare_program sample_count_overflow Dense source sample count exceeds the format capacity. prepare_program trajectory_digest_mismatch Dense content digest differs from the declared trajectory digest. prepare_program time_grid_invalid Dense sample clock is not on its declared positive time grid. prepare_program nonfinite_sample Dense time, position or velocity sample is not finite. prepare_program qd_limit_exceeded Dense supplied velocity exceeds its declared format limit. prepare_program torch_outside_weld Dense torch flag is set outside a weld segment. prepare_program q_step_exceeded Dense position step exceeds the format velocity allowance. prepare_program boundary_qd_nonzero Rejected dense segment endpoint velocity above the stationary boundary tolerance. prepare_program boundary_q_discontinuity Rejected dense segment position gap or accumulated normalisation correction above the boundary tolerance. prepare_program duration_mismatch Dense segment duration differs from its final sample clock. 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, recovery_status fault_persists At least one fault condition still prevents clearing. reset_fault recovery_observation_unavailable No matching native recovery observation arrived within 1 s. restore_anchor, reset_fault, recovery_status invalid_axis_mask The requested axis mask is empty or names an axis outside the configured group. restore_anchor anchor_missing No persisted anchor exists for the axis. restore_anchor anchor_identity_mismatch The persisted anchor was recorded under a different configuration digest, drive identity or home epoch. home, restore_anchor anchor_source_invalid The drive reports no valid absolute source for the axis. home, restore_anchor anchor_disagrees The current absolute source disagrees with the persisted anchor beyond the profile tolerance. 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, restore_anchor Cell I/O Reason Meaning Returned by io_not_configured named cell I/O is not configured. io_arm, io_disarm, prepare_trajectory, prepare_program, start_program io_not_armed Explicit io_arm is required before an ON intent. io_arm, io_disarm, prepare_trajectory, start_program io_fast_input_unsatisfied A cyclic fast contact is invalid or not satisfied. io_arm, io_disarm, prepare_trajectory, start_program io_readback_disagreement Independent physical feedback disagrees with commanded output. io_arm, io_disarm, prepare_trajectory, start_program io_torch_unqualified torch-class markers remain refused pending hardware qualification. io_arm, io_disarm, prepare_trajectory, prepare_program, start_program io_marker_late A process marker missed its one-cycle delivery bound. io_arm, io_disarm, prepare_trajectory, start_program io_exchange_lost Cell I/O has no current complete exchange. io_arm, io_disarm, prepare_trajectory, start_program io_marker_invalid A marker names an unknown output or has an invalid sample association. 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, jog_status, begin_jog, update_jog, end_jog jog_session_stale The jog session or its generation is not the current one. update_jog, end_jog jog_session_or_sequence_stale No open jog session for this generation, or the source sequence did not increase. update_jog jog_publisher_busy Another producer is publishing jog input at the same moment. update_jog jog_invalid_frame A binary jog frame could not be decoded. 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 jog_clock_unqualified Remote clock qualification is disabled (the --remote-jog-* flags are 0) or calibration is incomplete. update_jog jog_clock_invalid_budget_or_exchange A malformed calibration or Begin message, or an invalid timing budget. update_jog jog_clock_incarnation_mismatch The clock incarnation is empty or differs from the negotiated one. begin_jog, update_jog jog_clock_mapping_generation_mismatch The input was mapped with an out-of-date clock mapping. update_jog jog_clock_moved_backwards A source timestamp went backwards, or input predates the Begin sample. update_jog jog_clock_arithmetic_range Converting a jog timestamp would overflow. update_jog jog_clock_uncertainty_exceeded The calibrated offset interval is wider than --remote-jog-max-uncertainty-ns. update_jog jog_clock_calibration_expired The remote clock calibration is older than --remote-jog-calibration-max-age-ns. update_jog jog_clock_exchange_inconsistent The calibration timestamps are not causally consistent. update_jog jog_input_too_old The conservatively mapped input age exceeds the cell's input-age ceiling. update_jog jog_input_entirely_future The whole input interval lies in the host's future. update_jog jog_input_deadline_expired The input's deadline has already passed. update_jog Events, telemetry and resources Reason Meaning Returned by invalid_event_cursor Event cursor must be a single unsigned decimal uint64. subscribe_events event_cursor_ahead Cursor exceeds this adapter incarnation's newest event; obtain current status/incarnation before resuming. subscribe_events event_cursor_lost Event history was lost; reconcile status before resuming from a new cursor. subscribe_events. Client side: the C++ EventCursorLost exception. invalid_telemetry_cursor Telemetry after must be one unsigned decimal uint64 cursor. telemetry telemetry_cursor_ahead Telemetry cursor exceeds the current ring sequence; reconcile incarnation before restarting. 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 telemetry_unavailable A compatible live telemetry ring could not be observed. telemetry telemetry_label_invalid Telemetry label must contain 1..128 valid UTF-8 bytes. mark_telemetry resource_unknown Digest is malformed or absent from the loaded compiled resource set; HTTP 404. 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 "},{"title":"NATS subjects and streams","section":"Reference","url":"/docs/reference/nats-subjects","markdown":"/docs/reference/nats-subjects.md","description":"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.","headings":[{"id":"subject-map","text":"Subject map"},{"id":"observation-rt-natspublisher","text":"Observation: rt-natspublisher"},{"id":"streams","text":"Streams"},{"id":"status-payload","text":"Status payload"},{"id":"binary-telemetry","text":"Binary telemetry"},{"id":"commands-the-robot-command-subject","text":"Commands: the robot command subject"},{"id":"related-pages","text":"Related pages"}],"text":"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. 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. Tip Machine-readable. The subject map, streams and command tables below as JSON. Subject map <host>, <peer> and <cell> are deployment names, not fixed strings. <host> 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.<host>.status rt-natspublisher Displays, archives JSON robot.v4.rtcore.status.v1 None robot/v4/rtcore.<host>.telemetry.batch rt-natspublisher Displays, archives JSON robot.v4.rtcore.telemetry.batch.v1 None rosie/rt-core.<host>.telemetry.v1 rt-natspublisher Archives Binary TelemetryBatch None robot/v4/robot.<cell>.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.<peer>.status Cartesian motion server Controllers, displays JSON robot_v4_motion_server_cartesian_live_status_v1 None robot/v4/motion-server.<peer>.plan Cartesian motion server (self-test plan paths) — JSON None rt_core.status_subject, default <command subject>.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.<peer>.… or robot/v4/robot.<peer>.…. 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. 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 { \"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. 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.<host>.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. 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:<instance> 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 and Dense trajectory daemon. Related pages Architecture Motion paths and planning Ports, sockets and environment"},{"title":"Pendant controls","section":"Reference","url":"/docs/reference/pendant-controls","markdown":"/docs/reference/pendant-controls.md","description":"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.","headings":[{"id":"jog","text":"Jog"},{"id":"teach-arm-and-stop","text":"Teach, arm and stop"},{"id":"navigate","text":"Navigate"},{"id":"keyboard","text":"Keyboard"},{"id":"steam-input-layout","text":"Steam Input layout"},{"id":"related-pages","text":"Related pages"}],"text":"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. 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. 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. 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 Teach waypoints and moves Safety model"},{"title":"Ports, sockets and environment variables","section":"Reference","url":"/docs/reference/ports-and-environment","markdown":"/docs/reference/ports-and-environment.md","description":"Every TCP port, Unix socket and environment variable used by the RosieOS services, the dev stack and the offline programming launcher, with defaults.","headings":[{"id":"unix-sockets","text":"Unix sockets"},{"id":"tcp-ports","text":"TCP ports"},{"id":"dev-stack-dev-stacksh","text":"Dev stack (dev-stack.sh)"},{"id":"rt-control-and-installed-units","text":"rt-control and installed units"},{"id":"offline-programming-server","text":"Offline programming server"},{"id":"weld-planner","text":"Weld planner"},{"id":"virtual-pendant","text":"Virtual pendant"}],"text":"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, and rtctl flags in 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"},{"title":"Repository layout","section":"Contributing","url":"/docs/contributing/repo-layout","markdown":"/docs/contributing/repo-layout.md","description":"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.","headings":[{"id":"current","text":"Current components"},{"id":"inside-rt-core","text":"Inside rt-core/"},{"id":"generated","text":"Generated code"},{"id":"legacy","text":"Legacy folders"},{"id":"not-here","text":"Things that are not in this repository"}],"text":"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. 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, rt-control HTTP API robot_description/ data, Go, Python One directory per robot model, the Go module rosieos/robotdesc and the manifest tool Robot description motion-server/v1/ C++17 The Cartesian motion server robot-v4-cartesiand Cartesian motion server motion-server/joint-trajectory/v1/ C++17 The dense trajectory daemon Dense trajectory daemon offline-programming/v1/ Go; ui/ TypeScript The offline programming (OLP) server and browser app OLP HTTP API weld_planner/v1/ Python (pixi) Seam worker, CUDA weld motion planner and verifier, weldplan contracts Weld planner HTTP API tesseract/v1/, cadquery/v1/ Python (pixi) Planning and CAD environments used by the motion server and OLP Install the toolchain steamdeck/real/v5/ C++/Qt The Steam Deck teach pendant. Not qualified for real motion. 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 urdf/v1/ TypeScript URDF tooling; sphere_tool/ authors collision spheres Add a robot model dev-stack.sh Bash Starts the local simulated stack Run everything in simulation quality/ Node Lint, format and report runners Code style and quality tools/robot-stack-release/, releases/ Python, JSON Release bundling and release records Releasing .github/workflows/ YAML CI Building and testing 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. 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."},{"title":"Building and testing","section":"Contributing","url":"/docs/contributing/building-and-testing","markdown":"/docs/contributing/building-and-testing.md","description":"Build and test commands for each RosieOS component, the environment the rt-core tests expect, and what each CI workflow runs and gates.","headings":[{"id":"environment","text":"Test environment"},{"id":"rt-core","text":"rt-core"},{"id":"robot-description","text":"Robot descriptions"},{"id":"motion-servers","text":"Motion servers"},{"id":"olp","text":"Offline programming"},{"id":"simulation","text":"Simulation stack and virtual pendant"},{"id":"weld-planner","text":"Weld planner"},{"id":"pendants","text":"Pendants and deploy module"},{"id":"quality","text":"Quality checks"},{"id":"ci","text":"CI"}],"text":"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. A quick check of the controller and two of its consumers, on Linux or WSL: 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. 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 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. 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: 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 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. 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 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 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. Weld planner 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 covers the environments. Pendants and deploy module (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. Quality checks Formatting and lint runners live in quality/. See Code style and quality. 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."},{"title":"Code style and quality checks","section":"Contributing","url":"/docs/contributing/code-style","markdown":"/docs/contributing/code-style.md","description":"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.","headings":[{"id":"gate","text":"Gate commands"},{"id":"report","text":"Report commands"},{"id":"conventions","text":"Conventions"},{"id":"constants","text":"Every constant carries its evidence"},{"id":"units","text":"Units are in the name"},{"id":"docstrings","text":"Docstrings state the contract"},{"id":"errors","text":"Fail closed, with a named reason"},{"id":"tests","text":"Tests assert absolute units"},{"id":"generated","text":"Generated code is never edited"},{"id":"formatting","text":"Keep formatting separate"}],"text":"RosieOS has one quality runner, quality/quality.mjs, plus conventions that the code follows everywhere. Run the gate before you open a pull request: 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 <compile-db-dir> <source> 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.gogo // 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. 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.gogo // 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. 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."},{"title":"Releasing","section":"Contributing","url":"/docs/contributing/releasing","markdown":"/docs/contributing/releasing.md","description":"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.","headings":[{"id":"runtime-package","text":"rt-core runtime package"},{"id":"package-contents","text":"Package contents"},{"id":"manifest","text":"The manifest"},{"id":"component","text":"Component archive"},{"id":"options","text":"Options"},{"id":"verify","text":"Verify"},{"id":"robot-stack","text":"Robot-stack bundles"},{"id":"claims","text":"What a release claims"}],"text":"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: 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. Component archive Beside the package, DESTINATION.components/ holds what a release publishes: File Contents rosie-rt-core-<version>-linux-<arch>.tar.gz The runtime archive (<arch> 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-<version> Where the IgH COPYING files are. GO go Go command used to build rt-package. Verify 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: uv run --no-project python tools/robot-stack-release/bundle.py package \\ --spec releases/robot-stack/<release>/release.json --repo . --inputs <artifacts-dir> --bundle <new-bundle-dir> uv run --no-project python tools/robot-stack-release/bundle.py verify \\ --spec releases/robot-stack/<release>/release.json --bundle <bundle-dir> uv run --no-project python tools/robot-stack-release/bundle.py archives \\ --spec releases/robot-stack/<release>/release.json --bundle <bundle-dir> --output <dir> 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: <reason> 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: 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."},{"title":"Licence","section":"Contributing","url":"/docs/contributing/license","markdown":"/docs/contributing/license.md","description":"RosieOS is open source under the Apache License, Version 2.0.","headings":[],"text":"RosieOS is licensed under the Apache License, Version 2.0. The source is on GitHub at 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."}]