use std::process::ExitCode;
use std::time::Duration;
use ch32rv_contract::{ErrorKind, ProbeMode, ResultEnvelope, TargetReport, Warning};
use crate::args::{Cli, SwitchState};
use crate::cmd_probe::{
apply_probe_info, base_report, fail, mode_str, print_probe_human, select_entry,
};
use crate::parse;
use crate::session::Session;
const CMD: &str = "target.info";
pub fn info(cli: &Cli) -> ExitCode {
match crate::oep::addr(cli, "target.info") {
Ok(Some(a)) => return crate::oep::target_info(cli, &a),
Ok(None) => {}
Err(c) => return c,
}
let entry = match select_entry(cli, CMD) {
Ok(e) => e,
Err(code) => return code,
};
if entry.mode != ProbeMode::Riscv {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
format!(
"probe is in {} mode; attaching to a target requires RISC-V mode",
mode_str(entry.mode)
),
Some("switch to RISC-V mode with `ch32rv probe mode set riscv` (WCH-LinkE only)"),
);
}
let (speed, mut warnings) = match parse::speed(&cli.speed) {
Ok(v) => v,
Err(msg) => return fail(cli, CMD, ErrorKind::Usage, msg, None),
};
let timeout = Duration::from_millis(cli.timeout.map(|s| s * 1000).unwrap_or(3000));
let session = match Session::attach(
&entry,
speed,
timeout,
Duration::from_secs(cli.lock_timeout),
cli.chip.as_deref(),
cli.db.as_deref(),
&mut warnings,
) {
Ok(s) => s,
Err(e) => return crate::cmd_probe::session_error(cli, CMD, e),
};
let mut probe_report = base_report(&entry);
apply_probe_info(&mut probe_report, &session.probe_info, &mut warnings);
let family = session.family();
if family.starts_with("unknown") {
warnings.push(Warning {
code: "family-unknown".to_owned(),
msg: format!(
"target answered, but family byte 0x{:02x} is not registered; this is not a connection failure",
session.attach.family_byte
),
});
}
let db = session.db();
let resolution = db.resolve_with_chip(session.attach.chip_id, cli.chip.as_deref());
let mut sku_provisional = None;
let (sku, sku_verified, sku_line): (Option<String>, Option<bool>, String) = match &resolution {
ch32rv_target::Resolution::Sku(s) => {
sku_provisional = Some(s.provisional);
if s.provisional {
warnings.push(Warning {
code: "sku-provisional".to_owned(),
msg: match cli.db.as_deref() {
Some(path) => format!(
"{} comes from the --db overlay {}, not the generated DB",
s.sku,
path.display()
),
None => format!(
"{} comes from the in-tree provisional overlay, not the generated DB - the part number is pending a data request (docs/data-requests/)",
s.sku
),
},
});
}
(
Some(s.sku.clone()),
Some(s.verified),
format!(
"{} ({})",
s.sku,
match (s.provisional, s.verified) {
(true, true) => "provisional overlay, chip_id measured on silicon",
(true, false) => "provisional overlay, not verified on silicon",
(false, true) => "verified on silicon",
(false, false) => "generated DB, datasheet reference",
}
),
)
}
ch32rv_target::Resolution::Family(fam, cands) => {
let names: Vec<&str> = cands.iter().map(|c| c.sku.as_str()).collect();
warnings.push(Warning {
code: "sku-ambiguous".to_owned(),
msg: format!(
"chip_id matches {} SKUs in family {fam}: {} - pass --chip to disambiguate",
cands.len(),
names.join(", ")
),
});
(
None,
None,
format!("- ({} candidates in {fam})", cands.len()),
)
}
ch32rv_target::Resolution::Unknown => {
warnings.push(Warning {
code: "sku-unknown".to_owned(),
msg: format!(
"target answered with chip_id 0x{:08x}, but it is not registered in the DB; retain target info --json and --capture output for registration",
session.attach.chip_id
),
});
(None, None, "- (chip_id not in DB)".to_owned())
}
};
let identification = identification_status(&resolution);
let sram_bytes = match &resolution {
ch32rv_target::Resolution::Sku(s) => Some(s.sram_bytes),
ch32rv_target::Resolution::Family(_, c)
if c.windows(2).all(|w| w[0].sram_bytes == w[1].sram_bytes) =>
{
c.first().map(|s| s.sram_bytes)
}
_ => None,
};
let sku_candidates: Vec<&str> = match &resolution {
ch32rv_target::Resolution::Family(_, c) => c.iter().map(|s| s.sku.as_str()).collect(),
_ => Vec::new(),
};
let wiring = match &resolution {
ch32rv_target::Resolution::Sku(s) => ch32rv_target::debug_wiring(&s.series),
_ => None,
};
let (flash_bytes, flash_capacity_source) = session.flash_capacity();
let chip = session.chip;
let target = TargetReport {
sku,
family: Some(family),
chip_id: Some(format!("0x{:08x}", session.attach.chip_id)),
uid: chip.as_ref().map(|c| hex(&c.uuid)),
verified: sku_verified,
provisional: sku_provisional,
protected: None,
flash_bytes,
};
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.probe = Some(probe_report);
env.result = Some(serde_json::json!({
"connection": "connected",
"identification": identification,
"family_byte": format!("0x{:02x}", session.attach.family_byte),
"silicon_revision": (session.attach.chip_id >> 4) & 0x0f,
"sram_bytes": sram_bytes,
"sku_candidates": sku_candidates,
"flash_capacity_source": flash_capacity_source,
"flash_capacity_raw": chip.as_ref().map(|c| format!("0x{:04x}", c.flash_bytes / 1024)),
"protection_raw": chip.as_ref().map(|c| hex(&c.protection_raw)),
"chip_id_echo": chip.as_ref().map(|c| format!("0x{:08x}", c.chip_id_echo)),
"debug_wiring": wiring.as_ref().map(|w| serde_json::json!({
"wire": w.wire, "swdio": w.swdio, "swclk": w.swclk,
})),
}));
env.target = Some(target);
env.warnings = warnings;
crate::print_envelope(&env)
} else {
print_probe_human(&probe_report);
println!("---");
println!("target: connected ({identification})");
println!("family: {}", target.family.as_deref().unwrap_or("-"));
println!("family byte: 0x{:02x}", session.attach.family_byte);
println!(
"chip id: {} (bits [7:4] = silicon revision)",
target.chip_id.as_deref().unwrap_or("-")
);
println!("uid: {}", target.uid.as_deref().unwrap_or("-"));
match target.flash_bytes {
Some(b) if flash_capacity_source == "db" => println!(
"flash: {} KiB (DB; probe capacity unavailable)",
b / 1024
),
Some(b) => println!("flash: {} KiB", b / 1024),
None => println!("flash: -"),
}
println!("sku: {sku_line}");
if let Some(b) = sram_bytes {
println!("sram: {} KiB (DB)", b / 1024);
}
if let Some(w) = &wiring {
println!(
"debug: {} (SWDIO/DAT={}{})",
w.wire,
w.swdio,
if w.swclk == "-" {
String::new()
} else {
format!(", SWCLK={}", w.swclk)
}
);
}
for w in &warnings {
eprintln!("warning[{}]: {}", w.code, w.msg);
}
ExitCode::SUCCESS
}
}
fn identification_status(resolution: &ch32rv_target::Resolution<'_>) -> &'static str {
match resolution {
ch32rv_target::Resolution::Sku(_) => "identified",
ch32rv_target::Resolution::Family(_, _) => "ambiguous",
ch32rv_target::Resolution::Unknown => "unregistered",
}
}
pub fn option_get(cli: &Cli) -> ExitCode {
const CMD: &str = "target.option.get";
let mut warnings = match parse::speed(&cli.speed) {
Ok((_, w)) => w,
Err(msg) => return fail(cli, CMD, ErrorKind::Usage, msg, None),
};
let (family, db_family, raw) = match read_option_bytes_full(cli, CMD) {
Ok(v) => v,
Err(c) => return c,
};
let layout = crate::optbytes::Layout::of(&db_family);
if let Some(msg) = layout.interim_warning() {
warnings.push(Warning {
code: "option-decode-interim".to_owned(),
msg,
});
}
let v = crate::optbytes::result(
Some(&family),
Some(&db_family),
&hex(&raw),
&layout,
crate::optbytes::from_ob16(&raw),
);
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(v);
env.warnings = warnings;
crate::print_envelope(&env)
} else {
crate::optbytes::print(&v);
for w in &warnings {
eprintln!("warning[{}]: {}", w.code, w.msg);
}
ExitCode::SUCCESS
}
}
pub(crate) fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
pub(crate) struct DynDtm<'a>(&'a mut dyn ch32rv_dmi::DtmAccess);
impl ch32rv_dmi::DtmAccess for DynDtm<'_> {
fn selected_hart(&self) -> u32 {
self.0.selected_hart()
}
fn dmi_read(&mut self, addr: u8) -> Result<u32, ch32rv_dmi::DmiError> {
self.0.dmi_read(addr)
}
fn dmi_write(&mut self, addr: u8, value: u32) -> Result<(), ch32rv_dmi::DmiError> {
self.0.dmi_write(addr, value)
}
fn dmi_nop(&mut self) -> Result<(), ch32rv_dmi::DmiError> {
self.0.dmi_nop()
}
fn resume_hart(&mut self) -> Option<Result<(), ch32rv_dmi::DmiError>> {
self.0.resume_hart()
}
fn dmi_sequence(
&mut self,
ops: &[ch32rv_dmi::DmiOp],
) -> Result<Vec<u32>, ch32rv_dmi::DmiError> {
self.0.dmi_sequence(ops)
}
}
pub(crate) enum OnTarget<'a, 'b> {
Wch(&'a mut Session),
Oep(&'a mut crate::oep::Attached<'b>),
}
impl OnTarget<'_, '_> {
pub(crate) fn dtm(&mut self) -> DynDtm<'_> {
match self {
OnTarget::Wch(s) => DynDtm(s.link()),
OnTarget::Oep(x) => DynDtm(&mut x.t),
}
}
pub(crate) fn halt(&mut self) -> Result<(), ch32rv_dmi::DmiError> {
match self {
OnTarget::Wch(s) => s.dm().halt(),
OnTarget::Oep(x) => ch32rv_dmi::TargetAccess::halt(&mut x.t),
}
}
pub(crate) fn resume(&mut self) -> Result<(), ch32rv_dmi::DmiError> {
match self {
OnTarget::Wch(s) => s.dm().resume(),
OnTarget::Oep(x) => match ch32rv_dmi::DtmAccess::resume_hart(&mut x.t) {
Some(r) => r,
None => ch32rv_dmi::DebugModule::new(&mut x.t).resume(),
},
}
}
pub(crate) fn system_reset(&mut self) {
match self {
OnTarget::Wch(s) => {
let _ = s.link().soft_reset();
}
OnTarget::Oep(x) => {
let _ = ch32rv_dmi::TargetAccess::reset(&mut x.t, ch32rv_dmi::ResetMode::Run);
}
}
}
pub(crate) fn chip_id(&self) -> Option<u32> {
match self {
OnTarget::Wch(s) => Some(s.attach.chip_id),
OnTarget::Oep(x) => x.chip_id,
}
}
}
pub(crate) fn on_target(
cli: &Cli,
cmd: &str,
f: impl FnOnce(&mut OnTarget<'_, '_>, &str, &str) -> ExitCode,
) -> ExitCode {
on_target_halted(cli, cmd, true, f)
}
pub(crate) fn on_target_halted(
cli: &Cli,
cmd: &str,
halt: bool,
f: impl FnOnce(&mut OnTarget<'_, '_>, &str, &str) -> ExitCode,
) -> ExitCode {
match crate::oep::addr(cli, cmd) {
Ok(Some(_)) if cli.core != 0 => fail(
cli,
cmd,
ErrorKind::CapabilityUnsupported,
"--core 1 currently requires a direct WCH-Link connection",
None,
),
Ok(Some(a)) => crate::oep::with_attached(cli, cmd, &a, halt, |x| {
let family = x.family.clone().unwrap_or_else(|| "unknown".to_owned());
let db_family = family.clone();
f(&mut OnTarget::Oep(x), &family, &db_family)
}),
Ok(None) => {
let mut session = match crate::cmd_probe::attach(cli, cmd) {
Ok(s) => s,
Err(c) => return c,
};
let family = session.family();
let db_family = db_family_of(&mut session);
f(&mut OnTarget::Wch(&mut session), &family, &db_family)
}
Err(c) => c,
}
}
pub(crate) fn db_family_of(session: &mut Session) -> String {
let db = session.db();
match db.resolve_by_chip_id(session.attach.chip_id) {
ch32rv_target::Resolution::Sku(s) => s.family.clone(),
ch32rv_target::Resolution::Family(fam, _) => fam,
ch32rv_target::Resolution::Unknown => session.family(),
}
}
pub(crate) fn option_base(db_family: &str) -> Result<u32, String> {
ch32rv_target::option_bytes_layout(db_family)
.map(|l| l.base)
.ok_or_else(|| {
format!(
"the option-byte block location for {db_family} is not in the device DB, and it is not the same address on every part (CH32M030 uses 0x1FFF_F300) - refusing to guess"
)
})
}
fn option_method_warning(db_family: &str) -> Option<String> {
let layout = ch32rv_target::option_bytes_layout(db_family)?;
(layout.write_method == "ftpg" && db_family != "CH32L103").then(|| {
format!(
"{db_family}: the reference manual programs option bytes by the fast-page (FTPG) procedure, while this writes half-words (OPTPG). The half-word path is verified here on CH32L103 only"
)
})
}
pub(crate) const BLANKET_FACTORY: [u8; 16] = [
0xA5, 0x5A, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,
];
pub(crate) fn option_bytes_plausible(raw: &[u8; 16]) -> bool {
raw[0] ^ raw[1] == 0xFF && raw[2] ^ raw[3] == 0xFF
}
pub(crate) fn unprotect_image(current: &[u8; 16]) -> [u8; 16] {
let mut img = *current;
img[0] = 0xA5;
img[1] = 0x5A;
img
}
pub(crate) fn reset_user_byte(db_family: &str, current: u8) -> u8 {
ch32rv_target::option_user_fields(db_family)
.iter()
.fold(current, |user, f| f.set(user, f.default))
}
fn ob_confirm(cli: &Cli, prompt: &str) -> bool {
crate::cmd_probe::confirm_destructive(cli, prompt).is_ok()
}
fn parse_hex16(s: &str) -> Result<[u8; 16], String> {
let clean: String = s
.chars()
.filter(|c| !c.is_whitespace() && *c != ':' && *c != '_')
.collect();
if clean.len() != 32 || !clean.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(format!(
"expected 16 hex bytes (32 hex digits), got {} digit(s)",
clean.len()
));
}
let mut out = [0u8; 16];
for (i, byte) in out.iter_mut().enumerate() {
*byte = u8::from_str_radix(&clean[i * 2..i * 2 + 2], 16).map_err(|e| e.to_string())?;
}
Ok(out)
}
fn read_option_bytes(cli: &Cli, cmd: &str) -> Result<(String, [u8; 16]), ExitCode> {
read_option_bytes_full(cli, cmd).map(|(_, db_family, ob)| (db_family, ob))
}
fn read_option_bytes_full(cli: &Cli, cmd: &str) -> Result<(String, String, [u8; 16]), ExitCode> {
let mut got = None;
let code = on_target(cli, cmd, |t, family, db_family| {
let base = match option_base(db_family) {
Ok(b) => b,
Err(msg) => return fail(cli, cmd, ErrorKind::CapabilityUnsupported, msg, None),
};
if let Err(e) = t.halt() {
return fail(
cli,
cmd,
ErrorKind::AttachFailed,
format!("halt failed: {e}"),
None,
);
}
let mut d = t.dtm();
match ch32rv_dmi::DebugModule::new(&mut d).read_mem(base, 16) {
Ok(v) if v.len() >= 16 => {
let mut a = [0u8; 16];
a.copy_from_slice(&v[..16]);
got = Some((family.to_owned(), db_family.to_owned(), a));
ExitCode::SUCCESS
}
Ok(_) => fail(
cli,
cmd,
ErrorKind::TransferFailed,
"reading option bytes came back short",
None,
),
Err(e) => fail(
cli,
cmd,
ErrorKind::TransferFailed,
format!("reading option bytes failed: {e}"),
None,
),
}
});
got.ok_or(code)
}
fn program_option(cli: &Cli, cmd: &str, new: &[u8; 16]) -> ExitCode {
on_target(cli, cmd, |t, family, db_family| {
let base = match option_base(db_family) {
Ok(b) => b,
Err(msg) => return fail(cli, cmd, ErrorKind::CapabilityUnsupported, msg, None),
};
if let Some(w) = option_method_warning(db_family) {
crate::warn("option-write-method", w);
}
if let Err(e) = t.halt() {
return fail(
cli,
cmd,
ErrorKind::AttachFailed,
format!("halt failed: {e}"),
None,
);
}
let before = {
let mut d = t.dtm();
let mut dm = ch32rv_dmi::DebugModule::new(&mut d);
let before = match dm.read_mem(base, 16) {
Ok(v) => v,
Err(e) => {
return fail(
cli,
cmd,
ErrorKind::TransferFailed,
format!("reading option bytes failed: {e}"),
None,
);
}
};
if let Err(e) = dm.flash_program_option_bytes(base, new) {
return fail(
cli,
cmd,
ErrorKind::TransferFailed,
format!(
"programming option bytes failed: {e} - the target may be left with erased (read-protected) option bytes; recover with `ch32rv recover`"
),
None,
);
}
before
};
t.system_reset();
std::thread::sleep(Duration::from_millis(50));
if let Err(e) = t.halt() {
return fail(
cli,
cmd,
ErrorKind::AttachFailed,
format!("halt after the option reset failed: {e}"),
None,
);
}
let mut after = Vec::new();
{
let mut d = t.dtm();
let mut dm = ch32rv_dmi::DebugModule::new(&mut d);
for attempt in 0..3 {
if attempt > 0 {
std::thread::sleep(Duration::from_millis(50));
}
after = match dm.read_mem(base, 16) {
Ok(v) => v,
Err(e) => {
return fail(
cli,
cmd,
ErrorKind::TransferFailed,
format!("verify read failed: {e}"),
None,
);
}
};
if (0..16).step_by(2).all(|i| after.get(i) == Some(&new[i])) {
break;
}
}
}
let _ = t.resume();
if let Some(i) = (0..16).step_by(2).find(|&i| after.get(i) != Some(&new[i])) {
return fail(
cli,
cmd,
ErrorKind::VerifyMismatch,
format!(
"option byte {i} reads back 0x{:02x}, not the requested 0x{:02x} (before={}, after={})",
after.get(i).copied().unwrap_or(0),
new[i],
hex(&before),
hex(&after)
),
Some("the write did not take; check the target is not write-protected"),
);
}
if cli.json {
let mut env = ResultEnvelope::success(cmd);
env.result = Some(serde_json::json!({
"family": family,
"before": hex(&before),
"after": hex(&after),
"verified": true,
"note": "option bytes take effect after a power-on / system reset",
}));
crate::print_envelope(&env)
} else {
println!(
"option bytes: {} -> {} ({family})",
hex(&before),
hex(&after)
);
println!("note: option bytes take effect after a power-on / system reset");
ExitCode::SUCCESS
}
})
}
pub fn option_write_raw(cli: &Cli, hexstr: &str) -> ExitCode {
const CMD: &str = "target.option.write-raw";
let bytes = match parse_hex16(hexstr) {
Ok(b) => b,
Err(m) => {
return fail(
cli,
CMD,
ErrorKind::Usage,
m,
Some("e.g. a55aff00ff00ff00ff00ff00ff00ff00 (16 bytes: RDPR nRDPR USER nUSER ...)"),
);
}
};
if bytes[0] != 0xA5
&& !ob_confirm(
cli,
&format!(
"RDPR byte is 0x{:02x} (not 0xA5): this ENABLES read protection - flash becomes unreadable until you unprotect (which erases it). Continue?",
bytes[0]
),
)
{
return fail(
cli,
CMD,
ErrorKind::Usage,
"aborted: RDPR would enable read protection (pass --yes to force)",
None,
);
}
if !ob_confirm(cli, "Overwrite the target's option bytes?") {
return fail(cli, CMD, ErrorKind::Usage, "aborted (no --yes)", None);
}
program_option(cli, CMD, &bytes)
}
pub fn option_reset(cli: &Cli) -> ExitCode {
const CMD: &str = "target.option.reset";
let (db_family, current) = match read_option_bytes(cli, CMD) {
Ok(v) => v,
Err(c) => return c,
};
let user = reset_user_byte(&db_family, current[2]);
let mut defaults = BLANKET_FACTORY;
defaults[2] = user;
defaults[3] = !user;
if !ob_confirm(
cli,
&format!(
"Restore factory-default option bytes (RDPR off, USER=0x{user:02x}, Data/WRP cleared)?"
),
) {
return fail(cli, CMD, ErrorKind::Usage, "aborted (no --yes)", None);
}
if current[2] != user {
println!("option reset: USER 0x{:02x} -> 0x{user:02x}", current[2]);
}
program_option(cli, CMD, &defaults)
}
pub fn option_set(cli: &Cli, kv: &[String]) -> ExitCode {
const CMD: &str = "target.option.set";
let (db_family, ob) = match read_option_bytes(cli, CMD) {
Ok(v) => v,
Err(c) => return c,
};
let layout = crate::optbytes::Layout::of(&db_family);
let mut f = crate::optbytes::from_ob16(&ob);
let applied = match crate::optbytes::apply(&layout, &mut f, kv, 0xFF) {
Ok(a) => a,
Err(m) => {
return fail(
cli,
CMD,
ErrorKind::Usage,
m,
Some(&format!("keys for {db_family}: {}", layout.keys())),
);
}
};
let changes = applied.changes.join(", ");
let prompt = match applied.read_protect {
Some(false) => format!(
"Apply option changes [{changes}]? read-protect=off ERASES ALL FLASH (mass erase)."
),
Some(true) => format!(
"Apply option changes [{changes}]? read-protect=on makes the flash unreadable/undebuggable."
),
None => format!("Apply option changes [{changes}]?"),
};
if !ob_confirm(cli, &prompt) {
return fail(cli, CMD, ErrorKind::Usage, "aborted (no --yes)", None);
}
program_option(cli, CMD, &crate::optbytes::to_ob16(f))
}
fn already_in_state(cli: &Cli, cmd: &str, protected: bool) -> ExitCode {
if cli.json {
let mut env = ResultEnvelope::success(cmd);
env.result = Some(serde_json::json!({ "changed": false, "read_protected": protected }));
crate::print_envelope(&env)
} else {
println!(
"read protection is already {}",
if protected { "ON" } else { "OFF" }
);
ExitCode::SUCCESS
}
}
pub fn protect(cli: &Cli, state: SwitchState) -> ExitCode {
const CMD: &str = "target.protect";
let mut ob = match read_option_bytes(cli, CMD) {
Ok((_family, b)) => b,
Err(c) => return c,
};
match state {
SwitchState::On => {
if ob[0] != 0xA5 {
return already_in_state(cli, CMD, true);
}
if !ob_confirm(
cli,
"Enable read protection? The flash becomes unreadable/undebuggable until you turn it OFF (which ERASES all flash).",
) {
return fail(cli, CMD, ErrorKind::Usage, "aborted (no --yes)", None);
}
ob[0] = 0xFF;
ob[1] = 0x00;
}
SwitchState::Off => {
if ob[0] == 0xA5 {
return already_in_state(cli, CMD, false);
}
if !ob_confirm(
cli,
"Disable read protection? This ERASES ALL FLASH (mass erase) on the target.",
) {
return fail(cli, CMD, ErrorKind::Usage, "aborted (no --yes)", None);
}
ob[0] = 0xA5;
ob[1] = 0x5A;
}
}
program_option(cli, CMD, &ob)
}
pub fn list(cli: &Cli, identify: Option<crate::args::IdentifyScope>) -> ExitCode {
use crate::args::IdentifyScope;
use crate::cmd_probe::topology_key;
const CMD: &str = "target.list";
struct Place {
address: String,
mode: &'static str,
topology: String,
model: String,
label: String,
properties: serde_json::Value,
probe: Option<String>,
}
let mut places: Vec<Place> = Vec::new();
let mut links = crate::cmd_probe::wch_devices().unwrap_or_default();
links.sort_by_key(|e| topology_key(&e.dev.topology()));
for e in &links {
let topology = e.dev.topology();
let (address, probe) = match e.dev.serial() {
Some(sn) => (format!("wchlink://{sn}"), format!("serial:{sn}")),
None => (
format!("wchlink://usb-{topology}"),
format!("topology:{topology}"),
),
};
places.push(Place {
label: format!("WCH-Link {}", e.dev.serial().unwrap_or(&topology)),
address,
mode: crate::cmd_probe::mode_str(e.mode),
model: crate::cmd_probe::report_for(e).0.model,
properties: serde_json::json!({
"usb": e.dev.usb_id(), "serial": e.dev.serial(), "topology": topology,
}),
topology,
probe: Some(probe),
});
}
let mut oep = crate::oep::oep_devices();
oep.sort_by_key(|d| topology_key(&d.topology()));
let mut on_usb = Vec::new();
for d in &oep {
let id = crate::oep::probe_id(d);
on_usb.push(id.clone());
let ports = crate::oep::probe_for_listing(d)
.and_then(|mut p| ch32rv_oep::config::labels(&mut p).ok())
.map(|l| ch32rv_oep::config::ports_named(&l))
.unwrap_or_default();
let names = ports;
for port in names {
let address = format!("oep://{id}/{port}");
places.push(Place {
label: format!("{port} on OEP probe {id}"),
mode: "oep",
topology: d.topology(),
model: d.product().unwrap_or("OEP probe").to_owned(),
properties: serde_json::json!({
"usb": d.usb_id(), "serial": d.serial(), "topology": d.topology(),
"port_name": port, "ports": d.serial_ports(),
}),
probe: Some(address.clone()),
address,
});
}
}
let mut rest: Vec<Place> = crate::cmd_arduino::list_ports()
.into_iter()
.filter_map(|p| {
let protocol = p["protocol"].as_str().unwrap_or("").to_owned();
let address = p["address"].as_str().unwrap_or("-").to_owned();
let mode = match protocol.as_str() {
"oep" => {
let unit = address
.strip_prefix("oep://")
.and_then(|a| a.split('/').next())
.unwrap_or("");
if on_usb.iter().any(|u| u.eq_ignore_ascii_case(unit)) {
return None;
}
"oep"
}
"ch32rv-app" => "app",
"ch32rv-isp" => "isp",
"ch32rv-hid" => "hid",
_ => return None,
};
let topology = p["properties"]["topology"]
.as_str()
.unwrap_or("-")
.to_owned();
let model = match p["properties"]["tcp"].as_str() {
Some(addr) if mode == "oep" => crate::oep::net_model(addr),
_ => p["protocolLabel"].as_str().unwrap_or("-").to_owned(),
};
Some(Place {
probe: (mode == "oep").then(|| address.clone()),
label: p["label"].as_str().unwrap_or("-").to_owned(),
model,
properties: p["properties"].clone(),
address,
mode,
topology,
})
})
.collect();
let kind = |m: &str| match m {
"oep" => 0,
"app" => 1,
"isp" => 2,
_ => 3,
};
rest.sort_by(|a, b| {
(kind(a.mode), topology_key(&a.topology), &a.address).cmp(&(
kind(b.mode),
topology_key(&b.topology),
&b.address,
))
});
places.extend(rest);
let exe = std::env::current_exe().ok();
let mut rows = Vec::new();
let mut out = Vec::new();
for p in &places {
let wanted = match identify {
Some(IdentifyScope::All) => p.probe.is_some(),
Some(IdentifyScope::Oep) => p.mode == "oep",
None => false,
};
let mut chip = vec!["-".to_owned(); 5];
let mut target = serde_json::Value::Null;
if wanted && let (Some(sel), Some(exe)) = (&p.probe, &exe) {
match identify_at(exe, sel) {
Ok(t) => {
let kib = |v: &serde_json::Value| {
v.as_u64()
.map_or_else(|| "-".to_owned(), |b| format!("{} KiB", b / 1024))
};
chip = vec![
t["sku"]
.as_str()
.map(str::to_owned)
.or_else(|| candidates(&t["sku_candidates"]))
.or_else(|| t["family"].as_str().map(str::to_owned))
.unwrap_or_else(|| "unknown".to_owned()),
t["chip_id"].as_str().unwrap_or("-").to_owned(),
t["uid"].as_str().unwrap_or("-").to_owned(),
kib(&t["flash_bytes"]),
kib(&t["sram_bytes"]),
];
target = t;
}
Err(e) => {
chip[0] = if e.starts_with("name the pins") {
"(no pins labelled)".to_owned()
} else if e.contains("the request failed on the probe") {
"(no answer)".to_owned()
} else {
format!("({})", e.chars().take(40).collect::<String>())
};
target = serde_json::json!({ "error": e });
}
}
}
let mut row = vec![
p.address.clone(),
p.mode.to_owned(),
p.topology.clone(),
p.model.clone(),
];
row.extend(chip);
rows.push(row);
out.push(serde_json::json!({
"address": p.address,
"mode": p.mode,
"topology": p.topology,
"model": p.model,
"label": p.label,
"properties": p.properties,
"target": target,
}));
}
link_apps(&mut rows, &mut out);
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(serde_json::json!({ "places": out }));
crate::print_envelope(&env)
} else {
if rows.is_empty() {
eprintln!(
"no probe, application or bootloader found (run `ch32rv doctor` for diagnostics)"
);
} else {
let head = [
"ADDRESS", "MODE", "TOPOLOGY", "MODEL", "CHIP", "CHIP ID", "UID", "FLASH", "SRAM",
];
print!("{}", crate::parse::table(&head, &rows));
if identify.is_none() {
eprintln!(
"(`--identify` reads the chip at each OEP port without halting it; `--identify all` also attaches through each WCH-Link, which halts its target and sets its clock. An ADDRESS is a `--probe` selector as it is)"
);
}
}
ExitCode::SUCCESS
}
}
fn link_apps(rows: &mut [Vec<String>], out: &mut [serde_json::Value]) {
for i in 0..out.len() {
if out[i]["mode"] != "app" {
continue;
}
let Some(serial) = out[i]["address"]
.as_str()
.and_then(|a| a.rsplit_once("://"))
.map(|(_, s)| s.to_owned())
else {
continue;
};
let Some(j) = out.iter().position(|o| {
o["target"]["uid"]
.as_str()
.is_some_and(|u| u.eq_ignore_ascii_case(&serial))
}) else {
continue;
};
let chip = rows[j][4..].to_vec();
rows[i].splice(4.., chip);
out[i]["target"] = out[j]["target"].clone();
out[i]["runs_on"] = out[j]["address"].clone();
}
}
fn candidates(v: &serde_json::Value) -> Option<String> {
let names: Vec<&str> = v.as_array()?.iter().filter_map(|n| n.as_str()).collect();
let first = *names.first()?;
if names.len() == 1 {
return Some(first.to_owned());
}
let common = names.iter().fold(first.len(), |n, s| {
first
.bytes()
.zip(s.bytes())
.take(n)
.take_while(|(a, b)| a == b)
.count()
});
let keep = first[..common]
.rfind(|c: char| c.is_ascii_digit())
.map_or(0, |i| i + 1);
let rest: Vec<&str> = names[1..].iter().map(|n| &n[keep..]).collect();
Some(format!("{first}/{}", rest.join("/")))
}
fn identify_at(exe: &std::path::Path, selector: &str) -> Result<serde_json::Value, String> {
use std::io::Read;
let mut child = std::process::Command::new(exe)
.args([
"--json",
"--non-interactive",
"--probe",
selector,
"target",
"info",
])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| e.to_string())?;
let deadline = std::time::Instant::now() + Duration::from_secs(60);
loop {
match child.try_wait() {
Ok(Some(_)) => break,
Ok(None) if std::time::Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(50));
}
_ => {
let _ = child.kill();
let _ = child.wait();
return Err("no answer within 60 s".to_owned());
}
}
}
let mut text = String::new();
if let Some(mut o) = child.stdout.take() {
let _ = o.read_to_string(&mut text);
}
let v: serde_json::Value = serde_json::from_str(&text).map_err(|_| "no answer".to_owned())?;
if v["ok"] != serde_json::Value::Bool(true) {
return Err(v["error"]["msg"].as_str().unwrap_or("failed").to_owned());
}
let mut t = if v["target"].is_object() {
v["target"].clone()
} else {
v["result"]["target"].clone()
};
if let Some(m) = t.as_object_mut() {
m.insert(
"sku_candidates".to_owned(),
v["result"]["sku_candidates"].clone(),
);
}
if t["sram_bytes"].is_null() {
let sram = if v["result"]["sram_bytes"].is_null() {
v["result"]["target"]["sram_bytes"].clone()
} else {
v["result"]["sram_bytes"].clone()
};
if let Some(m) = t.as_object_mut() {
m.insert("sram_bytes".to_owned(), sram);
}
}
Ok(t)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::{
BLANKET_FACTORY, option_base, option_bytes_plausible, option_method_warning, parse_hex16,
reset_user_byte, unprotect_image,
};
#[test]
fn an_application_gets_the_chip_whose_uid_it_carries() {
let chip = |c: &str| vec![c.to_owned(); 5];
let head =
|a: &str, m: &str| vec![a.to_owned(), m.to_owned(), "-".to_owned(), "-".to_owned()];
let mut rows: Vec<Vec<String>> = [
("wchlink://FB868F068059", "riscv", "CH32X035F8U6"),
("ch32rv-app://C016ABCD283FBC5C", "app", "-"),
("ch32rv-app://B6C99C7F640EB63C", "app", "-"),
]
.iter()
.map(|(a, m, c)| [head(a, m), chip(c)].concat())
.collect();
let mut out = vec![
serde_json::json!({"address": "wchlink://FB868F068059", "mode": "riscv",
"target": {"uid": "c016abcd283fbc5c"}}),
serde_json::json!({"address": "ch32rv-app://C016ABCD283FBC5C", "mode": "app",
"target": null}),
serde_json::json!({"address": "ch32rv-app://B6C99C7F640EB63C", "mode": "app",
"target": null}),
];
super::link_apps(&mut rows, &mut out);
assert_eq!(rows[1][4], "CH32X035F8U6");
assert_eq!(out[1]["runs_on"], "wchlink://FB868F068059");
assert_eq!(out[1]["target"]["uid"], "c016abcd283fbc5c");
assert_eq!(rows[2][4], "-");
assert!(out[2]["runs_on"].is_null());
}
#[test]
fn sku_candidates_written_short() {
let v = serde_json::json!(["CH32V006K8U6", "CH32V006K8U7"]);
assert_eq!(super::candidates(&v).as_deref(), Some("CH32V006K8U6/U7"));
let one = serde_json::json!(["CH32V003F4P6"]);
assert_eq!(super::candidates(&one).as_deref(), Some("CH32V003F4P6"));
assert_eq!(super::candidates(&serde_json::json!([])), None);
assert_eq!(super::candidates(&serde_json::Value::Null), None);
}
#[test]
fn unprotect_keeps_everything_but_rdpr() {
let v20x = [
0xa5, 0x5a, 0x3f, 0xc0, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00,
0xff, 0x00,
];
let out = unprotect_image(&v20x);
assert_eq!(out[0], 0xA5, "RDPR cleared");
assert_eq!(out[1], 0x5A, "RDPR complement");
assert_eq!(
out[2], 0x3f,
"USER must survive (RAM_CODE_MOD = SRAM/flash split)"
);
assert_eq!(&out[4..], &v20x[4..], "Data/WRPR must survive");
let protected = [
0x00, 0xff, 0xf7, 0x08, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00,
0xff, 0x00,
];
let out = unprotect_image(&protected);
assert_eq!((out[0], out[1]), (0xA5, 0x5A));
assert_eq!(out[2], 0xf7, "CH32V003 RST_MODE must survive");
}
#[test]
fn plausibility_checks_rdpr_and_user_only() {
let v103 = [
0xa5, 0x5a, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff,
];
assert!(
option_bytes_plausible(&v103),
"CH32V103 as shipped is valid"
);
let garbage = [0u8; 16];
assert!(!option_bytes_plausible(&garbage));
let half = [
0xa5, 0x5a, 0xff, 0xff, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00,
0xff, 0x00,
];
assert!(!option_bytes_plausible(&half), "USER/complement disagree");
assert!(option_bytes_plausible(&BLANKET_FACTORY));
}
#[test]
fn reset_restores_known_bits_and_keeps_the_rest() {
assert_eq!(reset_user_byte("CH32V20x", 0x3f), 0x3f);
assert_eq!(reset_user_byte("CH32V307", 0xbf), 0xbf);
assert_eq!(reset_user_byte("CH32V003", 0xf7), 0xf7);
assert_eq!(reset_user_byte("CH32L103", 0xff), 0xff);
assert_eq!(reset_user_byte("CH32V20x", 0x3e), 0x3f);
assert_eq!(reset_user_byte("CH32V999", 0x12), 0x12);
}
#[test]
fn option_base_comes_from_the_db_and_m030_differs() {
assert_eq!(option_base("CH32V103").unwrap(), 0x1FFF_F800);
assert_eq!(option_base("CH32L103").unwrap(), 0x1FFF_F800);
assert_eq!(option_base("CH32V307").unwrap(), 0x1FFF_F800);
assert_eq!(
option_base("CH32M030").unwrap(),
0x1FFF_F300,
"CH32M030 keeps its option bytes at 0x1FFF_F300"
);
}
#[test]
fn option_base_fails_closed_for_an_unknown_family() {
assert!(option_base("CH32V999").is_err());
assert!(option_base("unknown (family byte 0x4e)").is_err());
}
#[test]
fn option_method_warning_only_for_untested_fast_page_families() {
assert!(option_method_warning("CH32L103").is_none());
assert!(option_method_warning("CH32V103").is_none()); assert!(option_method_warning("CH32M030").is_some());
}
#[test]
fn parses_16_contiguous_bytes() {
let b = parse_hex16("a55aff00ff00ff00ff00ff00ff00ff00").unwrap();
assert_eq!(b[0], 0xA5);
assert_eq!(b[1], 0x5A);
assert_eq!(b[15], 0x00);
}
#[test]
fn accepts_separators() {
let a = parse_hex16("a5:5a:ff:00:ff:00:ff:00:ff:00:ff:00:ff:00:ff:00").unwrap();
let b = parse_hex16("a5 5a ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00").unwrap();
assert_eq!(a, b);
assert_eq!(a[0], 0xA5);
}
#[test]
fn rejects_wrong_length() {
assert!(parse_hex16("abcd").is_err());
assert!(parse_hex16("a55aff00ff00ff00ff00ff00ff00ff0000").is_err()); }
#[test]
fn rejects_non_hex() {
assert!(parse_hex16("zz5aff00ff00ff00ff00ff00ff00ff00").is_err());
}
}