dvb-ci-runtime 0.6.1

Pure-Rust EN 50221 DVB Common Interface driver runtime — device I/O, TPDU/SPDU poll loop, and resource state machines over the dvb-ci codecs.
Documentation
//! `ci-probe` — discover and engage an installed CAM over a Linux DVB CI device.
//!
//! Built only with the `linux` feature on Linux (`required-features`). Wires the
//! `dvb-ci-runtime` `Driver` + `LinuxCaDevice` + `trace` decoder to argv — no
//! extra dependencies.
//!
//! ```text
//! ci-probe list                              # enumerate /dev/dvb/adapterN/caM + slot status
//! ci-probe info       [adapter] [ca]         # run the handshake; print app-info + CAIDs
//! ci-probe descramble <adapter> <ca> <pmt>   # query->reply->ok for a PMT section file
//! ci-probe mmi        [adapter] [ca]         # interactive MMI menus/enquiries
//! ```
//! Append `--trace` to any command to dump an annotated link trace on exit.

#[cfg(all(feature = "linux", target_os = "linux"))]
fn main() -> std::process::ExitCode {
    imp::run()
}

#[cfg(not(all(feature = "linux", target_os = "linux")))]
fn main() -> std::process::ExitCode {
    eprintln!("ci-probe requires the `linux` feature on a Linux host (DVB CA device access).");
    std::process::ExitCode::FAILURE
}

#[cfg(all(feature = "linux", target_os = "linux"))]
mod imp {
    use std::io::{self, Write};
    use std::path::Path;
    use std::process::ExitCode;
    use std::time::{Duration, Instant};

    use dvb_ci_runtime::device::RecordingCaDevice;
    use dvb_ci_runtime::event::MmiEvent;
    use dvb_ci_runtime::linux::LinuxCaDevice;
    use dvb_ci_runtime::{trace, CaDevice, Driver, Notification};

    const PUMP: Duration = Duration::from_millis(100);
    const READY_TIMEOUT: Duration = Duration::from_secs(10);

    type Dev = RecordingCaDevice<LinuxCaDevice>;

    pub fn run() -> ExitCode {
        let args: Vec<String> = std::env::args().collect();
        let trace = args.iter().any(|a| a == "--trace");
        let pos: Vec<&str> = args[1..]
            .iter()
            .map(String::as_str)
            .filter(|a| !a.starts_with("--"))
            .collect();

        let result = match pos.first().copied() {
            Some("list") => list(),
            Some("info") | None => info(arg_u32(&pos, 1, 0), arg_u32(&pos, 2, 0), trace),
            Some("descramble") => match (pos.get(1), pos.get(2), pos.get(3)) {
                (Some(a), Some(c), Some(file)) => {
                    descramble(parse_u32(a), parse_u32(c), file, trace)
                }
                _ => {
                    usage();
                    Ok(())
                }
            },
            Some("mmi") => mmi(arg_u32(&pos, 1, 0), arg_u32(&pos, 2, 0), trace),
            Some(other) => {
                eprintln!("unknown command: {other}");
                usage();
                Ok(())
            }
        };

        match result {
            Ok(()) => ExitCode::SUCCESS,
            Err(e) => {
                eprintln!("error: {e}");
                ExitCode::FAILURE
            }
        }
    }

    fn usage() {
        eprintln!(
            "usage:\n  ci-probe list\n  ci-probe info [adapter] [ca]\n  \
             ci-probe descramble <adapter> <ca> <pmt-file>\n  ci-probe mmi [adapter] [ca]\n  \
             (append --trace to dump an annotated link trace)"
        );
    }

    fn parse_u32(s: &str) -> u32 {
        s.parse().unwrap_or(0)
    }
    fn arg_u32(pos: &[&str], i: usize, default: u32) -> u32 {
        pos.get(i).map(|s| parse_u32(s)).unwrap_or(default)
    }

    /// Enumerate `/dev/dvb/adapterN/caM` and report each slot's status.
    fn list() -> io::Result<()> {
        let mut found = false;
        for adapter in 0..16 {
            let base = format!("/dev/dvb/adapter{adapter}");
            if !Path::new(&base).exists() {
                continue;
            }
            for ca in 0..4 {
                let path = format!("{base}/ca{ca}");
                if !Path::new(&path).exists() {
                    continue;
                }
                found = true;
                match LinuxCaDevice::open(adapter, ca) {
                    Ok(mut dev) => match dev.slot_info() {
                        Ok(si) => {
                            println!("{path}  slot {}  module_ready={}", si.num, si.module_ready)
                        }
                        Err(e) => println!("{path}  (slot_info failed: {e})"),
                    },
                    Err(e) => println!("{path}  (open failed: {e})"),
                }
            }
        }
        if !found {
            println!("no /dev/dvb/adapterN/caM devices found");
        }
        Ok(())
    }

    /// Open a recording driver for `adapter`/`ca`.
    fn open(adapter: u32, ca: u32) -> io::Result<Driver<Dev>> {
        let dev = RecordingCaDevice::new(LinuxCaDevice::open(adapter, ca)?);
        Ok(Driver::new(dev))
    }

