# Connect to a cell and run a program

> Commission a cell for offline programming, describe it in the cell catalogue, select it, and Home, Arm, Load, Play and Stop a planned weld program, with the checks OLP makes at each step and how to read its refusals.

URL: https://advancedmetalresearch.com/docs/guides/connect-a-cell
Section: RosieOS docs / Guides
Last updated: 2026-10-10

This guide takes the OLP server from offline work to a real cell: you describe the cell in a catalogue, select it, then Home, Arm, Load and Play a planned weld program. Each step shows the UI action and the HTTP call behind it, so you can script it or debug it.

Try the whole sequence on the simulated cell first. The dev stack adds a `local-simulation` cell to the catalogue for you. See [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation).

> [!WARNING] **Energised motion.** This moves the robot. RosieOS has no software E-stop: keep the hardware E-stop within reach and clear the cell before you arm. Jog, moves and Home are checked against joint limits only, not against collisions. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model).

## Before you start

- The cell host runs `rosie-rt-core` and `rt-control` with the mutual-TLS listener enabled. See [Install rt-core on a cell host](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host) and [Remote access (mTLS)](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#enable-the-listener).
- You have a client certificate for OLP from the cell's PKI, issued with `remote-pki.sh issue-client`. It carries exactly one `rosie-pair:<pair_id>` URI, and OLP refuses the connection if that pair differs from the catalogue's binding (`session_principal_mismatch`). See [Provision certificates](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#provision-certificates).
- The weld planner is running and OLP can reach it (`OFFLINE_PROGRAMMING_WELD_PLANNER_MOTION_ORIGIN`). Load fetches programs from it.
- The hardware E-stop has been tested this session.

## 1. Commission the cell in the repository

OLP only drives a cell whose deployment manifest is in the repository, at `rt-core/config/cells/<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](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners#cell-config) describes every field.

rt-core/config/cells/cell-a.json (native node):

```json
{
  "rt_core": {
    "machine_config": "rt-core/config/machines/cell-a.json",
    "configuration_sha256": "<64 hex from rtctl compile>",
    "pair_id": "cell-a",
    "pair_revision": 1,
    "remote_listen": "127.0.0.1:8443"
  }
}
```

Check it with the repository validator:

```bash
cd rt-core
go test -count=1 ./config/cells -run TestRepositoryCompatibility
```

> [!IMPORTANT] In this release OLP resolves only the cell IDs it knows by name. The list is `cellManifestName` in `offline-programming/v1/internal/targets/cells.go`. To add a cell, add its manifest and add its ID to that function. An ID that is not listed fails with `cell_id_unknown`.

## 2. Write the cell catalogue

The catalogue is a JSON file, schema `offline-programming.cell-catalogue.v1`, that lists the cells the operator can choose from. Unknown fields are refused.

olp-cells.json:

```json
{
  "schema": "offline-programming.cell-catalogue.v1",
  "cells": [
    {
      "cell_id": "cell-a",
      "label": "Cell A",
      "role": "WELD CELL",
      "models": ["rosie_1400_v3"],
      "requested_mode": "real",
      "execution": {
        "address": "https://127.0.0.1:8443",
        "ca_file": "pki/ca.pem",
        "certificate_file": "pki/clients/olp-1.pem",
        "key_file": "pki/clients/olp-1-key.pem",
        "binding": {"pair_id": "cell-a", "revision": 1,
                    "configuration_sha256": "<64 hex from rtctl compile>"},
        "axis_mask": 511,
        "expected_backend": "ethercat"
      }
    }
  ]
}
```

| Field | Required | Rule |
|---|---|---|
| `cell_id` | yes | Unique. Must be a commissioned cell (step 1). |
| `label`, `role` | yes | Non-empty. Shown in the Cells dialog. |
| `models` | yes | Exactly one known model: `rosie_1400_v3`, `rosie_1420_v1`, `bench_one_motor_1to1` or `bench_nine_motors_1to1`. It must be the model the manifest's machine config compiles to. |
| `requested_mode` | yes | `real` or `simulation`. At selection it must match what the cell reports: `real` for an `ethercat` backend, `simulation` for `simulation`. |
| `execution.address` | yes | `https://host:port`, no path or query. Must equal `https://` + the manifest's `remote_listen`. |
| `execution.ca_file`, `certificate_file`, `key_file` | yes | PEM files. Relative paths resolve from the catalogue's directory. They must exist. |
| `execution.binding.pair_id`, `revision` | yes | Must equal the manifest's `pair_id` and `pair_revision` |
| `execution.binding.configuration_sha256` | no | If given, must equal the compiled digest. OLP fills it in from the compile either way. |
| `execution.axis_mask` | yes | The axes OLP commands, 1–511, J1 = bit 0. Home, Arm and Load address exactly these axes. |
| `execution.expected_backend` | yes | `ethercat` or `simulation`. Describe must report the same. |

Because the address must match `remote_listen`, a cell pinned to `127.0.0.1:8443` is reachable only from the cell host itself or through a forwarded port.

A simulated cell served on the same machine uses a local entry instead. It needs the local binding file in `OFFLINE_PROGRAMMING_RT_CORE_CONFIG` (the dev stack writes it):

```json
{"cell_id": "local-simulation", "label": "Local simulation", "role": "LOCAL SIMULATION",
 "models": ["rosie_1400_v3"], "requested_mode": "simulation", "execution": {"local": true}}
```

## 3. Start OLP with the catalogue

```bash
export OFFLINE_PROGRAMMING_CELLS=/path/to/olp-cells.json
export OFFLINE_PROGRAMMING_WELD_PLANNER_MOTION_ORIGIN=http://127.0.0.1:8796
bash offline-programming/v1/start-offline-programming.sh
```

With the dev stack, set `DEV_STACK_OLP_CELLS=/path/to/olp-cells.json` instead. Its cells are added after `local-simulation`, and relative credential paths are resolved against your file.

The server checks the whole catalogue at startup and refuses to start on a bad entry, naming the rule: for example `cell_endpoint_mismatch`, `cell_pair_mismatch`, `cell_configuration_mismatch`, `cell_credentials_missing` or `cell_model_mismatch`. A cell whose manifest names no machine config stays listed but unavailable, with `cell_configuration_missing`. On success it prints:

```text
dense execution: choose a machine; joint and Cartesian jog share its armed control session
```

## 4. Select the cell

In the UI, open **Cells** and choose the cell. Over HTTP:

```bash
OLP=http://127.0.0.1:8794/api/offline-programming/v1
curl -s $OLP/targets | jq '.targets[] | {cell_id, backend, simulation, reason}'
curl -s -X POST $OLP/targets/select -d '{"cell_id":"cell-a","model_id":"rosie_1400_v3"}'
```

Selection connects to `rt-control`, runs Describe and checks it against the catalogue: backend, configuration digest, contract version, axis count. It takes no authority. If another cell was selected, OLP stops and releases it first.

Then confirm you have the machine you expect:

```bash
curl -s $OLP/dense-execution/status | jq '{target, robot_cell}'
curl -s $OLP/dense-execution/capabilities | jq '.rt_core.description | {backend, configuration_sha256}'
```

`target.backend` must say `ethercat` for a real cell. `robot_cell.valid` must be `true`, and `robot_cell.model_id` and `robot_description_sha256` identify the robot description the machine was compiled with.

Every later request that can move the robot must carry `X-RT-Target-Generation: <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](https://advancedmetalresearch.com/docs/apis/olp-http#target-fencing). For the commands below:

```bash
STATUS=$(curl -s $OLP/dense-execution/status)
FENCE=(-H "X-RT-Target-Generation: $(echo "$STATUS" | jq -r .target.selection_generation)"
       -H "X-RT-Target-Cell: $(echo "$STATUS" | jq -r .target.cell_id)")
```

## 5. Home

Clear the cell. In the UI, press **Home**. Over HTTP, with `FENCE` set to the two headers:

```bash
curl -s -X POST $OLP/dense-execution/home "${FENCE[@]}"
```

Home acquires the `rt-control` lease, runs native Home on every axis in the catalogue's `axis_mask`, and returns when Home is valid on all of them. Send `{"axes": [0, 1]}` to home only some axes. Home moves the robot but never arms it.

## 6. Arm

```bash
curl -s -X POST $OLP/dense-execution/arm "${FENCE[@]}" -d '{"armed": true}'
```

Arm enables and arms the drives and waits until every configured axis reports `ready`. If an execution fault is latched and a reset clears it, Arm resets it first. From now on, while OLP holds the lease:

- Send a heartbeat at least every 5 s: `POST /dense-execution/heartbeat` with `{"session_id": "<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](https://advancedmetalresearch.com/docs/guides/offline-programming).

Then Load it. In the UI, choose the **Dry run · process outputs off** run mode and press **Load**. Over HTTP, send the plan identity from the plan response:

```bash
curl -s -X POST $OLP/dense-execution/load "${FENCE[@]}" -d '{
  "trajectory_digest": "sha256:…", "plan_id": "bracket_fillet:3f1c0a9d2b7e",
  "program_id": "bracket_fillet", "program_digest": "sha256:…",
  "manifest_revision": 1, "plan_revision": 3, "dry_run": true}'
```

Load refuses before any byte reaches the robot unless:

1. the `.rdt` exists in the planner's store, which it only does if every segment passed the verifier
2. its header matches the identity you sent
3. the plan's robot description and cell calibration match the machine
4. every configured axis has valid Home

It then uploads the program with `prepare_program`, and `rt-control` checks positions, velocities and continuity against the live machine. The answer carries a `session_id` and the trajectory's segments.

`dry_run: true` removes the torch bits first. A program that still needs process outputs is refused: torch output is not supported in this release. See [Process I/O and sensing](https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing).

## 8. Play

Replay the program in simulation first if you have not: **Simulate** in OLP plays the exact bytes in the viewer or on the local simulator. Then:

```bash
curl -s -X POST $OLP/dense-execution/play "${FENCE[@]}" -d '{"session_id": "ds-9b1e4f07a2c3"}'
```

Play waits until the machine is armed and ready, then starts the program. Keep the heartbeat going. Watch `status.session.state` (`playing`) and `status.rt_core.status` for execution progress. The first point may start with a short alignment ramp from the held position; see [Starting from rest](https://advancedmetalresearch.com/docs/concepts/motion-and-planning#starting-from-rest).

## 9. Stop and disconnect

Press **STOP** in the UI, or:

```bash
curl -s -X POST $OLP/dense-execution/stop
```

Stop needs no fence and no body. It inhibits outputs at once, then releases the lease. Retry it until it answers `"ok": true`. If a Stop or Release cannot be confirmed, OLP blocks further motion with `rt_core_inhibited` until a Stop succeeds.

A Stop receipt is not proof of standstill. Watch the robot. The STOP button is software; the hardware E-stop is the emergency stop.

To give up the cell, disarm (`{"armed": false}` on `/arm`, which runs Stop and Release) and clear the selection with `POST /dense-execution/target` and `{"cell": ""}`.

## When something is refused

| Code | Step | What to do |
|---|---|---|
| `cell_id_unknown` | 3, 4 | The ID is not a commissioned cell known to OLP. See step 1. |
| `cell_endpoint_mismatch`, `cell_pair_mismatch`, `cell_pair_revision_mismatch`, `cell_configuration_mismatch` | 3, 4 | The catalogue disagrees with the manifest, or the running `rt-control` serves another configuration. Recompile and update the pins. |
| `cell_mode_mismatch`, `cell_backend_mismatch` | 4 | The cell reports `simulation` where you asked for `real`, or the reverse |
| `cell_unreachable`, `rt_core_transport_lost` | 4 | OLP cannot reach the listener. Check the address, the port forward and the certificates. |
| `session_principal_mismatch` | 4 | The client certificate's pair differs from the binding |
| `target_changed` | 5–8 | Another client selected a cell. Read the status again and use the new generation. |
| `control_already_owned` | 5–8 | Another controller (a pendant, a motion server, another OLP) holds `rt-control`. Release it there. |
| `home_required` | 7 | Home the machine first |
| `robot_cell_mismatch`, `robot_cell_missing` | 7 | The plan was made for another robot description or cell calibration, or before the cell was checked. Replan against this cell. |
| `robot_cell_unavailable` | 7 | The machine cannot say which robot it is. Check its compiled configuration. |
| `dense_blob_not_found` | 7 | The planner's store no longer has that digest. Replan. |
| `dense_identity_mismatch` | 7 | The identity you sent differs from the `.rdt` header |
| `program_identity_or_process_mismatch` | 7 | The program still needs process outputs. Load with `dry_run: true`. |
| `native_limit_exceeded`, `native_segment_rate_exceeded`, `outside_limits_outward` | 7 | The program leaves the machine's limits. `limit_violation` in the answer names the segment, sample and axis. |
| `not_ready`, `jog_not_ready` | 6–8 | An axis is not ready. `rt_core.status` shows the first failing gate per axis. |
| `ui_heartbeat_lost` | 6–8 | The client stopped sending heartbeats. Arm again. |
| `rt_core_inhibited` | any | Retry Stop until it succeeds |

All codes are in the [Offline programming HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http#error-codes) and [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes).

## Related pages

- [Safety model](https://advancedmetalresearch.com/docs/get-started/safety-model)
- [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning#olp)
- [Offline programming HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http)
- [Cells, machines and positioners](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners)
- [Dense trajectory (.rdt) format](https://advancedmetalresearch.com/docs/reference/rdt-format#robot-cell)

## Sources

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

- `offline-programming/v1/internal/targets/cells.go:20-237`
- `offline-programming/v1/internal/targets/picker.go:16-250`
- `offline-programming/v1/internal/targets/descriptors.go:69-102`
- `offline-programming/v1/internal/denseexec/rt_core.go:30-44,148-173,189-239,399-504,556-616,660-825,827-871,1024-1030,1292-1406,1428-1518`
- `offline-programming/v1/internal/denseexec/robot.go:12-104`
- `offline-programming/v1/dense_execution.go:24-63,115-305`
- `offline-programming/v1/cell_targets.go:10-47`
- `offline-programming/v1/dense_cells.go:37-57`
- `offline-programming/v1/main.go:311-346,896-918`
- `offline-programming/v1/start-offline-programming.sh:649-650`
- `motion-server/v1/local-rt-core.sh:103-127`
- `rt-core/config/templates/cell.json:8-18`
- `weld_planner/v1/python/weldplan/dense_joint_trajectory.py:726`
