# rtctl command reference

> Every rtctl subcommand and flag, with defaults and exit codes, for compiling and validating machine configurations, checking a cell host, inspecting a running cell through rt-control, and converting telemetry dumps.

URL: https://advancedmetalresearch.com/docs/reference/rtctl
Section: RosieOS docs / Reference
Last updated: 2026-10-10

`rtctl` is the operator CLI for rt-core. It compiles and validates machine configurations, launches the core, checks a cell host, and sends single requests to [rt-control](https://advancedmetalresearch.com/docs/apis/rt-control-http) for inspection and administration.

```bash
cd rt-core
make control                                   # builds build/rtctl
build/rtctl describe --socket /run/rosie-rt-core/control.sock
build/rtctl status --json | head -c 400; echo
```

> [!IMPORTANT] **rtctl is not a motion client.** Each control command sends one request and exits. rtctl never renews the lease, and on the `lan` profile a lease lasts at most 500 ms, so it has lapsed before your next command runs and the core inhibits outputs. There is no `jog` or `renew` command. Use rtctl to observe, fetch resources, stop and recover. For anything that moves the robot, use the [Go SDK](https://advancedmetalresearch.com/docs/apis/go-sdk) or the [C++ client](https://advancedmetalresearch.com/docs/apis/cpp-client), which renew in the background.

rtctl builds and runs on Linux only. There is no `rosie` command in this repository; READMEs that mention `rosie …` or `./rosie.sh …` refer to an internal tool.

## Commands at a glance

| Group | Commands |
|---|---|
| [Configuration](https://advancedmetalresearch.com/docs/reference/rtctl#configuration-commands) | `compile`, `validate`, `run`, `control-env` |
| [Host](https://advancedmetalresearch.com/docs/reference/rtctl#host-commands) | `hostcheck`, `inventory`, `recover-encoder` |
| [Control API](https://advancedmetalresearch.com/docs/reference/rtctl#control-commands) | `describe`, `status`, `events`, `resources fetch`, `acquire`, `release`, `stop`, `halt`, `enable`, `arm`, `home`, `reset-fault`, `prepare`, `start`, `discard`, `program-prepare`, `program-start` |
| [Telemetry](https://advancedmetalresearch.com/docs/reference/rtctl#telemetry) | `telemetry dump-to-jsonl` |

Every command rejects positional arguments it does not expect.

## Exit codes

| Code | Configuration commands | `hostcheck` | `inventory` | Control commands |
|---|---|---|---|---|
| 0 | Success | Every check passed | Every read matched | Success |
| 1 | Compile or file error | — | Compile or file error | — |
| 2 | Usage error | A check failed, or usage error | A slave mismatched, the machine is not `live`, or usage error | Rejected by rt-control, invalid options, or usage error |
| 3 | — | Required evidence was unreadable, or the rt-core root was not found | A slave or the I/O terminal was unreadable | Transport, protocol or closed-connection failure |

`validate` returns the daemon's own exit status.

## Where rtctl finds rt-core

Configuration and host commands need the rt-core root, the directory that holds `config/drives/`. rtctl looks beside its own executable first (`<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.

```bash
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](https://advancedmetalresearch.com/docs/reference/configuration#compiled-output).

### `validate`

Compiles, then runs the daemon with `--validate-config`, so the daemon checks its own private format without starting a runtime session.

```bash
build/rtctl validate --backend simulation --config config/machines/simulation/simulation-program.json
# daemon exit status: 0
```

| Flag | Type | Default | Description |
|---|---|---|---|
| `--config` | path | — | Machine config JSON. Required. |
| `--backend` | `simulation` \| `ipc-only` \| `live` | — | Required. Selects the daemon binary. |
| `--binary` | path | `<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.

```bash
build/rtctl run --backend simulation \
  --config config/machines/simulation/simulation-program.json \
  --out "$TMPDIR/config" --socket "$TMPDIR/ipc.sock"
```

| Flag | Type | Default | Description |
|---|---|---|---|
| `--config` | path | — | Machine config JSON. Required. |
| `--backend` | `simulation` \| `ipc-only` \| `live` | — | Required. Runs `<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](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host).

```bash
rtctl control-env --config machine.json --pair-id cell-a --pair-revision 1 \
  --out /tmp/staging --installed-out /etc/rosie-rt-core/compiled
```

| Flag | Type | Default | Description |
|---|---|---|---|
| `--config` | path | — | Live machine config. Required. Must have `"backend": "live"`. |
| `--pair-id` | token | — | Deployment pair id: letters, digits, `_`, `.`, `:`, `-`. |
| `--pair-revision` | uint64 | — | Positive decimal, no leading zeros. |
| `--out` | dir | — | Private staging directory for the compiled output. Required. |
| `--installed-out` | dir | — | Where the compiled directory will live once installed. Required; used in the paths it prints. |

It prints these variables on stdout:

```text
ROSIE_RT_PAIR_ID=cell-a
ROSIE_RT_PAIR_REVISION=1
ROSIE_RT_CONFIGURATION_SHA256=<64 hex>
ROSIE_RT_CORE_ARGS="<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`.

```bash
sudo rtctl hostcheck --config /etc/rosie-rt-core/machine.json
# pass rt_cpu: observed="2" expected="configured nonnegative runtime.cpu (or deployed --rt-cpu)"
# …
```

| Flag | Type | Default | Description |
|---|---|---|---|
| `--config` | path | — | Machine config to read `runtime.cpu` from. Without it, hostcheck reads `--rt-cpu` from `ROSIE_RT_CORE_ARGS` in `/etc/rosie-rt-core/control.env`. |
| `--json` | bool | `false` | Print a `rosie-rt-core.hostcheck.v1` JSON report instead of lines. |
| `--expected-release` | path | — | A `rosie-rt-core.host-identity.v1` manifest. Also compares the installed services and loaded executables against it, read-only. |

It checks, among others: the RT CPU; kernel version and build; PREEMPT_RT (`/sys/kernel/realtime` or `PREEMPT_RT` in `uname -v`); the loaded IgH `ec_master` version (1.6.9); `/dev/EtherCAT0` permissions for the device group; the RT CPU in `isolated` and `nohz_full`; the CPU frequency governor (`performance`); EtherCAT NIC IRQ affinity; clock source, timer slack and resolution; service users, groups and directories.

Each line is `<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`).

```bash
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.

```bash
sudo systemctl stop rosie-rt-control rosie-rt-core
sudo rtctl recover-encoder --backend ethercat --slave 3
```

| Flag | Type | Default | Description |
|---|---|---|---|
| `--slave` | integer | — | Absolute slave position on master 0, 0 to 65535. Required. |
| `--backend` | `ethercat` | — | Required. |

It refuses to write unless master 0 is idle (stop the daemon first), the slave is identified as a supported drive, is in PREOP and is disabled, and shows the specific encoder alarm it can clear. It refuses on any unexpected alarm between steps. It never starts the daemon or Home.

> [!CAUTION] An encoder reset invalidates the axis's saved Home anchor. Home the axis again before you enable it, and do not restore an old anchor.

## Control commands

These send one request to rt-control through the Go SDK and print the result as indented JSON (compact with `--json`). `describe` and `status` print the description or status document itself; the other commands print the response envelope (`schema`, `operation`, `sequence`, `handle`, `data`, `error`).

### Common flags

| Flag | Type | Default | Description |
|---|---|---|---|
| `--socket` | path | `/run/rosie-rt-core/control.sock` | Local control socket. |
| `--remote` | URL | — | Mutual-TLS HTTPS origin, for example `https://rosie.local:8443`. Exclusive with `--socket`. |
| `--ca`, `--cert`, `--key` | path | — | Server CA, client certificate and client key PEM files. Only with `--remote`. See [Remote access](https://advancedmetalresearch.com/docs/apis/remote-access-mtls). |
| `--session` | string | — | Session token from `acquire`. |
| `--generation` | uint64 | 0 | Control generation from `acquire`. |
| `--timeout` | duration | `10s` | Deadline for the HTTP operation. Must be positive. |
| `--request-id` | string | generated | Reuse only to retry an identical request. See [idempotent retries](https://advancedmetalresearch.com/docs/apis/rt-control-http#idempotent-retries). |
| `--json` | bool | `false` | Compact JSON output. |

### Observation

| Command | Flags | Sends |
|---|---|---|
| `describe` | — | [`describe`](/docs/apis/rt-control-http#describe) |
| `status` | — | [`status`](/docs/apis/rt-control-http#status) |
| `events` | `--after N` (default 0), `--follow` | [`subscribe_events`](/docs/apis/rt-control-http#subscribe-events). With `--follow`, streams one JSON event per line until Ctrl-C, then exits 0. `--timeout` does not apply to the stream. |

```bash
build/rtctl events --after 0 --follow --socket "$TMPDIR/control.sock"
```

### `resources fetch`

Downloads the cell's robot description files, verified by digest.

```bash
sha=$(build/rtctl describe --json | jq -r '.robot.resources[0].sha256')
build/rtctl resources fetch "$sha" --out ./cell-model
# ./cell-model/resources.json
```

| Argument or flag | Description |
|---|---|
| `<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`](/docs/apis/rt-control-http#acquire). Prints the response envelope; its `data` is the grant, whose `session` and `generation` are the fence. rtctl does not set a requested lease, so the cell's ceiling applies. |
| `release` | fence | [`release`](/docs/apis/rt-control-http#release) |
| `stop` | fence | [`stop`](/docs/apis/rt-control-http#stop). A known session still stops after its lease expired. |
| `halt` | fence | [`halt`](/docs/apis/rt-control-http#halt) |
| `enable` | fence, `--axis-mask` | [`enable`](/docs/apis/rt-control-http#enable) |
| `arm` | fence | [`arm`](/docs/apis/rt-control-http#arm) |
| `home` | fence, `--axis-mask` | [`home`](/docs/apis/rt-control-http#home) |
| `reset-fault` | fence | [`reset_fault`](/docs/apis/rt-control-http#reset-fault) (interim capability) |

"Fence" means `--session` and `--generation`. The axis mask is a bit per axis, 0 to 0xffffffff.

```bash
# Take authority, then stop and give it back. Each call is one request.
build/rtctl acquire --pair-id readme --pair-revision 1 --configuration-sha256 "$digest"
build/rtctl stop    --session "$session" --generation "$generation"
build/rtctl release --session "$session" --generation "$generation"
```

Because rtctl does not renew, commands after `acquire` that need a live lease are usually refused with an authority reason once the lease lapses. See [authority reasons](https://advancedmetalresearch.com/docs/reference/error-codes#reasons-authority).

### Trajectories and programs

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

| Command | Flags | Sends |
|---|---|---|
| `prepare` | fence, `--axis-mask`, `--file points.json` | [`prepare_trajectory`](/docs/apis/rt-control-http#prepare-trajectory). The file is a JSON array of [`Point`](/docs/apis/rt-control-http#type-point): `time_ns`, `position` (rad or m), optional `velocity`. |
| `start` | fence, `--handle` | [`start_trajectory`](/docs/apis/rt-control-http#start-trajectory) |
| `discard` | fence, `--handle` | [`discard_trajectory`](/docs/apis/rt-control-http#discard-trajectory) |
| `program-prepare` | fence, `--file program.rdt` | [`POST /v1/program`](/docs/apis/rt-control-http#prepare-program) with the binary `.rdt`. See [Dense trajectory format](https://advancedmetalresearch.com/docs/reference/rdt-format). |
| `program-start` | fence, `--file identity.json` | [`start_program`](/docs/apis/rt-control-http#start-program). The file is an [`Identity`](/docs/apis/rt-control-http#type-identity) object. |

rtctl commands bypass the weld planner's verifier: the core admits them against joint limits, velocity and continuity only. And because the lease lapses between commands, a prepared trajectory or program usually cannot be started from a second rtctl call. Drive motion from an SDK client instead.

## Telemetry

### `telemetry dump-to-jsonl`

Converts a raw telemetry dump written by the core (`runtime.telemetry.directory`) into JSON Lines, one record per cycle.

```bash
build/rtctl telemetry dump-to-jsonl /dev/shm/rt-core/telemetry/<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](https://advancedmetalresearch.com/docs/apis/rt-control-http): what each control command sends
- [Configuration files](https://advancedmetalresearch.com/docs/reference/configuration)
- [Install rt-core on a cell host](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host)
- [Your first motion](https://advancedmetalresearch.com/docs/get-started/first-motion): why the SDK, not rtctl, moves the robot
- [Ports, sockets and environment variables](https://advancedmetalresearch.com/docs/reference/ports-and-environment)

## Sources

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

- `rt-core/tools/rtctl/cmd/rtctl/main.go:10-34`
- `rt-core/tools/rtctl/command.go:16-138,141-172`
- `rt-core/tools/rtctl/run.go:13-76`
- `rt-core/tools/rtctl/control_env.go:22-90`
- `rt-core/tools/rtctl/control.go:1-194`
- `rt-core/tools/rtctl/resources_cli.go:1-98`
- `rt-core/tools/rtctl/hostcheck.go:21-100,169-294`
- `rt-core/tools/rtctl/hostidentity.go:17-60,161`
- `rt-core/tools/rtctl/inventory.go:60-146,245-322`
- `rt-core/tools/rtctl/recover_encoder.go:13-60,110-273`
- `rt-core/tools/rtctl/telemetry.go:12-261`
- `rt-core/sdk/control/client.go:210-228`
- `rt-core/adapters/rosie/control/api_generated.go:426-434`
- `rt-core/adapters/rosie/control/resources.go:51-56,358-375`
- `rt-core/Makefile:296-301`
- `rt-core/config/templates/machine.json (runtime.telemetry, control)`
