#[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 clap::{Args, Parser, Subcommand};
use dvb_ci_runtime::device::RecordingCaDevice;
use dvb_ci_runtime::event::{MmiEvent, MmiMenu};
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>;
#[derive(Parser)]
#[command(name = "ci-probe", version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Command,
#[arg(long, global = true)]
trace: bool,
}
#[derive(Subcommand)]
enum Command {
List,
Info(DevArgs),
Descramble {
#[command(flatten)]
dev: DevArgs,
#[arg(long)]
pmt: String,
},
Mmi(DevArgs),
}
#[derive(Args)]
struct DevArgs {
#[arg(short, long, default_value_t = 0)]
adapter: u32,
#[arg(short, long, default_value_t = 0)]
ca: u32,
}
pub fn run() -> ExitCode {
let cli = Cli::parse();
let trace = cli.trace;
let result = match cli.command {
Command::List => list(),
Command::Info(d) => info(d.adapter, d.ca, trace),
Command::Descramble { dev, pmt } => descramble(dev.adapter, dev.ca, &pmt, trace),
Command::Mmi(d) => mmi(d.adapter, d.ca, trace),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
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(())
}
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())
);
}
}
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(¬e);
}
}
if !got_ca_info {
eprintln!("timed out before ca_info (CAM may not have completed the handshake)");
}
dump_trace(&driver, trace);
Ok(())
}
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(¬e);
}
}
}
if !done {
eprintln!("timed out before ca_pmt_reply");
}
dump_trace(&driver, trace);
Ok(())
}
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(m)) => {
print_menu_header(&m);
for (i, choice) in m.choices.iter().enumerate() {
println!(" {}) {choice}", i + 1);
}
println!(" 0) back");
let choice = prompt("select> ")?;
driver.mmi_menu_answer(choice.trim().parse().unwrap_or(0))?;
}
Notification::Mmi(MmiEvent::List(m)) => {
print_menu_header(&m);
for item in &m.choices {
println!(" - {item}");
}
prompt("(press Enter)")?;
driver.mmi_menu_answer(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 print_menu_header(m: &MmiMenu) {
println!("\n== {} ==", m.title);
for line in [&m.subtitle, &m.bottom] {
if !line.trim().is_empty() {
println!("{line}");
}
}
}
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:?}"),
}
}
}