Skip to main content

dvb_ci_runtime/
driver.rs

1//! The driver — the one place I/O happens. It pumps a [`CaDevice`] against the
2//! sans-IO [`CiStack`]: reads frames in, executes the stack's [`Action`]s
3//! (writes/ioctls) out, tracks the requested poll timer, and collects
4//! [`Notification`]s for the host application.
5
6use std::io;
7use std::time::Duration;
8
9use crate::device::CaDevice;
10use crate::event::{Action, Event, HostRequest, Notification};
11use crate::stack::CiStack;
12
13/// Drives a [`CaDevice`] with the [`CiStack`].
14pub struct Driver<D: CaDevice> {
15    device: D,
16    stack: CiStack,
17    notifications: Vec<Notification>,
18    /// Delay the stack last asked to be polled after (`None` = none pending).
19    next_timer: Option<Duration>,
20    /// Read buffer for one link-layer frame.
21    buf: Vec<u8>,
22}
23
24impl<D: CaDevice> Driver<D> {
25    /// New driver over `device`, single transport connection.
26    #[must_use]
27    pub fn new(device: D) -> Self {
28        Self {
29            device,
30            stack: CiStack::new(),
31            notifications: Vec::new(),
32            next_timer: None,
33            buf: vec![0u8; 4096],
34        }
35    }
36
37    /// Borrow the underlying device (e.g. to inspect a mock's recorded ops).
38    pub fn device(&self) -> &D {
39        &self.device
40    }
41
42    /// The poll delay the stack most recently requested, if any.
43    pub fn next_timer(&self) -> Option<Duration> {
44        self.next_timer
45    }
46
47    /// Drain the notifications collected so far.
48    pub fn take_notifications(&mut self) -> Vec<Notification> {
49        core::mem::take(&mut self.notifications)
50    }
51
52    /// Bring the interface up (reset + open the transport connection).
53    pub fn init(&mut self) -> io::Result<()> {
54        let actions = self.stack.handle(Event::Host(HostRequest::Init));
55        self.run(actions)
56    }
57
58    /// Request the module descramble the services in `ca_pmt` (a serialized
59    /// `ca_pmt` APDU body, e.g. from `dvb_ci::build_ca_pmt`).
60    pub fn send_ca_pmt(&mut self, ca_pmt: &[u8]) -> io::Result<()> {
61        let actions = self
62            .stack
63            .handle(Event::Host(HostRequest::SendCaPmt(ca_pmt)));
64        self.run(actions)
65    }
66
67    /// Descramble the services in a PMT section: the stack filters the PMT's
68    /// `CA_descriptor`s to the CAM's advertised CAIDs and sends a `ca_pmt`
69    /// (`list_management = only`, `cmd_id = ok_descrambling`). The outcome
70    /// surfaces as [`Notification::CaPmtReply`]. Call after the CAM is ready and
71    /// its `ca_info` has been received (otherwise no CAID filter is applied).
72    pub fn descramble(&mut self, pmt_section: &[u8]) -> io::Result<()> {
73        let actions = self
74            .stack
75            .handle(Event::Host(HostRequest::Descramble(pmt_section)));
76        self.run(actions)
77    }
78
79    /// Descramble a set of programmes in one CA-PMT list (`first`/`more`/`last`),
80    /// replacing any previously selected set. Each element is a raw PMT section.
81    pub fn descramble_programs(&mut self, pmt_sections: &[&[u8]]) -> io::Result<()> {
82        let actions = self
83            .stack
84            .handle(Event::Host(HostRequest::DescramblePrograms(pmt_sections)));
85        self.run(actions)
86    }
87
88    /// Add one programme to the descrambled set (`list_management = add`) without
89    /// re-listing the others — for a capacity manager adding a viewer's service.
90    pub fn add_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
91        let actions = self
92            .stack
93            .handle(Event::Host(HostRequest::AddProgram(pmt_section)));
94        self.run(actions)
95    }
96
97    /// Remove one programme from the descrambled set (`list_management = update`,
98    /// `cmd_id = not_selected`) — tells the CAM to stop descrambling it.
99    pub fn remove_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
100        let actions = self
101            .stack
102            .handle(Event::Host(HostRequest::RemoveProgram(pmt_section)));
103        self.run(actions)
104    }
105
106    /// Answer an MMI menu/list by 1-based `choice_ref` (0 = back/cancel).
107    pub fn mmi_menu_answer(&mut self, choice_ref: u8) -> io::Result<()> {
108        let actions = self
109            .stack
110            .handle(Event::Host(HostRequest::MmiMenuAnswer(choice_ref)));
111        self.run(actions)
112    }
113
114    /// Answer an MMI enquiry with the user's input (EN 300 468 Annex A bytes).
115    pub fn mmi_enquiry_answer(&mut self, text: &[u8]) -> io::Result<()> {
116        let actions = self
117            .stack
118            .handle(Event::Host(HostRequest::MmiEnquiryAnswer(text)));
119        self.run(actions)
120    }
121
122    /// Abort the current MMI dialogue (`answ` with `answ_id = cancel`).
123    pub fn mmi_cancel(&mut self) -> io::Result<()> {
124        let actions = self.stack.handle(Event::Host(HostRequest::MmiCancel));
125        self.run(actions)
126    }
127
128    /// Ask the module to open its MMI menu (`enter_menu`) — e.g. to read card /
129    /// entitlement info from the module's own menus.
130    pub fn enter_menu(&mut self) -> io::Result<()> {
131        let actions = self.stack.handle(Event::Host(HostRequest::EnterMenu));
132        self.run(actions)
133    }
134
135    /// One pump step: if the device is readable within `timeout`, read a frame
136    /// and feed it; otherwise advance the stack's timers by `timeout` (driving
137    /// the poll cadence). Returns whether a frame was processed.
138    pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
139        if self.device.poll(timeout)? {
140            let n = self.device.read(&mut self.buf)?;
141            if n > 0 {
142                let frame = self.buf[..n].to_vec();
143                let actions = self.stack.handle(Event::Readable(&frame));
144                self.run(actions)?;
145                return Ok(true);
146            }
147        }
148        let actions = self.stack.handle(Event::Tick { elapsed: timeout });
149        self.run(actions)?;
150        Ok(false)
151    }
152
153    /// Execute the stack's actions against the device.
154    fn run(&mut self, actions: Vec<Action>) -> io::Result<()> {
155        for action in actions {
156            match action {
157                Action::Write(bytes) => self.device.write(&bytes)?,
158                Action::Reset => self.device.reset()?,
159                Action::QuerySlot => {
160                    self.device.slot_info()?;
161                }
162                Action::SetTimer { after } => self.next_timer = Some(after),
163                Action::Notify(n) => self.notifications.push(n),
164            }
165        }
166        Ok(())
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::device::{DeviceOp, MockCaDevice};
174    use dvb_ci::tpdu::tags;
175
176    #[test]
177    fn init_drives_reset_slotinfo_and_create_tc_to_device() {
178        let mut d = Driver::new(MockCaDevice::new([]));
179        d.init().unwrap();
180        let ops = &d.device().ops;
181        assert_eq!(ops[0], DeviceOp::Reset);
182        assert_eq!(ops[1], DeviceOp::SlotInfo);
183        assert!(matches!(&ops[2], DeviceOp::Write(w) if w[0] == tags::CREATE_T_C));
184    }
185
186    #[test]
187    fn reads_reply_then_polls_on_pump() {
188        // Script the module accepting the connection.
189        let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
190        let mut d = Driver::new(dev);
191        d.init().unwrap();
192        // first pump reads the C_T_C_Reply (activates the connection)
193        assert!(d.pump(Duration::from_millis(100)).unwrap());
194        // next pump has nothing to read → ticks → emits a poll write
195        assert!(!d.pump(Duration::from_millis(100)).unwrap());
196        let last = d.device().ops.last().unwrap();
197        assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
198    }
199}