mod args;
mod broker;
mod broker_wch;
mod cmd_arduino;
mod cmd_boot;
mod cmd_capabilities;
mod cmd_db;
mod cmd_dbg;
mod cmd_doctor;
mod cmd_flash;
mod cmd_gdb;
mod cmd_isp;
mod cmd_monitor;
mod cmd_probe;
mod cmd_recover;
mod cmd_run;
mod cmd_target;
mod cmd_write;
mod config;
mod mdns;
mod oep;
mod optbytes;
mod parse;
mod progress;
mod session;
mod source;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use clap::{CommandFactory, FromArgMatches};
use args::*;
use ch32rv_contract::{self as contract, ErrorKind, ResultEnvelope};
fn main() -> std::process::ExitCode {
let matches = Cli::command().get_matches();
let mut cli = Cli::from_arg_matches(&matches).unwrap_or_else(|e| e.exit());
cli.probe_on_command_line = probe_on_command_line(&matches);
JSON.store(cli.json, Ordering::Relaxed);
if cli.chip.is_none() {
cli.chip = config::default_chip();
}
if cli
.chip
.as_deref()
.is_some_and(|c| c.trim().eq_ignore_ascii_case("auto"))
{
cli.chip = None;
}
if let Some(path) = cli.replay.as_deref() {
if cli.capture.is_some() {
eprintln!("error: --replay and --capture cannot be used together");
return ErrorKind::Usage.exit_code().into();
}
if let Err(e) = ch32rv_usb::replay::start(path) {
eprintln!("error: --replay: cannot load {}: {e}", path.display());
return ErrorKind::Usage.exit_code().into();
}
}
if let Some(path) = cli.capture.as_deref()
&& let Err(e) = ch32rv_usb::capture::start(path)
{
warn(
"capture-disabled",
format!("--capture disabled: cannot write {}: {e}", path.display()),
);
}
let code = run_command(&cli);
for w in take_warnings() {
eprintln!("warning[{}]: {}", w.code, w.msg);
}
if let Some((divergences, underruns)) = ch32rv_usb::replay::summary()
&& (divergences > 0 || underruns > 0)
{
eprintln!(
"warning: replay diverged from the recording ({divergences} write mismatch(es), {underruns} short read(s)) - the code may produce a different protocol than the capture"
);
}
code
}
fn borrow_link(cli: &Cli) -> Result<Option<broker::Lend>, broker::ClientLinkError> {
if cli.core != 0 {
return Ok(None);
}
use ch32rv_contract::policy::MonitorSource;
let direct = match &cli.command {
Command::Probe(p) => !matches!(p, ProbeCmd::List { .. }),
Command::Target(t) => !matches!(t, TargetCmd::Info | TargetCmd::List { .. }),
Command::Dbg(_)
| Command::Erase(_)
| Command::Recover(_)
| Command::Capabilities
| Command::Write(_)
| Command::Run(_) => true,
Command::Monitor(m) => {
m.cmd.is_some() || matches!(m.source, Some(MonitorSource::Sdi | MonitorSource::Rtt))
}
_ => false,
};
if !direct {
return Ok(None);
}
match oep::running_wch_broker(cli, "broker") {
Some(oep::OepAddr::Wch(t)) => match broker::lend(&t) {
Ok(l) => Ok(Some(l)),
Err(e) if e.other_version => Err(e),
Err(_) => Ok(None),
},
_ => Ok(None),
}
}
fn run_command(cli: &Cli) -> std::process::ExitCode {
if cli.dry_run
&& !matches!(
cli.command,
Command::Probe(ProbeCmd::Firmware(FirmwareCmd::Update { .. }))
| Command::Boot(BootCmd::Hid(HidBootCmd::Flash { .. }))
)
{
let name = canonical_name(&cli.command);
return cmd_probe::fail(
cli,
name,
ch32rv_contract::ErrorKind::Usage,
format!("`{name}` does not support --dry-run yet; nothing was done"),
Some("--dry-run works with `probe firmware update` and `boot hid flash`"),
);
}
if let Command::Boot(_) = &cli.command
&& cli.probe_on_command_line
{
let name = canonical_name(&cli.command);
let addr = cli.probe.as_deref().unwrap_or("<address>");
return cmd_probe::fail(
cli,
name,
ch32rv_contract::ErrorKind::Usage,
format!("`{name}` writes through no debug probe; --probe names one"),
Some(&format!("name the device to write with --device {addr}")),
);
}
if let Command::Isp(a) = &cli.command
&& let Some(addr) = a.app_device.as_deref()
{
return cmd_probe::fail(
cli,
canonical_name(&cli.command),
ch32rv_contract::ErrorKind::Usage,
"--app-device is gone; --device names the application as well as the bootloader",
Some(&format!("write --device {addr}")),
);
}
let core_supported = cli.core == 0
|| (cli.core == 1
&& matches!(
&cli.command,
Command::Dbg(
args::DbgCmd::Halt { reset: false }
| args::DbgCmd::Resume { .. }
| args::DbgCmd::Step { .. }
| args::DbgCmd::Regs
| args::DbgCmd::Reg(_)
| args::DbgCmd::Dmi(_)
) | Command::Read(_)
));
if !core_supported {
let what = format!(
"--core {}: supported nonzero selection is --core 1 with H417 dbg (without reset) or read",
cli.core
);
return cmd_probe::fail(
cli,
canonical_name(&cli.command),
ch32rv_contract::ErrorKind::CapabilityUnsupported,
what,
None,
);
}
if (cli.pins.is_some() || cli.release_dm) && !matches!(cli.command, Command::Arduino(_)) {
let name = canonical_name(&cli.command);
let oep = match oep::oep_route(cli, name) {
Ok(v) => v,
Err(c) => return c,
};
let refused = if cli.pins.is_some() && !oep {
Some("--pins names an OEP probe's pins; this command does not go to an OEP probe")
} else if cli.release_dm && oep {
Some("--release-dm is a WCH-Link session's last step; an OEP probe has no such step")
} else {
None
};
if let Some(m) = refused {
return cmd_probe::fail(
cli,
name,
ch32rv_contract::ErrorKind::CapabilityUnsupported,
m,
None,
);
}
}
oep::LOCK_WAIT_S.store(cli.lock_timeout, std::sync::atomic::Ordering::Relaxed);
oep::LINK_TIMEOUT_S.store(
cli.timeout.unwrap_or(0),
std::sync::atomic::Ordering::Relaxed,
);
session::CONNECT_UNDER_RESET.store(
cli.connect_under_reset,
std::sync::atomic::Ordering::Relaxed,
);
session::RELEASE_DM.store(cli.release_dm, std::sync::atomic::Ordering::Relaxed);
session::ATTACH_WAIT_S.store(
cli.attach_wait.unwrap_or(0),
std::sync::atomic::Ordering::Relaxed,
);
if let Some(pins) = &cli.pins {
match oep::parse_pins(pins) {
Ok(v) => {
let _ = oep::PINS.set(v);
}
Err(m) => {
return cmd_probe::fail(
cli,
canonical_name(&cli.command),
ch32rv_contract::ErrorKind::Usage,
m,
None,
);
}
}
}
session::RESUME_OTHER_HARTS.store(
!matches!(cli.command, Command::Dbg(_))
|| matches!(cli.command, Command::Dbg(DbgCmd::Resume { all: true })),
std::sync::atomic::Ordering::Relaxed,
);
if cli.chip.as_deref().is_some_and(|c| c.trim().is_empty()) {
return cmd_probe::fail(
cli,
canonical_name(&cli.command),
ch32rv_contract::ErrorKind::Usage,
"--chip is empty",
Some(
"leave --chip out to detect the target, or name a family / SKU (`ch32rv db list`)",
),
);
}
if let (Some(c), Ok(db)) = (cli.chip.as_deref(), cmd_db::db_for(cli))
&& db.families_for_chip_name(c).is_empty()
{
return cmd_probe::fail(
cli,
canonical_name(&cli.command),
ch32rv_contract::ErrorKind::TargetNotInDb,
format!("--chip {c} is not in the target DB"),
Some(
"name a family, series or SKU from `ch32rv db list`, or leave --chip out to detect the target",
),
);
}
let _lend = match borrow_link(cli) {
Ok(l) => l,
Err(e) => return oep::broker_fail(cli, canonical_name(&cli.command), e),
};
match &cli.command {
Command::Version => cmd_version(cli),
Command::Probe(ProbeCmd::List { watch }) => cmd_probe::list(cli, *watch),
Command::Probe(ProbeCmd::Info) => cmd_probe::info(cli),
Command::Probe(ProbeCmd::Mode(ModeCmd::Get)) => cmd_probe::mode_get(cli),
Command::Probe(ProbeCmd::Mode(ModeCmd::Set { mode })) => cmd_probe::mode_set(cli, *mode),
Command::Probe(ProbeCmd::Power(p)) => cmd_probe::power(cli, p),
Command::Probe(ProbeCmd::Firmware(FirmwareCmd::Info)) => cmd_probe::firmware_info(cli),
Command::Probe(ProbeCmd::Firmware(FirmwareCmd::Check { min })) => {
cmd_probe::firmware_check(cli, min.as_deref())
}
Command::Probe(ProbeCmd::Firmware(FirmwareCmd::ExitIap)) => {
cmd_probe::firmware_exit_iap(cli)
}
Command::Probe(ProbeCmd::Firmware(FirmwareCmd::Update { image })) => {
cmd_probe::firmware_update(cli, image)
}
Command::Target(TargetCmd::List { identify }) => cmd_target::list(cli, *identify),
Command::Target(TargetCmd::Info) => cmd_target::info(cli),
Command::Target(TargetCmd::Opt(OptionCmd::Get)) => cmd_target::option_get(cli),
Command::Target(TargetCmd::Opt(OptionCmd::WriteRaw { hex })) => {
cmd_target::option_write_raw(cli, hex)
}
Command::Target(TargetCmd::Opt(OptionCmd::Set { kv })) => cmd_target::option_set(cli, kv),
Command::Target(TargetCmd::Opt(OptionCmd::Reset)) => cmd_target::option_reset(cli),
Command::Target(TargetCmd::Protect { state }) => cmd_target::protect(cli, *state),
Command::Dbg(DbgCmd::Regs) => cmd_dbg::regs(cli),
Command::Dbg(DbgCmd::Halt { reset }) => cmd_dbg::halt(cli, *reset),
Command::Dbg(DbgCmd::Resume { .. }) => cmd_dbg::resume(cli),
Command::Dbg(DbgCmd::Step { n }) => cmd_dbg::step(cli, *n),
Command::Dbg(DbgCmd::Reg(sub)) => cmd_dbg::reg(cli, sub),
Command::Dbg(DbgCmd::Dmi(sub)) => cmd_dbg::dmi(cli, sub),
Command::Read(args) => cmd_dbg::read(cli, args),
Command::Flash(args) => cmd_flash::flash(cli, args),
Command::Verify(args) => cmd_flash::verify(cli, args),
Command::Erase(args) => cmd_flash::erase(cli, args),
Command::Reset(args) => cmd_flash::reset(cli, args),
Command::Recover(args) => cmd_flash::recover(cli, args),
Command::Doctor(args) => cmd_doctor::doctor(cli, args),
Command::Monitor(args) => cmd_monitor::monitor(cli, args),
Command::Gdb(args) => cmd_gdb::gdb(cli, args),
Command::Db(DbCmd::List {
family,
verified_only,
}) => cmd_db::list(cli, family.as_deref(), *verified_only),
Command::Db(DbCmd::Info { sku }) => cmd_db::info(cli, sku),
Command::Capabilities => cmd_capabilities::capabilities(cli),
Command::Write(args) => cmd_write::write(cli, args),
Command::Arduino(ArduinoCmd::Discovery) => cmd_arduino::discovery(cli),
Command::Arduino(ArduinoCmd::Monitor { protocol }) => cmd_arduino::monitor(cli, protocol),
Command::Run(args) => cmd_run::run(cli, args),
Command::Broker(BrokerCmd::Serve) => broker::serve(cli),
Command::Broker(BrokerCmd::Endpoint) => broker::endpoint(cli),
Command::Boot(BootCmd::Hid(HidBootCmd::Flash {
file,
usb_id,
wait,
device,
erase,
})) => cmd_boot::hid_flash(
cli,
file,
usb_id.as_deref(),
*wait,
device.as_deref(),
*erase,
),
Command::Boot(BootCmd::Dfu(DfuCmd::Flash {
file,
format,
at,
device,
erase,
})) => cmd_boot::dfu_flash(cli, file, *format, at.as_deref(), device.as_deref(), *erase),
Command::Boot(BootCmd::Dfu(DfuCmd::Info { device })) => {
cmd_boot::dfu_info(cli, device.as_deref())
}
Command::Isp(a) => cmd_isp::isp(cli, a),
Command::Complete(a) => cmd_complete(a.shell),
}
}
fn probe_on_command_line(m: &clap::ArgMatches) -> bool {
if m.value_source("probe") == Some(clap::parser::ValueSource::CommandLine) {
return true;
}
m.subcommand()
.is_some_and(|(_, sub)| probe_on_command_line(sub))
}
fn cmd_complete(shell: Shell) -> std::process::ExitCode {
let target = match shell {
Shell::Bash => clap_complete::Shell::Bash,
Shell::Zsh => clap_complete::Shell::Zsh,
Shell::Fish => clap_complete::Shell::Fish,
Shell::Powershell => clap_complete::Shell::PowerShell,
};
let mut cmd = Cli::command();
clap_complete::generate(target, &mut cmd, "ch32rv", &mut std::io::stdout());
std::process::ExitCode::SUCCESS
}
fn cmd_version(cli: &Cli) -> std::process::ExitCode {
let git_rev = env!("CH32RV_GIT_REV");
let stub_digest = ch32rv_flash::stub::stub_digest();
let db = ch32rv_target::provenance();
let mut env = ResultEnvelope::success("version");
env.result = Some(serde_json::json!({
"version": env!("CARGO_PKG_VERSION"),
"git_rev": git_rev,
"target_db": {
"source": "ch32-device-data",
"source_rev": db.source_rev,
"digest": db.digest,
},
"flash_stub_digest": stub_digest,
"build": {
"os": std::env::consts::OS,
"arch": std::env::consts::ARCH,
},
}));
if cli.json {
print_envelope(&env)
} else {
println!("ch32rv {} ({git_rev})", env!("CARGO_PKG_VERSION"));
println!("contract: {}", contract::CONTRACT_VERSION);
println!(
"target db: ch32-device-data@{} ({})",
db.source_rev.as_deref().unwrap_or("unknown"),
db.digest
);
println!("flash stub: {stub_digest}");
println!(
"build: {}-{}",
std::env::consts::ARCH,
std::env::consts::OS
);
std::process::ExitCode::SUCCESS
}
}
static JSON: AtomicBool = AtomicBool::new(false);
static WARNINGS: Mutex<Vec<contract::envelope::Warning>> = Mutex::new(Vec::new());
pub(crate) fn warn(code: &str, msg: impl Into<String>) {
let msg = msg.into();
if JSON.load(Ordering::Relaxed) {
if let Ok(mut w) = WARNINGS.lock()
&& !w.iter().any(|x| x.code == code && x.msg == msg)
{
w.push(contract::envelope::Warning {
code: code.to_owned(),
msg,
});
}
} else {
eprintln!("warning[{code}]: {msg}");
}
}
fn take_warnings() -> Vec<contract::envelope::Warning> {
WARNINGS
.lock()
.map(|mut w| std::mem::take(&mut *w))
.unwrap_or_default()
}
pub(crate) fn print_envelope(env: &ResultEnvelope) -> std::process::ExitCode {
let pending = take_warnings();
let merged;
let env = if pending.is_empty() {
env
} else {
let mut e = env.clone();
e.warnings.extend(pending);
merged = e;
&merged
};
match serde_json::to_string(env) {
Ok(s) => {
println!("{s}");
if env.ok {
std::process::ExitCode::SUCCESS
} else {
env.error
.as_ref()
.map(|e| std::process::ExitCode::from(e.code))
.unwrap_or(contract::ExitCode::Internal.into())
}
}
Err(e) => {
eprintln!("ch32rv: internal error: failed to serialize result: {e}");
contract::ExitCode::Internal.into()
}
}
}
pub(crate) fn canonical_name(cmd: &Command) -> &'static str {
match cmd {
Command::Flash(_) => "flash",
Command::Verify(_) => "verify",
Command::Read(_) => "read",
Command::Write(_) => "write",
Command::Erase(_) => "erase",
Command::Reset(_) => "reset",
Command::Run(_) => "run",
Command::Recover(_) => "recover",
Command::Probe(p) => match p {
ProbeCmd::List { .. } => "probe.list",
ProbeCmd::Info => "probe.info",
ProbeCmd::Power(_) => "probe.power",
ProbeCmd::Mode(ModeCmd::Get) => "probe.mode.get",
ProbeCmd::Mode(ModeCmd::Set { .. }) => "probe.mode.set",
ProbeCmd::Firmware(FirmwareCmd::Info) => "probe.firmware.info",
ProbeCmd::Firmware(FirmwareCmd::Check { .. }) => "probe.firmware.check",
ProbeCmd::Firmware(FirmwareCmd::ExitIap) => "probe.firmware.exit-iap",
ProbeCmd::Firmware(FirmwareCmd::Update { .. }) => "probe.firmware.update",
},
Command::Target(t) => match t {
TargetCmd::List { .. } => "target.list",
TargetCmd::Info => "target.info",
TargetCmd::Opt(OptionCmd::Get) => "target.option.get",
TargetCmd::Opt(OptionCmd::Set { .. }) => "target.option.set",
TargetCmd::Opt(OptionCmd::Reset) => "target.option.reset",
TargetCmd::Opt(OptionCmd::WriteRaw { .. }) => "target.option.write-raw",
TargetCmd::Protect { .. } => "target.protect",
},
Command::Dbg(d) => match d {
DbgCmd::Halt { .. } => "dbg.halt",
DbgCmd::Resume { .. } => "dbg.resume",
DbgCmd::Step { .. } => "dbg.step",
DbgCmd::Regs => "dbg.regs",
DbgCmd::Reg(_) => "dbg.reg",
DbgCmd::Dmi(_) => "dbg.dmi",
},
Command::Monitor(m) => match &m.cmd {
Some(MonitorCmd::List) => "monitor.list",
Some(MonitorCmd::Sdi { .. }) => "monitor.sdi",
None => "monitor",
},
Command::Gdb(_) => "gdb",
Command::Isp(i) => match &i.cmd {
IspCmd::List => "isp.list",
IspCmd::Info => "isp.info",
IspCmd::Enter { .. } => "isp.enter",
IspCmd::Flash { .. } => "isp.flash",
IspCmd::Verify { .. } => "isp.verify",
IspCmd::Erase => "isp.erase",
IspCmd::Config(IspConfigCmd::Get) => "isp.config.get",
IspCmd::Config(IspConfigCmd::Set { .. }) => "isp.config.set",
IspCmd::Reset => "isp.reset",
},
Command::Boot(b) => match b {
BootCmd::Dfu(DfuCmd::Flash { .. }) => "boot.dfu.flash",
BootCmd::Dfu(DfuCmd::Info { .. }) => "boot.dfu.info",
BootCmd::Hid(HidBootCmd::Flash { .. }) => "boot.hid.flash",
},
Command::Db(d) => match d {
DbCmd::List { .. } => "db.list",
DbCmd::Info { .. } => "db.info",
},
Command::Capabilities => "capabilities",
Command::Doctor(_) => "doctor",
Command::Version => "version",
Command::Complete(_) => "complete",
Command::Broker(b) => match b {
BrokerCmd::Serve => "broker.serve",
BrokerCmd::Endpoint => "broker.endpoint",
},
Command::Arduino(a) => match a {
ArduinoCmd::Discovery => "arduino.discovery",
ArduinoCmd::Monitor { .. } => "arduino.monitor",
},
}
}