Skip to content

Rust SDK

The adamo crate is a safe, idiomatic Rust wrapper over the Adamo SDK shared library. Same primitives as the Python and C SDKs.

Terminal window
cargo add adamo
cargo add adamo --features video # video capture + caller-fed tracks

Pre-built libadamo.so ships for aarch64-unknown-linux-gnu. For local dev on other targets, build adamo-c from source and point the build at it via ADAMO_LIB_DIR=/path/to/dir/with/libadamo.{so,dylib}.

The crate is published as two:

CratePurpose
adamo-sysRaw unsafe extern "C" FFI bindings + bundled libadamo.{so,dylib}.
adamoSafe wrapper. This is what you depend on.

pub struct Session

The pub/sub, query, liveliness, latency, and control session. Constructed by Session::open_default or Session::open.

Send + Sync — safe to share across threads.

impl Session {
pub fn open_default(api_key: &str) -> Result<Self>;
}

Open an authenticated session using the default transport.

impl Session {
pub fn open(api_key: &str, protocol: Protocol) -> Result<Self>;
}

Open an authenticated session with an explicit transport. Use Session::open_default unless you need to select a specific transport.

pub fn org(&self) -> Result<&str>;

The organisation slug resolved from the API key.

pub fn put(&self, key: &str, payload: &[u8], opts: PublishOptions) -> Result<()>;

One-shot publish. The key is auto-scoped to your organisation; you write {robot}/{topic}.

pub fn publisher(&self, key: &str, opts: PublisherOptions) -> Result<Publisher<'_>>;

Declare a persistent publisher tied to the session’s lifetime. Dropped → undeclared.

pub fn log(&self, name: &str, message: &str, level: &str) -> Result<()>;

Publish a log line from a robot. The web operator console subscribes to this stream and renders entries in real time.

level is a free-form string; the standard values "info", "warn", "error", "debug" are rendered with colour. Messages are truncated at 10,000 characters. Each entry is stamped with the fabric clock so lines from multiple robots stay ordered in the operator view.

use adamo::Session;
let session = Session::open_default("ak_...")?;
session.log("my-robot", "booted and attached camera", "info")?;
session.log("my-robot", "encoder dropped a frame", "warn")?;
pub fn subscribe(&self, key: &str) -> Result<Subscriber<'_>>;

Pull-style subscriber. Use recv / try_recv to receive samples. Wildcards in key:

PatternMatches
my-arm/sensors/**Everything under sensors/.
my-arm/sensors/*One level under sensors/.
*/sensors/imuIMU from any robot.
pub fn subscribe_with<F>(&self, key: &str, callback: F) -> Result<CallbackSubscriber<'_>>
where
F: Fn(Sample) + Send + Sync + 'static;

Callback subscriber. The callback runs on a background receive thread — keep it short. The returned CallbackSubscriber owns the boxed callback; drop it to undeclare.

pub fn get(&self, key: &str, timeout: Duration) -> Result<Vec<Sample>>;

One-shot query. Collects every reply that arrives within timeout. Returns an empty Vec if nothing replies.

pub fn alive(&self, token_key: &str) -> Result<LivelinessToken<'_>>;

Declare this client as alive at {token_key}/alive. The returned token is RAII — drop it to undeclare.

pub fn live_tokens(&self, pattern: &str) -> Result<Vec<String>>;

Query currently-live tokens matching pattern. Pass "**/alive" for every live token in the org.

pub fn on_liveliness<F>(
&self,
pattern: &str,
history: bool,
callback: F,
) -> Result<LivelinessSubscriber<'_>>
where
F: Fn(String, bool) + Send + Sync + 'static;

Watch for liveliness changes. The callback fires with (prefix-stripped key, is_alive) whenever a token appears or disappears. history=true delivers the current set up front.

pub fn relay_rtt(&self) -> Result<Option<Duration>>;

Most recent best RTT to the connected relay’s time plugin. Returns Ok(None) until the time-sync loop has received its first pong.

pub fn measure_rtt(&self, robot: &str, timeout: Duration) -> Result<Duration>;

Measure round-trip time to robot by publishing a ping and waiting for the echoed pong. Blocks the calling thread.

pub fn watch_latency(&self, robot: &str) -> Result<Subscriber<'_>>;

Subscribe to robot’s heartbeat topic. Each received sample can be parsed with LatencyStats::parse.


#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Protocol {
Udp,
Quic,
Tcp,
}

Transport selection. Use QUIC for the normal path — reliable streams over a single multiplexed UDP socket. Udp is the lowest-latency unreliable transport. Tcp is a fallback for environments that block UDP/QUIC.


#[derive(Debug, Clone, Copy)]
pub struct PublishOptions {
pub priority: u8, // 0–255 mapped to 8 priority classes; ≥240 = REAL_TIME
pub express: bool, // bypass batching for lower latency
}
impl Default for PublishOptions {
fn default() -> Self {
Self { priority: 128, express: false }
}
}

#[derive(Debug, Clone, Copy)]
pub struct PublisherOptions {
pub priority: u8,
pub express: bool,
pub reliable: bool, // BEST_EFFORT (false) vs RELIABLE (true, default)
}
impl Default for PublisherOptions {
fn default() -> Self {
Self { priority: 128, express: false, reliable: true }
}
}

pub struct Publisher<'a> {
// tied to the session by lifetime
}
impl Publisher<'_> {
pub fn put(&self, payload: &[u8]) -> Result<()>;
}

