# Install rt-core on a cell host

> Provision a Linux cell host for rt-core, build and install a runtime package with inactive systemd units, bind it to a compiled machine configuration, optionally set up mutual TLS, check the host, start the services and roll back.

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

This guide takes a PREEMPT_RT Linux host from a bare OS to running `rosie-rt-core` and `rt-control` as systemd services against real drives. It uses the scripts in `rt-core/host/` and `rt-core/tools/`.

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

Installing is owner-operated work. The scripts refuse to start anything on their own: every install leaves the units inactive, and you start them when the cell is ready. Passing every check in this guide does not qualify the cell for powered motion. Hardware bring-up and qualification are manual steps for the cell owner.

To try rt-core without hardware, use [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation) instead.

## What you need

- A cell host that matches a supported foundation profile (below), with its EtherCAT NIC wired to the drives.
- A **live** machine config for this cell (`"backend": "live"`) that compiles cleanly. See [Configuration files](https://advancedmetalresearch.com/docs/reference/configuration).
- A deployment pair id and a positive pair revision for this cell.
- A build machine of the **same CPU architecture** as the host, with the [toolchain](https://advancedmetalresearch.com/docs/get-started/installation), to build the package.
- Root on the host, and network access from it to the IgH EtherCAT source archive during provisioning.

## 1. Provision the EtherCAT foundation

`host/ethercat-foundation.sh` installs the IgH EtherCAT master for the running kernel, pinned by version and archive SHA-256, and configures its device access. It accepts only these host profiles:

| Profile | OS | Kernel | IgH |
|---|---|---|---|
| `revpi-debian12` | Debian 12 | `*-revpi*-rpi-v8` | 1.6.9 |
| `rpi5-debian13` | Debian 13 | `6.18.*-rpi-v8-rt` | 1.6.12 |

Any other host is refused. On other hardware you provision PREEMPT_RT and IgH yourself, and `rtctl hostcheck` (step 5) checks the result.

The script needs these variables. It refuses to run without `NO_REAL_MOVEMENT=1`.

| Variable | Description |
|---|---|
| `NO_REAL_MOVEMENT` | Must be `1`. |
| `ROSIE_RT_FOUNDATION_ETHERCAT_INTERFACE` | The EtherCAT NIC's interface name. |
| `ROSIE_RT_FOUNDATION_ETHERCAT_MAC`, `…_ETHERCAT_PERMANENT_MAC` | Its current and permanent MAC. |
| `ROSIE_RT_FOUNDATION_UPLINK_INTERFACE`, `…_UPLINK_MAC`, `…_UPLINK_PERMANENT_MAC` | The host's other (uplink) NIC, so the two are never confused. |
| `ROSIE_RT_FOUNDATION_EXPECTED_SLAVES` | Number of EtherCAT slaves expected on the bus. |
| `ROSIE_RT_FOUNDATION_RECEIPT_PATH` | Where to write the foundation receipt. |
| `ROSIE_RT_FOUNDATION_PROFILE` | Optional. Must match the detected profile. |
| `ROSIE_RT_ETHERCAT_GROUP` | Optional. Group given access to `/dev/EtherCAT0`. Default `ethercat`. |

Run it in three stages: `inspect` changes nothing, `install` provisions, `verify` checks the result and runs `rtctl hostcheck`.

```bash
export NO_REAL_MOVEMENT=1 \
  ROSIE_RT_FOUNDATION_ETHERCAT_INTERFACE=eth1 \
  ROSIE_RT_FOUNDATION_ETHERCAT_MAC=<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:

```bash
cd rt-core
bash tools/build-igh-userlib.sh                 # 1.6.9 by default; pass 1.6.12 for rpi5-debian13
export PKG_CONFIG_PATH="$PWD/build/deps/igh-prefix/lib/pkgconfig"
make live control
bash tools/package-runtime.sh /tmp/rt-package
# package status=staged backend=live
# component status=staged output=/tmp/rt-package.components
bash tools/verify-package.sh /tmp/rt-package
```

The package holds `bin/` (`rosie-rt-core`, `rt-control`, `rtctl`, `rt-package`, and `rt-natspublisher` if built), `lib/libethercat.so.1` with its licences, `host/` (the install scripts and the three unit files), `config/`, every robot description's registered files, and `rt_package.json`, the manifest that `verify-package.sh` checks. [Releasing](https://advancedmetalresearch.com/docs/contributing/releasing) describes the package and its companion archive.

The unit files in the package point at `/opt/rosie-rt-core/current`. Set `ROSIE_RT_PACKAGE_SLOT_ROOT` before packaging to use another absolute path (not under `/home`, `/root` or `/run/user`).

Copy `/tmp/rt-package` to the host.

## 3. Install the package, inactive

```bash
sudo bash /tmp/rt-package/host/install.sh --from-package /tmp/rt-package --live
# Installed. No units were started.
```

This:

1. verifies the package, and with `--live` refuses a simulation package
2. creates the groups `rosie-rt`, `rosie-ctl`, `rosie-rt-clients` and the device group, and the system users `rosie-rt` (the core) and `rosie-ctl` (the adapter), with no home and no login shell
3. copies the package to `/opt/rosie-rt-core/<git-sha>/` and points `/opt/rosie-rt-core/current` at it, keeping the old target as `previous`
4. installs `rosie-rt-core.service`, `rosie-rt-control.service` and `rosie-rt-natspublisher.service` in `/etc/systemd/system/`, and runs `systemctl daemon-reload`

It refuses to replace a stored release with different bytes under the same git identity. Nothing is enabled or started, and there is no deployment binding yet, so the units cannot start: each has `ConditionPathExists=/etc/rosie-rt-core/control.env`.

To configure the optional NATS observer, set `ROSIE_RT_NATS_URL`, `ROSIE_RT_NATS_HOST` and `ROSIE_RT_NATS_CADENCE` when you run `install.sh`. It then writes `/etc/rosie-rt-core/natspublisher.env`.

## 4. Set up remote access (optional)

Skip this if every client runs on the cell host and uses the local socket. For remote clients, `host/remote-pki.sh` creates one CA per deployment pair. Run it after step 3, because it needs the `rosie-ctl` group. Keep the PKI directory outside the repository.

```bash
export ROSIE_RT_REMOTE_SERVER_SAN="DNS:rosie.local,DNS:localhost,IP:127.0.0.1"
sudo -E bash /opt/rosie-rt-core/current/host/remote-pki.sh init /var/lib/rosie-rt-pki/cell-a cell-a
sudo bash /opt/rosie-rt-core/current/host/remote-pki.sh issue-client /var/lib/rosie-rt-pki/cell-a pendant-1
```

The identity given to `init` is the pair id and must match step 5. `issue-client` writes `clients/<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)](https://advancedmetalresearch.com/docs/apis/remote-access-mtls).

> [!WARNING] **The CRL expires after 7 days.** Every `init`, `issue-client` and `revoke` regenerates `crl.pem` with a 7-day validity, and `remote-pki.sh` has no refresh command. rt-control checks the CRL on every handshake and refuses all remote connections once it has expired (`remote CRL not current`). Worse, if the CRL has already expired when rt-control starts, rt-control exits at startup, which also takes the local control socket down. Reissue the CRL before it expires, as below.

To reissue the CRL, run the same command the script ends with, in the PKI directory, and publish the result where rt-control reads it:

```bash
cd /var/lib/rosie-rt-pki/cell-a
sudo openssl ca -batch -config ca.cnf -gencrl -out crl.pem
sudo install -m 0640 -o root -g rosie-ctl crl.pem /etc/rosie-rt-core/pki/crl.pem
```

rt-control reloads a changed CRL before each full handshake, so no restart is needed. Run this on a schedule shorter than 7 days.

## 5. Bind the deployment

Put the machine config on the host (keeping a copy at `/etc/rosie-rt-core/machine.json` lets `hostcheck` find it later), then compile it into the installed binding:

```bash
sudo install -m 0644 machine.json /etc/rosie-rt-core/machine.json
sudo bash /opt/rosie-rt-core/current/host/generate-control-env.sh \
  --remote-pki /var/lib/rosie-rt-pki/cell-a \
  /etc/rosie-rt-core/machine.json cell-a 1
# configuration_sha256=<64 hex>
```

Drop `--remote-pki` if you skipped step 4. The script runs [`rtctl control-env`](/docs/reference/rtctl#control-env), which refuses any machine config that is not `live`. Only if compilation succeeds does it atomically replace:

- `/etc/rosie-rt-core/control.env` (mode 0640, group `rosie-ctl`), with the pair, the configuration digest, the core's arguments and `ROSIE_RT_REMOTE_ARGS`
- `/etc/rosie-rt-core/compiled/<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](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners#cell-config).

> [!CAUTION] At this revision the installer copies only `axes.conf` and `argv.json` into the compiled directory, and that directory is readable by the core's group only. `control.env` also points rt-control at it through `ROSIE_RT_COMPILED_CONFIG`, and rt-control reads `resources.json` and `configuration.identity` from there when it starts. Check that `rosie-rt-control` starts in step 7 before you rely on an installed cell.

## 6. Check the host

```bash
sudo /opt/rosie-rt-core/current/bin/rtctl hostcheck --config /etc/rosie-rt-core/machine.json
sudo /opt/rosie-rt-core/current/bin/rtctl inventory --config /etc/rosie-rt-core/machine.json
```

- [`hostcheck`](/docs/reference/rtctl#hostcheck) is read-only. It checks PREEMPT_RT, the IgH master version, EtherCAT device permissions, CPU isolation and `nohz_full` for the RT CPU, the frequency governor, NIC IRQ affinity, timers, and the service users and directories. Exit 0 means every check passed; it never measures cycle latency.
- [`inventory`](/docs/reference/rtctl#inventory) reads each slave's identity from the bus and compares it with the compiled configuration. Exit 0 means every slave matched.

If you have an owner-supplied host identity manifest, add `--expected-release <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:

```bash
sudo systemctl enable rosie-rt-core.service rosie-rt-control.service
sudo systemctl start rosie-rt-control.service     # also starts rosie-rt-core
systemctl status rosie-rt-core rosie-rt-control
```

The core runs as `rosie-rt` with only `CAP_SYS_NICE` and `CAP_IPC_LOCK`, the adapter as `rosie-ctl` with no capabilities. The public sockets are `/run/rosie-rt-core/control.sock` and `/run/rosie-rt-core/jog.sock` (symlinks into `public/`), group `rosie-rt-clients`. Add the account that runs your clients to that group:

```bash
sudo usermod -aG rosie-rt-clients "$USER"      # log in again afterwards
/opt/rosie-rt-core/current/bin/rtctl describe
```

Describe should report the `ethercat` backend and the digest from step 5. Starting the services does not arm or move anything: a client must acquire the lease, Home and arm first. The NATS observer, if configured, is `rosie-rt-natspublisher.service`; it has no command authority.

## Update and roll back

To update, build and install a new package (steps 2 and 3). The new release becomes `current` and the old one `previous`. Rerun step 5 if the machine config changed, then restart the units.

To go back one release:

```bash
sudo bash /opt/rosie-rt-core/current/host/install.sh --rollback
# Rolled back current to <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](https://advancedmetalresearch.com/docs/reference/rtctl)
- [Configuration files](https://advancedmetalresearch.com/docs/reference/configuration)
- [Remote access (mTLS)](https://advancedmetalresearch.com/docs/apis/remote-access-mtls)
- [Ports, sockets and environment variables](https://advancedmetalresearch.com/docs/reference/ports-and-environment)
- [Releasing](https://advancedmetalresearch.com/docs/contributing/releasing)

## Sources

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

- `rt-core/host/ethercat-foundation.sh:1-60,86-97,125-176,264-265,462-596`
- `rt-core/host/install.sh:1-224`
- `rt-core/host/generate-control-env.sh:1-79`
- `rt-core/host/remote-pki.sh:1-101`
- `rt-core/host/rosie-rt-core.service:1-43`
- `rt-core/host/rosie-rt-control.service:1-36`
- `rt-core/host/rosie-rt-natspublisher.service:1-41`
- `rt-core/tools/package-runtime.sh:1-101`
- `rt-core/tools/verify-package.sh:1-9`
- `rt-core/tools/build-igh-userlib.sh:9-40`
- `rt-core/tools/packageinfo/component.go:57-110`
- `rt-core/tools/rtctl/control_env.go:22-90`
- `rt-core/tools/rtctl/hostcheck.go:169-294`
- `rt-core/tools/rtctl/inventory.go:74-146`
- `rt-core/cmd/rt-control/main.go:24-68`
- `rt-core/adapters/rosie/control/controller.go:109-130`
- `rt-core/adapters/rosie/control/resources.go:80-100`
- `rt-core/adapters/rosie/control/remote_tls.go:48-140`
- `.github/workflows/rt-core.yml:62-72`
