From b47335ffe86cfc860f244ccc3b9b7bdb5f38963e Mon Sep 17 00:00:00 2001 From: Karolis Stasaitis Date: Tue, 29 Sep 2026 20:20:05 +0200 Subject: [PATCH] platform: sh68f83, device: crowview-note --- Cargo.lock | 1 + Cargo.toml | 7 +-- src/akira_read.rs | 113 +++++++++++++++++++++++++++++++++++++++++ src/device_selector.rs | 13 +++-- src/device_spec.rs | 45 +++++++++++++++- src/flasher.rs | 46 ++++++++++++++--- src/isp_device.rs | 35 +++++++------ src/main.rs | 5 +- src/platform_spec.rs | 6 +++ tests/protocol_test.rs | 2 +- 10 files changed, 238 insertions(+), 35 deletions(-) create mode 100644 src/akira_read.rs diff --git a/Cargo.lock b/Cargo.lock index 3a6008c..d30e789 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -856,6 +856,7 @@ dependencies = [ "itertools", "log", "md5", + "nusb", "phf", "predicates", "serial_test", diff --git a/Cargo.toml b/Cargo.toml index 93b5020..c2589b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,10 +28,10 @@ cli = [ "dep:md5", "dep:simple_logger", "dep:log", + "nusb", ] -# No-op, kept so an existing `--features nusb` still resolves: hidra compiles -# both backends in and this crate names the one it wants. -nusb = [] +# The CLI's sh68f83 unlock read drives nusb control transfers directly. +nusb = ["dep:nusb"] [[bin]] name = "sinowisp" @@ -54,6 +54,7 @@ indicatif = { version = "0.17.11", optional = true } itertools = { version = "0.14.0", optional = true } md5 = { version = "0.7", optional = true } log = { version = "0.4", features = ["max_level_debug"], optional = true } +nusb = { version = "0.2", optional = true } [dependencies.simple_logger] version = "5.1" diff --git a/src/akira_read.rs b/src/akira_read.rs new file mode 100644 index 0000000..e45501e --- /dev/null +++ b/src/akira_read.rs @@ -0,0 +1,113 @@ +use std::time::Duration; + +use nusb::transfer::{ControlIn, ControlOut, ControlType, Recipient}; +use nusb::MaybeFuture; + +use crate::device_selector::{ + GAMING_KB_IFACE, GAMING_KB_PRODUCT_ID, GAMING_KB_V2_PRODUCT_ID, GAMING_KB_VENDOR_ID, +}; + +const TIMEOUT: Duration = Duration::from_millis(2000); +const SET_REPORT: u8 = 0x09; +const GET_REPORT: u8 = 0x01; +const REPORT_TYPE_FEATURE: u16 = 0x03; +const REPORT_ID_CMD: u8 = 0x05; +const REPORT_ID_RAW: u8 = 0x41; +const AKIRA: [u8; 6] = [REPORT_ID_CMD, b'A', b'K', b'I', b'R', b'A']; +const READ_LOCK_ADDR: u16 = 0xfe27; +const READ_LOCK_UNLOCKED: u8 = 0xa5; +const CHUNK: usize = 64; + +#[derive(thiserror::Error, Debug)] +pub enum RawReadError { + #[error("ISP device {0:04x}:{1:04x} not found")] + NotFound(u16, u16), + #[error(transparent)] + Usb(#[from] nusb::Error), + #[error("USB control transfer failed ({0})")] + Transfer(nusb::transfer::TransferError), +} + +impl From for RawReadError { + fn from(e: nusb::transfer::TransferError) -> Self { + RawReadError::Transfer(e) + } +} + +#[cfg(target_os = "windows")] +type Handle = nusb::Interface; +#[cfg(not(target_os = "windows"))] +type Handle = nusb::Device; + +fn set_report(h: &Handle, report_id: u8, index: u16, data: &[u8]) -> Result<(), RawReadError> { + h.control_out( + ControlOut { + control_type: ControlType::Class, + recipient: Recipient::Interface, + request: SET_REPORT, + value: (REPORT_TYPE_FEATURE << 8) | report_id as u16, + index, + data, + }, + TIMEOUT, + ) + .wait()?; + Ok(()) +} + +fn get_report(h: &Handle, report_id: u8, index: u16, length: u16) -> Result, RawReadError> { + Ok(h.control_in( + ControlIn { + control_type: ControlType::Class, + recipient: Recipient::Interface, + request: GET_REPORT, + value: (REPORT_TYPE_FEATURE << 8) | report_id as u16, + index, + length, + }, + TIMEOUT, + ) + .wait()?) +} + +fn open() -> Result { + let pids = [GAMING_KB_PRODUCT_ID, GAMING_KB_V2_PRODUCT_ID]; + let info = nusb::list_devices() + .wait()? + .find(|d| d.vendor_id() == GAMING_KB_VENDOR_ID && pids.contains(&d.product_id())) + .ok_or(RawReadError::NotFound( + GAMING_KB_VENDOR_ID, + GAMING_KB_PRODUCT_ID, + ))?; + let dev = info.open().wait()?; + #[cfg(target_os = "windows")] + return Ok(dev.claim_interface(GAMING_KB_IFACE as u8).wait()?); + #[cfg(not(target_os = "windows"))] + Ok(dev) +} + +pub fn read( + start_addr: usize, + length: usize, + progress: &dyn Fn(usize, usize), +) -> Result, RawReadError> { + let h = open()?; + let iface = GAMING_KB_IFACE as u16; + + set_report(&h, REPORT_ID_CMD, iface, &AKIRA)?; + set_report(&h, REPORT_ID_RAW, READ_LOCK_ADDR, &[READ_LOCK_UNLOCKED])?; + + let mut out: Vec = Vec::with_capacity(length); + while out.len() < length { + let addr = start_addr + out.len(); + let n = CHUNK.min(length - out.len()); + let data = get_report(&h, REPORT_ID_RAW, addr as u16, n as u16)?; + if data.is_empty() { + break; + } + out.extend_from_slice(&data); + progress(out.len().min(length), length); + } + out.truncate(length); + Ok(out) +} diff --git a/src/device_selector.rs b/src/device_selector.rs index 89e7c8f..72e925e 100644 --- a/src/device_selector.rs +++ b/src/device_selector.rs @@ -19,10 +19,10 @@ const CMD_ISP_MODE: u8 = 0x75; const REPORT_ID_XFER: u8 = 0x06; -const GAMING_KB_VENDOR_ID: u16 = 0x0603; -const GAMING_KB_PRODUCT_ID: u16 = 0x1020; -const GAMING_KB_V2_PRODUCT_ID: u16 = 0x1021; -const GAMING_KB_IFACE: i32 = 0; +pub(crate) const GAMING_KB_VENDOR_ID: u16 = 0x0603; +pub(crate) const GAMING_KB_PRODUCT_ID: u16 = 0x1020; +pub(crate) const GAMING_KB_V2_PRODUCT_ID: u16 = 0x1021; +pub(crate) const GAMING_KB_IFACE: i32 = 0; const COMMAND_LENGTH: usize = 6; @@ -716,7 +716,10 @@ mod tests { state.sent(), vec![ ("kbd1".to_string(), ISP_MODE.to_vec()), - (ISP_PATH.to_string(), vec![0x05, 0x45, 0, 0, 0, 0]), + ( + ISP_PATH.to_string(), + vec![0x05, 0x45, 0x45, 0x45, 0x45, 0x45] + ), ] ); } diff --git a/src/device_spec.rs b/src/device_spec.rs index 1a7db38..65bd319 100644 --- a/src/device_spec.rs +++ b/src/device_spec.rs @@ -1,13 +1,21 @@ use phf::{phf_map, Map}; use crate::platform_spec::{ - PlatformSpec, PLATFORM_SH68F881, PLATFORM_SH68F89, PLATFORM_SH68F90, PLATFORM_SH68F902, - PLATFORM_SH68F903, + PlatformSpec, PLATFORM_SH68F83, PLATFORM_SH68F881, PLATFORM_SH68F89, PLATFORM_SH68F90, + PLATFORM_SH68F902, PLATFORM_SH68F903, }; const DEFAULT_ISP_IFACE_NUM: i32 = 1; const DEFAULT_ISP_REPORT_ID: u32 = 5; const DEFAULT_REBOOT: bool = true; +const DEFAULT_READ_MODE: ReadMode = ReadMode::Standard; +const DEFAULT_CHECK_BOOTLOADER: bool = true; + +#[derive(Clone, Copy, PartialEq)] +pub enum ReadMode { + Standard, + Akira, +} /// Undoes mangling some ISP bootloaders apply in transit; must invert each other. #[derive(Clone, Copy, PartialEq)] @@ -43,6 +51,10 @@ pub struct DeviceSpec { pub reboot: bool, + pub read_mode: ReadMode, + + pub check_bootloader: bool, + pub isp_transform: Option, } @@ -53,6 +65,8 @@ pub const DEVICE_BASE_SH68F90: DeviceSpec = DeviceSpec { isp_iface_num: DEFAULT_ISP_IFACE_NUM, isp_report_id: DEFAULT_ISP_REPORT_ID, reboot: DEFAULT_REBOOT, + read_mode: DEFAULT_READ_MODE, + check_bootloader: DEFAULT_CHECK_BOOTLOADER, isp_transform: None, }; @@ -63,6 +77,8 @@ pub const DEVICE_BASE_SH68F89: DeviceSpec = DeviceSpec { isp_iface_num: DEFAULT_ISP_IFACE_NUM, isp_report_id: DEFAULT_ISP_REPORT_ID, reboot: DEFAULT_REBOOT, + read_mode: DEFAULT_READ_MODE, + check_bootloader: DEFAULT_CHECK_BOOTLOADER, isp_transform: None, }; @@ -73,6 +89,8 @@ pub const DEVICE_BASE_SH68F881: DeviceSpec = DeviceSpec { isp_iface_num: DEFAULT_ISP_IFACE_NUM, isp_report_id: DEFAULT_ISP_REPORT_ID, reboot: DEFAULT_REBOOT, + read_mode: DEFAULT_READ_MODE, + check_bootloader: DEFAULT_CHECK_BOOTLOADER, isp_transform: None, }; @@ -83,6 +101,20 @@ pub const DEVICE_BASE_SH68F902: DeviceSpec = DeviceSpec { isp_iface_num: DEFAULT_ISP_IFACE_NUM, isp_report_id: DEFAULT_ISP_REPORT_ID, reboot: DEFAULT_REBOOT, + read_mode: DEFAULT_READ_MODE, + check_bootloader: DEFAULT_CHECK_BOOTLOADER, + isp_transform: None, +}; + +pub const DEVICE_BASE_SH68F83: DeviceSpec = DeviceSpec { + vendor_id: 0x0000, + product_id: 0x0000, + platform: PLATFORM_SH68F83, + isp_iface_num: DEFAULT_ISP_IFACE_NUM, + isp_report_id: DEFAULT_ISP_REPORT_ID, + reboot: false, + read_mode: ReadMode::Akira, + check_bootloader: false, isp_transform: None, }; @@ -93,6 +125,8 @@ pub const DEVICE_BASE_SH68F903: DeviceSpec = DeviceSpec { isp_iface_num: DEFAULT_ISP_IFACE_NUM, isp_report_id: DEFAULT_ISP_REPORT_ID, reboot: DEFAULT_REBOOT, + read_mode: DEFAULT_READ_MODE, + check_bootloader: DEFAULT_CHECK_BOOTLOADER, isp_transform: None, }; @@ -126,6 +160,12 @@ pub const DEVICE_CIY_X77: DeviceSpec = DeviceSpec { ..DEVICE_BASE_SH68F89 }; +pub const DEVICE_CROWVIEW_NOTE: DeviceSpec = DeviceSpec { + vendor_id: 0x6080, + product_id: 0x8060, + ..DEVICE_BASE_SH68F83 +}; + pub const DEVICE_DELTACO_WK95R: DeviceSpec = DeviceSpec { vendor_id: 0x258a, product_id: 0x0049, @@ -403,6 +443,7 @@ pub static DEVICES: Map<&'static str, DeviceSpec> = phf_map! { "aula-f75" => DEVICE_AULA_F75, "aula-f87" => DEVICE_AULA_F87, "ciy-x77" => DEVICE_CIY_X77, + "crowview-note" => DEVICE_CROWVIEW_NOTE, "deltaco-wk95r" => DEVICE_DELTACO_WK95R, "dierya-dk68se" => DEVICE_DIERYA_DK68SE, "digitalalliance-meca-warrior-x" => DEVICE_DIGITALALLIANCE_MECA_WARRIOR_X, diff --git a/src/flasher.rs b/src/flasher.rs index 77a8224..c06e964 100644 --- a/src/flasher.rs +++ b/src/flasher.rs @@ -8,9 +8,12 @@ use hidra::MaybeFuture; use indicatif::ProgressBar; use log::{debug, error, warn}; use sinowisp::{ - check_bootloader, is_expected_error, verify, ISPDevice, ISPError, ReadSection, Transport, + check_bootloader, is_expected_error, verify, ISPDevice, ISPError, ReadMode, ReadSection, + Transport, }; +use crate::{akira_read, CLIError}; + /// Time the device needs to settle after an erase or reboot before it will /// accept (or has finished acting on) further commands. const SETTLE_DELAY: Duration = if cfg!(test) { @@ -22,12 +25,9 @@ const SETTLE_DELAY: Duration = if cfg!(test) { pub fn read_cycle( device: &ISPDevice, section: ReadSection, -) -> Result, ISPError> { +) -> Result, CLIError> { let spec = *device.device_spec(); - eprintln!("Enabling firmware..."); - device.enable_firmware().wait()?; - let (start_addr, length) = match section { ReadSection::Firmware => (0, spec.platform.firmware_size), ReadSection::Bootloader => (spec.platform.firmware_size, spec.platform.bootloader_size), @@ -37,14 +37,32 @@ pub fn read_cycle( ), }; - let firmware = read(device, start_addr, length)?; + let firmware = match spec.read_mode { + ReadMode::Standard => { + eprintln!("Enabling firmware..."); + device.enable_firmware().wait()?; + read(device, start_addr, length)? + } + ReadMode::Akira => { + if !cfg!(any(target_os = "macos", target_os = "windows")) { + return Err(ISPError::Unsupported( + "reading this device requires the raw AKIRA unlock, which only works on macOS and Windows", + ) + .into()); + } + read_akira(start_addr, length)? + } + }; let bootloader = match section { ReadSection::Firmware => None, ReadSection::Bootloader => Some(&firmware[..]), ReadSection::Full => firmware.get(spec.platform.firmware_size..), }; - if let Some(Err(err)) = bootloader.map(check_bootloader) { + if let Some(Err(err)) = bootloader + .filter(|_| spec.check_bootloader) + .map(check_bootloader) + { warn!("{err}"); } @@ -111,6 +129,18 @@ fn read( Ok(result) } +fn read_akira(start_addr: usize, length: usize) -> Result, CLIError> { + eprintln!("Reading..."); + let bar = ProgressBar::new(length as u64); + + let result = akira_read::read(start_addr, length, &|done, _total| { + bar.set_position(done as u64); + })?; + + bar.finish(); + Ok(result) +} + fn write( device: &ISPDevice, start_addr: usize, @@ -202,7 +232,7 @@ mod tests { assert_eq!( commands(&fake), vec![ - [0x05, 0x45, 0x00, 0x00], + [0x05, 0x45, 0x45, 0x45], [0x05, 0x57, 0x00, 0x00], [0x05, 0x52, 0x00, 0x00], [0x05, 0x55, 0x00, 0x00], diff --git a/src/isp_device.rs b/src/isp_device.rs index 2795fc7..f32d524 100644 --- a/src/isp_device.rs +++ b/src/isp_device.rs @@ -125,6 +125,8 @@ pub enum ISPError { VerificationError(#[from] VerificationError), #[error("Read/Write operation mistmatch")] ReadWriteMismatch, + #[error("{0}")] + Unsupported(&'static str), } #[derive(Debug, Clone)] @@ -208,16 +210,19 @@ impl ISPDevice { Ok(()) } - /// Initializes the read operation / sets the initial read address - pub async fn init_read(&self, start_addr: usize) -> Result<(), ISPError> { - let cmd: [u8; COMMAND_LENGTH] = [ + fn init_command(&self, command: u8, start_addr: usize) -> [u8; COMMAND_LENGTH] { + [ REPORT_ID_CMD, - CMD_INIT_READ, + command, (start_addr & 0xff) as u8, (start_addr >> 8) as u8, 0, 0, - ]; + ] + } + + pub async fn init_read(&self, start_addr: usize) -> Result<(), ISPError> { + let cmd = self.init_command(CMD_INIT_READ, start_addr); self.cmd_device .send_feature_report(&cmd) .await @@ -225,16 +230,8 @@ impl ISPDevice { Ok(()) } - /// Initializes the write operation / sets the initial write address pub async fn init_write(&self, start_addr: usize) -> Result<(), ISPError> { - let cmd: [u8; COMMAND_LENGTH] = [ - REPORT_ID_CMD, - CMD_INIT_WRITE, - (start_addr & 0xff) as u8, - (start_addr >> 8) as u8, - 0, - 0, - ]; + let cmd = self.init_command(CMD_INIT_WRITE, start_addr); self.cmd_device .send_feature_report(&cmd) .await @@ -349,7 +346,15 @@ impl ISPDevice { /// The device needs time to settle afterwards; the caller is responsible for /// the delay before issuing further commands. pub async fn erase(&self) -> Result<(), ISPError> { - let cmd: [u8; COMMAND_LENGTH] = [REPORT_ID_CMD, CMD_ERASE, 0, 0, 0, 0]; + // Different bootloaders check a different amount of bytes of this erase command. + let cmd: [u8; COMMAND_LENGTH] = [ + REPORT_ID_CMD, + CMD_ERASE, + CMD_ERASE, + CMD_ERASE, + CMD_ERASE, + CMD_ERASE, + ]; self.cmd_device .send_feature_report(&cmd) .await diff --git a/src/main.rs b/src/main.rs index 1e19448..5e7e8a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ use sinowisp::{ DEVICE_BASE_SH68F90, PLATFORMS, }; +mod akira_read; #[cfg(test)] mod cli_tests; mod device_selector; @@ -42,6 +43,8 @@ pub enum CLIError { PayloadConversionError(#[from] PayloadConversionError), #[error(transparent)] DeviceSelectorError(#[from] DeviceSelectorError), + #[error(transparent)] + RawReadError(#[from] akira_read::RawReadError), } #[derive(Clone, Copy)] @@ -184,7 +187,7 @@ fn run( let device = ds .try_fetch_isp_device(device_spec, retry_count) .map_err(CLIError::from)?; - let firmware = flasher::read_cycle(&device, section).map_err(CLIError::from)?; + let firmware = flasher::read_cycle(&device, section)?; if firmware.iter().all(|b| *b == 0) { eprintln!("Warning: read {} bytes of zeros.", firmware.len()); diff --git a/src/platform_spec.rs b/src/platform_spec.rs index f207951..39ddbf3 100644 --- a/src/platform_spec.rs +++ b/src/platform_spec.rs @@ -35,6 +35,11 @@ pub const PLATFORM_SH68F902: PlatformSpec = PlatformSpec { firmware_size: 16384 - 3072, // 13312 until bootloader bootloader_size: 3072, page_size: 1024, +}; + +pub const PLATFORM_SH68F83: PlatformSpec = PlatformSpec { + firmware_size: 16384 - 2048, + bootloader_size: 2048, ..PLATFORM_DEFAULT }; @@ -44,6 +49,7 @@ pub const PLATFORM_SH68F903: PlatformSpec = PlatformSpec { }; pub static PLATFORMS: Map<&'static str, PlatformSpec> = phf_map! { + "sh68f83" => PLATFORM_SH68F83, "sh68f89" => PLATFORM_SH68F89, "sh68f881" => PLATFORM_SH68F881, "sh68f90" => PLATFORM_SH68F90, diff --git a/tests/protocol_test.rs b/tests/protocol_test.rs index a98529e..fd6113c 100644 --- a/tests/protocol_test.rs +++ b/tests/protocol_test.rs @@ -45,7 +45,7 @@ fn test_commands() { vec![0x05, 0x55, 0x00, 0x00, 0x00, 0x00], vec![0x05, 0x52, 0x34, 0x12, 0x00, 0x00], vec![0x05, 0x57, 0x00, 0xf0, 0x00, 0x00], - vec![0x05, 0x45, 0x00, 0x00, 0x00, 0x00], + vec![0x05, 0x45, 0x45, 0x45, 0x45, 0x45], vec![0x05, 0x5a, 0x00, 0x00, 0x00, 0x00], ] );