Descriptor patches
A descriptor patch overwrites bytes in what the clone presents at enumeration. Patches are keyed by (section, cfg, index, offset) and stored per real device (VID:PID) in the box's NVS.
Unlike a rewrite rule, a patch is configuration, not session state. It survives a reconnect and clears on clear_patch or factory_reset. The box stores a patch whatever the opt-in, but applies the stored set only with the opt-in on.
real device the box (host chip | device chip = the clone) game PC
descriptors --------------------> [ descriptor patches ] -----enumerate----> device descriptor
overwrite what the configuration
clone presents report / string / BOS
HID report ---IN---> [ HID_IN ]--> renderer --> [ EMIT ]---interrupt-IN----> reads report
control <-- EP0 -> [ CONTROL ]<-- proxy ------------------- EP0 <--------> class, vendor requestsA patch keeps a descriptor's length, except a STRING patch, which replaces the whole string. Patched descriptors that fail the box's clone or consistency checks are served unpatched, and the box logs which check failed. Applying needs allow_imperfect_clones. With it off, apply_patch returns Error::ImperfectRequired.
| Re-presented at | With |
|---|---|
apply_patch | The stored set, when it differs from the one the clone serves. |
allow_imperfect_clones changing the set the clone serves | Turned on: the stored set, unless the box refused it. Turned off: no patches, if the clone serves some. |
| The real device attaching | The stored set, under the opt-in. |
clear_patch | No patches, if the clone serves some. |
Each presentation is a re-clone, which releases the session. The library re-sends what it holds once the new clone is up (session recovery).
The setters are fire-and-forget. query_patches reads back the stored set and its apply state.
set_patch Store or overwrite one patch
fn set_patch(&self, patch: &Patch) -> Result<()>
Fire-and-forget
Patches are keyed by (section, cfg, index, offset). Setting an existing key overwrites the patch and moves it to the end of the set, unless the patch already holds those bytes. Empty bytes removes it. The set belongs to the attached real device, so a patch stored with none attached is dropped.
The clone serves the set it was presented with, STRING patches included. A stored change reaches the game PC at the next presentation. The limits and wire layout are on the native PATCH command.
use medius::{Device, Patch, PatchSection};
let device = Device::find()?;
// Overwrite idVendor in the device descriptor (offset 8, little-endian), then re-present.
device.set_patch(&Patch::new(PatchSection::Device, 8, [0x34, 0x12]))?;
device.allow_imperfect_clones(true)?;
device.apply_patch()?;apply_patch Re-present the clone with the stored set
fn apply_patch(&self) -> Result<()>
Fire-and-forget
Re-presents the clone with the stored patch set (native APPLY): one unplug/replug to the game PC. Gated on allow_imperfect_clones. With the opt-in off, it returns Error::ImperfectRequired.
| Stored set | Box action |
|---|---|
Differs from the set the clone serves (pending) | Re-presents the clone with it: a re-clone that releases the session. An emptied set re-presents the clone unpatched. A set that fails a check is served unpatched, with refused set. |
| The set the clone serves | Nothing. |
| Refused, and unchanged since | Nothing, though it reads pending: change the set, or clear it. |
| Any, with no real device attached | Nothing. |
device.allow_imperfect_clones(true)?;
device.apply_patch()?; // the clone replugs and re-presents patchedclear_patch Erase the stored set
fn clear_patch(&self) -> Result<()>
Fire-and-forget
Erases every patch stored for this real device (VID:PID). With the real device unplugged, it erases the set of the last one attached since boot (native CLEAR).
| Clone | Box action |
|---|---|
| Serves patches | Erases the set and re-presents the clone unpatched: one unplug/replug to the game PC, and a re-clone that releases the session. |
| Serves none | Erases the set and leaves the clone as is. |
device.clear_patch()?;query_patches Stored set and apply state
fn query_patches(&self) -> Result<PatchSet>
Blocks
Returns a PatchSet: the four apply-state flags and a row per stored patch, without its bytes.
let set = device.query_patches()?;
println!("{} patches, applied={} pending={}", set.entries.len(), set.applied, set.pending);
if set.refused {
eprintln!("the set failed a check, so the clone is unpatched; the box log names it");
}query_patch_entry One patch in full
fn query_patch_entry(&self, index: u8) -> Result<Patch>
Blocks
| Parameter | Type | Description |
|---|---|---|
index | u8 | The row in the query_patches summary. |
Returns one Patch in full, in the shape set_patch takes.
let set = device.query_patches()?;
for i in 0..set.entries.len() as u8 {
let patch = device.query_patch_entry(i)?; // replayable as a set
let _ = patch;
}On AsyncDevice apply_patch and the queries await; set and clear fire
AsyncDevice makes apply_patch, query_patches and query_patch_entry futures (apply_patch awaits the opt-in check). set_patch and clear_patch stay synchronous.
use futures::executor::block_on;
use medius::{AsyncDevice, Patch, PatchSection};
let device = AsyncDevice::open("/dev/ttyACM0")?;
device.set_patch(&Patch::new(PatchSection::Device, 8, [0x34, 0x12]))?; // sync
device.allow_imperfect_clones(true)?;
block_on(device.apply_patch())?; // awaits the opt-in gate
let set = block_on(device.query_patches())?; // query awaits