lib: Add shmancy firmware dumping and verbose printing

This commit is contained in:
John 2025-08-02 06:33:44 -04:00
parent 8d4238fe3e
commit 8b56a1bc5d
3 changed files with 171 additions and 32 deletions

3
.gitignore vendored
View File

@ -1 +1,4 @@
/target /target
# Firmware dump files
*.bin

View File

@ -1,8 +1,7 @@
//! Library for initializing a pro controller //! Library for initializing a pro controller
//! //!
//! Code stolen from https://handheldlegend.github.io/procon2tool/ //! Code stolen from [HandHeldLegend procon2tool](https://handheldlegend.github.io/procon2tool/)
#![allow(non_upper_case_globals)] #![allow(non_upper_case_globals, clippy::identity_op, dead_code)]
#![allow(dead_code)]
use rusb::DeviceHandle; use rusb::DeviceHandle;
use std::time::Duration; use std::time::Duration;
@ -21,49 +20,126 @@ pub mod descriptor {
pub struct Controller<'t, Ctx: rusb::UsbContext> { pub struct Controller<'t, Ctx: rusb::UsbContext> {
device: &'t DeviceHandle<Ctx>, device: &'t DeviceHandle<Ctx>,
verbose: bool,
} }
impl<'t, Ctx: rusb::UsbContext> Controller<'t, Ctx> { impl<'t, Ctx: rusb::UsbContext> Controller<'t, Ctx> {
pub fn new(device: &'t DeviceHandle<Ctx>) -> Self { pub fn new(device: &'t DeviceHandle<Ctx>) -> Self {
Self { device } Self {
device,
verbose: false,
}
}
pub fn verbose(self, verbose: bool) -> Self {
Self { verbose, ..self }
}
fn dbg_write(&self, data: &[u8], timeout: Duration) -> rusb::Result<usize> {
let mut buf = [0; 128];
#[allow(unused_assignments)] // Not actually unused, the linter is dumb
let mut len = 0;
let out = self.write(data, timeout)?;
len = self.read(&mut buf, Duration::from_millis(100)).unwrap_or(0);
if self.verbose {
println!("OUT: {data:02x?}\n => {:02x?}", &buf[..len]);
}
Ok(out)
}
fn write(&self, data: &[u8], timeout: Duration) -> rusb::Result<usize> {
self.device.write_bulk(0x02, data, timeout)
}
fn read(&self, buf: &mut [u8], timeout: Duration) -> rusb::Result<usize> {
self.device.read_bulk(0x82, buf, timeout)
} }
pub fn init(&self) -> rusb::Result<usize> { pub fn init(&self) -> rusb::Result<usize> {
let Self { device } = self;
let mut out: usize = 0; let mut out: usize = 0;
let timeout = Duration::from_millis(100); let timeout = Duration::from_secs(1);
let endpoint: u8 = 2;
out += device.write_bulk(endpoint, INIT_COMMAND_0x03, timeout)?; if self.verbose {
out += device.write_bulk(endpoint, UNKNOWN_COMMAND_0x07, timeout)?; println!("Initializing:");
out += device.write_bulk(endpoint, UNKNOWN_COMMAND_0x16, timeout)?; }
out += device.write_bulk(endpoint, REQUEST_CONTROLLER_MAC, timeout)?;
// out += device.write_bulk(endpoint, LTK_REQUEST, timeout)?; out += self.dbg_write(INIT_COMMAND_0x03, timeout)?;
out += device.write_bulk(endpoint, UNKNOWN_COMMAND_0x15_ARG_0x03, timeout)?; out += self.dbg_write(UNKNOWN_COMMAND_0x07, timeout)?;
out += self.dbg_write(UNKNOWN_COMMAND_0x16, timeout)?;
out += self.dbg_write(REQUEST_CONTROLLER_MAC, timeout)?;
out += self.dbg_write(LTK_REQUEST, timeout)?;
out += self.dbg_write(UNKNOWN_COMMAND_0x15_ARG_0x03, timeout)?;
out += self.set_player_led(0)?; out += self.set_player_led(0)?;
out += device.write_bulk(endpoint, IMU_COMMAND_0x02, timeout)?; out += self.dbg_write(IMU_COMMAND_0x02, timeout)?;
out += device.write_bulk(endpoint, OUT_UNKNOWN_COMMAND_0x11, timeout)?; out += self.dbg_write(OUT_UNKNOWN_COMMAND_0x11, timeout)?;
out += device.write_bulk(endpoint, UNKNOWN_COMMAND_0x0A, timeout)?; out += self.dbg_write(UNKNOWN_COMMAND_0x0A, timeout)?;
out += device.write_bulk(endpoint, IMU_COMMAND_0x04, timeout)?; out += self.dbg_write(IMU_COMMAND_0x04, timeout)?;
out += device.write_bulk(endpoint, ENABLE_HAPTICS, timeout)?; out += self.dbg_write(ENABLE_HAPTICS, timeout)?;
out += device.write_bulk(endpoint, OUT_UNKNOWN_COMMAND_0x10, timeout)?; out += self.dbg_write(OUT_UNKNOWN_COMMAND_0x10, timeout)?;
out += device.write_bulk(endpoint, OUT_UNKNOWN_COMMAND_0x01, timeout)?; out += self.dbg_write(OUT_UNKNOWN_COMMAND_0x01, timeout)?;
out += device.write_bulk(endpoint, OUT_UNKNOWN_COMMAND_0x03, timeout)?; out += self.dbg_write(OUT_UNKNOWN_COMMAND_0x03, timeout)?;
out += device.write_bulk(endpoint, OUT_UNKNOWN_COMMAND_0x0A_ALT, timeout)?; out += self.dbg_write(OUT_UNKNOWN_COMMAND_0x0A_ALT, timeout)?;
Ok(out) Ok(out)
} }
pub fn set_player_led(&self, pattern: u8) -> rusb::Result<usize> { pub fn set_player_led(&self, pattern: u8) -> rusb::Result<usize> {
let Self { device } = self; let timeout = Duration::from_secs(1);
let mut resp = [0; 16];
#[rustfmt::skip] #[rustfmt::skip]
let buf: [u8; 16] = [ let cmd: [u8; 16] = [
0x09, 0x91, 0x00, 0x07, 0x00, 0x08, 0x00, 0x00, 0x09, 0x91, 0x00, 0x07, 0x00, 0x08, 0x00, 0x00,
pattern, // LED bitfield - replace 0x00 with desired LED pattern pattern, // LED bitfield - replace 0x00 with desired LED pattern
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]; ];
device.write_bulk(2, &buf, Duration::from_millis(16)) let out = self.write(&cmd, timeout)?;
// ACK? [09 01 00 07 00 f8 00 00]
let len = self.read(&mut resp, timeout)?;
if self.verbose {
println!("LED: {cmd:02x?}\n => {:02x?}", &resp[..len]);
}
Ok(out)
}
pub fn dump_spi(&self, address: usize) -> rusb::Result<[u8; 64]> {
let Self { device, .. } = self;
let mut buf = [0; 16 + 64];
let txpoint = 2;
let rxpoint = 130;
let timeout = Duration::from_millis(100);
#[rustfmt::skip]
let command = [
// Command
0x02, 0x91, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00,
0x40, // Length (64 bytes)
0x7e, // Read command
0x00, 0x00,
// Address (little endian)
(address >> 0) as u8, // Address field 3
(address >> 8) as u8, // Address field 2
(address >> 16) as u8, // Address field 1
(address >> 24) as u8, // Address field 0
];
device.write_bulk(txpoint, &command, timeout)?;
device.read_bulk(rxpoint, &mut buf, timeout)?;
// debug-print the header
if self.verbose {
println!("HEAD: {:02x?}", &buf[..16]);
}
let mut data = [0; 64];
data.copy_from_slice(&buf[16..]);
Ok(data)
} }
} }
@ -77,6 +153,7 @@ const INIT_COMMAND_0x03: &[u8] = &[
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // Console MAC Address (Little Endian) 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // Console MAC Address (Little Endian)
]; ];
/// Probably an ACK of some kind
#[rustfmt::skip] #[rustfmt::skip]
const UNKNOWN_COMMAND_0x07: &[u8] = &[ const UNKNOWN_COMMAND_0x07: &[u8] = &[
0x07, 0x91, 0x00, 0x01, 0x07, 0x91, 0x00, 0x01,

View File

@ -1,10 +1,9 @@
use std::{thread::sleep, time::Duration}; use std::{error::Error, fmt::Display, io::Write, path::Path, thread::sleep, time::Duration};
use procon2::{ use procon2::{
Controller, Controller,
descriptor::{VID, pid}, descriptor::{VID, pid},
}; };
use rusb::Result;
#[rustfmt::skip] #[rustfmt::skip]
const PLAYER: [u8; 16] = [ const PLAYER: [u8; 16] = [
@ -26,7 +25,17 @@ const PLAYER: [u8; 16] = [
0b1010, 0b1010,
]; ];
fn main() -> Result<()> { fn main() -> Result<(), Box<dyn Error>> {
let (mut dump, mut verbose) = (false, false);
for arg in std::env::args() {
match arg.as_str() {
"dump" => dump = true,
"verbose" => verbose = true,
_ => {}
}
}
let mut devices = vec![]; let mut devices = vec![];
for device in rusb::devices()?.iter() { for device in rusb::devices()?.iter() {
@ -36,25 +45,75 @@ fn main() -> Result<()> {
println!("Found player {}: {device:?}!", devices.len() + 1); println!("Found player {}: {device:?}!", devices.len() + 1);
let device = device.open()?; let device = device.open()?;
let controller = Controller::new(&device); let controller = Controller::new(&device);
controller.verbose(verbose).init()?;
controller.init()?; device.claim_interface(1)?;
devices.push(device); devices.push(device);
} }
} }
if dump {
for (player, device) in devices.iter().enumerate() {
let controller = Controller::new(device).verbose(verbose);
let descriptor = device.device().device_descriptor()?;
let name = if let Some(nid) = descriptor.product_string_index() {
device.read_string_descriptor_ascii(nid)?
} else {
String::from("procon")
};
let path = format!(
"{name} {:04x}_{:04x}_{}-{player:x}.bin",
descriptor.vendor_id(),
descriptor.product_id(),
descriptor.device_version()
);
procon_dump(controller, path)?;
}
return Ok(());
}
// Play a shmancy pattern in-sync on all controllers // Play a shmancy pattern in-sync on all controllers
for pattern in &PLAYER[0..8] { for pattern in &PLAYER[0..8] {
for device in devices.iter() { for device in devices.iter() {
Controller::new(device).set_player_led(*pattern)?; Controller::new(device)
.verbose(verbose)
.set_player_led(*pattern)?;
} }
sleep(Duration::from_millis(100)); sleep(Duration::from_millis(100));
} }
// Set the controller IDs // Set the controller IDs
for (count, device) in devices.iter().enumerate() { for (count, device) in devices.iter().enumerate() {
Controller::new(device).set_player_led(PLAYER[(count + 1) & 0xf])?; Controller::new(device)
.verbose(verbose)
.set_player_led(PLAYER[(count + 1) & 0xf])?;
} }
Ok(()) Ok(())
} }
fn procon_dump<Ctx: rusb::UsbContext>(
controller: Controller<'_, Ctx>,
path: impl AsRef<Path> + Display,
) -> Result<(), Box<dyn Error>> {
let mut file = std::fs::File::create(path.as_ref()).unwrap();
print!("Dumping firmware");
std::io::stdout().flush()?;
for addr in (0..0x200000).step_by(64) {
let data = controller.dump_spi(addr)?;
file.write_all(&data)?;
// progress bar
if addr % 0x8000 == 0 {
controller.set_player_led(PLAYER[7 - (addr >> 15 & 7)])?;
print!(".");
std::io::stdout().flush()?;
sleep(Duration::from_millis(0));
}
}
controller.set_player_led(0)?;
println!("\nDumped to file {path}");
Ok(())
}