Medius - Rust LibraryRequests

Requests

One QUERY frame out, one RESP frame back

Unlike the fire-and-forget calls, the queries block: one QUERY frame out, one RESP frame back. They are query_version, query_health, device_info, caps, query_rate, query_stats, query_locks, and query_catch, plus query_status and query_config on the clip handle.

query_version

Firmware identity, round-trip
fn query_version(&self) -> Result<Version>

Blocks

Returns a Version. The box's name rides on it, in the name field.

EXAMPLE
use medius::Device;

let device = Device::find()?;          // or Device::open("/dev/ttyACM0")?
let v = device.query_version()?;
println!("{v}");                       // fw 3.2.0
println!("proto {}", v.proto_ver);     // proto 5
println!("name {}", v.name);           // Loki

Device::find() already runs a version query during the handshake; calling query_version again just re-reads it.

query_health

The status bits of the device-to-box-to-PC path
fn query_health(&self) -> Result<Health>

Blocks

Returns a Health, eight booleans from one status byte. link_up, mouse_attached, and clone_configured must all be true before injection is emitted at all.

EXAMPLE
use medius::Device;

let device = Device::find()?;
let h = device.query_health()?;
if h.link_up && h.mouse_attached && h.clone_configured {
    // chain is live, safe to inject
} else {
    eprintln!("not ready: {h:?}");
}

device_info

USB identity, kind, and product of the clone
fn device_info(&self) -> Result<DeviceInfo>

Blocks

Returns a DeviceInfo: the vid, pid, USB version, a DeviceKind, and the product string the box read off the real device. Every field is zero/empty when nothing is cloned. Display prints VVVV:PPPP product.

EXAMPLE
use medius::{Device, DeviceKind};

let device = Device::find()?;
let d = device.device_info()?;
if d.vid == 0 {
    eprintln!("nothing cloned yet");
} else {
    println!("{d}");                    // 046D:C08B G502
    println!("usb {:#06x}", d.bcd_usb);
    println!("kind={} serial={} bos={}", d.kind, d.has_serial, d.has_bos);
    if d.kind == DeviceKind::Mouse {
        // the clone is a mouse
    }
}

caps

Feature-detect the whole device
fn caps(&self) -> Result<Caps>

Blocks

Returns a Caps: a mouse half, a keyboard half, and the per-class change-driven flags. An absent class reads all-zero; has_mouse() and has_keyboard() say which are bound. An inject for a usage the device lacks is dropped with no error, so feature-detect here first.

EXAMPLE
use medius::Device;

let device = Device::find()?;
let caps = device.caps()?;
println!("{} buttons", caps.mouse.n_buttons);
if caps.mouse.has_wheel {
    device.wheel(1)?;
}
if caps.has_keyboard() && caps.keyboard.has_consumer {
    device.press(medius::MediaKey::MUTE)?;
}

query_rate

Read the live native report rate
fn query_rate(&self) -> Result<Rate>

Blocks

Returns a Rate. native_hz() converts the period to a frequency, returning None while native_period_us is still 0 (not learned yet). confident is true once the estimator window is full and the value is trustworthy.

EXAMPLE
use medius::Device;

let device = Device::find()?;
let r = device.query_rate()?;
match r.native_hz() {
    Some(hz) if r.confident => println!("{hz:.0} Hz"),
    Some(hz)                => println!("{hz:.0} Hz (still settling)"),
    None                    => println!("rate not learned yet"),
}

query_stats

Read the delivery counters
fn query_stats(&self) -> Result<Stats>

Blocks

Returns a Stats. inject_emits counts pure-injection reports emitted; a nonzero tx_drops or tx_wedges is the signal that delivery degraded under load. The narrowed counters saturate, so a maxed field clamps instead of wrapping.

EXAMPLE
use medius::Device;

let device = Device::find()?;
let s = device.query_stats()?;
println!("{} emits", s.inject_emits);
if s.tx_drops > 0 || s.tx_wedges > 0 {
    eprintln!("delivery degraded: {} drops, {} wedges", s.tx_drops, s.tx_wedges);
}

query_locks

Read the active input scales
fn query_locks(&self) -> Result<Locks>

Blocks

Returns a Locks, every direction currently weighed by scale. scale_of(target, direction) reads the percentage in effect and is_locked(target, direction) reports whether it is blocked outright. What a blanket, a media usage, and a vector-mode relative direction report is on Locks.

EXAMPLE
use medius::{Device, Axis, Direction};

let device = Device::find()?;
let locks = device.query_locks()?;
if locks.is_locked(Axis::X, Direction::Both) {
    println!("horizontal motion is frozen");
}
println!("opposing the injection at {}%", locks.scale_of(Axis::X, Direction::Against));

query_catch

Read the active catch subscription
fn query_catch(&self) -> Result<CatchState>

Blocks

Returns a CatchState: the live subscription table as a list of CatchEntry, the table_full flag, the box-wide dropped count, and a ClockEstimate relating the two chips' timers.

