use std::path::{Path, PathBuf};
use std::process::Command;
use std::{fs, io::Read};
fn main() {
let args: Vec<String> = std::env::args().collect();
let subcommand = if args.len() > 2 {
let first = &args[2];
if first == "debug" || first == "d" {
Some("debug")
} else {
None
}
} else {
None
};
if args.len() > 2 {
let sub_args: Vec<&str> = args[2..].iter().map(|s| s.as_str()).collect();
if sub_args.contains(&"--help") || sub_args.contains(&"-h") {
print_help();
return;
}
}
let config = load_config("Cargo.toml");
let pkg_name = config.pkg_name.clone();
let target_triple = config.target_triple.clone();
let mut cargo_args = vec!["build".to_string()];
let mut release = false;
let mut user_gdb: Option<String> = None; let mut gdb_port: u16 = 3333;
let mut i = 2;
while i < args.len() {
let arg = &args[i];
if arg == "--release" {
release = true;
cargo_args.push("--release".to_string());
} else if arg == "--help" || arg == "-h" {
} else if arg == "debug" || arg == "d" {
} else if arg == "--gdb" || arg == "--rust-gdb" {
let gdb_name = if arg == "--gdb" {
"gdb"
} else {
"rust-gdb"
};
if !check_gdb(gdb_name) {
eprintln!("[ERROR] 指定的 GDB '{}' 未找到或不可执行", gdb_name);
eprintln!(" 请确保已安装后再使用 --{}", if arg == "--gdb" { "gdb" } else { "rust-gdb" });
std::process::exit(1);
}
user_gdb = Some(gdb_name.to_string());
} else if arg == "--port" {
i += 1;
if i >= args.len() {
eprintln!("[ERROR] --port 参数需要指定端口号");
eprintln!(" 用法: cargo ocd d --port 3333");
std::process::exit(1);
}
match args[i].parse::<u16>() {
Ok(port) => gdb_port = port,
Err(_) => {
eprintln!("[ERROR] 无效的端口号: '{}'", args[i]);
eprintln!(" 端口号应为 1-65535 之间的数字");
std::process::exit(1);
}
}
} else {
cargo_args.push(arg.clone());
}
i += 1;
}
println!("[BUILD] Compiling firmware...");
let status = Command::new("cargo")
.env("RUSTFLAGS", "-C link-arg=-Tlink.x")
.args(&cargo_args)
.arg("--target")
.arg(&target_triple)
.status()
.expect("编译失败");
if !status.success() {
eprintln!("[ERROR] Build failed");
std::process::exit(1);
}
let target_dir = if release {
format!("target/{}/release", target_triple)
} else {
format!("target/{}/debug", target_triple)
};
let elf_path = PathBuf::from(&target_dir).join(&pkg_name);
if !elf_path.exists() {
eprintln!("[ERROR] Firmware not found: {:?}", elf_path);
std::process::exit(1);
}
println!();
show_firmware_size(&elf_path);
match subcommand {
Some("debug") => {
if release {
eprintln!("[ERROR] Release 模式不支持调试,请使用 Debug 模式");
eprintln!(" cargo ocd d # Debug 编译 + 烧录 + GDB 调试");
std::process::exit(1);
}
run_debug(&config, &elf_path, user_gdb, gdb_port);
}
_ => run_flash(&config, &elf_path),
}
}
fn run_flash(config: &OcdConfig, elf_path: &Path) {
println!();
let elf_str = elf_path.to_string_lossy().replace('\\', "/");
println!("[FLASH] Firmware: {}", elf_str);
println!("[FLASH] Programming via OpenOCD...");
let status = Command::new("openocd")
.args(&[
"-f",
&config.interface,
"-f",
&config.target_chip,
"-c",
&format!("program {} verify reset exit", elf_str),
])
.status()
.expect("无法执行 openocd,请确保已安装");
if !status.success() {
eprintln!("[ERROR] Flash failed");
std::process::exit(1);
}
println!();
println!("[DONE] Flash complete!");
}
fn run_debug(config: &OcdConfig, elf_path: &Path, user_gdb: Option<String>, gdb_port: u16) {
let target_arch = detect_target_arch(&config.target_triple);
println!();
let elf_str = elf_path.to_string_lossy().replace('\\', "/");
println!("[DEBUG] Firmware: {}", elf_str);
println!("[DEBUG] Programming & starting GDB server...");
let flash_status = Command::new("openocd")
.args(&[
"-f",
&config.interface,
"-f",
&config.target_chip,
"-c",
&format!("program {} verify reset exit", elf_str),
])
.status()
.expect("无法执行 openocd,请确保已安装");
if !flash_status.success() {
eprintln!("[ERROR] Flash failed");
std::process::exit(1);
}
println!();
println!("[DEBUG] Starting OpenOCD GDB server on port {}...", gdb_port);
println!("[DEBUG] Connect GDB with: target remote :{}", gdb_port);
println!("[DEBUG] ELF file: {}", elf_str);
println!();
let mut openocd = Command::new("openocd")
.args(&[
"-f",
&config.interface,
"-f",
&config.target_chip,
"-c",
&format!("gdb_port {}", gdb_port),
"-c",
&format!("program {}", elf_str),
"-c",
"reset halt",
])
.spawn()
.expect("无法执行 openocd,请确保已安装");
std::thread::sleep(std::time::Duration::from_secs(2));
eprintln!("[DEBUG] 等待 GDB 服务器端口 {} 就绪...", gdb_port);
let max_wait = std::time::Duration::from_secs(10);
let poll_interval = std::time::Duration::from_millis(200);
let start = std::time::Instant::now();
let mut port_ready = false;
while start.elapsed() < max_wait {
if std::net::TcpStream::connect(("127.0.0.1", gdb_port)).is_ok() {
port_ready = true;
break;
}
std::thread::sleep(poll_interval);
}
if port_ready {
eprintln!("[DEBUG] GDB 服务器端口 {} 已就绪(耗时 {:.1}s)", gdb_port, start.elapsed().as_secs_f64());
} else {
eprintln!("[WARN] GDB 服务器端口 {} 在 {:.0}s 内未就绪,仍尝试连接...", gdb_port, max_wait.as_secs_f64());
}
let gdb = match user_gdb {
Some(name) => name,
None => find_gdb(&target_arch),
};
println!("[DEBUG] Starting GDB: {}", gdb);
println!("[DEBUG] Auto-executing: target remote :{}, break main, continue", gdb_port);
println!("[DEBUG] 已自动在 main() 设置断点,程序将在 main 入口处暂停");
println!("[DEBUG] 之后可使用 GDB 命令单步调试(见文档 9.3 节)");
println!();
let gdb_args: Vec<String> = vec![
"-ex".to_string(),
format!("target remote :{}", gdb_port),
"-ex".to_string(),
"break main".to_string(),
"-ex".to_string(),
"continue".to_string(),
elf_str.to_string(),
];
let gdb_status = Command::new(&gdb)
.args(&gdb_args)
.status()
.expect("无法启动 GDB,请确保已安装 gdb 或 rust-gdb 最后考虑 arm-none-eabi-gdb");
let _ = openocd.kill();
println!("[DEBUG] Debug session ended.");
if !gdb_status.success() {
eprintln!();
eprintln!("[ERROR] GDB exited with error.");
eprintln!("提示: 请确保已安装 GDB 调试器");
let os = std::env::consts::OS;
match os {
"macos" => {
eprintln!(" macOS 推荐: brew install gdb");
}
"windows" => {
eprintln!(" Windows 推荐: arm-none-eabi-gdb(ARM GCC 工具链)");
eprintln!(" 下载: https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads");
}
"linux" => {
eprintln!(" Linux 推荐: sudo apt install gdb-arm-none-eabi");
}
_ => {
eprintln!(" 请安装 GDB(如 gdb-multiarch、rust-gdb 或 arm-none-eabi-gdb)");
}
}
eprintln!();
std::process::exit(1);
}
}
fn detect_target_arch(target_triple: &str) -> &str {
if target_triple.starts_with("thumbv")
|| target_triple.starts_with("armv")
|| target_triple.starts_with("arm")
{
"arm"
} else if target_triple.starts_with("riscv") {
"riscv"
} else {
"unknown"
}
}
fn check_gdb(name: &str) -> bool {
Command::new(name)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok()
}
fn find_gdb(target_arch: &str) -> String {
let os = std::env::consts::OS;
let candidates: &[&str] = match (target_arch, os) {
("arm", "macos") => &["rust-gdb", "gdb", "arm-none-eabi-gdb"],
("arm", "windows") => &["arm-none-eabi-gdb"],
("arm", "linux") => &["arm-none-eabi-gdb"],
("riscv", "macos") => &["riscv64-unknown-elf-gdb", "rust-gdb", "gdb"],
("riscv", "windows") => &["riscv64-unknown-elf-gdb"],
("riscv", "linux") => &["riscv64-unknown-elf-gdb"],
(_, "macos") => &["rust-gdb", "gdb", "arm-none-eabi-gdb"],
(_, "windows") => &["arm-none-eabi-gdb"],
(_, "linux") => &["arm-none-eabi-gdb"],
_ => &["rust-gdb", "gdb", "arm-none-eabi-gdb"],
};
for name in candidates {
if check_gdb(name) {
return name.to_string();
}
}
eprintln!();
eprintln!("[ERROR] 未找到任何可用的 GDB 调试器!");
eprintln!();
match (target_arch, os) {
("arm", "macos") => {
eprintln!(" ARM 目标 | macOS 系统推荐使用系统 GDB:");
eprintln!(" brew install gdb");
eprintln!();
eprintln!(" 或使用 arm-none-eabi-gdb(ARM 官方工具链)");
}
("arm", "windows") => {
eprintln!(" ARM 目标 | Windows 系统请安装 arm-none-eabi-gdb:");
eprintln!(" 从 ARM 官网下载 ARM GCC 工具链:");
eprintln!(" https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads");
eprintln!();
eprintln!(" 或通过 MSYS2 安装:");
eprintln!(" pacman -S mingw-w64-x86_64-arm-none-eabi-gdb");
}
("arm", "linux") => {
eprintln!(" ARM 目标 | Linux 系统请安装 arm-none-eabi-gdb:");
eprintln!(" sudo apt install gdb-arm-none-eabi");
eprintln!();
eprintln!(" 或从 ARM 官网下载 ARM GCC 工具链:");
eprintln!(" https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads");
}
("riscv", "macos") => {
eprintln!(" RISC-V 目标 | macOS 系统推荐使用 riscv64-unknown-elf-gdb:");
eprintln!(" 通过 Homebrew 安装 RISC-V 工具链:");
eprintln!(" brew install riscv64-elf-gdb");
eprintln!();
eprintln!(" 或使用 xPack 发布的 RISC-V GDB:");
eprintln!(" https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases");
eprintln!();
eprintln!(" 也可尝试系统 GDB(功能可能受限):");
eprintln!(" brew install gdb");
}
("riscv", "windows") => {
eprintln!(" RISC-V 目标 | Windows 系统请安装 riscv64-unknown-elf-gdb:");
eprintln!(" 从 xPack 下载 RISC-V 工具链:");
eprintln!(" https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases");
eprintln!();
eprintln!(" 或通过 MSYS2 安装:");
eprintln!(" pacman -S mingw-w64-x86_64-riscv64-unknown-elf-gdb");
}
("riscv", "linux") => {
eprintln!(" RISC-V 目标 | Linux 系统请安装 riscv64-unknown-elf-gdb:");
eprintln!(" sudo apt install gdb-riscv64-unknown-elf");
eprintln!();
eprintln!(" 或从 xPack 下载 RISC-V 工具链:");
eprintln!(" https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases");
}
(_, "macos") => {
eprintln!(" macOS 系统推荐使用系统 GDB:");
eprintln!(" brew install gdb");
}
(_, "windows") => {
eprintln!(" Windows 系统请安装 arm-none-eabi-gdb:");
eprintln!(" 从 ARM 官网下载 ARM GCC 工具链:");
eprintln!(" https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads");
}
(_, "linux") => {
eprintln!(" Linux 系统请安装 arm-none-eabi-gdb:");
eprintln!(" sudo apt install gdb-arm-none-eabi");
}
_ => {
eprintln!(" 请安装 GDB 调试器(如 rust-gdb、gdb-multiarch 或 arm-none-eabi-gdb)");
}
}
eprintln!();
eprintln!(" 或使用 --gdb / --rust-gdb 参数手动指定已安装的 GDB:");
eprintln!(" cargo ocd d --gdb");
eprintln!(" cargo ocd d --rust-gdb");
eprintln!();
std::process::exit(1);
}
struct OcdConfig {
pkg_name: String,
interface: String,
target_chip: String,
target_triple: String,
}
fn load_config(cargo_toml_path: &str) -> OcdConfig {
let content = fs::read_to_string(cargo_toml_path).unwrap_or_else(|_| {
eprintln!("[ERROR] Cargo.toml not found. Run this command from the project root.");
std::process::exit(1);
});
let lines: Vec<&str> = content.lines().collect();
let pkg_name = parse_toml_value(&lines, "name")
.unwrap_or_else(|| "firmware".to_string());
let ocd_section = extract_section(&lines, "[package.metadata.ocd]");
let interface = parse_section_value(&ocd_section, "interface")
.unwrap_or_else(|| "interface/cmsis-dap.cfg".to_string());
let target_chip = parse_section_value(&ocd_section, "target")
.unwrap_or_else(|| "target/stm32f1x.cfg".to_string());
let target_triple = parse_section_value(&ocd_section, "target-triple")
.unwrap_or_else(|| "thumbv7m-none-eabi".to_string());
OcdConfig {
pkg_name,
interface,
target_chip,
target_triple,
}
}
fn parse_toml_value(lines: &[&str], key: &str) -> Option<String> {
for line in lines {
let line = line.trim();
if line.starts_with('#') {
continue;
}
if let Some(eq_pos) = line.find('=') {
let k = line[..eq_pos].trim();
if k == key {
let v = line[eq_pos + 1..].trim();
let v = v.trim_matches('"').trim_matches('\'');
return Some(v.to_string());
}
}
}
None
}
fn extract_section(lines: &[&str], section_name: &str) -> Vec<String> {
let mut in_section = false;
let mut result = Vec::new();
for line in lines {
let trimmed = line.trim();
if trimmed.starts_with('[') {
if in_section {
break;
}
if trimmed == section_name {
in_section = true;
}
continue;
}
if in_section {
result.push(line.to_string());
}
}
result
}
fn parse_section_value(lines: &[String], key: &str) -> Option<String> {
for line in lines {
let line = line.trim();
if line.starts_with('#') {
continue;
}
if let Some(eq_pos) = line.find('=') {
let k = line[..eq_pos].trim();
if k == key {
let v = line[eq_pos + 1..].trim();
let v = v.trim_matches('"').trim_matches('\'');
return Some(v.to_string());
}
}
}
None
}
fn show_firmware_size(path: &Path) {
let mut file = match fs::File::open(path) {
Ok(f) => f,
Err(_) => {
println!(" [WARN] Cannot read firmware file");
return;
}
};
let mut data = Vec::new();
if file.read_to_end(&mut data).is_err() || data.len() < 52 {
return;
}
let e_shoff = u32::from_le_bytes(data[0x20..0x24].try_into().unwrap()) as usize;
let e_shentsize = u16::from_le_bytes(data[0x2E..0x30].try_into().unwrap()) as usize;
let e_shnum = u16::from_le_bytes(data[0x30..0x32].try_into().unwrap()) as usize;
let mut flash_used: u64 = 0;
let mut ram_used: u64 = 0;
let (flash_origin, flash_len, ram_origin, ram_len) = parse_memory_x_addrs("memory.x");
let flash_origin = flash_origin as u32;
let flash_end = (flash_origin as u64 + flash_len) as u32;
let ram_origin = ram_origin as u32;
let ram_end = (ram_origin as u64 + ram_len) as u32;
for i in 0..e_shnum {
let sh_off = e_shoff + i * e_shentsize;
if sh_off + 24 > data.len() {
break;
}
let sh_flags = u32::from_le_bytes(data[sh_off + 8..sh_off + 12].try_into().unwrap());
let sh_addr = u32::from_le_bytes(data[sh_off + 12..sh_off + 16].try_into().unwrap());
let sh_size = u32::from_le_bytes(data[sh_off + 20..sh_off + 24].try_into().unwrap());
if sh_flags & 0x2 != 0 {
if sh_addr >= ram_origin && sh_addr < ram_end {
ram_used += sh_size as u64;
} else if sh_addr >= flash_origin && sh_addr < flash_end {
flash_used += sh_size as u64;
}
}
}
let (flash_total, ram_total) = parse_memory_x("memory.x");
let flash_pct = flash_used as f64 * 100.0 / flash_total as f64;
let ram_pct = ram_used as f64 * 100.0 / ram_total as f64;
let flash_used_str = format_bytes(flash_used);
let flash_total_str = format_bytes(flash_total);
let ram_used_str = format_bytes(ram_used);
let ram_total_str = format_bytes(ram_total);
let flash_bar = progress_bar(flash_pct, 30);
let ram_bar = progress_bar(ram_pct, 30);
let bar_width = 32; let total_width = 2 + 5 + 1 + bar_width + 2 + 6 + 2 + 8 + 3 + 8 + 1;
let line_flash = format!(
" {:<5} {} {:>5.1}% {:>6} / {:<6} ",
"FLASH", flash_bar, flash_pct, flash_used_str, flash_total_str
);
let line_ram = format!(
" {:<5} {} {:>5.1}% {:>6} / {:<6} ",
"RAM", ram_bar, ram_pct, ram_used_str, ram_total_str
);
let pad = |s: &str, w: usize| {
let chars: Vec<char> = s.chars().collect();
if chars.len() >= w {
chars[..w].iter().collect()
} else {
format!("{}{}", s, " ".repeat(w - chars.len()))
}
};
let border = format!("+{}+", "-".repeat(total_width));
println!(" [FIRMWARE SIZE]");
println!(" {}", border);
println!(" |{}|", pad(&line_flash, total_width));
println!(" |{}|", pad(&line_ram, total_width));
println!(" {}", border);
}
fn progress_bar(pct: f64, width: usize) -> String {
let fill = if pct <= 0.0 {
0
} else {
let f = (pct / 100.0 * width as f64) as usize;
if f < 1 { 1 } else { f }.min(width)
};
let empty = width - fill;
format!("[{}{}]", "█".repeat(fill), "░".repeat(empty))
}
fn parse_memory_x(path: &str) -> (u64, u64) {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return (65536, 20480),
};
let mut flash_size: u64 = 0;
let mut ram_size: u64 = 0;
for line in content.lines() {
let line = line.trim();
if line.is_empty()
|| line.starts_with("/*")
|| line.starts_with('*')
|| line.starts_with("//")
{
continue;
}
if line.starts_with("FLASH") || line.starts_with("RAM") {
if let Some(len_str) = line.split(',').nth(1) {
if let Some(eq) = len_str.find('=') {
let val_str = len_str[eq + 1..].trim();
let size = parse_size(val_str);
if line.starts_with("FLASH") {
flash_size = size;
} else {
ram_size = size;
}
}
}
}
}
if flash_size == 0 {
flash_size = 65536;
}
if ram_size == 0 {
ram_size = 20480;
}
(flash_size, ram_size)
}
fn parse_memory_x_addrs(path: &str) -> (u64, u64, u64, u64) {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return (0x0800_0000, 65536, 0x2000_0000, 20480),
};
let mut flash_origin: u64 = 0x0800_0000;
let mut flash_len: u64 = 65536;
let mut ram_origin: u64 = 0x2000_0000;
let mut ram_len: u64 = 20480;
for line in content.lines() {
let line = line.trim();
if line.is_empty()
|| line.starts_with("/*")
|| line.starts_with('*')
|| line.starts_with("//")
{
continue;
}
if line.starts_with("FLASH") || line.starts_with("RAM") {
let is_flash = line.starts_with("FLASH");
if let Some(origin_str) = line.split(',').nth(0) {
if let Some(eq) = origin_str.find("ORIGIN") {
let after_eq = origin_str[eq + "ORIGIN".len()..].trim();
let after_assign = after_eq.trim_start_matches('=').trim();
let val_str = after_assign.trim_matches(' ').trim();
let addr = if val_str.starts_with("0x") || val_str.starts_with("0X") {
u64::from_str_radix(&val_str[2..], 16).unwrap_or(0)
} else {
val_str.parse().unwrap_or(0)
};
if is_flash {
flash_origin = addr;
} else {
ram_origin = addr;
}
}
}
if let Some(len_str) = line.split(',').nth(1) {
if let Some(eq) = len_str.find("LENGTH") {
let val_str = len_str[eq + "LENGTH".len()..].trim();
let val_str = val_str.trim_start_matches('=').trim();
let size = parse_size(val_str);
if is_flash {
flash_len = size;
} else {
ram_len = size;
}
}
}
}
}
(flash_origin, flash_len, ram_origin, ram_len)
}
fn print_help() {
println!("cargo-ocd — 一键编译并通过 OpenOCD 烧录/调试嵌入式固件");
println!();
println!("用法: cargo ocd [SUBCOMMAND] [OPTIONS]");
println!();
println!("子命令:");
println!(" debug, d 编译、烧录并启动 GDB 调试会话(仅 Debug 模式)");
println!();
println!("选项:");
println!(" --release 使用 Release 模式编译(默认 debug 模式)");
println!(" --gdb 调试时使用系统 GDB(macOS 可用)");
println!(" --rust-gdb 调试时使用 rust-gdb(macOS/Linux 可用)");
println!(" --port <PORT> 指定 GDB 服务器端口(默认 3333)");
println!(" --help, -h 显示此帮助信息");
println!();
println!("GDB 自动选择策略(按优先级):");
println!(" ARM 架构:");
println!(" macOS: rust-gdb > gdb > arm-none-eabi-gdb");
println!(" Windows: arm-none-eabi-gdb");
println!(" Linux: arm-none-eabi-gdb");
println!(" RISC-V 架构:");
println!(" macOS: riscv64-unknown-elf-gdb > rust-gdb > gdb");
println!(" Windows: riscv64-unknown-elf-gdb");
println!(" Linux: riscv64-unknown-elf-gdb");
println!();
println!("配置方式(在项目的 Cargo.toml 中):");
println!();
println!(" [package.metadata.ocd]");
println!(" interface = \"interface/cmsis-dap.cfg\" # 下载器配置");
println!(" target = \"target/stm32f1x.cfg\" # 芯片配置");
println!(" target-triple = \"thumbv7m-none-eabi\" # Rust 编译目标");
println!();
println!("示例:");
println!(" cargo ocd # Debug 模式编译 + 烧录");
println!(" cargo ocd --release # Release 模式编译 + 烧录");
println!(" cargo ocd d # Debug 编译 + 烧录 + GDB 调试");
println!(" cargo ocd d --gdb # 使用系统 GDB 调试");
println!(" cargo ocd d --rust-gdb # 使用 rust-gdb 调试");
println!(" cargo ocd d --port 3334 # 指定 GDB 服务器端口");
println!();
println!("GDB 调试提示:");
println!(" 进入 GDB 后,依次执行:");
println!(" (gdb) break main # 在 main 函数设断点");
println!(" (gdb) continue # 运行到断点");
println!(" (gdb) step # 单步执行");
println!(" (gdb) print variable # 查看变量");
println!(" --其余指令见GDB调试协议-- ");
println!();
println!("支持的下载器: CMSIS-DAP / ST-Link / J-Link(通过 interface 配置)");
println!("支持的芯片: 任何 OpenOCD 兼容的目标(通过 target 和 target-triple 配置)");
println!();
println!("详细文档: 基础环境配置与使用.md");
}
fn parse_size(s: &str) -> u64 {
let s = s.trim().to_uppercase();
if s.ends_with("K") {
let num: f64 = s[..s.len() - 1].trim().parse().unwrap_or(0.0);
(num * 1024.0) as u64
} else if s.ends_with("M") {
let num: f64 = s[..s.len() - 1].trim().parse().unwrap_or(0.0);
(num * 1024.0 * 1024.0) as u64
} else {
s.parse().unwrap_or(0)
}
}
fn format_bytes(bytes: u64) -> String {
if bytes >= 1024 * 1024 {
format!("{} MB", bytes / (1024 * 1024))
} else if bytes >= 1024 {
format!("{} KB", bytes / 1024)
} else {
format!("{} B", bytes)
}
}