reapplyRe-send held overrides

fn reapply(&self) -> Result<()>

Fire-and-forget

Sends one frame per held override. reconnect does the same after a drop.

EXAMPLE
device.press(Button::LEFT)?;
device.reapply()?; // re-assert the held override, e.g. after a box reset
EXAMPLE
// The no-op case: reset clears every override, so reapply sends nothing.
device.reset()?;
device.reapply()?;

Overrides are keyed by Usage (button, key, or media). press and force_release add a held override. release and reset clear them.

reconnectRescan, reopen the port, and restore held state

fn reconnect(&self) -> Result<()>

Blocks

The reader thread auto-reconnects on any read error. Call this only to force a rescan. It blocks while it rescans, reopens the port and matches the box's MAC from its version reply. It errors if the box can't be found or opened.

On each call:

  1. Rescans for the box by USB identity: vendor ID 0x1A86, product ID 0x55D3.
  2. Reopens the port. A box on another protocol, as after a reflash, returns Error::BadProtoVer and stays disconnected.
  3. Re-applies held overrides, as reapply does. If the box's hello arrives during the rescan, the box booted while the link was down. It then gets held state through session recovery.
  4. Bumps the reconnect counter.
EXAMPLE
// After an unplug and replug, force a rescan and confirm it.
let before = device.counters().reconnects;
device.reconnect()?;
let after = device.counters().reconnects;
assert!(after > before);

reconnects is a diagnostics counter.

Session recoveryRe-sending what the box dropped

The box drops what a host set in two cases: when its device chip boots, and when it releases the session without a boot (counted in Stats::session). The library notices both, waits for a clone and re-sends what it holds. restarts counts only the boots.

  box                                                    library
  boots    ---- RESP(VERSION), SEQ 0 ----------------->  a hello it did not ask for: a restart
  release  <--- QUERY(STATS) --------------------------  on the keepalive
           ---- RESP(STATS) -------------------------->  session moved: a release
           <--- QUERY(CAPS), repeated -----------------  until a HID interface is bound,
           ---- RESP(CAPS) --------------------------->  or, with none,
           <--- QUERY(DEVICE_INFO) --------------------  until a device is cloned
           ---- RESP(DEVICE_INFO) -------------------->
           <--- INJECT, LOCK, CATCH -------------------  } the held state
           <--- REWRITE, TRANSFORM --------------------  }
           <--- CLIP_SET, CLIP_TRIGGER ----------------  the clip's settings and triggers
           <--- QUERY(CLIP) ---------------------------
           ---- RESP(CLIP) --------------------------->  an empty ring: the loaded clip is lost
                                                         restarts += 1, for a restart only
TriggerDetected by
A device-chip restartAn unasked ready hello from the box after the handshake.
A session release: a RESET the library did not send, the inter-chip link dropping, the real device detaching, a re-clone (apply_patch, clear_patch, an opt-in toggle that re-presents the clone, another real device), a configuration switch by the game PC that unbinds a role, control-PC silence, the opt-in turned offStats::session moving. The keepalive reads it each tick while anything is held, and every 20 ms for 5 s after apply_patch, clear_patch or allow_imperfect_clones.
The library's resetCounted by the box. The library holds nothing after it, so nothing is re-sent.
Held stateAfter a recovery
Held presses, locks and scales, catch subscriptions, rewrite rules, transformsRe-sent once the clone is up: at once if it stayed up, or after a detach, when a device is cloned again.
Clip settings, input triggers and packet triggersRe-sent.
Rewrite rules and consuming packet triggers, after allow_imperfect_clones(false)Dropped by the library as the box drops them.
A loaded clipGone with the ring. ClipHandle::lost reports it.
An LED overrideNot held, so not re-sent: call led again.
Options and patch setsKept in the box's NVS.
EXAMPLE
use medius::{Axis, ClipBuilder, Device, Direction};

let device = Device::find()?;
let clip = device.clip();
let mut burst = ClipBuilder::new();
burst.move_by(4, 0);
device.scale(Axis::X, Direction::Both, 40)?;
clip.append(&burst)?;

let before = device.query_stats()?.session;
device.allow_imperfect_clones(true)?;
device.apply_patch()?;          // a re-clone: the box releases the session
// ... the library re-sends the scale once the new clone is up ...
if device.query_stats()?.session != before && clip.lost() {
    clip.append(&burst)?;       // the caller reloads the ring
}

On AsyncDevicereapply and reconnect, still direct

AsyncDevice exposes reapply and reconnect directly, with the same signatures. reapply fires and forgets; reconnect blocks.

EXAMPLE
use medius::AsyncDevice;

let device = AsyncDevice::open("/dev/ttyACM0")?;
device.reapply()?;     // re-assert held overrides
device.reconnect()?;   // blocks: rescan + reopen

into_inner and into_async switch between the two views. The full async surface is on async.