use std::process::ExitCode;
use ch32rv_boot::{HidBoot, HidBootError};
use ch32rv_contract::{ErrorKind, ResultEnvelope};
use crate::args::Cli;
use crate::cmd_probe::fail;
pub fn hid_flash(cli: &Cli, file: &std::path::Path, usb_id: Option<&str>) -> ExitCode {
const CMD: &str = "boot.hid.flash";
let id = match usb_id.map(parse_usb_id).transpose() {
Ok(id) => id,
Err(message) => {
return fail(
cli,
CMD,
ErrorKind::Usage,
message,
Some("use VID:PID, e.g. 1209:b803"),
);
}
};
let image = match std::fs::read(file) {
Ok(bytes) if !bytes.is_empty() => bytes,
Ok(_) => {
return fail(
cli,
CMD,
ErrorKind::Usage,
"image is empty".to_owned(),
None,
);
}
Err(e) => {
return fail(
cli,
CMD,
ErrorKind::Usage,
format!("cannot read {}: {e}", file.display()),
None,
);
}
};
if cli.dry_run {
println!(
"would flash {} byte(s) through HID {}",
image.len(),
usb_id.unwrap_or("1209:b803 or 1209:b003")
);
return ExitCode::SUCCESS;
}
let topology = match crate::cmd_probe::parse_selector(cli, CMD) {
Ok(None) => None,
Ok(Some(ch32rv_usb::Selector::Topology(t))) => Some(t),
Ok(Some(ch32rv_usb::Selector::Port(p))) if p.starts_with("hid://") => {
p.strip_prefix("hid://").map(str::to_owned)
}
Ok(Some(_)) => {
return fail(
cli,
CMD,
ErrorKind::Usage,
"a HID bootloader is selected by position only",
Some("use --probe usb:<bus>-<ports> or port:hid://<bus>-<ports>"),
);
}
Err(c) => return c,
};
let boot = match HidBoot::open_at(id, topology.as_deref()) {
Ok(boot) => boot,
Err(e) => return hid_error(cli, CMD, e),
};
let report = match boot.flash(&image, true) {
Ok(report) => report,
Err(e) => return hid_error(cli, CMD, e),
};
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(serde_json::json!({
"usb": format!("{:04x}:{:04x}", report.vid, report.pid),
"bytes": report.bytes,
"sectors_written": report.sectors_written,
"sector_size": 64,
"verified": true,
"run": true,
}));
crate::print_envelope(&env)
} else {
println!(
"flashed {} byte(s) through {:04x}:{:04x}: {} sector(s) changed, verified, application started",
report.bytes, report.vid, report.pid, report.sectors_written
);
ExitCode::SUCCESS
}
}
fn parse_usb_id(value: &str) -> Result<(u16, u16), String> {
let Some((vid, pid)) = value.split_once(':') else {
return Err(format!("invalid USB ID `{value}`"));
};
if pid.contains(':') {
return Err(format!("invalid USB ID `{value}`"));
}
let vid = u16::from_str_radix(vid.trim_start_matches("0x"), 16)
.map_err(|_| format!("invalid VID in `{value}`"))?;
let pid = u16::from_str_radix(pid.trim_start_matches("0x"), 16)
.map_err(|_| format!("invalid PID in `{value}`"))?;
Ok((vid, pid))
}
fn hid_error(cli: &Cli, cmd: &str, error: HidBootError) -> ExitCode {
let kind = match error {
HidBootError::NotFound(..) | HidBootError::NotAt(_) => ErrorKind::DeviceNotFound,
HidBootError::Ambiguous(_) => ErrorKind::DeviceAmbiguous,
HidBootError::Init(_) | HidBootError::Transfer(_) => ErrorKind::DeviceOpenFailed,
HidBootError::ReadProtected(_) => ErrorKind::TargetProtected,
HidBootError::Verify(_) => ErrorKind::VerifyMismatch,
HidBootError::ImageTooLarge { .. } | HidBootError::Payload => ErrorKind::Usage,
HidBootError::Timeout | HidBootError::FlashLocked(_) | HidBootError::FlashStatus(_) => {
ErrorKind::TransferFailed
}
};
fail(cli, cmd, kind, error.to_string(), None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_usb_id() {
assert_eq!(parse_usb_id("1209:b803"), Ok((0x1209, 0xb803)));
assert_eq!(parse_usb_id("0x1209:0xb003"), Ok((0x1209, 0xb003)));
assert!(parse_usb_id("1209").is_err());
assert!(parse_usb_id("1209:b803:x").is_err());
}
}