Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
113 changes: 113 additions & 0 deletions src/akira_read.rs
Original file line number Diff line number Diff line change
@@ -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<nusb::transfer::TransferError> 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<Vec<u8>, 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<Handle, RawReadError> {
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<Vec<u8>, 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<u8> = 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)
}
13 changes: 8 additions & 5 deletions src/device_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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]
),
]
);
}
Expand Down
45 changes: 43 additions & 2 deletions src/device_spec.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -43,6 +51,10 @@ pub struct DeviceSpec {

pub reboot: bool,

pub read_mode: ReadMode,

pub check_bootloader: bool,

pub isp_transform: Option<IspTransform>,
}

Expand All @@ -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,
};

Expand All @@ -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,
};

Expand All @@ -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,
};

Expand All @@ -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,
};

Expand All @@ -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,
};

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
46 changes: 38 additions & 8 deletions src/flasher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -22,12 +25,9 @@ const SETTLE_DELAY: Duration = if cfg!(test) {
pub fn read_cycle<T: Transport>(
device: &ISPDevice<T>,
section: ReadSection,
) -> Result<Vec<u8>, ISPError> {
) -> Result<Vec<u8>, 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),
Expand All @@ -37,14 +37,32 @@ pub fn read_cycle<T: Transport>(
),
};

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}");
}

Expand Down Expand Up @@ -111,6 +129,18 @@ fn read<T: Transport>(
Ok(result)
}

fn read_akira(start_addr: usize, length: usize) -> Result<Vec<u8>, 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<T: Transport>(
device: &ISPDevice<T>,
start_addr: usize,
Expand Down Expand Up @@ -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],
Expand Down
Loading
Loading