Send + Sync. Drop calls the underlying free.


pub struct Subscriber<'a> {
// tied to the session by lifetime
}
impl Subscriber<'_> {
pub fn recv(&self, timeout: Option<Duration>) -> Result<Sample>;
pub fn try_recv(&self) -> Result<Option<Sample>>;
}

recv(None) blocks indefinitely. recv(Some(d)) returns Err(Error::Timeout) if d elapses. try_recv returns Ok(None) when the queue is empty.

Send. Drop calls the underlying free.


pub struct CallbackSubscriber<'a> {
// owns the boxed callback for as long as the subscriber lives
}

Returned by Session::subscribe_with. Send + Sync. The callback runs on a background receive thread and stays registered until this handle is dropped.


pub struct LivelinessToken<'a>;

A declared liveliness token. Send + Sync. Drop → undeclared.


pub struct LivelinessSubscriber<'a> {
// owns the boxed callback for as long as the subscriber lives
}

Watching subscriber returned by Session::on_liveliness. Send + Sync. The callback runs on a background receive thread and stays registered until this handle is dropped.


#[derive(Debug, Clone)]
pub struct Sample {
pub key: String, // prefix-stripped key
pub payload: Vec<u8>,
pub is_delete: bool, // true for DELETE samples (liveliness-gone)
pub timestamp_us: Option<u64>,
}

pub fn adamo::fabric_now_us() -> u64;
pub fn adamo::fabric_synced() -> bool;

Microseconds since the Unix epoch on the adamo fabric clock — the shared time axis every node on the network sees. Use these instead of SystemTime::now() whenever a timestamp will be subtracted from a stamp produced on a different node (capture timestamps, latency measurements).

fabric_synced() returns true once the first sync completes (typically under 100 ms after Session::open). Until then fabric_now_us() falls back to the local wall clock.

These are free functions on adamo — they don’t take a session, so any code on the host can stamp timestamps consistently.


pub enum Regime {
Stable,
Degrading,
Volatile,
Recovering,
}
pub struct LatencyStats {
pub regime: Regime,
pub jitter_hint_ms: f32,
pub garch_sigma_ms: f32,
pub target_bitrate_kbps: u32,
pub loss_rate: f32,
pub queuing_delay_ms: f32,
pub timestamp_ms: u64,
}
impl LatencyStats {
pub fn parse(payload: &[u8]) -> Option<Self>;
}

Use Session::watch_latency(robot) to subscribe, then pass each sample payload to LatencyStats::parse.

use adamo::{LatencyStats, Session};
use std::time::Duration;
let session = Session::open_default("ak_...")?;
let sub = session.watch_latency("my-arm")?;
loop {
let sample = sub.recv(Some(Duration::from_secs(5)))?;
if let Some(stats) = LatencyStats::parse(&sample.payload) {
println!("{:?}: {:.1} ms jitter", stats.regime, stats.jitter_hint_ms);
}
}

#[cfg(feature = "video")]
pub use robot::{Robot, VideoTrack, detect_encoder};
pub struct Robot;
impl Robot {
pub fn new_default(api_key: &str, name: Option<&str>) -> Result<Self>;
pub fn new(api_key: &str, name: Option<&str>, protocol: Protocol) -> Result<Self>;
/// Caller-fed track. Returns a VideoTrack you push raw frames to.
pub fn video(
&mut self,
name: &str,
width: u32,
height: u32,
pixel_format: &str, // "BGRA", "RGB", "I420", "NV12", ...
fps: u32,
bitrate_kbps: u32,
stereo: bool,
) -> Result<VideoTrack<'_>>;
/// V4L2 capture. The SDK owns capture and encoding.
pub fn attach_v4l2(
&mut self,
name: &str,
device: &str, // "/dev/video0"
width: u32,
height: u32,
fps: u32,
bitrate_kbps: u32,
stereo: bool,
) -> Result<()>;
/// Block driving the pipeline forever. Consumes self.
pub fn run(self) -> Result<()>;
}

Send. Drop releases the handle (unless run() consumed it).

pub struct VideoTrack<'a>;
impl VideoTrack<'_> {
pub fn send(&mut self, frame: &[u8]) -> Result<()>;
}

Push one frame. frame.len() must equal width × height × bytes-per-pixel for the format declared on Robot::video. Tied to the parent Robot by lifetime; Send; drop releases the track.

pub fn detect_encoder() -> Result<&'static str>;

Returns the best available H.264 encoder for the host (NVENC, VA-API, VideoToolbox, x264) or "none" if no encoder is available.


pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
/// An error reported by the underlying SDK shared library.
Ffi(String),
/// A Rust string contained an interior NUL byte and could not cross FFI.
InteriorNul,
/// FFI returned non-UTF-8 data.
InvalidUtf8,
/// A blocking receive timed out.
Timeout,
}
impl std::fmt::Display for Error;
impl std::error::Error for Error;
impl From<std::ffi::NulError> for Error;

Errors are printable via Display. Underlying SDK error messages come through Error::Ffi(String).


  • Session is Send + Sync — share a reference across threads when the lifetime works, or open a separate session for independent workers.
  • All Session methods are blocking at the Rust level. They use the SDK’s internal runtime under the hood.
  • subscribe_with callbacks run on a background receive thread. Keep them short or hand off via a channel.
  • Robot::run() blocks driving the encoder. Spawn it on its own thread if you need to do other work concurrently.