Requests
One QUERY frame out, one RESP frame backUnlike 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-tripfn query_version(&self) -> Result<Version>
Blocks
Returns a Version. The box's name rides on it, in the name field.
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); // LokiDevice::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 pathfn 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.
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 clonefn 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.
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 devicefn 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.
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 ratefn 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.
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 countersfn 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.
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 scalesfn 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.
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 subscriptionfn 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.
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.
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 statefn 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).
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 backfn 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.
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 stagedfn firmware_info(&self) -> Result<FirmwareInfo>
Blocks
Backs QUERY(FIRMWARE). The other firmware calls live on the update page.
| Field | Type | Notes |
|---|---|---|
device | ChipFirmware | version, slot, and image state |
host | Option<ChipFirmware> | None when the host chip has not answered over the inter-chip link |
slot_size | u32 | usable bytes in a spare slot, the same on both chips |
device_staged | bool | an image is written and waiting to be activated |
host_staged | bool | the same, for the host chip |
This is the only call that reports the host chip's version. query_version reports the device chip alone.
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 AsyncDeviceasync 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 asyncWith 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.
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);