# Build and install the pendant

> Build the v5 Steam Deck teach pendant, test it, run it on a workstation, package and deploy it to a Deck with hash checks, add it as a Steam shortcut with its controller layout, and write the per-Deck site files.

URL: https://advancedmetalresearch.com/docs/guides/install-the-pendant
Section: RosieOS docs / Guides
Last updated: 2026-10-10

The v5 pendant ships as one directory, `RosiePendantV5`, that you build on a Linux workstation and copy to a Steam Deck. It holds the native app, OLP's program logic bundled for Qt's JavaScript engine, the OLP server it runs headless on the Deck, and the source trees that server reads. To use it once it is installed, see [Program from the Steam Deck pendant](https://advancedmetalresearch.com/docs/guides/program-from-the-pendant).

> [!CAUTION] **Not qualified for real motion.** The v5 pendant has not been qualified on physical stick, trigger and rear-button input, real robot motion, network faults, or a full plan, Load and Play on the Deck, and no CI workflow builds it. Its hold-to-enable trigger is a software deadman, not a safety-rated enabling device. Keep the cell's hardware E-stop within reach whenever the drives are powered, and use the pendant in simulation until the cell owner has qualified it. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model).

## Before you start

On the build workstation, Ubuntu 22.04 or WSL2:

```bash
sudo apt install g++ make pkg-config qtbase5-dev libassimp-dev curl
```

You also need:

- **Qt5Qml.** Install `qtdeclarative5-dev`, or, without root, extract it to a sysroot (below).
- **Node 22** and the OLP UI's packages, for esbuild and OLP's TypeScript sources.
- **Go 1.26.2**, to build the OLP server for the Deck.
- The C++17 toolchain for `motion-server/v1`, which the build uses for the pendant's Cartesian solver.
- `ssh` access to the Deck, for deploying.

See [Install the toolchain](https://advancedmetalresearch.com/docs/get-started/installation).

### Qt5Qml without root

The Makefile looks for a sysroot at `~/.rosie/qt5qml-sysroot`, or wherever `QML_SYSROOT` points:

```bash
mkdir -p ~/.rosie/qt5qml-sysroot/debs && cd ~/.rosie/qt5qml-sysroot/debs
apt-get download qtdeclarative5-dev libqt5qml5 libqt5qmlmodels5 libqt5quick5
for d in *.deb; do dpkg-deb -x "$d" ..; done
```

## Build

From the repository root:

```bash
(cd offline-programming/v1/ui && npm ci)
make -C steamdeck/real/v5 -j4 all
```

This builds three things in `steamdeck/real/v5/build/`:

| File | What it is |
|---|---|
| `rosie-pendant-v5` | The native Qt 5.15 app |
| `olp-core.js` | OLP's program and machine logic, from `offline-programming/v1/ui/src` and `steamdeck/real/v5/olp-core`, bundled by esbuild as ES2016 for Qt's engine |
| `robot-v4-cartesiand` | The Cartesian solver the OLP server uses, built from `motion-server/v1` |

The bundle depends on every non-test `.ts` file under OLP's `ui/src`, so a change to OLP rebuilds it. That is how the pendant and the desktop stay one implementation.

## Test

```bash
make -C steamdeck/real/v5 check
```

| Check | What it covers |
|---|---|
| `session.test.ts`, `machine.test.ts` | The pendant's session and machine logic, under Node |
| `core-check` | The built `olp-core.js` inside Qt's JavaScript engine, as the app runs it |
| `capture-check` | The app's C++ capture TCP pose against OLP's own poses, for both robot descriptions |
| `view-check` | Offscreen: the input interlock, display kinematics, description identity and both robots' mesh sets |

## Run it on the workstation

Start an OLP server on port 8794 (the [quickstart](https://advancedmetalresearch.com/docs/get-started/quickstart) does), then:

```bash
cd steamdeck/real/v5
LD_LIBRARY_PATH=~/.rosie/qt5qml-sysroot/usr/lib/x86_64-linux-gnu \
  build/rosie-pendant-v5 --windowed --robots ../../../robot_description/robots
```

Leave out `LD_LIBRARY_PATH` if Qt5Qml is installed system-wide.

| Option | Default | Description |
|---|---|---|
| `--olp <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

```bash
steamdeck/real/v5/package.sh                       # the app and the OLP tree
steamdeck/real/v5/package.sh --with-tesseract-env  # also the Tesseract pixi environment (about 1 GB)
```

`package.sh` runs `make all`, builds the OLP server for Linux x86-64, and stages `steamdeck/real/v5/build/package/RosiePendantV5`:

| Path | Contents |
|---|---|
| `run.sh`, `start-olp.sh`, `controller.vdf` | The launcher, the OLP server's start script, and the Steam Input layout |
| `bin/` | `rosie-pendant-v5`, `olp-core.js`, `robot-v4-cartesiand` |
| `lib/` | `libassimp.so.5`, `libdraco.so.4` and `libminizip.so.1` from the build host, with their Debian copyright files. SteamOS ships Qt 5.15 but not Assimp. |
| `olp/` | The files of `offline-programming/v1`, `motion-server/v1`, `robot_description`, `tesseract/v1`, `weld_planner/v1`, `cadquery/v1`, and `rt-core/config/{cells,machines,drives}`, which the server needs to resolve remote cells. Also the server binary and a stub `ui/dist/index.html`, because `serve` refuses to start without one. |
| `SHA256SUMS` | A hash of every file except the Tesseract environment |

Pass `--with-tesseract-env` on the first deploy, and again whenever `tesseract/v1/pixi.lock` changes. The OLP server needs that environment.

`package.sh` adds the `~/.rosie` toolchain directories to the front of `PATH`. If your Go and Node live elsewhere, have them on `PATH` already.

The package never contains site files: the cell catalogue, the rt-core binding, `planner.env` or credentials.

## Deploy

Exit the pendant on the Deck first. Then, with your Deck's address:

```bash
steamdeck/real/v5/deploy.sh deck@rosie-deck.local
```

The optional second argument is the directory on the Deck. It defaults to `devkit-game/RosiePendantV5` in the `deck` user's home. `deploy.sh`:

1. refuses while the pendant or its `rosie-v5-olp` unit is running, so nothing that holds a machine grant is replaced underneath it
2. removes the files of the retired browser kiosk from the app directory
3. copies the package over `ssh`
4. checks every staged hash on the Deck with `sha256sum -c SHA256SUMS`, and prints `deploy: all staged hashes match`

Site files on the Deck are left alone.

## Add it to Steam

1. Add `run.sh` as a non-Steam game (or devkit) shortcut.
2. Set `controller.vdf` as that shortcut's Steam Input layout. It maps the rear buttons L4, L5, R4 and R5 to F1, F2, F3 and F4, makes the right trackpad an absolute mouse, and makes the left trackpad scroll. See [Pendant controls](https://advancedmetalresearch.com/docs/reference/pendant-controls#steam-input-layout).

`run.sh`:

- starts `start-olp.sh` as the systemd user unit `rosie-v5-olp`, unless it is already running
- waits up to 30 s for `http://127.0.0.1:8794/api/offline-programming/v1/health`
- starts the app full screen, with Steam's `LD_PRELOAD` and `LD_LIBRARY_PATH` removed, `LD_LIBRARY_PATH` set to the package's `lib/`, `QT_QPA_PLATFORM=xcb` and a scale factor of 1
- stops the unit when the app exits. The server's shutdown stops any machine it armed before it releases control.

Arguments to `run.sh` go to the app. The server's log is `journalctl --user -u rosie-v5-olp`.

## Site files

Put these beside `run.sh` on each Deck. They are never committed and never packaged:

| File | Read by | Contents |
|---|---|---|
| `olp-cells.json` | OLP server, as `OFFLINE_PROGRAMMING_CELLS` | The cell catalogue: which cells the CELL page lists. See [Connect to a cell and run a program](https://advancedmetalresearch.com/docs/guides/connect-a-cell). |
| `olp-rt-core.json` | OLP server, as `OFFLINE_PROGRAMMING_RT_CORE_CONFIG` | The server-only rt-core backend configuration, for a local simulation entry |
| `planner.env` | `start-olp.sh`, if present | Where the weld planner is, and how to run the seam worker |
| client credentials (`*.pem`) | OLP server, through the catalogue | The mutual-TLS credentials for remote cells, named by the catalogue. See [Remote access (mTLS)](https://advancedmetalresearch.com/docs/apis/remote-access-mtls). |

`start-olp.sh` also sets `OFFLINE_PROGRAMMING_EXECUTION_BACKEND=rt_core`, `OFFLINE_PROGRAMMING_JOG_BACKEND=rt_core` and the solver path, and starts `offline-programming-linux serve --listen 127.0.0.1:8794 --enable-local-simulator`. It leaves `ROSIE_RT_*` unset on purpose: OLP's disconnected local jog would otherwise take the simulation adapter's only controller slot and lock the pendant out.

### planner.env

Planning and Load both need a weld planner on a workstation the Deck can reach. Every plan also runs the seam worker once on the Deck, to pack the `.weldplan`. The package ships the seam worker's sources but no pixi environment. Packing uses only the Python standard library, so the Deck's system Python can run it:

planner.env:

```bash
OFFLINE_PROGRAMMING_WELD_PLANNER_MOTION_ORIGIN=http://planner.local:8796
SEAM_WORKER_PYTHON=/usr/bin/python3
# Spare workers preload OpenCASCADE, which the system Python does not have.
SEAM_WORKER_SPARES=0
```

Without an origin, the RUN page shows the server's refusal and nothing is planned or loaded. Without `SEAM_WORKER_PYTHON` and without pixi on the Deck, planning fails because the seam worker cannot start. The alternative is pixi on the Deck and `pixi install -e default` in `olp/weld_planner/v1`.

> [!WARNING] The weld planner must listen on the network for the Deck to reach it, and it has no authentication. Firewall port 8796 so that only the Deck and your own machine can reach it. See [Bind it safely](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner#bind).

A complete plan, Load and Play from the Deck against the weld planner has not been qualified.

## Related pages

- [Program from the Steam Deck pendant](https://advancedmetalresearch.com/docs/guides/program-from-the-pendant)
- [Pendant controls](https://advancedmetalresearch.com/docs/reference/pendant-controls)
- [Run the weld planner](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner)
- [Use the virtual pendant](https://advancedmetalresearch.com/docs/guides/virtual-pendant)

## Sources

Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS):

- `steamdeck/real/v5/Makefile:1-56`
- `steamdeck/real/v5/package.sh:1-86`
- `steamdeck/real/v5/deploy.sh:1-26`
- `steamdeck/real/v5/run.sh:1-37`
- `steamdeck/real/v5/start-olp.sh:1-28`
- `steamdeck/real/v5/controller.vdf`
- `steamdeck/real/v5/src/main.cpp:11-24`
- `steamdeck/real/v5/olp-core/run.ts:102-113`
- `offline-programming/v1/main.go:301-312`
- `offline-programming/v1/internal/seam/worker.go:152-178,483-495`
- `offline-programming/v1/internal/seam/spares.go:152`
- `offline-programming/v1/internal/denseexec/session.go:236-244`
- `weld_planner/v1/python/seam_worker/workers.py:548-623`
- `weld_planner/v1/python/weldplan/urdf.py:51-56`
- `weld_planner/v1/python/weldplan/program_v2.py:51-60`
