ch32rv 0.12.2

Flashing and debugging tool for WCH CH32 RISC-V microcontrollers
//! en: `db list` / `db info` (docs/cli.ja.md §4.7): inspect the generated target DB. No device is
//! needed - these read the built-in DB embedded from `crates/target/generated/` at compile time.
//! ja: `db list` / `db info`。生成済み target DB を見る。デバイス不要(コンパイル時埋め込みの内蔵 DB
//! を読むだけ)。

use std::process::ExitCode;

use ch32rv_contract::{ErrorKind, ResultEnvelope};

use crate::args::Cli;
use crate::cmd_probe::fail;

const OVERLAY_HINT: Option<&str> =
    Some("--db takes the same columns as crates/target/generated/skus.csv");

/// en: The target DB for a command that needs no device: the built-in tables, plus the `--db`
/// overlay when one is given. Device-bound commands get theirs from the [`crate::session::Session`].
/// ja: デバイス不要コマンド用の target DB。内蔵 + `--db` overlay。デバイスを開くコマンドは Session 側。
pub(crate) fn db_for(cli: &Cli) -> Result<ch32rv_target::Db, String> {
    match cli.db.as_deref() {
        Some(path) => ch32rv_target::Db::with_overlay(path),
        None => Ok(ch32rv_target::Db::builtin()),
    }
}

/// `db list [--family <f>] [--verified-only]`: enumerate SKUs in the built-in DB.
pub fn list(cli: &Cli, family: Option<&str>, verified_only: bool) -> ExitCode {
    const LIST_CMD: &str = "db.list";
    let db = match db_for(cli) {
        Ok(db) => db,
        Err(m) => return fail(cli, LIST_CMD, ErrorKind::Usage, m, OVERLAY_HINT),
    };
    let mut skus: Vec<_> = db
        .skus()
        .iter()
        .filter(|s| {
            family
                .map(|f| {
                    s.family.eq_ignore_ascii_case(f)
                        || s.sku
                            .to_ascii_uppercase()
                            .starts_with(&f.to_ascii_uppercase())
                })
                .unwrap_or(true)
        })
        .filter(|s| !verified_only || s.verified)
        .collect();
    skus.sort_by(|a, b| a.sku.cmp(&b.sku));

    if cli.json {
        let rows: Vec<_> = skus
            .iter()
            .map(|s| {
                serde_json::json!({
                    "sku": s.sku,
                    "family": s.family,
                    "device_id": s.device_id.map(|d| format!("0x{d:08x}")),
                    "flash_bytes": s.flash_bytes,
                    "sram_bytes": s.sram_bytes,
                    "verified": s.verified,
                })
            })
            .collect();
        let mut env = ResultEnvelope::success("db.list");
        env.result = Some(serde_json::json!({ "count": rows.len(), "skus": rows }));
        crate::print_envelope(&env)
    } else {
        println!(
            "{:<16} {:<10} {:<12} {:>6} {:>6}  VERIFIED",
            "SKU", "FAMILY", "DEVICE_ID", "FLASH", "SRAM"
        );
        for s in &skus {
            println!(
                "{:<16} {:<10} {:<12} {:>5}K {:>5}K  {}",
                s.sku,
                s.family,
                s.device_id
                    .map(|d| format!("0x{d:08x}"))
                    .unwrap_or_else(|| "-".to_owned()),
                s.flash_bytes / 1024,
                s.sram_bytes / 1024,
                if s.verified { "yes" } else { "" }
            );
        }
        println!("({} SKUs)", skus.len());
        ExitCode::SUCCESS
    }
}

