fix(host/input): serve Switch Pro SPI reads by range, not exact pairs #444
@@ -121,27 +121,16 @@ impl SwitchProPad {
|
||||
id,
|
||||
&device_info_payload(&switch_mac(self.index)),
|
||||
),
|
||||
// SPI flash read: echoed addr + len + the canned calibration bytes. An unmapped
|
||||
// range answers zeroes (echoed header, zero data) — the driver then warns and uses
|
||||
// its defaults instead of stalling through 2 × 1 s timeouts.
|
||||
// SPI flash read: echoed addr + len + the flash bytes living at that address. Any
|
||||
// range is served (unmodelled addresses read as zero), because the kernel and SDL
|
||||
// ask for the same calibration in different shapes — see `spi_flash_read`.
|
||||
0x10 => {
|
||||
let addr = args
|
||||
.get(..4)
|
||||
.map(|a| u32::from_le_bytes([a[0], a[1], a[2], a[3]]))
|
||||
.unwrap_or(0);
|
||||
let len = args.get(4).copied().unwrap_or(0);
|
||||
let payload = spi_flash_read(addr, len).unwrap_or_else(|| {
|
||||
tracing::debug!(
|
||||
addr = format!("{addr:#x}"),
|
||||
len,
|
||||
"unmapped SPI read — zero fill"
|
||||
);
|
||||
let mut p = Vec::with_capacity(5 + len as usize);
|
||||
p.extend_from_slice(&addr.to_le_bytes());
|
||||
p.push(len);
|
||||
p.extend(std::iter::repeat_n(0u8, len as usize));
|
||||
p
|
||||
});
|
||||
let payload = spi_flash_read(addr, len);
|
||||
build_subcmd_reply(&st, self.timer, 0x90, id, &payload)
|
||||
}
|
||||
// Everything else the driver sends (input mode 0x03, IMU 0x40, vibration 0x48,
|
||||
|
||||
@@ -348,44 +348,62 @@ pub fn switch_mac(index: u8) -> [u8; 6] {
|
||||
[0x7C, 0xBB, 0x8A, 0xDF, 0x00, index]
|
||||
}
|
||||
|
||||
/// The canned SPI-flash contents (subcommand `0x10`): reply payload = echoed LE address +
|
||||
/// echoed length + the flash bytes. `None` for an unmapped range (the caller then replies with
|
||||
/// zeroes — the driver falls back to defaults rather than aborting).
|
||||
/// The modelled contents of the pad's SPI flash as `(start address, bytes)` blocks. Everything
|
||||
/// outside them reads back zero.
|
||||
///
|
||||
/// Served ranges:
|
||||
/// - `0x8010`/`0x801B`/`0x8026` (user-cal magics, 2 B): NOT `0xB2 0xA1` → user cal absent, the
|
||||
/// driver takes the factory path.
|
||||
/// - `0x603D`/`0x6046` (factory stick cal, 9 B): [`STICK_CENTER`] ± [`STICK_RANGE`] on every
|
||||
/// axis. **Byte order differs**: left = max-above ++ center ++ min-below; right = center ++
|
||||
/// min-below ++ max-above (`joycon_read_stick_calibration`).
|
||||
/// - `0x6020` (factory IMU cal, 24 B): offsets 0, accel scale 16384, gyro scale 13371 — the
|
||||
/// driver's own defaults, making its per-sample math the identity (accel) / ×1000 (gyro).
|
||||
pub fn spi_flash_read(addr: u32, len: u8) -> Option<Vec<u8>> {
|
||||
/// - `0x603D`/`0x6046` (factory stick cal, 9 B each, contiguous): [`STICK_CENTER`] ±
|
||||
/// [`STICK_RANGE`] on every axis. **Byte order differs**: left = max-above ++ center ++
|
||||
/// min-below; right = center ++ min-below ++ max-above (`joycon_read_stick_calibration`).
|
||||
/// - `0x8010`/`0x801B`/`0x8026` (user-cal magics): NOT `0xB2 0xA1` → user cal absent, so every
|
||||
/// consumer takes the factory path.
|
||||
fn flash_blocks() -> [(u32, Vec<u8>); 6] {
|
||||
let cal_pair = pack12(STICK_RANGE, STICK_RANGE);
|
||||
let center_pair = pack12(STICK_CENTER, STICK_CENTER);
|
||||
let data: Vec<u8> = match (addr, len) {
|
||||
(0x8010 | 0x801B | 0x8026, 2) => vec![0xFF, 0xFF],
|
||||
(0x603D, 9) => [cal_pair, center_pair, cal_pair].concat(),
|
||||
(0x6046, 9) => [center_pair, cal_pair, cal_pair].concat(),
|
||||
(0x6020, 24) => {
|
||||
let mut v = Vec::with_capacity(24);
|
||||
v.extend_from_slice(&[0u8; 6]); // accel offsets = 0
|
||||
for _ in 0..3 {
|
||||
v.extend_from_slice(&16384u16.to_le_bytes()); // accel scale (driver default)
|
||||
let mut imu = Vec::with_capacity(24);
|
||||
imu.extend_from_slice(&[0u8; 6]); // accel offsets = 0
|
||||
for _ in 0..3 {
|
||||
imu.extend_from_slice(&16384u16.to_le_bytes()); // accel scale (driver default)
|
||||
}
|
||||
imu.extend_from_slice(&[0u8; 6]); // gyro offsets = 0
|
||||
for _ in 0..3 {
|
||||
imu.extend_from_slice(&13371u16.to_le_bytes()); // gyro scale (driver default)
|
||||
}
|
||||
[
|
||||
(0x6020, imu),
|
||||
(0x603D, [cal_pair, center_pair, cal_pair].concat()),
|
||||
(0x6046, [center_pair, cal_pair, cal_pair].concat()),
|
||||
(0x8010, vec![0xFF, 0xFF]),
|
||||
(0x801B, vec![0xFF, 0xFF]),
|
||||
(0x8026, vec![0xFF, 0xFF]),
|
||||
]
|
||||
}
|
||||
|
||||
/// Serve an SPI-flash read (subcommand `0x10`): reply payload = echoed LE address + echoed
|
||||
/// length + the `len` bytes living at `addr` in [`flash_blocks`].
|
||||
///
|
||||
/// **Answer by RANGE, never by exact `(addr, len)`.** Two consumers read the same flash with
|
||||
/// different shapes, and only one of them is the kernel: `hid-nintendo` reads the two factory
|
||||
/// stick blocks as separate 9-byte reads, while SDL — which is Steam's own Switch driver, and
|
||||
/// therefore what a game actually sees — reads all 18 bytes at `0x603D` in one go and the
|
||||
/// 22-byte user block at `0x8010`. Matching exact pairs served the kernel and silently
|
||||
/// zero-filled Steam, which zeroed its stick centre and pinned both sticks to a corner.
|
||||
pub fn spi_flash_read(addr: u32, len: u8) -> Vec<u8> {
|
||||
let mut data = vec![0u8; len as usize];
|
||||
for (start, bytes) in flash_blocks() {
|
||||
for (i, slot) in data.iter_mut().enumerate() {
|
||||
let a = addr.saturating_add(i as u32);
|
||||
if let Some(b) = a.checked_sub(start).and_then(|o| bytes.get(o as usize)) {
|
||||
*slot = *b;
|
||||
}
|
||||
v.extend_from_slice(&[0u8; 6]); // gyro offsets = 0
|
||||
for _ in 0..3 {
|
||||
v.extend_from_slice(&13371u16.to_le_bytes()); // gyro scale (driver default)
|
||||
}
|
||||
v
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
let mut payload = Vec::with_capacity(5 + data.len());
|
||||
payload.extend_from_slice(&addr.to_le_bytes());
|
||||
payload.push(len);
|
||||
payload.extend_from_slice(&data);
|
||||
Some(payload)
|
||||
payload
|
||||
}
|
||||
|
||||
/// One decoded host-bound output report from the driver.
|
||||
@@ -568,7 +586,7 @@ mod tests {
|
||||
#[test]
|
||||
fn spi_blobs_valid() {
|
||||
for addr in [0x8010u32, 0x801B, 0x8026] {
|
||||
let p = spi_flash_read(addr, 2).unwrap();
|
||||
let p = spi_flash_read(addr, 2);
|
||||
assert_eq!(&p[..4], &addr.to_le_bytes());
|
||||
assert_eq!(p[4], 2);
|
||||
assert!(!(p[5] == 0xB2 && p[6] == 0xA1));
|
||||
@@ -579,7 +597,7 @@ mod tests {
|
||||
(a, y)
|
||||
};
|
||||
// Left: max-above ++ center ++ min-below.
|
||||
let l = spi_flash_read(0x603D, 9).unwrap();
|
||||
let l = spi_flash_read(0x603D, 9);
|
||||
let (data, hdr) = (&l[5..], &l[..5]);
|
||||
assert_eq!(hdr, &[0x3D, 0x60, 0, 0, 9]);
|
||||
let (max_above, _) = unpack(&data[0..3]);
|
||||
@@ -588,18 +606,46 @@ mod tests {
|
||||
assert_eq!(center, STICK_CENTER);
|
||||
assert!(center - min_below < center && center < center + max_above);
|
||||
// Right: center ++ min-below ++ max-above.
|
||||
let r = spi_flash_read(0x6046, 9).unwrap();
|
||||
let r = spi_flash_read(0x6046, 9);
|
||||
let (rc, _) = unpack(&r[5..8]);
|
||||
assert_eq!(rc, STICK_CENTER);
|
||||
// IMU: offsets 0, driver-default scales — the identity calibration.
|
||||
let imu = spi_flash_read(0x6020, 24).unwrap();
|
||||
let imu = spi_flash_read(0x6020, 24);
|
||||
let d = &imu[5..];
|
||||
assert_eq!(&d[0..6], &[0; 6]);
|
||||
assert_eq!(&d[6..8], &16384u16.to_le_bytes());
|
||||
assert_eq!(&d[12..18], &[0; 6]);
|
||||
assert_eq!(&d[18..20], &13371u16.to_le_bytes());
|
||||
// Unmapped range → None.
|
||||
assert!(spi_flash_read(0x6050, 12).is_none());
|
||||
// An unmodelled range still answers, echoing addr+len over zero data.
|
||||
let gap = spi_flash_read(0x6050, 12);
|
||||
assert_eq!(&gap[..5], &[0x50, 0x60, 0, 0, 12]);
|
||||
assert_eq!(&gap[5..], &[0u8; 12]);
|
||||
}
|
||||
|
||||
/// ⭐ SDL — Steam's own Switch driver, and so what a GAME sees — reads the same calibration
|
||||
/// in shapes `hid-nintendo` never asks for: all 18 factory bytes at `0x603D` in one read,
|
||||
/// and the 22-byte user block at `0x8010`. Serving reads by exact `(addr, len)` answered the
|
||||
/// kernel and zero-filled Steam, which zeroed its stick centre so every raw value read as
|
||||
/// positive — both sticks pinned toward one corner, unable to travel past centre.
|
||||
#[test]
|
||||
fn spi_serves_sdl_read_shapes() {
|
||||
// Factory: one 18-byte read = the left block then the right block, contiguous.
|
||||
let f = spi_flash_read(0x603D, 18);
|
||||
assert_eq!(&f[..5], &[0x3D, 0x60, 0, 0, 18]);
|
||||
assert_eq!(&f[5..14], &spi_flash_read(0x603D, 9)[5..]);
|
||||
assert_eq!(&f[14..], &spi_flash_read(0x6046, 9)[5..]);
|
||||
// SDL's own centre parse over its left half must land on our centre, not 0.
|
||||
let cal = &f[5..];
|
||||
let cx = (((cal[4] as u16) << 8) & 0xF00) | cal[3] as u16;
|
||||
let cy = ((cal[5] as u16) << 4) | ((cal[4] as u16) >> 4);
|
||||
assert_eq!((cx, cy), (STICK_CENTER, STICK_CENTER));
|
||||
// User: one 22-byte read; both magics sit where SDL looks and neither is B2 A1.
|
||||
let u = spi_flash_read(0x8010, 22);
|
||||
assert_eq!(&u[..5], &[0x10, 0x80, 0, 0, 22]);
|
||||
let user = &u[5..];
|
||||
assert_eq!(user.len(), 22);
|
||||
assert_eq!(&user[0..2], &[0xFF, 0xFF]); // left magic @ 0x8010
|
||||
assert_eq!(&user[11..13], &[0xFF, 0xFF]); // right magic @ 0x801B
|
||||
}
|
||||
|
||||
/// Motion unit conversion: wire (20 LSB/°·s, 10000 LSB/g) → raw (14.247 LSB/°·s, 4096 LSB/g).
|
||||
|
||||
Reference in New Issue
Block a user