From 68a6f6a92c11899edb6d6d975b6911214f1b53eb Mon Sep 17 00:00:00 2001 From: Karolis Stasaitis Date: Sat, 9 Mar 2024 21:29:08 +0100 Subject: [PATCH] reboot functionality, win device enumeration fix, list hid devices, rk84-iso support (#47) Initiated from https://github.com/carlossless/sinowealth-kb-tool/issues/45 and https://github.com/carlossless/sinowealth-kb-tool/pull/46 This PR adds: * Sorting to device enumeration, so that `data` and `request` devices on Windows are selected deterministically. * Reboot functionality for devices that use bootloaders that are currently known to support it - `cfc8661d`. Also exposed as an arg, so that it can be enabled/disabled whenever that's not the default option for the device/bootloader. * A section in the readme tracking bootloader / host platform support and bootloader / function support. * The Royal Kludge RK84 part. * A new debugging function meant to list out all connected HID devices. Thanks to @Luro02 who tracked down the device enumeration issue and proposed a fix for it (along with the reboot feature, list feature, and rk84 device report)! --------- Co-authored-by: Luro02 <24826124+Luro02@users.noreply.github.com> --- README.md | 36 +++++++++++++++++++++++ src/isp.rs | 62 +++++++++++++++++++++++++++++++++++++++- src/main.rs | 15 +++++++++- src/part.rs | 17 ++++++++++- tools/functional-test.sh | 7 ++--- 5 files changed, 129 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0f1f35c..cd0cb71 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ sinowealth-kb-tool read \ --isp_usage_page 0xff00 \ # optional --isp_usage 0x0001 \ # optional --isp_index 0 \ # optional + --reboot false \ # optional foobar.hex ``` @@ -59,6 +60,7 @@ sinowealth-kb-tool write \ --isp_usage_page 0xff00 \ # optional --isp_usage 0x0001 \ # optional --isp_index 0 \ # optional + --reboot false \ # optional foobar.hex ``` @@ -81,11 +83,36 @@ sinowealth-kb-tool write \ | Royal Kludge RK68 BT Dual | cfc8661da8c9d7e351b36c0a763426aa | SH68F90? | BYK901 | ✅ | ✅ | | Royal Kludge RK68 ISO Return | ❓ | SH68F90? | BYK916 | ✅ | ❓ | | [Royal Kludge RK71](http://en.rkgaming.com/product/12/) | cfc8661da8c9d7e351b36c0a763426aa | SH68F90? | ❓ | ✅ | ✅ | +| [Royal Kludge RK84](http://en.rkgaming.com/product/16/) | cfc8661da8c9d7e351b36c0a763426aa | SH68F90? | BYK916 | ✅ | ✅ | | Terport TR95 | 2d169670eae0d36eae8188562c1f66e8 | SH68F90A | BYK916 | ✅ | ❓ | | Weikav Sugar65 | 2d169670eae0d36eae8188562c1f66e8 | SH68F90 | SH68F90S | ✅ | ❓ | | Xinmeng K916 | cfc8661da8c9d7e351b36c0a763426aa | SH68F90 | ❓ | ✅ | ✅ | | Xinmeng XM-RF68 | 2d169670eae0d36eae8188562c1f66e8 | SH68F90 | SH68F90U | ✅ | ✅ | +## Bootloader Support + +### Platforms + +| ISP MD5 | Windows | macOS | Linux | +| -------------------------------- | -------- | -------- | ----- | +| 3e0ebd0c440af5236d7ff8872343f85d | ok | ok | ok | +| cfc8661da8c9d7e351b36c0a763426aa | ok | fail[^1] | ok | +| 2d169670eae0d36eae8188562c1f66e8 | ok | ? | ok | +| e57490acebcaabfcff84a0ff013955d9 | ok | ? | ? | +| 13df4ce2933f9654ffef80d6a3c27199 | ? | ? | ok | + +[^1]: macOS does not recognize the composite device as an HID device + +### Functions + +| ISP MD5 | Reboot | +| -------------------------------- | ------ | +| 3e0ebd0c440af5236d7ff8872343f85d | no | +| cfc8661da8c9d7e351b36c0a763426aa | yes | +| 2d169670eae0d36eae8188562c1f66e8 | ? | +| e57490acebcaabfcff84a0ff013955d9 | ? | +| 13df4ce2933f9654ffef80d6a3c27199 | ? | + ## Prerequisites ### Linux @@ -100,6 +127,15 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="0603", ATTRS{idProduct}=="1020", MODE="0660 Make sure your user is part of the `plugdev` group. +### macOS + +If you encounter errors like: +``` +hid_open_path: failed to open IOHIDDevice from mach entry... +``` + +Ensure that your terminal application has [access to input monitoring](https://support.apple.com/guide/mac-help/control-access-to-input-monitoring-on-mac-mchl4cedafb6/mac). + ## Acknowledgments Thanks to [@gashtaan](https://github.com/gashtaan) for analyzing and explaining the inner workings of the ISP bootloaders. Without his help, this tool wouldn't be here! diff --git a/src/isp.rs b/src/isp.rs index 4f56f8a..3169fda 100644 --- a/src/isp.rs +++ b/src/isp.rs @@ -17,7 +17,9 @@ const GAMING_KB_PRODUCT_ID: u16 = 0x1020; const COMMAND_LENGTH: usize = 6; +#[cfg(not(target_os = "linux"))] const HID_ISP_USAGE_PAGE: u16 = 0xff00; +#[cfg(not(target_os = "linux"))] const HID_ISP_USAGE: u16 = 0x0001; const REPORT_ID_CMD: u8 = 0x05; @@ -28,6 +30,7 @@ const CMD_ENABLE_FIRMWARE: u8 = 0x55; const CMD_INIT_READ: u8 = 0x52; const CMD_INIT_WRITE: u8 = 0x57; const CMD_ERASE: u8 = 0x45; +const CMD_REBOOT: u8 = 0x5a; const XFER_READ_PAGE: u8 = 0x72; const XFER_WRITE_PAGE: u8 = 0x77; @@ -75,6 +78,42 @@ impl ISPDevice { }) } + /// Prints out all connected HID devices and their paths. + pub fn print_connected_devices() -> Result<(), ISPError> { + let api = ISPDevice::hidapi(); + + info!("Listing all connected HID devices..."); + let mut devices: Vec<_> = api.device_list().collect(); + + devices.sort_by_key(|d| d.path()); + + for d in &devices { + #[cfg(not(target_os = "linux"))] + info!( + "{:}: ID {:04x}:{:04x} manufacturer=\"{:}\" product=\"{:}\" usage_page={:#06x} usage={:#06x}", + d.path().to_str().unwrap(), + d.vendor_id(), + d.product_id(), + d.manufacturer_string().unwrap_or("None"), + d.product_string().unwrap_or("None"), + d.usage_page(), + d.usage() + ); + #[cfg(target_os = "linux")] + info!( + "{:}: ID {:#04x}:{:#04x} manufacturer=\"{:}\" product=\"{:}\"", + d.path().to_str().unwrap(), + d.vendor_id(), + d.product_id(), + d.manufacturer_string().unwrap_or("None"), + d.product_string().unwrap_or("None") + ); + } + info!("Found {} devices", devices.len()); + + Ok(()) + } + fn hidapi() -> HidApi { let api = HidApi::new().unwrap(); @@ -87,7 +126,7 @@ impl ISPDevice { fn open_isp_devices() -> Result { let api = Self::hidapi(); - let devices: Vec<_> = api + let mut devices: Vec<_> = api .device_list() .filter(|d| { #[cfg(not(target_os = "linux"))] @@ -103,6 +142,8 @@ impl ISPDevice { }) .collect(); + devices.sort_by_key(|d| d.path()); + for d in &devices { #[cfg(not(target_os = "linux"))] debug!( @@ -255,6 +296,10 @@ impl ISPDevice { ReadType::Full => self.read(0, self.part.firmware_size + self.part.bootloader_size)?, }; + if self.part.reboot { + self.reboot()?; + } + return Ok(firmware); } @@ -274,6 +319,11 @@ impl ISPDevice { util::verify(firmware, &read_back).map_err(ISPError::from)?; self.enable_firmware()?; + + if self.part.reboot { + self.reboot()?; + } + Ok(()) } @@ -401,4 +451,14 @@ impl ISPDevice { thread::sleep(time::Duration::from_millis(2000)); Ok(()) } + + fn reboot(&self) -> Result<(), ISPError> { + info!("Rebooting..."); + let cmd: [u8; COMMAND_LENGTH] = [REPORT_ID_CMD, CMD_REBOOT, 0, 0, 0, 0]; + // explicitly ignore the result + let _ = self.request_device.send_feature_report(&cmd); + + thread::sleep(time::Duration::from_millis(2000)); + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index e5becfe..a9c4475 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use std::{ process::ExitCode, }; -use clap::{arg, ArgMatches, Command}; +use clap::{arg, value_parser, ArgMatches, Command}; use clap_num::maybe_hex; use log::{error, info}; use simple_logger::SimpleLogger; @@ -45,6 +45,11 @@ fn cli() -> Command { .subcommand_required(true) .arg_required_else_help(true) .author("Karolis Stasaitis") + .subcommand( + Command::new("list") + .short_flag('l') + .about("List all connected devices and their identifiers. This is useful to find the manufacturer and product id for your keyboard.") + ) .subcommand( Command::new("read") .short_flag('r') @@ -138,6 +143,9 @@ fn err_main() -> Result<(), CLIError> { let isp = ISPDevice::new(part).map_err(CLIError::from)?; isp.write_cycle(&mut firmware).map_err(CLIError::from)?; } + Some(("list", _)) => { + ISPDevice::print_connected_devices().map_err(CLIError::from)?; + } _ => unreachable!(), } Ok(()) @@ -175,6 +183,7 @@ impl PartCommand for Command { .arg(arg!(--isp_usage_page ).value_parser(maybe_hex::)) .arg(arg!(--isp_usage ).value_parser(maybe_hex::)) .arg(arg!(--isp_index ).value_parser(maybe_hex::)) + .arg(arg!(--reboot ).value_parser(value_parser!(bool))) } } @@ -195,6 +204,7 @@ fn get_part_from_matches(sub_matches: &ArgMatches) -> Part { let isp_usage_page = sub_matches.get_one::("isp_usage_page"); let isp_usage = sub_matches.get_one::("isp_usage"); let isp_index = sub_matches.get_one::("isp_index"); + let reboot = sub_matches.get_one::("reboot"); if let Some(firmware_size) = firmware_size { part.firmware_size = *firmware_size; @@ -223,5 +233,8 @@ fn get_part_from_matches(sub_matches: &ArgMatches) -> Part { if let Some(isp_index) = isp_index { part.isp_index = *isp_index; } + if let Some(reboot) = reboot { + part.reboot = *reboot; + } return part; } diff --git a/src/part.rs b/src/part.rs index aa1aeea..5bb66bb 100644 --- a/src/part.rs +++ b/src/part.rs @@ -1,6 +1,6 @@ use phf::{phf_map, Map}; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq)] pub struct Part { pub firmware_size: usize, pub bootloader_size: usize, @@ -18,6 +18,8 @@ pub struct Part { pub isp_usage: u16, /// Index of matching (usage_page && usage) collection at which the ISP report appears in. pub isp_index: usize, + + pub reboot: bool, } pub const PART_BASE_DEFAULT: Part = Part { @@ -32,6 +34,8 @@ pub const PART_BASE_DEFAULT: Part = Part { isp_usage_page: 0xff00, isp_usage: 0x0001, isp_index: 0, + + reboot: false, }; pub const PART_BASE_SH68F90: Part = Part { @@ -118,18 +122,28 @@ pub const PART_ROYALKLUDGE_RK68_ISO_RETURN: Part = Part { pub const PART_ROYALKLUDGE_RK68_BT_DUAL: Part = Part { vendor_id: 0x258a, product_id: 0x008b, + reboot: true, ..PART_BASE_SH68F90 }; pub const PART_ROYALKLUDGE_RK71: Part = Part { vendor_id: 0x258a, product_id: 0x00ea, + reboot: true, + ..PART_BASE_SH68F90 +}; + +pub const PART_ROYALKLUDGE_RK84_ISO_RETURN: Part = Part { + vendor_id: 0x258a, + product_id: 0x00f4, + reboot: true, ..PART_BASE_SH68F90 }; pub const PART_ROYALKLUDGE_RK100: Part = Part { vendor_id: 0x258a, product_id: 0x0056, + reboot: true, ..PART_BASE_SH68F90 }; @@ -158,6 +172,7 @@ pub static PARTS: Map<&'static str, Part> = phf_map! { "redragon-k614-anivia" => PART_REDRAGON_ANIVIA_K614, "redragon-k617-fizz" => PART_REDRAGON_FIZZ_K617, "royalkludge-rk100" => PART_ROYALKLUDGE_RK100, + "royalkludge-rk84-iso-return" => PART_ROYALKLUDGE_RK84_ISO_RETURN, "royalkludge-rk61" => PART_ROYALKLUDGE_RK61, "royalkludge-rk68-bt-dual" => PART_ROYALKLUDGE_RK68_BT_DUAL, "royalkludge-rk68-iso-return" => PART_ROYALKLUDGE_RK68_ISO_RETURN, diff --git a/tools/functional-test.sh b/tools/functional-test.sh index 3fe925e..f56ae8e 100755 --- a/tools/functional-test.sh +++ b/tools/functional-test.sh @@ -18,11 +18,8 @@ FILE_POST_WRITE="$FILE_PREFIX-post-write.hex" FILE_POST_WRITE_CUSTOM="$FILE_PREFIX-post-write-custom.hex" function reboot_device () { - echo "Turning off port..." - uhubctl -a off -p 1 -l 65-1 - sleep 1 - echo "Turning on port..." - uhubctl -a on -p 1 -l 65-1 + echo "Cycling port power..." + uhubctl -a cycle -l "3-3.3.4.4" -p 4 -d 1 echo "Waiting..." sleep 5 }