Advanced Metal Research
GitHub Contact AMR

C++ client

On this page
  1. Example: run one program
  2. Build
  3. Connect
  4. Methods
  5. Retries and request IDs
  6. Renewal
  7. Exceptions
  8. Jog
  9. Remote transport
  10. Related pages

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/:

HeaderContents
rt_control_client.hppRtControlClient, the exception types, HttpStream and StreamFactory.
rt_control_api_generated.hppThe generated contract types (Grant, Binding, Description, ProcessStatus …) and reason constants.
rt_jog_producer.hppRtJogProducer: local jog over jog.sock.
rt_jog_remote_producer.hppRtJogRemoteProducer: 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:

rt_execute.cpp (abridged)C++
#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:

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#

ConstructorUse
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#

MethodOperationReturns
describe(d)describeDescription; also records the round trip and the cell's lease and jog ceilings
status(d)statusProcessStatus
jog_clock(d), jog_status(d), jog_ingress(d)jog_clock, jog_status, jog_ingressLocalJogClock, JogObservation, JogIngressObservation
acquire(controller, binding, d)acquireGrant; sizes the lease to the measured round trip, and remembers the fence
renew(d)renewGrant
stop(d)stopGrant (urgent connection)
release(d)releaseGrant; stops renewal first
enable(mask, d), arm(d), home(mask, d)enable, arm, homeResponse
restore_anchor(mask, d)restore_anchorRecoveryStatus
reset_fault(mask, d), recovery_status(d)reset_fault, recovery_statusResponse, RecoveryStatus
io_arm(d), io_disarm(d)io_arm, io_disarmResponse
begin_jog(mask, clock, origin_ns, deadline_ns, d), end_jog(generation, d)begin_jog, end_jogResponse
jog(mask, velocity, timeout_ms, d)jog (test only)Response
prepare_trajectory(mask, points, d)prepare_trajectoryResponse (Handle)
start_trajectory(handle, d), discard_trajectory(handle, d)start_trajectory, discard_trajectoryResponse
prepare_program(bytes, d)prepare_programProgram; takes the .rdt as a binary std::string, NUL bytes included
start_program(identity, d)start_programResponse
events(after, d)subscribe_eventsEventBatch
events_stream(after, callback, stop, d)subscribe_events (SSE)Calls callback per Event until stop() returns true
telemetry(after, d)telemetryTelemetryPublication (Header, Records)
telemetry_stream(after, callback, stop, d)telemetry (stream)Calls callback per batch until stop() returns true
command(request, d, stop)any JSON operationResponse

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#

ExceptionBaseMeaning
Rejectedstd::runtime_errorHTTP 409. reason is the reason code; native_result and native_jog_result are optional native receipts.
LeaseTimingRejectedRejectedThe lease cannot cover the measured round trip. Carries round_trip and ceiling.
EventCursorLostRejectedAn event stream reported events_dropped. event is the loss record; resume with after = event.Sequence after reconciling Status.
TransportErrorstd::runtime_errorThe connection failed; the outcome may be unknown.
ProtocolErrorTransportErrorA malformed or unexpected reply.
TelemetryLayoutMismatchErrorProtocolErrorA telemetry batch uses another record layout. Carries stored_digest and expected_digest.
DeadlineExceededTransportErrorThe call's deadline passed; the outcome may be unknown.
ConnectionFailureTransportErrorThe 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:

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

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.