Skip to content

Commit

Permalink
reboot functionality, win device enumeration fix, list hid devices, r…
Browse files Browse the repository at this point in the history
…k84-iso support (#47)

Initiated from
#45 and
#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 <[email protected]>
  • Loading branch information
carlossless and Luro02 authored Mar 9, 2024
1 parent 0ff0965 commit 68a6f6a
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 8 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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
```

Expand All @@ -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
Expand All @@ -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!
62 changes: 61 additions & 1 deletion src/isp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();

Expand All @@ -87,7 +126,7 @@ impl ISPDevice {
fn open_isp_devices() -> Result<HIDDevices, ISPError> {
let api = Self::hidapi();

let devices: Vec<_> = api
let mut devices: Vec<_> = api
.device_list()
.filter(|d| {
#[cfg(not(target_os = "linux"))]
Expand All @@ -103,6 +142,8 @@ impl ISPDevice {
})
.collect();

devices.sort_by_key(|d| d.path());

for d in &devices {
#[cfg(not(target_os = "linux"))]
debug!(
Expand Down Expand Up @@ -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);
}

Expand All @@ -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(())
}

Expand Down Expand Up @@ -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(())
}
}
15 changes: 14 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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(())
Expand Down Expand Up @@ -175,6 +183,7 @@ impl PartCommand for Command {
.arg(arg!(--isp_usage_page <PAGE>).value_parser(maybe_hex::<u16>))
.arg(arg!(--isp_usage <USAGE>).value_parser(maybe_hex::<u16>))
.arg(arg!(--isp_index <INDEX>).value_parser(maybe_hex::<usize>))
.arg(arg!(--reboot <BOOL>).value_parser(value_parser!(bool)))
}
}

Expand All @@ -195,6 +204,7 @@ fn get_part_from_matches(sub_matches: &ArgMatches) -> Part {
let isp_usage_page = sub_matches.get_one::<u16>("isp_usage_page");
let isp_usage = sub_matches.get_one::<u16>("isp_usage");
let isp_index = sub_matches.get_one::<usize>("isp_index");
let reboot = sub_matches.get_one::<bool>("reboot");

if let Some(firmware_size) = firmware_size {
part.firmware_size = *firmware_size;
Expand Down Expand Up @@ -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;
}
17 changes: 16 additions & 1 deletion src/part.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
};

Expand Down Expand Up @@ -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,
Expand Down
7 changes: 2 additions & 5 deletions tools/functional-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 68a6f6a

Please sign in to comment.