Options
Seven box settings, each set and read separately, all persisted in NVS across a reboot. The wire contract is on the native OPTION command. factory_reset returns all seven to their defaults.
| Option | Set | Read |
|---|---|---|
| imperfect clone | allow_imperfect_clones | query_imperfect |
| movement riding | set_movement_riding | query_movement_riding |
| emit-rate pacing | set_emit_pace | query_emit_pace |
| box name | set_name / clear_name | Version::name |
| bearing | set_bearing | query_bearing |
| motion texture | set_render | query_render |
| injection spreading | set_spread | query_spread |
allow_imperfect_clones Clone anyway, and enable the advanced control layer
fn allow_imperfect_clones(&self, allow: bool) -> Result<()>
Fire-and-forget
By default the box refuses a real device it can't clone faithfully. true clones it anyway, as an imperfect clone. If such a device is attached, or a forced rate is pending, a change reboots the device chip to re-clone. Otherwise the clone re-presents only if the change alters the patch set it serves: turning the opt-in on with a stored set the box hasn't refused, or off while it serves a set.
| Parameter | Type | Description |
|---|---|---|
allow | bool | Clone anyway, or stay faithful-only. |
| Feature | Opt-in off |
|---|---|
| a real device the box can't clone faithfully | Refused: no clone appears. |
a forced rate, set_emit_pace's force_hz | Not applied. |
| rewrite rules | set_rewrite returns ImperfectRequired. clear_rewrite still runs, and turning the opt-in off clears the table. |
| descriptor patches | Stored, not applied. apply_patch returns ImperfectRequired. |
| control transfers | Replied Refused. |
| raw reports, and a clip's raw and transfer items | Dropped by the box. Turning the opt-in off drops queued clip transfers. |
| a clip packet trigger that consumes | Refused by the box. Turning the opt-in off removes held ones. |
The box can't clone a real device faithfully if it: has >4 IN endpoints live at once or >6 HID interfaces; runs at high speed; has configurations the box didn't capture; has a vendor bulk or isochronous endpoint; has a setting carrying more endpoint descriptors than it declares (what fits is cloned, the rest left undriven); or has a report descriptor truncated or never captured.
A change that reboots the device chip or re-presents the clone drops the box's session state and a loaded clip. The library re-sends what it holds once the new clone is up (session recovery).
use medius::Device;
let device = Device::find()?;
device.allow_imperfect_clones(true)?; // re-clones if the attached device needs the opt-inset_movement_riding Inject motion only on a native move
fn set_movement_riding(&self, window: Option<Duration>) -> Result<()>
Fire-and-forget
Some(window) turns riding on. Injected cursor and wheel motion then ride a native cursor-motion report received within window, with no synthetic motion frame. Motion still unridden after the window is dropped, not emitted on the next move. None (the default) is off.
The window rounds to whole milliseconds and clamps to 65535 ms. A non-zero Some is ≥1 ms.
While riding is on, idle injection (motion while the mouse is still) needs move_rel_now. Button, key and media injection are unaffected.
A change drops motion held for a ride and clears the standing bearing, so every With / Against scale stops applying at once.
| Parameter | Type | Description |
|---|---|---|
window | Option<Duration> | Some with the ride window, or None to turn it off. |
use std::time::Duration;
use medius::Device;
let device = Device::find()?;
device.set_movement_riding(Some(Duration::from_millis(20)))?; // ride native moves
device.set_movement_riding(None)?; // back to gapless fillset_emit_pace Injected-motion pace and clone rate
fn set_emit_pace(&self, pace: EmitPace, force_hz: Option<u16>) -> Result<()>
Fire-and-forget
force_hz writes a bInterval onto every HID interrupt-IN endpoint the clone serves, and the box polls the real device at that interval. A mouse declaring 125 Hz can then run at 1 kHz. The rate resolves to a power-of-two bInterval: 1000/force_hz ms rounded to the nearest millisecond, then down to a power of two (600 Hz gives 500 Hz). A low-speed clone floors at bInterval 16 (62 Hz).
A forced rate applies only with allow_imperfect_clones on, because the descriptor stops matching the real device. Changing the resolved interval re-clones the box: the device chip reboots, and queries time out for a few seconds.
| Parameter | Type | Description |
|---|---|---|
pace | EmitPace | Rate ceiling for injected motion. |
force_hz | Option<u16> | The rate the clone advertises and the box polls the real device at. None keeps the native bInterval. |
use medius::{Device, EmitPace};
let device = Device::find()?;
device.set_emit_pace(EmitPace::Fixed(1000), None)?; // a fixed 1 kHz ceiling
device.set_emit_pace(EmitPace::Learned, None)?; // back to the defaultset_name Human-readable box name
fn set_name(&self, name: &str) -> Result<()>
Fire-and-forget
Sets the box's name, the readable partner to its MAC. The firmware keeps the leading printable-ASCII run, capped at 32 bytes. An empty string clears the name. It reads back on Version::name.
| Parameter | Type | Description |
|---|---|---|
name | &str | The new name, 1-32 printable ASCII characters. |
use medius::Device;
let device = Device::find()?;
device.set_name("Loki")?; // stored in NVS, reported on Version
let name = device.query_version()?.name; // read it back off Versionclear_name Back to the synthesised default
fn clear_name(&self) -> Result<()>
Fire-and-forget
Clears the custom name. The box reverts to the Medius-XXXX default derived from its MAC.
use medius::Device;
let device = Device::find()?;
device.clear_name()?; // back to "Medius-XXXX"set_bearing What With and Against are measured against
fn set_bearing(&self, window: Option<Duration>, mode: BearingMode) -> Result<()>
Fire-and-forget
Sets the bearing: the direction the box is injecting. Direction::With and Direction::Against in scale weigh against it. Each axis holds the direction of its last injected delta until window passes with no injected motion still owed. Then the axis has no bearing, and both relative directions stop applying.
| Parameter | Type | Description |
|---|---|---|
window | Option<Duration> | Some with the hold window, or None to turn the bearing off. With it off, the relative directions do nothing, whatever their scale. |
mode | BearingMode | PerAxis or Vector. |
BEARING_WINDOW_DEFAULT (20 ms) in PerAxis is the factory setting.
Vector weighs a report twice. If Y negative is blocked while the injection runs diagonally, a purely horizontal flick can come out with its vertical share removed.
A change to either field drops the standing bearing and the box's banked fractions. While a relative scale is live, this shows as a visible step.
use std::time::Duration;
use medius::{Axis, BearingMode, Device, Direction};
let device = Device::find()?;
device.set_bearing(Some(Duration::from_millis(20)), BearingMode::PerAxis)?;
device.scale(Axis::X, Direction::Against, 40)?; // motion opposing the injection, at 40%
device.set_bearing(None, BearingMode::PerAxis)?; // and off againset_render Motion texture
fn set_render(&self, mode: RenderMode, full: bool) -> Result<()>
Fire-and-forget
Off is the paced fill: one frame per emit-rate tick while injection is pending. The other three render motion through an ABCurves model (MIT) fitted live from native reports. They differ only in the smoother that feeds the model.
full picks what the model renders: injected motion alone, or injected and native cursor motion as one stream (the native delta is taken out of the relayed report). Buttons, the wheel and every other field stay relayed either way.
Rendering adds the smoother's group delay: 4 ms Stock, 2.7 ms Despiked, none Unsmoothed. With full on, native motion gets that delay +1 ms. move_rel_now, flush_motion and discard_motion skip the model and take the paced path. With full on, the rendered stream ignores movement riding.
The renderer stays out of the path until a profile arms, and while mode is Off. query_render reports both.
| Parameter | Type | Description |
|---|---|---|
mode | RenderMode | Rendering texture. The box boots at Despiked. |
full | bool | Render native motion instead of relaying it. The box boots with it off. |
use medius::{Device, RenderMode};
let device = Device::find()?;
device.set_render(RenderMode::Despiked, false)?; // the default
device.set_render(RenderMode::Despiked, true)?; // render native motion too
device.set_render(RenderMode::Off, false)?; // renderer out of the path, the paced fillset_spread Injected-delta spread in time
fn set_spread(&self, percent: u16) -> Result<()>
Fire-and-forget
A command loop slower than the native report rate sends deltas worth several native reports. percent is the share of the command interval that the box spreads each delta across. 0 emits the whole delta on the next report. 100 spreads it evenly over one interval. Above 100 carries a standing backlog.
| Parameter | Type | Description |
|---|---|---|
percent | u16 | Share of the command interval, in percent. The box boots at 100. |
On average, spreading adds half the command interval of latency (~4 ms on a 125 Hz loop). The delivered total never changes. A loop at the native report rate keeps each command whole, alone on one report.
move_rel_now, flush_motion, discard_motion and wheel motion are never spread.
use medius::Device;
let device = Device::find()?;
device.set_spread(100)?; // the default: one whole command interval
device.set_spread(50)?; // half the interval, for half the added latency
device.set_spread(0)?; // off: the whole delta on the next reportquery_imperfect Imperfect-clone state
fn query_imperfect(&self) -> Result<ImperfectStatus>
Blocks
Returns an ImperfectStatus: the opt-in toggle, whether the attached real device is over-capacity, and whether the live clone is imperfect.
use medius::Device;
let device = Device::find()?;
let status = device.query_imperfect()?;
if status.over_capacity && !status.allowed {
// the device was refused; opt in to clone it imperfectly
device.allow_imperfect_clones(true)?;
}query_movement_riding Ride window
fn query_movement_riding(&self) -> Result<Option<Duration>>
Blocks
Returns the current ride window as a Duration, or None when movement riding is off.
use medius::Device;
let device = Device::find()?;
match device.query_movement_riding()? {
Some(window) => println!("riding, window {window:?}"),
None => println!("off"),
}query_emit_pace Pacing mode and clone rate
fn query_emit_pace(&self) -> Result<EmitPaceStatus>
Blocks
Returns an EmitPaceStatus with the pace and the rates. advertised_hz is the clone's current rate: the native rate while nothing is forced, otherwise the forced rate. It keeps no record of the native rate from before the force.
use medius::{Device, EmitPace};
let device = Device::find()?;
let status = device.query_emit_pace()?;
if let EmitPace::Fixed(hz) = status.mode {
println!("fixed {hz} Hz, emitting at {} Hz", status.resolved_hz);
}
println!("the clone advertises {} Hz", status.advertised_hz);query_bearing Bearing window and geometry
fn query_bearing(&self) -> Result<Bearing>
Blocks
Returns the configured Bearing: the window and the mode.
use medius::Device;
let device = Device::find()?;
let bearing = device.query_bearing()?;
if bearing.is_live() {
println!("{:?} over {:?}", bearing.mode, bearing.window);
}query_render Texture, and whether a profile armed
fn query_render(&self) -> Result<RenderStatus>
Blocks
Returns a RenderStatus. ready is false until a profile arms, and while the box's two chips haven't yet agreed which one holds the motion. Until then, motion is relayed and injection takes the paced fill, whatever mode says.
use medius::Device;
let device = Device::find()?;
let status = device.query_render()?;
println!("{:?}, native motion rendered: {}", status.mode, status.full);
if !status.ready {
println!("move the mouse: nothing is rendered until a profile arms");
}query_spread Percent and interval in effect
fn query_spread(&self) -> Result<SpreadStatus>
Blocks
Returns a SpreadStatus. span_us is 0 in three cases: while percent is 0, until the box has learned the command interval, and while the box's two chips haven't yet agreed which one holds the motion. In each case the whole delta is emitted on the next report.
use medius::Device;
let device = Device::find()?;
let status = device.query_spread()?;
println!("{}% over {} us", status.percent, status.span_us);
if status.span_us == 0 {
println!("send a few moves: the command interval is not learned yet");
}On AsyncDevice setters fire, queries await
AsyncDevice keeps the setters fire-and-forget (no await) and makes query_imperfect, query_movement_riding, query_bearing, query_emit_pace, query_render, and query_spread futures.
use std::time::Duration;
use medius::Device;
let device = Device::find()?.into_async();
device.set_movement_riding(Some(Duration::from_millis(20)))?; // sync, no await
let window = device.query_movement_riding().await?; // awaits