1use std::io;
7use std::time::Duration;
8
9use crate::device::CaDevice;
10use crate::event::{Action, Event, HostRequest, Notification};
11use crate::stack::CiStack;
12
13pub struct Driver<D: CaDevice> {
15 device: D,
16 stack: CiStack,
17 notifications: Vec<Notification>,
18 next_timer: Option<Duration>,
20 buf: Vec<u8>,
22}
23
24impl<D: CaDevice> Driver<D> {
25 #[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 pub fn device(&self) -> &D {
39 &self.device
40 }
41
42 pub fn device_mut(&mut self) -> &mut D {
45 &mut self.device
46 }
47
48 pub fn next_timer(&self) -> Option<Duration> {
50 self.next_timer
51 }
52
53 pub fn take_notifications(&mut self) -> Vec<Notification> {
55 core::mem::take(&mut self.notifications)
56 }
57
58 pub fn init(&mut self) -> io::Result<()> {
60 let actions = self.stack.handle(Event::Host(HostRequest::Init));
61 self.run(actions)
62 }
63
64 pub fn send_ca_pmt(&mut self, ca_pmt: &[u8]) -> io::Result<()> {
67 let actions = self
68 .stack
69 .handle(Event::Host(HostRequest::SendCaPmt(ca_pmt)));
70 self.run(actions)
71 }
72
73 pub fn descramble(&mut self, pmt_section: &[u8]) -> io::Result<()> {
79 let actions = self
80 .stack
81 .handle(Event::Host(HostRequest::Descramble(pmt_section)));
82 self.run(actions)
83 }
84
85 pub fn descramble_programs(&mut self, pmt_sections: &[&[u8]]) -> io::Result<()> {
88 let actions = self
89 .stack
90 .handle(Event::Host(HostRequest::DescramblePrograms(pmt_sections)));
91 self.run(actions)
92 }
93
94 pub fn add_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
97 let actions = self
98 .stack
99 .handle(Event::Host(HostRequest::AddProgram(pmt_section)));
100 self.run(actions)
101 }
102
103 pub fn remove_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
106 let actions = self
107 .stack
108 .handle(Event::Host(HostRequest::RemoveProgram(pmt_section)));
109 self.run(actions)
110 }
111
112 pub fn mmi_menu_answer(&mut self, choice_ref: u8) -> io::Result<()> {
114 let actions = self
115 .stack
116 .handle(Event::Host(HostRequest::MmiMenuAnswer(choice_ref)));
117 self.run(actions)
118 }
119
120 pub fn mmi_enquiry_answer(&mut self, text: &[u8]) -> io::Result<()> {
122 let actions = self
123 .stack
124 .handle(Event::Host(HostRequest::MmiEnquiryAnswer(text)));
125 self.run(actions)
126 }
127
128 pub fn mmi_cancel(&mut self) -> io::Result<()> {
130 let actions = self.stack.handle(Event::Host(HostRequest::MmiCancel));
131 self.run(actions)
132 }
133
134 pub fn enter_menu(&mut self) -> io::Result<()> {
137 let actions = self.stack.handle(Event::Host(HostRequest::EnterMenu));
138 self.run(actions)
139 }
140
141 pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
145 if self.device.poll(timeout)? {
146 let n = self.device.read(&mut self.buf)?;
147 if n > 0 {
148 let frame = self.buf[..n].to_vec();
149 let actions = self.stack.handle(Event::Readable(&frame));
150 self.run(actions)?;
151 return Ok(true);
152 }
153 }
154 let actions = self.stack.handle(Event::Tick { elapsed: timeout });
155 self.run(actions)?;
156 Ok(false)
157 }
158
159 fn run(&mut self, actions: Vec<Action>) -> io::Result<()> {
161 for action in actions {
162 match action {
163 Action::Write(bytes) => self.device.write(&bytes)?,
164 Action::Reset => self.device.reset()?,
165 Action::QuerySlot => {
166 self.device.slot_info()?;
167 }
168 Action::SetTimer { after } => self.next_timer = Some(after),
169 Action::Notify(n) => self.notifications.push(n),
170 }
171 }
172 Ok(())
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::device::{DeviceOp, MockCaDevice};
180 use crate::event::{HostControlEvent, Notification};
181 use broadcast_common::Serialize;
182 use dvb_ci::tpdu::tags;
183
184 fn ser<S: Serialize>(s: &S) -> Vec<u8> {
185 let mut b = vec![0u8; s.serialized_len()];
186 match s.serialize_into(&mut b) {
187 Ok(n) => b.truncate(n),
188 Err(_) => b.clear(),
189 }
190 b
191 }
192
193 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
196 use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
197 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
198 v.extend_from_slice(spdu);
199 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
200 v
201 }
202
203 fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
206 use dvb_ci::spdu::SessionNumber;
207 let mut spdu = ser(&SessionNumber { session_nb });
208 spdu.extend_from_slice(apdu);
209 r_data(1, &spdu)
210 }
211
212 fn sb() -> Vec<u8> {
215 use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
216 vec![tpdu_tags::SB, 0x02, 0x01, SbValue::new(false).0]
217 }
218
219 fn feed(d: &mut Driver<MockCaDevice>, frame: Vec<u8>) {
222 d.device_mut().inbound.push_back(frame);
223 d.pump(Duration::from_millis(10)).unwrap();
224 for _ in 0..8 {
225 d.device_mut().inbound.push_back(sb());
226 d.pump(Duration::from_millis(10)).unwrap();
227 }
228 }
229
230 fn driver_with_sessions() -> Driver<MockCaDevice> {
234 use dvb_ci::objects::resource_manager::Profile;
235 use dvb_ci::resource::{
236 APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, HOST_CONTROL, MMI,
237 RESOURCE_MANAGER,
238 };
239 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
240
241 let mut d = Driver::new(MockCaDevice::new([]));
242 d.init().unwrap();
243 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
245 feed(
247 &mut d,
248 r_data(
249 1,
250 &ser(&OpenSessionRequest {
251 resource: RESOURCE_MANAGER,
252 }),
253 ),
254 );
255 feed(
258 &mut d,
259 r_apdu(
260 1,
261 &ser(&Profile {
262 resources: vec![
263 APPLICATION_INFORMATION,
264 CONDITIONAL_ACCESS_SUPPORT,
265 MMI,
266 HOST_CONTROL,
267 ],
268 }),
269 ),
270 );
271 for (nb, res) in [
273 (2u16, APPLICATION_INFORMATION),
274 (3, CONDITIONAL_ACCESS_SUPPORT),
275 (4, MMI),
276 (5, HOST_CONTROL),
277 ] {
278 feed(
279 &mut d,
280 r_data(
281 1,
282 &ser(&CreateSessionResponse {
283 status: SessionStatus::Ok,
284 resource: res,
285 session_nb: nb,
286 }),
287 ),
288 );
289 }
290 d
291 }
292
293 const RM_SESSION: u16 = 1;
297 const MMI_SESSION: u16 = 4;
298 const HOST_CONTROL_SESSION: u16 = 5;
299
300 #[test]
301 fn host_control_tune_apdu_surfaces_notification_via_driver() {
302 use dvb_ci::objects::host_control::Tune;
303
304 let mut d = driver_with_sessions();
305 let hc_nb = HOST_CONTROL_SESSION;
306 d.take_notifications(); let tune = Tune {
310 network_id: 0x1122,
311 original_network_id: 0x3344,
312 transport_stream_id: 0x5566,
313 service_id: 0x7788,
314 };
315 feed(&mut d, r_apdu(hc_nb, &ser(&tune)));
316
317 let notes = d.take_notifications();
319 assert!(
320 notes.contains(&Notification::HostControl(HostControlEvent::Tune {
321 network_id: 0x1122,
322 original_network_id: 0x3344,
323 transport_stream_id: 0x5566,
324 service_id: 0x7788,
325 })),
326 "expected HostControl(Tune) notification, got {notes:?}"
327 );
328 }
329
330 #[test]
331 fn profile_reply_advertises_host_control() {
332 use broadcast_common::Parse;
333 use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
334 use dvb_ci::resource::{HOST_CONTROL, RESOURCE_MANAGER};
335
336 let mut d = Driver::new(MockCaDevice::new([]));
337 d.init().unwrap();
338 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
339 feed(
341 &mut d,
342 r_data(
343 1,
344 &ser(&dvb_ci::spdu::OpenSessionRequest {
345 resource: RESOURCE_MANAGER,
346 }),
347 ),
348 );
349 feed(&mut d, r_apdu(RM_SESSION, &ser(&ProfileEnq)));
351
352 let want = dvb_ci::tag::PROFILE.to_bytes();
355 let found = d.device().ops.iter().any(|op| {
356 if let DeviceOp::Write(w) = op {
357 if let Some(pos) = w.windows(3).position(|x| x == want) {
358 if let Ok(p) = Profile::parse(&w[pos..]) {
359 return p.resources.contains(&HOST_CONTROL);
360 }
361 }
362 }
363 false
364 });
365 assert!(found, "profile reply must advertise HOST_CONTROL");
366 }
367
368 #[test]
369 fn mmi_menu_answ_and_answ_are_byte_exact_on_the_mmi_session() {
370 use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
371
372 let mut d = driver_with_sessions();
373 let mmi_nb = MMI_SESSION;
374
375 d.mmi_menu_answer(2).unwrap();
378 d.device_mut().inbound.push_back(sb());
379 d.pump(Duration::from_millis(10)).unwrap();
380 assert_apdu_on_session(&d, mmi_nb, &ser(&MenuAnsw { choice_ref: 2 }));
381
382 d.mmi_enquiry_answer(b"1234").unwrap();
384 d.device_mut().inbound.push_back(sb());
385 d.pump(Duration::from_millis(10)).unwrap();
386 assert_apdu_on_session(
387 &d,
388 mmi_nb,
389 &ser(&Answ {
390 answ_id: AnswId::Answer,
391 text_chars: b"1234",
392 }),
393 );
394 }
395
396 fn assert_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) {
399 use dvb_ci::spdu::SessionNumber;
400 let mut want = ser(&SessionNumber { session_nb });
401 want.extend_from_slice(apdu);
402 let hit = d.device().ops.iter().any(|op| match op {
403 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
404 _ => false,
405 });
406 assert!(
407 hit,
408 "expected APDU {apdu:02X?} on session {session_nb} (session-prefixed {want:02X?}) in writes"
409 );
410 }
411
412 #[test]
413 fn init_drives_reset_slotinfo_and_create_tc_to_device() {
414 let mut d = Driver::new(MockCaDevice::new([]));
415 d.init().unwrap();
416 let ops = &d.device().ops;
417 assert_eq!(ops[0], DeviceOp::Reset);
418 assert_eq!(ops[1], DeviceOp::SlotInfo);
419 assert!(matches!(&ops[2], DeviceOp::Write(w) if w[0] == tags::CREATE_T_C));
420 }
421
422 #[test]
423 fn reads_reply_then_polls_on_pump() {
424 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
426 let mut d = Driver::new(dev);
427 d.init().unwrap();
428 assert!(d.pump(Duration::from_millis(100)).unwrap());
430 assert!(!d.pump(Duration::from_millis(100)).unwrap());
432 let last = d.device().ops.last().unwrap();
433 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
434 }
435}