use std::io::Write;
use std::process::ExitCode;
use std::time::{Duration, Instant};
use ch32rv_contract::ErrorKind;
use ch32rv_contract::policy::MonitorSource;
use crate::args::{Cli, MonitorArgs, MonitorCmd, SwitchState};
use crate::cmd_probe::{Entry, fail, mode_str, select_entry};
use crate::parse;
use crate::session::Session;
pub fn monitor(cli: &Cli, args: &MonitorArgs) -> ExitCode {
match &args.cmd {
Some(MonitorCmd::List) => return list(cli),
Some(MonitorCmd::Sdi { state }) => return sdi_toggle(cli, *state),
None => {}
}
match args.source {
MonitorSource::Uart => run_uart(cli, args),
MonitorSource::Sdi => run_sdi(cli, args),
MonitorSource::Dmdata => run_dmdata(cli, args),
MonitorSource::Rtt => run_rtt(cli, args),
}
}
fn run_duration(cli: &Cli) -> Option<Duration> {
cli.duration.map(Duration::from_secs)
}
fn resolve_port(
cli: &Cli,
cmd: &str,
entry: &Entry,
explicit: &Option<String>,
) -> Result<String, ExitCode> {
match explicit {
Some(p) => Ok(p.clone()),
None => entry.dev.serial_ports().into_iter().next().ok_or_else(|| {
fail(
cli,
cmd,
ErrorKind::DeviceNotFound,
"no CDC serial port found for this probe",
Some("pass --port /dev/ttyACMx, or check the probe's serial interface"),
)
}),
}
}
fn stream_port(
cli: &Cli,
cmd: &str,
port_path: &str,
baud: u32,
label: &str,
raw: bool,
) -> ExitCode {
if !cli.json {
eprintln!("monitor: {label} on {port_path} @ {baud} baud (Ctrl-C to stop)");
}
let deadline = run_duration(cli).map(|d| Instant::now() + d);
let mut out = std::io::stdout().lock();
let mut buf = [0u8; 512];
#[cfg(unix)]
if raw {
use std::io::Read;
let mut file = match std::fs::File::open(port_path) {
Ok(f) => f,
Err(e) => {
return fail(
cli,
cmd,
ErrorKind::DeviceOpenFailed,
format!("open {port_path}: {e}"),
None,
);
}
};
loop {
if let Some(dl) = deadline
&& Instant::now() >= dl
{
break;
}
match file.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
let _ = out.write_all(&buf[..n]);
let _ = out.flush();
}
Err(e) => {
eprintln!("\nmonitor: serial error: {e}");
break;
}
}
}
return ExitCode::SUCCESS;
}
#[cfg(not(unix))]
let _ = raw;
let mut sp = match serialport::new(port_path, baud)
.timeout(Duration::from_millis(200))
.open()
{
Ok(s) => s,
Err(e) => {
return fail(
cli,
cmd,
ErrorKind::DeviceOpenFailed,
format!("open {port_path}: {e}"),
None,
);
}
};
loop {
if let Some(dl) = deadline
&& Instant::now() >= dl
{
break;
}
match std::io::Read::read(&mut sp, &mut buf) {
Ok(0) => {}
Ok(n) => {
let _ = out.write_all(&buf[..n]);
let _ = out.flush();
}
Err(ref e) if e.kind() == std::io::ErrorKind::TimedOut => {}
Err(e) => {
eprintln!("\nmonitor: serial error: {e}");
break;
}
}
}
ExitCode::SUCCESS
}
fn run_uart(cli: &Cli, args: &MonitorArgs) -> ExitCode {
const CMD: &str = "monitor";
let entry = match select_entry(cli, CMD) {
Ok(e) => e,
Err(c) => return c,
};
let _lock = match crate::cmd_probe::lock_probe(cli, CMD, &entry) {
Ok(l) => l,
Err(c) => return c,
};
let port = match resolve_port(cli, CMD, &entry, &args.port) {
Ok(p) => p,
Err(c) => return c,
};
stream_port(cli, CMD, &port, args.baud, "uart", false)
}
fn run_sdi(cli: &Cli, args: &MonitorArgs) -> ExitCode {
const CMD: &str = "monitor";
let entry = match select_entry(cli, CMD) {
Ok(e) => e,
Err(c) => return c,
};
if entry.mode != ch32rv_contract::ProbeMode::Riscv {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
"sdi needs a RISC-V-mode LinkE",
None,
);
}
let _lock = match crate::cmd_probe::lock_probe(cli, CMD, &entry) {
Ok(l) => l,
Err(c) => return c,
};
let (speed, mut warnings) = match parse::speed(&cli.speed) {
Ok(v) => v,
Err(m) => return fail(cli, CMD, ErrorKind::Usage, m, None),
};
let _ = &mut warnings;
{
let mut link = match ch32rv_wchlink::WchLink::open(&entry.dev) {
Ok(l) => l,
Err(e) => return fail(cli, CMD, ErrorKind::DeviceOpenFailed, e.to_string(), None),
};
match link.probe_info() {
Ok(info) if matches!(info.variant, ch32rv_wchlink::Variant::LinkE) => {}
Ok(_) => {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
"SDI print forwarding is only available on a WCH-LinkE",
Some(
"use --source dmdata (host-side DMI) which works on any probe including the CH549 Link",
),
);
}
Err(e) => return fail(cli, CMD, ErrorKind::DeviceOpenFailed, e.to_string(), None),
}
let _ = link.set_speed_default(speed);
let attach = match link.attach_chip() {
Ok(a) => a,
Err(e) => return fail(cli, CMD, ErrorKind::AttachFailed, e.to_string(), None),
};
let _ = link.set_speed(attach.family_byte, speed);
if let Err(e) = link.set_sdi_print_enabled(true) {
return fail(
cli,
CMD,
ErrorKind::TransferFailed,
format!("enable SDI failed: {e}"),
None,
);
}
}
std::thread::sleep(Duration::from_millis(200));
let port = match resolve_port(cli, CMD, &entry, &args.port) {
Ok(p) => p,
Err(c) => return c,
};
if !cli.json {
eprintln!(
"note: sdi CDC forwarding is not yet reliable from ch32rv; if nothing appears, use \
`--source dmdata` (SerialDMDATA) or `wlink sdi-print enable`."
);
}
stream_port(cli, CMD, &port, args.baud, "sdi", true)
}
fn run_dmdata(cli: &Cli, _args: &MonitorArgs) -> ExitCode {
const CMD: &str = "monitor";
let entry = match select_entry(cli, CMD) {
Ok(e) => e,
Err(c) => return c,
};
if entry.mode != ch32rv_contract::ProbeMode::Riscv {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
format!(
"dmdata monitor needs a RISC-V-mode probe (this is {})",
mode_str(entry.mode)
),
None,
);
}
let (speed, mut warnings) = match parse::speed(&cli.speed) {
Ok(v) => v,
Err(m) => return fail(cli, CMD, ErrorKind::Usage, m, None),
};
let mut session = match Session::attach(
&entry,
speed,
Duration::from_millis(1000),
Duration::from_secs(cli.lock_timeout),
cli.chip.as_deref(),
&mut warnings,
) {
Ok(s) => s,
Err(e) => return crate::cmd_probe::session_error(cli, CMD, e),
};
if !cli.json {
eprintln!(
"monitor: dmdata (DMI poll, core runs) via {} (Ctrl-C to stop)",
entry.dev.serial().unwrap_or("?")
);
}
let deadline = run_duration(cli).map(|d| Instant::now() + d);
let mut out = std::io::stdout().lock();
let mut dm = session.dm();
let _ = dm.resume();
loop {
if let Some(dl) = deadline
&& Instant::now() >= dl
{
break;
}
match dm.dmdata_poll(&[]) {
Ok(Some(bytes)) if !bytes.is_empty() => {
let _ = out.write_all(&bytes);
let _ = out.flush();
}
Ok(_) => std::thread::sleep(Duration::from_millis(2)),
Err(e) => {
eprintln!("\nmonitor: dmi error: {e}");
break;
}
}
}
ExitCode::SUCCESS
}
const RTT_RAM_BASE: u32 = 0x2000_0000;
const RTT_MAGIC: &[u8] = b"SEGGER RTT";
const RTT_MAX_BUF: u32 = 0x1_0000;
const RTT_DEFAULT_SCAN: u32 = 8 * 1024;
const RTT_READ_CHUNK: u32 = 8192;
fn le32(b: &[u8], off: usize) -> u32 {
u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
fn read_region(session: &mut Session, base: u32, len: u32) -> Vec<u8> {
let mut buf = Vec::with_capacity(len as usize);
let mut off = 0u32;
while off < len {
let want = RTT_READ_CHUNK.min(len - off);
match session.link().read_mem(base + off, want) {
Ok(mut chunk) => {
buf.append(&mut chunk);
off += want;
}
Err(_) => break,
}
}
buf
}
fn find_control_block(snap: &[u8]) -> Option<usize> {
let mut from = 0usize;
while let Some(rel) = snap[from..]
.windows(RTT_MAGIC.len())
.position(|w| w == RTT_MAGIC)
{
let pos = from + rel;
let d = pos + 24;
if d + 24 <= snap.len() {
let buffer = le32(snap, d + 4);
let size = le32(snap, d + 8);
let wr = le32(snap, d + 12);
let rd = le32(snap, d + 16);
if size > 0 && size <= RTT_MAX_BUF && wr < size && rd < size && buffer >= RTT_RAM_BASE {
return Some(pos);
}
}
from = pos + 1;
}
None
}
fn run_rtt(cli: &Cli, _args: &MonitorArgs) -> ExitCode {
const CMD: &str = "monitor";
let entry = match select_entry(cli, CMD) {
Ok(e) => e,
Err(c) => return c,
};
if entry.mode != ch32rv_contract::ProbeMode::Riscv {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
format!(
"rtt monitor needs a RISC-V-mode probe (this is {})",
mode_str(entry.mode)
),
None,
);
}
let (speed, mut warnings) = match parse::speed(&cli.speed) {
Ok(v) => v,
Err(m) => return fail(cli, CMD, ErrorKind::Usage, m, None),
};
let mut session = match Session::attach(
&entry,
speed,
Duration::from_millis(1000),
Duration::from_secs(cli.lock_timeout),
cli.chip.as_deref(),
&mut warnings,
) {
Ok(s) => s,
Err(e) => return crate::cmd_probe::session_error(cli, CMD, e),
};
let scan_len = {
let db = ch32rv_target::Db::builtin();
match db.resolve_by_chip_id(session.attach.chip_id) {
ch32rv_target::Resolution::Sku(s) if s.sram_bytes > 0 => s.sram_bytes.min(64 * 1024),
_ => RTT_DEFAULT_SCAN,
}
};
let cb_base = 'find: {
for _ in 0..10 {
let _ = session.dm().halt();
let snap = read_region(&mut session, RTT_RAM_BASE, scan_len);
if let Some(cb) = find_control_block(&snap) {
break 'find Some(RTT_RAM_BASE + cb as u32);
}
let _ = session.dm().resume();
std::thread::sleep(Duration::from_millis(100));
}
None
};
let cb_base = match cb_base {
Some(b) => b,
None => {
let _ = session.dm().resume();
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
format!(
"no SEGGER RTT control block in the first {scan_len} bytes of RAM (from 0x{RTT_RAM_BASE:08x})"
),
Some("flash a SerialRTT/RTT sketch first; the block only appears after begin()"),
);
}
};
if !cli.json {
eprintln!(
"monitor: rtt (RAM ring @ 0x{cb_base:08x}, core briefly halts per poll) via {} (Ctrl-C to stop)",
entry.dev.serial().unwrap_or("?")
);
}
let up = cb_base + 24;
let deadline = run_duration(cli).map(|d| Instant::now() + d);
let mut out = std::io::stdout().lock();
loop {
if let Some(dl) = deadline
&& Instant::now() >= dl
{
break;
}
let _ = session.dm().halt();
let desc = match session.link().read_mem(up, 24) {
Ok(d) if d.len() >= 24 => d,
_ => {
let _ = session.dm().resume();
break;
}
};
let buffer = le32(&desc, 4);
let size = le32(&desc, 8);
let wr = le32(&desc, 12);
let rd = le32(&desc, 16);
if size == 0 || size > RTT_MAX_BUF || wr >= size || rd >= size || buffer < RTT_RAM_BASE {
let _ = session.dm().resume();
std::thread::sleep(Duration::from_millis(50));
continue;
}
if wr != rd {
let bytes = if wr > rd {
session.link().read_mem(buffer + rd, wr - rd).ok()
} else {
let mut v = Vec::new();
if let Ok(a) = session.link().read_mem(buffer + rd, size - rd) {
v.extend_from_slice(&a);
}
if let Ok(b) = session.link().read_mem(buffer, wr) {
v.extend_from_slice(&b);
}
Some(v)
};
if let Some(bytes) = bytes {
let _ = out.write_all(&bytes);
let _ = out.flush();
}
let _ = session.dm().write_mem32(up + 16, wr);
}
let _ = session.dm().resume();
std::thread::sleep(Duration::from_millis(50));
}
ExitCode::SUCCESS
}
fn list(cli: &Cli) -> ExitCode {
let entries = crate::cmd_probe::wch_devices().unwrap_or_default();
if cli.json {
let ports: Vec<_> = entries
.iter()
.map(|e| {
serde_json::json!({
"probe": e.dev.serial(),
"mode": mode_str(e.mode),
"cdc_ports": e.dev.serial_ports(),
})
})
.collect();
let mut env = ch32rv_contract::ResultEnvelope::success("monitor.list");
env.result = Some(serde_json::json!({ "probes": ports }));
crate::print_envelope(&env)
} else {
println!(
"{:<16} {:<7} CDC PORTS (uart / sdi share these)",
"PROBE", "MODE"
);
for e in &entries {
println!(
"{:<16} {:<7} {}",
e.dev.serial().unwrap_or("-"),
mode_str(e.mode),
render_ports(&e.dev.serial_ports())
);
}
println!("\ndmdata / rtt do not use a CDC port (host reads them over DMI).");
ExitCode::SUCCESS
}
}
fn render_ports(ports: &[String]) -> String {
if ports.is_empty() {
"-".to_owned()
} else {
ports.join(", ")
}
}
fn sdi_toggle(cli: &Cli, state: SwitchState) -> ExitCode {
const CMD: &str = "monitor.sdi";
let entry: Entry = match select_entry(cli, CMD) {
Ok(e) => e,
Err(c) => return c,
};
let mut link = match ch32rv_wchlink::WchLink::open(&entry.dev) {
Ok(l) => l,
Err(e) => return fail(cli, CMD, ErrorKind::DeviceOpenFailed, e.to_string(), None),
};
let on = matches!(state, SwitchState::On);
let _ = link.probe_info();
let (speed, _) = parse::speed(&cli.speed).unwrap_or((ch32rv_wchlink::Speed::High, Vec::new()));
let _ = link.set_speed_default(speed);
if let Ok(attach) = link.attach_chip() {
let _ = link.set_speed(attach.family_byte, speed);
}
if let Err(e) = link.set_sdi_print_enabled(on) {
return fail(
cli,
CMD,
ErrorKind::TransferFailed,
format!("set SDI failed: {e}"),
None,
);
}
if cli.json {
let mut env = ch32rv_contract::ResultEnvelope::success(CMD);
env.result = Some(serde_json::json!({ "sdi": on }));
crate::print_envelope(&env)
} else {
println!(
"SDI print forwarding {}",
if on { "enabled" } else { "disabled" }
);
ExitCode::SUCCESS
}
}
#[cfg(test)]
mod tests {
use super::*;
fn snapshot_with_cb(at: usize, buffer: u32, size: u32, wr: u32, rd: u32) -> Vec<u8> {
let mut ram = vec![0u8; at + 24 + 24 + 8];
ram[at..at + RTT_MAGIC.len()].copy_from_slice(RTT_MAGIC);
let d = at + 24; ram[d + 4..d + 8].copy_from_slice(&buffer.to_le_bytes());
ram[d + 8..d + 12].copy_from_slice(&size.to_le_bytes());
ram[d + 12..d + 16].copy_from_slice(&wr.to_le_bytes());
ram[d + 16..d + 20].copy_from_slice(&rd.to_le_bytes());
ram
}
#[test]
fn finds_valid_control_block() {
let ram = snapshot_with_cb(64, RTT_RAM_BASE + 0x100, 256, 10, 0);
assert_eq!(find_control_block(&ram), Some(64));
}
#[test]
fn skips_magic_with_bogus_descriptor() {
let ram = snapshot_with_cb(64, 0, 0xFFFF_FFFF, 0, 0);
assert_eq!(find_control_block(&ram), None);
}
#[test]
fn no_magic_returns_none() {
assert_eq!(find_control_block(&[0u8; 64]), None);
}
#[test]
fn le32_reads_little_endian() {
assert_eq!(le32(&[0x78, 0x56, 0x34, 0x12], 0), 0x1234_5678);
}
}