Descriptor patches
Overwrite the bytes the clone presentsA descriptor patch overwrites bytes in what the clone presents at enumeration, keyed by (section, cfg, index, offset) and persisted per 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 only on clear_patch. The box stores a patch whatever the opt-in, and applies the stored set only under it.
native 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 <--------> GET_DESCRIPTOR, SET_*A patch never changes a descriptor's byte count; the box refuses (and logs) an apply that would. Applying is gated on the imperfect-clone opt-in; with allow_imperfect_clones off, apply_patch returns Error::ImperfectRequired.
The setters are fire-and-forget; query_patches reads back the stored set and its apply state.
set_patch
Store or overwrite one patchfn set_patch(&self, patch: &Patch) -> Result<()>
Fire-and-forget
| Parameter | Type | Description |
|---|---|---|
patch | Patch | The overwrite: its section, address, offset, and bytes. |
A patch is keyed by (section, cfg, index, offset); setting one whose key exists overwrites it, and empty bytes removes it. Storing is not gated and does not re-present the clone; apply_patch does.
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 setfn apply_patch(&self) -> Result<()>
Fire-and-forget
Re-presents the clone with the stored patch set: one unplug/replug to the game PC. Gated on allow_imperfect_clones; with the opt-in off it returns Error::ImperfectRequired. A refused apply shows in query_patches's refused flag.
device.allow_imperfect_clones(true)?;
device.apply_patch()?; // the clone replugs and re-presents patchedclear_patch
Drop every patch for this devicefn clear_patch(&self) -> Result<()>
Fire-and-forget
Drops every patch stored for this device (VID:PID) and re-presents the clone unpatched. This is the only thing that clears the stored set; a reconnect does not.
device.clear_patch()?;query_patches
Read the stored set and its apply statefn 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 last apply was refused: a patched length no longer matched what it serves");
}query_patch_entry
Read one patch in fullfn 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 fireAsyncDevice makes apply_patch a future (it awaits the opt-in check), as are query_patches and query_patch_entry. 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