    fn dump_trace(driver: &Driver<Dev>, enabled: bool) {
        if enabled {
            eprintln!(
                "\n--- link trace ---\n{}",
                trace::decode_log(driver.device().log())
            );
        }
    }

    /// Run the handshake and print application-info + the CAM's CAIDs.
    fn info(adapter: u32, ca: u32, trace: bool) -> io::Result<()> {
        let mut driver = open(adapter, ca)?;
        driver.init()?;
        let deadline = Instant::now() + READY_TIMEOUT;
        let mut got_ca_info = false;
        while Instant::now() < deadline && !got_ca_info {
            driver.pump(PUMP)?;
            for note in driver.take_notifications() {
                got_ca_info |= matches!(note, Notification::CaInfo { .. });
                print_note(&note);
            }
        }
        if !got_ca_info {
            eprintln!("timed out before ca_info (CAM may not have completed the handshake)");
        }
        dump_trace(&driver, trace);
        Ok(())
    }

    /// Feed a PMT-section file and run the query → reply → ok descramble sequence.
    fn descramble(adapter: u32, ca: u32, pmt_file: &str, trace: bool) -> io::Result<()> {
        let pmt = std::fs::read(pmt_file)?;
        let mut driver = open(adapter, ca)?;
        driver.init()?;
        let deadline = Instant::now() + READY_TIMEOUT;
        let mut sent = false;
        let mut done = false;
        while Instant::now() < deadline && !done {
            driver.pump(PUMP)?;
            for note in driver.take_notifications() {
                if matches!(note, Notification::CaInfo { .. }) && !sent {
                    println!("ca_info received → sending descramble request");
                    driver.descramble(&pmt)?;
                    sent = true;
                }
                if let Notification::CaPmtReply {
                    program_number,
                    descrambling_ok,
                } = note
                {
                    println!(
                        "ca_pmt_reply: program {program_number} descrambling_ok={descrambling_ok}"
                    );
                    done = true;
                } else {
                    print_note(&note);
                }
            }
        }
        if !done {
            eprintln!("timed out before ca_pmt_reply");
        }
        dump_trace(&driver, trace);
        Ok(())
    }

    /// Interactive MMI: display module menus/enquiries and send the user's answer.
    fn mmi(adapter: u32, ca: u32, trace: bool) -> io::Result<()> {
        let mut driver = open(adapter, ca)?;
        driver.init()?;
        println!("MMI session — Ctrl-C to quit. Waiting for the module to present a menu…");
        let mut closed = false;
        while !closed {
            driver.pump(PUMP)?;
            for note in driver.take_notifications() {
                match note {
                    Notification::Mmi(MmiEvent::Menu { title, items }) => {
                        println!("\n== {title} ==");
                        for (i, item) in items.iter().enumerate() {
                            println!("  {}) {item}", i + 1);
                        }
                        println!("  0) back");
                        let choice = prompt("select> ")?;
                        driver.mmi_menu_answer(choice.trim().parse().unwrap_or(0))?;
                    }
                    Notification::Mmi(MmiEvent::Enquiry {
                        prompt: p, blind, ..
                    }) => {
                        println!("\n{p}{}", if blind { " (hidden)" } else { "" });
                        let answer = prompt("answer> ")?;
                        driver.mmi_enquiry_answer(answer.trim().as_bytes())?;
                    }
                    Notification::Mmi(MmiEvent::Close) => {
                        println!("(module closed the MMI dialogue)");
                        closed = true;
                    }
                    other => print_note(&other),
                }
            }
        }
        dump_trace(&driver, trace);
        Ok(())
    }

    fn prompt(p: &str) -> io::Result<String> {
        print!("{p}");
        io::stdout().flush()?;
        let mut line = String::new();
        io::stdin().read_line(&mut line)?;
        Ok(line)
    }

    fn print_note(note: &Notification) {
        match note {
            Notification::CamReady => println!("CAM ready (resource-manager handshake complete)"),
            Notification::ApplicationInfo {
                application_type,
                manufacturer,
                code,
                menu,
            } => println!(
                "application_info: type=0x{application_type:02X} manufacturer=0x{manufacturer:04X} \
                 code=0x{code:04X} menu={menu:?}"
            ),
            Notification::CaInfo { ca_system_ids } => {
                let ids: Vec<String> = ca_system_ids.iter().map(|c| format!("0x{c:04X}")).collect();
                println!("ca_info: {} CA_system_id(s): {}", ids.len(), ids.join(", "));
            }
            Notification::Mmi(ev) => println!("mmi: {ev:?}"),
            Notification::SessionOpened { resource } => {
                println!("session opened: {}", resource.name())
            }
            Notification::SessionClosed { session_nb } => {
                println!("session {session_nb} closed")
            }
            Notification::Error { detail } => eprintln!("stack error: {detail}"),
            other => println!("{other:?}"),
        }
    }
}