# C++ client

> The header-only C++17 client for rt-control, used by the motion servers. Connect, acquire and renew, upload and start programs, jog locally or over WebSocket, and handle the exception types.

URL: https://advancedmetalresearch.com/docs/apis/cpp-client
Section: RosieOS docs / APIs
Last updated: 2026-10-10

`rosie::rt_control::RtControlClient` is a header-only C++17 client for [rt-control](https://advancedmetalresearch.com/docs/apis/rt-control-http). The Cartesian motion server and the dense trajectory daemon are built on it. It uses the generated contract types from `rt_control_api_generated.hpp`, keeps separate persistent connections for lifecycle, urgent, bulk and renewal traffic, and reports refusals as a `Rejected` exception carrying the reason code.

The headers are in `rt-core/clients/cpp/include/rosie/`:

| Header | Contents |
|---|---|
| `rt_control_client.hpp` | `RtControlClient`, the exception types, `HttpStream` and `StreamFactory`. |
| `rt_control_api_generated.hpp` | The generated contract types (`Grant`, `Binding`, `Description`, `ProcessStatus` …) and reason constants. |
| `rt_jog_producer.hpp` | `RtJogProducer`: local jog over `jog.sock`. |
| `rt_jog_remote_producer.hpp` | `RtJogRemoteProducer`: remote jog over WebSocket. |

> [!WARNING] The example below enables and arms the drives and starts a program. Run it against the simulated core first. On real hardware, keep the hardware E-stop within reach: it is the only emergency stop, and RosieOS has no software E-stop. Only planned weld programs pass the planner's collision and limit check; see the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model).

## Example: run one program

`clients/cpp/examples/rt_execute.cpp` acquires, renews every 50 ms, enables and arms every axis, waits for readiness, uploads an `.rdt` program, starts it, waits for completion and releases. This is its core, shortened:

rt_execute.cpp (abridged):

```cpp
#include "rosie/rt_control_client.hpp"
using namespace rosie::rt_control;

int main(int argc, char** argv) {
    // argv: CONTROL_SOCKET PAIR_ID PAIR_REVISION CONFIGURATION_SHA256 PROGRAM.rdt
    RtControlClient client(argv[1]);
    try {
        Binding binding;
        binding.PairID = argv[2];
        binding.Revision = std::stoull(argv[3]);
        binding.ConfigurationSHA256 = argv[4];
        client.acquire("motion-server", binding);
        client.start_renewal(std::chrono::milliseconds(50));

        const auto count = client.describe().Description.Axes.size();
        const std::uint32_t mask = (1U << count) - 1;
        client.enable(mask);
        client.arm();
        // ... poll client.status() until Core.Armed == 1, every axis is Operation Enabled
        //     and Core.SafetyFaultMask == 0 ...

        std::string bytes = read_file(argv[5]);             // the immutable .rdt blob
        Program program = client.prepare_program(bytes);
        auto before = client.status();
        client.start_program(program.Identity);
        // ... poll client.status() until Execution.State == "completed" for
        //     Execution.Generation == before.Execution.Generation + 1 ...

        client.release();
        return 0;
    } catch (const Rejected& e) {
        std::cerr << e.reason << '\n';   // a reason code, e.g. not_ready
        client.stop_renewal();
        try { client.stop(); client.release(); } catch (...) {}
        return 2;
    } catch (const std::exception& e) {  // TransportError and friends: outcome unknown
        std::cerr << e.what() << '\n';
        client.stop_renewal();
        try { client.stop(); client.release(); } catch (...) {}
        return 3;
    }
}
```

Build and run the full example against a running rt-control:

```bash
make -C rt-core clients
rt-core/build/rt-execute /run/rosie-rt-core/control.sock cell-a 1 "$CONFIGURATION_SHA256" program.rdt
```

It prints the final status JSON. The exit code is 0 on completion, 2 for a rejection (it prints the reason) and 3 for a transport or observation failure. On failure it attempts Stop and Release. Two more examples sit beside it: `motion_server_sequence.cpp` (Home, program upload and completion) and `remote_jog_sequence.cpp` (remote jog).

## Build

Compile with C++17, `-Irt-core/clients/cpp/include` and `-pthread`. The jog producers and the telemetry decoders also need the generated frame codec, so add `-Irt-core/include`. The client includes `protocol_generated.hpp` by a relative path, so if you copy the client headers elsewhere, copy `rt-core/include/protocol_generated.hpp` with them.

## Connect

| Constructor | Use |
|---|---|
| `RtControlClient(const std::string& socket)` | The local Unix socket, for example `/run/rosie-rt-core/control.sock`. |
| `RtControlClient(StreamFactory factory)` | Any other transport, in practice mutual TLS; see [below](https://advancedmetalresearch.com/docs/apis/cpp-client#remote-transport). |

The client is not copyable. Join your other calls before destroying it; the destructor stops renewal but does not release authority.

Every call takes an optional absolute `Deadline` (a `std::chrono::steady_clock::time_point`). The default is 5 s from the call, and it bounds the call including its retry.

## Methods

| Method | Operation | Returns |
|---|---|---|
| `describe(d)` | `describe` | `Description`; also records the round trip and the cell's lease and jog ceilings |
| `status(d)` | `status` | `ProcessStatus` |
| `jog_clock(d)`, `jog_status(d)`, `jog_ingress(d)` | `jog_clock`, `jog_status`, `jog_ingress` | `LocalJogClock`, `JogObservation`, `JogIngressObservation` |
| `acquire(controller, binding, d)` | `acquire` | `Grant`; sizes the lease to the measured round trip, and remembers the fence |
| `renew(d)` | `renew` | `Grant` |
| `stop(d)` | `stop` | `Grant` (urgent connection) |
| `release(d)` | `release` | `Grant`; stops renewal first |
| `enable(mask, d)`, `arm(d)`, `home(mask, d)` | `enable`, `arm`, `home` | `Response` |
| `restore_anchor(mask, d)` | `restore_anchor` | `RecoveryStatus` |
| `reset_fault(mask, d)`, `recovery_status(d)` | `reset_fault`, `recovery_status` | `Response`, `RecoveryStatus` |
| `io_arm(d)`, `io_disarm(d)` | `io_arm`, `io_disarm` | `Response` |
| `begin_jog(mask, clock, origin_ns, deadline_ns, d)`, `end_jog(generation, d)` | `begin_jog`, `end_jog` | `Response` |
| `jog(mask, velocity, timeout_ms, d)` | `jog` (test only) | `Response` |
| `prepare_trajectory(mask, points, d)` | `prepare_trajectory` | `Response` (`Handle`) |
| `start_trajectory(handle, d)`, `discard_trajectory(handle, d)` | `start_trajectory`, `discard_trajectory` | `Response` |
| `prepare_program(bytes, d)` | `prepare_program` | `Program`; takes the `.rdt` as a binary `std::string`, NUL bytes included |
| `start_program(identity, d)` | `start_program` | `Response` |
| `events(after, d)` | `subscribe_events` | `EventBatch` |
| `events_stream(after, callback, stop, d)` | `subscribe_events` (SSE) | Calls `callback` per `Event` until `stop()` returns true |
| `telemetry(after, d)` | `telemetry` | `TelemetryPublication` (`Header`, `Records`) |
| `telemetry_stream(after, callback, stop, d)` | `telemetry` (stream) | Calls `callback` per batch until `stop()` returns true |
| `command(request, d, stop)` | any JSON operation | `Response` |

There are no named methods for `halt`, `mark_telemetry` or `resource`; send `halt` and `mark_telemetry` with `command()`.

### Retries and request IDs

Every mutation carries a random `request_id`. To retry a lost reply, keep the `Request` object and pass it to `command()` again without changing its ID, fence or body: the adapter replays the original outcome. The client itself retries once with the same request. `release()` and `.rdt` uploads are never replayed automatically. An uncertain upload must not lead to a Start: inspect state or Stop first.

### Renewal

`start_renewal(interval)` starts a background thread that renews at `interval` (0 means a third of the lease; anything larger is refused). It throws `Rejected` with `control_session_stale` when the lease cannot cover three round trips plus the interval. Poll `take_renewal()`, which returns the latest `Renewal{grant, error}` if there is one. A grant with `Stopping` set is a successful renewal during a Stop. Any error ends renewal, and the thread never reacquires authority. `stop_renewal()` joins the thread.

`start_connection_keepalive()` keeps idle connections open at a third of the Describe `control_idle_timeout_ns`.

## Exceptions

| Exception | Base | Meaning |
|---|---|---|
| `Rejected` | `std::runtime_error` | HTTP 409. `reason` is the [reason code](https://advancedmetalresearch.com/docs/reference/error-codes); `native_result` and `native_jog_result` are optional native receipts. |
| `LeaseTimingRejected` | `Rejected` | The lease cannot cover the measured round trip. Carries `round_trip` and `ceiling`. |
| `EventCursorLost` | `Rejected` | An event stream reported `events_dropped`. `event` is the loss record; resume with `after = event.Sequence` after reconciling Status. |
| `TransportError` | `std::runtime_error` | The connection failed; the outcome may be unknown. |
| `ProtocolError` | `TransportError` | A malformed or unexpected reply. |
| `TelemetryLayoutMismatchError` | `ProtocolError` | A telemetry batch uses another record layout. Carries `stored_digest` and `expected_digest`. |
| `DeadlineExceeded` | `TransportError` | The call's deadline passed; the outcome may be unknown. |
| `ConnectionFailure` | `TransportError` | The connection closed. `sent` says whether any request bytes were transmitted. |

Catch `Rejected` first for refusals, then `TransportError` for everything whose outcome is uncertain. After an uncertain outcome, stop producing motion, attempt `stop()`, and reconcile `status()` before acquiring again.

## Jog

`RtJogProducer` jogs over the local `jog.sock`:

```cpp
#include "rosie/rt_jog_producer.hpp"

// After acquire(), start_renewal(), enable(), arm() and observed readiness:
auto deadline = host_monotonic_ns() + 100000000;                 // 100 ms input lifetime
RtJogProducer jog(client, "/run/rosie-rt-core/jog.sock", /*mask*/ 1, deadline);
std::vector<double> v(axis_count, 0.0);
v[0] = 0.05;                                                      // rad/s on axis 0
if (!jog.update(v, host_monotonic_ns() + 100000000)) {
    // Congested: this sample was not sent. Drop it and send a fresh one.
}
jog.end();                                                        // ends the jog generation
```

The constructor reads `/v1/jog/clock` and calls `begin_jog`. `update()` returns false when the datagram could not be sent; discard that input and sample again. Stop cancels the producer, so never reuse it after a Stop. For remote jog, use `RtJogRemoteProducer` on a client built with a mutual-TLS factory; see [remote jog](https://advancedmetalresearch.com/docs/apis/remote-access-mtls#remote-jog-over-websocket).

## Remote transport

The header-only client does no TLS itself. To reach the [remote listener](https://advancedmetalresearch.com/docs/apis/remote-access-mtls), construct the client with a `StreamFactory`: a function that takes a `Deadline` and returns a new `std::unique_ptr<HttpStream>` connected over mutual TLS. `HttpStream` has two methods, `read(data, size, deadline)` and `write(data, size, deadline)`.

Your factory must return an independent stream for every call, verify the server certificate and host name, and present the client certificate and key of the paired principal. Every read and write must honour the absolute deadline and never replay bytes. A read that times out throws `DeadlineExceeded` without consuming data. Keep the lifecycle, renewal and jog connections under the same authenticated identity. The client's own tests exercise the WebSocket jog path through a bridge that performs TLS on the Go side, so the C++ TLS path itself is not covered by CI.

## Related pages

- [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http)
- [Go SDK](https://advancedmetalresearch.com/docs/apis/go-sdk)
- [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority)

## Sources

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

- `rt-core/clients/cpp/include/rosie/rt_control_client.hpp:22-90 (exceptions, HttpStream, StreamFactory)`
- `rt-core/clients/cpp/include/rosie/rt_control_client.hpp:359-404 (Renewal, telemetry decoding)`
- `rt-core/clients/cpp/include/rosie/rt_control_client.hpp:405-910 (RtControlClient)`
- `rt-core/clients/cpp/include/rosie/rt_jog_producer.hpp:16-80`
- `rt-core/clients/cpp/include/rosie/rt_jog_remote_producer.hpp:89-270`
- `rt-core/clients/cpp/examples/rt_execute.cpp:1-86`
- `rt-core/Makefile:138 (clients); narrative: rt-core/clients/cpp/README.md (build flags, CI note)`
