C++ client
On this page
rosie::rt_control::RtControlClient is a header-only C++17 client for rt-control. 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.
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:
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:
make -C rt-core clients
rt-core/build/rt-execute /run/rosie-rt-core/control.sock cell-a 1 "$CONFIGURATION_SHA256" program.rdtIt 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. |
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; 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:
// 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 generationThe 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.
Remote transport#
The header-only client does no TLS itself. To reach the remote listener, 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.