Lifecycle
The library holds overrides past the box's silence-timeout clear, and restores them after a dropped link or when the box drops them.
| Call | Does |
|---|---|
keepalive (automatic) | Holds an override past the silence timeout. |
reapply | Re-sends the held overrides so the box matches the library. |
reconnect | Rescans, reopens the port, and restores held state after a dropped link. |
| session recovery (automatic) | After a device-chip restart or a session release, re-sends held state once the box has a clone again. |
reapplyRe-send held overrides
fn reapply(&self) -> Result<()>
Fire-and-forget
Sends one frame per held override. reconnect does the same after a drop.
device.press(Button::LEFT)?;
device.reapply()?; // re-assert the held override, e.g. after a box reset// 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:
- Rescans for the box by USB identity: vendor ID
0x1A86, product ID0x55D3. - Reopens the port. A box on another protocol, as after a reflash, returns
Error::BadProtoVerand stays disconnected. - Re-applies held overrides, as
reapplydoes. 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. - Bumps the reconnect counter.
// 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 recovery Re-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| Trigger | Detected by |
|---|---|
| A device-chip restart | An 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 off | Stats::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 reset | Counted by the box. The library holds nothing after it, so nothing is re-sent. |
| Held state | After a recovery |
|---|---|
| Held presses, locks and scales, catch subscriptions, rewrite rules, transforms | Re-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 triggers | Re-sent. |
Rewrite rules and consuming packet triggers, after allow_imperfect_clones(false) | Dropped by the library as the box drops them. |
| A loaded clip | Gone with the ring. ClipHandle::lost reports it. |
| An LED override | Not held, so not re-sent: call led again. |
| Options and patch sets | Kept in the box's NVS. |
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 AsyncDevice reapply and reconnect, still direct
AsyncDevice exposes reapply and reconnect directly, with the same signatures. reapply fires and forgets; reconnect blocks.
use medius::AsyncDevice;
let device = AsyncDevice::open("/dev/ttyACM0")?;
device.reapply()?; // re-assert held overrides
device.reconnect()?; // blocks: rescan + reopeninto_inner and into_async switch between the two views. The full async surface is on async.