# The real-time core

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

URL: https://advancedmetalresearch.com/docs/concepts/real-time-core
Section: RosieOS docs / Concepts
Last updated: 2026-10-10

`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](https://advancedmetalresearch.com/docs/apis/rt-control-http), 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.

![Clients call rt-control, which reaches rosie-rt-core over private IPC. The core reads feedback, checks readiness, computes a trajectory or jog target and applies a final output permit each cycle, then writes to the drives over EtherCAT. The hardware E-stop chain acts on drive power directly and is only observed by the core.](https://advancedmetalresearch.com/assets/docs/rt-core-cycle.svg)

*Figure: 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](https://advancedmetalresearch.com/docs/get-started/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](https://advancedmetalresearch.com/docs/reference/configuration) 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](https://advancedmetalresearch.com/docs/concepts/control-authority)).

1. **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.
2. **Enable** the axes with `enable` and an axis mask. The drives move through the DS402 states to *Operation enabled*.
3. **Arm** the core with `arm`.
4. **Wait for readiness.** Poll Status until every axis you will move reports `readiness: "ready"`.
5. **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](https://advancedmetalresearch.com/docs/reference/error-codes#axis-readiness) 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](https://advancedmetalresearch.com/docs/reference/error-codes#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](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry#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](https://advancedmetalresearch.com/docs/concepts/control-authority): leases, fences and the jog lane.
- [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http): the calls that drive the core.
- [Error codes and fault states](https://advancedmetalresearch.com/docs/reference/error-codes).
- [Architecture](https://advancedmetalresearch.com/docs/concepts/architecture): where the core sits among the other processes.

## Sources

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

- `rt-core/src/main.cpp:21`
- `rt-core/engine/cycle_machine.hpp:3155-3175 (controlword zeroed)`
- `rt-core/engine/cycle_machine.hpp:3514-3515 (motion_permitted_mask)`
- `rt-core/engine/cycle_machine.hpp:3539-3543 (cell I/O permit)`
- `rt-core/engine/cycle_machine.hpp:4121-4141 (PREEMPT_RT and FIFO admission)`
- `rt-core/include/motion_readiness.hpp:14-21 (readiness order)`
- `rt-core/include/motion_readiness.hpp:26-31 (axis ready: mode 8, OperationEnabled)`
- `rt-core/include/motion_readiness.hpp:97-137 (start gate)`
- `rt-core/include/fault_recovery.hpp:9-64`
- `rt-core/adapters/rosie/control/reset_linux.go:22-154`
- `rt-core/adapters/rosie/control/controller.go:830-1053 (home, anchors, restore)`
- `rt-core/adapters/rosie/control/anchors/store.go:44-49`
- `rt-core/engine/brake.hpp:8-40`
- `rt-core/include/axis_profiles.hpp:28-42 (collision watchdog bounds)`
- `rt-core/engine/cycle_capture.hpp:42 (retention default)`
- `rt-core/engine/jog.hpp:16-55`
- `rt-core/protocol/control.json (native_enums DS402_*, BRAKE_*)`
- `rt-core/protocol/application-v1.schema.json (AxisDescription.interpolation, checks, endpoint; rules.external_enable)`
- `rt-core/config/templates/machine.json:6-7,156-158`
- `rt-core/Makefile:39-56`