/// `db info <SKU>`: show one SKU's DB record.
pub fn info(cli: &Cli, sku_name: &str) -> ExitCode {
    const CMD: &str = "db.info";
    let db = match db_for(cli) {
        Ok(db) => db,
        Err(m) => return fail(cli, CMD, ErrorKind::Usage, m, OVERLAY_HINT),
    };
    let Some(s) = db
        .skus()
        .iter()
        .find(|s| s.sku.eq_ignore_ascii_case(sku_name))
    else {
        return fail(
            cli,
            CMD,
            ErrorKind::Usage,
            format!("unknown SKU {sku_name:?} (not in the generated DB)"),
            Some("list known SKUs with `ch32rv db list`"),
        );
    };
    let wiring = ch32rv_target::debug_wiring(&s.series);
    let geo = ch32rv_target::flash_geometry(&s.family);
    let method = ch32rv_target::flash_program_method(&s.family);
    if cli.json {
        let mut env = ResultEnvelope::success(CMD);
        env.result = Some(serde_json::json!({
            "sku": s.sku,
            "family": s.family,
            "series": s.series,
            "device_id": s.device_id.map(|d| format!("0x{d:08x}")),
            "flash_bytes": s.flash_bytes,
            "sram_bytes": s.sram_bytes,
            "verified": s.verified,
            "provisional": s.provisional,
            "debug_wiring": wiring.as_ref().map(|w| serde_json::json!({
                "wire": w.wire, "swdio": w.swdio, "swclk": w.swclk,
            })),
            "flash_geometry": geo.map(|g| serde_json::json!({
                "page_erase": g.page_erase, "fast_erase": g.fast_erase,
                "fast_program": g.fast_program, "block_erase": g.block_erase,
                "erased_word": g.erased_word.map(|w| format!("0x{w:08x}")),
            })),
            "flash_program_method": method.as_ref().map(|m| serde_json::json!({
                "mode": m.mode, "commit": m.commit,
                "buffer_load_bits": m.buffer_load_bits, "confidence": m.confidence,
            })),
        }));
        crate::print_envelope(&env)
    } else {
        println!("sku:        {}", s.sku);
        println!("family:     {}", s.family);
        println!("series:     {}", s.series);
        println!(
            "device_id:  {}  (bits [7:4] = silicon revision, masked when matching)",
            s.device_id
                .map(|d| format!("0x{d:08x}"))
                .unwrap_or_else(|| "-".to_owned())
        );
        println!("flash:      {} KiB", s.flash_bytes / 1024);
        println!("sram:       {} KiB", s.sram_bytes / 1024);
        if let Some(g) = &geo {
            let blk = if g.block_erase == 0 {
                "-".to_owned()
            } else {
                format!("{}B", g.block_erase)
            };
            println!(
                "flash geom: page-erase={}B  fast-erase={}B  fast-program={}B  block-erase={blk}",
                g.page_erase, g.fast_erase, g.fast_program
            );
            if let Some(w) = g.erased_word {
                println!("erased:     0x{w:08x}  (value a blank flash word reads back as)");
            }
        }
        if let Some(m) = &method {
            let bits = m
                .buffer_load_bits
                .map(|b| format!("  buffer-load={b}bit"))
                .unwrap_or_default();
            println!(
                "flash prog: {}  commit={}{bits}  ({})",
                if m.mode.is_empty() { "-" } else { &m.mode },
                m.commit,
                m.confidence
            );
        }
        if let Some(w) = &wiring {
            println!(
                "debug:      {} (SWDIO/DAT={}{})",
                w.wire,
                w.swdio,
                if w.swclk == "-" {
                    String::new()
                } else {
                    format!(", SWCLK={}", w.swclk)
                }
            );
        }
        println!(
            "verified:   {}",
            if s.verified {
                "yes (device_id confirmed on real silicon)"
            } else {
                "no (generated from datasheet/reference data)"
            }
        );
        ExitCode::SUCCESS
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::expect_used)]

    /// en: Every family the bench drives through the FLASH controller must keep the DB rows that
    /// path needs: a per-page fast erase, a known erased-cell value, and a programming procedure the
    /// data repo did not flag `conflict`. A delivery that empties one of these would silently turn
    /// off `erase --range` and flash software breakpoints, so fail here instead.
    /// ja: 実機で駆動している family は、DB 側に page 消去粒度・消去後の値・conflict でない書込手順が
    /// 揃っていること。納品でどれかが欠けると機能が黙って無効化されるので、ここで落とす。
    #[test]
    fn db_keeps_what_the_controller_path_needs() {
        // (family_byte, DB family string)
        let cases = [
            (0x05u8, "CH32V20x"),
            (0x06, "CH32V307"),
            (0x09, "CH32V003"),
            (0x0D, "CH32X035"),
            (0x0E, "CH32L103"),
            (0x01, "CH32V103"),
        ];
        for (fb, fam) in cases {
            let geo = ch32rv_target::flash_geometry(fam).expect("flash geometry for family");
            let method =
                ch32rv_target::flash_program_method(fam).expect("flash program method for family");
            assert_ne!(geo.fast_erase, 0, "{fam}: no per-page fast erase in the DB");
            assert!(
                geo.erased_word.is_some(),
                "{fam}: DB does not say what an erased word reads back as"
            );
            assert_ne!(
                method.confidence, "conflict",
                "{fam}: the data repo now flags the programming procedure as conflicting"
            );
            let profile =
                ch32rv_flash::flash_controller_profile(fb).expect("controller profile for family");
            assert_eq!(
                profile.page_size, geo.fast_erase,
                "family_byte 0x{fb:02x} / {fam}: profile page_size != DB fast_erase"
            );
        }
    }
}