catch_events is fire-and-forget: the box sends no reply to a subscription. Each entry returns the class / id / direction / capture the box accepted, so checking the list against the filters you sent is the only way to confirm every one was accepted.

A filter that is missing was refused. table_full says which reason: the 32-entry table was full, or the filter itself was malformed.

DROPS AND THE CLOCK

CatchState::dropped is box-wide and CatchEntry::dropped is per entry, with a lost event charged to every entry it resolved against. Drops on the entry you care about mean the subscription is too broad for the link.

The two chips stamp events on clocks that share no epoch. clock is the measured gap between them, and what its fields mean is on ClockEstimate.

EXAMPLE
use medius::{Capture, CatchFilter, Class, Device, TrafficClass};

let device = Device::find()?;
// Bind the stream: dropping it unsubscribes, and the query below would then find an empty table.
let _events = device.catch_events([
    CatchFilter::watch_class(Class::Key),
    CatchFilter::traffic(TrafficClass::VendorBulk, 0x02).with_capture(Capture::First(16)),
])?;

let c = device.query_catch()?;
if c.table_full {
    eprintln!("the 32-entry table is full: some filters were refused");
}
for e in &c.entries {
    let f = e.filter;
    println!("{:?} {:?} capture={:?} dropped={}", f.class(), f.id(), f.capture(), e.dropped);
}
println!("{} dropped box-wide", c.dropped);
if let Some(age) = c.clock.age {
    println!("clocks differ by {} us (+/- {}, {age:?} old)", c.clock.offset_us, c.clock.delay_us / 2);
}

query_status (clip)

Read the buffered-clip ring depth, progress, and playback state
fn query_status(&self) -> Result<ClipStatus>

Blocks

On the ClipHandle from device.clip(), not Device itself. Returns a ClipStatus: state (including Faulted), ring free, retained played/total, the drain counters, and the held usages. Backs QUERY(CLIP).

EXAMPLE
use medius::Device;

let device = Device::find()?;
let clip = device.clip();
let s = clip.query_status()?;
if s.state == medius::ClipState::Faulted { clip.clear()?; }
println!("{} free, {} played", s.free, s.played);

query_config (clip)

Read the whole clip config back
fn query_config(&self) -> Result<ClipSettings>

Blocks

The config view of the same QUERY(CLIP) frame query_status reads, also on the ClipHandle. Returns a ClipSettings with the auto-lock, loop, retain, finalized flag, and triggers you set. Every setting round-trips.

EXAMPLE
use medius::Device;

let device = Device::find()?;
let cfg = device.clip().query_config()?;
println!("{} triggers, loop={}", cfg.triggers.len(), cfg.loop_);

firmware_info

Read both chips' versions, slots, and what is staged
fn firmware_info(&self) -> Result<FirmwareInfo>

Blocks

Backs QUERY(FIRMWARE). The other firmware calls live on the update page.

RETURNS
FieldTypeNotes
deviceChipFirmwareversion, slot, and image state
hostOption<ChipFirmware>None when the host chip has not answered over the inter-chip link
slot_sizeu32usable bytes in a spare slot, the same on both chips
device_stagedboolan image is written and waiting to be activated
host_stagedboolthe same, for the host chip

This is the only call that reports the host chip's version. query_version reports the device chip alone.

EXAMPLE
let fw = device.firmware_info()?;
println!("device {}", fw.device);
match fw.host {
    Some(h) => println!("host {h}"),
    None => println!("host chip not answering"),
}

Async queries

The same queries on AsyncDevice
async fn query_version(&self) -> Result<Version>
async fn query_health(&self) -> Result<Health>
async fn device_info(&self) -> Result<DeviceInfo>
async fn caps(&self) -> Result<Caps>
async fn query_rate(&self) -> Result<Rate>
async fn query_stats(&self) -> Result<Stats>
async fn query_locks(&self) -> Result<Locks>
async fn query_catch(&self) -> Result<CatchState>
async fn query_imperfect(&self) -> Result<ImperfectStatus>
async fn query_movement_riding(&self) -> Result<Option<Duration>>
async fn query_bearing(&self) -> Result<Bearing>
async fn query_emit_pace(&self) -> Result<EmitPaceStatus>
async fn query_status(&self) -> Result<ClipStatus>
async fn query_config(&self) -> Result<ClipSettings>
async fn firmware_info(&self) -> Result<FirmwareInfo>

Blocks

cargo add medius --features async

With the async feature, Device::into_async() yields an AsyncDevice whose queries are futures; other methods stay synchronous. The crate is runtime-agnostic (no tokio), so drive a future with anything, such as futures::executor::block_on.

EXAMPLE
use futures::executor::block_on;
use medius::Device;

let device = Device::find()?.into_async();
let v = block_on(device.query_version())?;
let h = block_on(device.query_health())?;
println!("{v} link_up={}", h.link_up);