1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
use crate::error::{Error, Result};
use crate::protocol::command::{patch_apply_payload, patch_clear_payload, patch_payload};
use crate::protocol::opcode::{Q_PATCH_ENTRY, Q_PATCHES};
use crate::protocol::{FrameType, Resp, parse_resp};
use crate::types::patch::patch_entry_from_payload;
use crate::types::{Patch, PatchSet};
use super::Device;
impl Device {
/// `PATCH` (§3.14): store one descriptor patch, keyed by `(section, cfg, index, offset)`.
///
/// Overwrites bytes the clone presents at enumeration, persisted per device (VID:PID) in the
/// box's NVS. Empty [`bytes`](Patch::bytes) removes the patch at that key. The set belongs to
/// the attached device, so a patch stored with no device attached is dropped. A patch is
/// configuration: it survives a reconnect and clears on [`clear_patch`](Device::clear_patch) or
/// [`factory_reset`](Device::factory_reset).
///
/// The box stores a patch with the opt-in on or off. The clone serves it only with
/// [`allow_imperfect_clones`](Device::allow_imperfect_clones) on, from its next presentation:
/// an [`apply_patch`](Device::apply_patch), the opt-in turning on, or the real device
/// attaching. Every section but [`String`](crate::PatchSection::String) keeps the descriptor's
/// byte count. If a patched descriptor fails the box's clone or consistency checks, it is
/// served unpatched and the box logs which ([`PatchSet::refused`]). If the box couldn't read
/// the real device's stored set from flash, it drops edits until it can. Fire-and-forget;
/// [`query_patches`](Device::query_patches) confirms.
pub fn set_patch(&self, patch: &Patch) -> Result<()> {
self.link.send(
FrameType::Patch,
&patch_payload(
patch.section.as_u8(),
patch.cfg,
patch.index,
patch.offset,
&patch.bytes,
),
)
}
/// `PATCH` APPLY (§3.14): re-present the clone with the stored patch set (one unplug/replug to
/// the game PC). Gated on [`allow_imperfect_clones`](Device::allow_imperfect_clones): with the
/// opt-in off the box ignores APPLY, so this returns
/// [`Error::ImperfectRequired`](crate::Error::ImperfectRequired). The box re-presents only if
/// the stored set differs from the served one ([`PatchSet::pending`]). Applying an emptied set
/// serves the descriptors unpatched; an unchanged [refused](PatchSet::refused) set stays as is.
///
/// Re-presenting releases the session like a device replug: once the new clone is up the crate
/// re-sends what it holds, and [`ClipHandle::lost`](crate::ClipHandle::lost) reports a dropped
/// clip.
pub fn apply_patch(&self) -> Result<()> {
self.require_imperfect()?;
self.apply_patch_send()
}
// No opt-in pre-check, so the async wrapper can gate on the async query path.
pub(crate) fn apply_patch_send(&self) -> Result<()> {
self.link.restart_watch().expect_represent();
self.link.send(FrameType::Patch, &patch_apply_payload())
}
/// `PATCH` CLEAR (§3.14): erase the real device's stored set (with no device attached, the last
/// attached device's). A clone serving patches re-presents unpatched (one unplug/replug to the
/// game PC). That releases the session as [`apply_patch`](Device::apply_patch) does, and the
/// crate re-sends held state the same way.
pub fn clear_patch(&self) -> Result<()> {
self.link.restart_watch().expect_represent();
self.link.send(FrameType::Patch, &patch_clear_payload())
}
/// `QUERY(PATCHES)` → [`PatchSet`] (§4.17): stored patch set and apply state (applied, pending,
/// refused, full), a row per patch without its bytes.
/// [`query_patch_entry`](Device::query_patch_entry) reads one in full.
pub fn query_patches(&self) -> Result<PatchSet> {
let payload = self.link.query(Q_PATCHES)?;
match parse_resp(&payload) {
Some(Resp::Patches(s)) => Ok(s),
_ => Err(Error::NoReply),
}
}
/// `QUERY(PATCH_ENTRY, index)` → [`Patch`] (§4.17): one patch in full, in
/// [`set_patch`](Device::set_patch)'s shape so it replays as a set. `index` is the row in the
/// [`query_patches`](Device::query_patches) summary.
pub fn query_patch_entry(&self, index: u8) -> Result<Patch> {
let payload = self.link.query_indexed(Q_PATCH_ENTRY, index)?;
patch_entry_from_payload(&payload).ok_or(Error::NoReply)
}
}