#[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)
}
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 { 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:?}"),
}
}
}