Skip to main content

dvb_ci_runtime/
device.rs

1//! The hardware-abstraction boundary: [`CaDevice`].
2//!
3//! EN 50221 runs over the Linux CA device (`/dev/dvb/adapterN/caM`): the
4//! application reads/writes the TPDU link-layer byte stream and issues a few
5//! ioctls (reset, slot info, capabilities). The runtime is written entirely
6//! against this trait so it can be driven by a real device (the `linux`
7//! feature) *or* by an in-memory mock — which is what makes the state machines
8//! testable without hardware, and enables differential testing against an
9//! external reference (feed both the same mock, compare the emitted
10//! write/ioctl sequences).
11
12use std::io;
13
14/// CA-device slot status (subset of the Linux `ca_slot_info` the runtime needs).
15///
16/// The DVB-CA slot reports two independent bits (uapi `linux/dvb/ca.h`
17/// `ca_slot_info.flags`): `CA_CI_MODULE_PRESENT` (a module is physically
18/// inserted) and `CA_CI_MODULE_READY` (that module has completed its own
19/// init and is usable). A module can be present-but-not-ready briefly after
20/// insertion; the runtime's hot-plug edge detection
21/// ([`Notification::HotPlug`](crate::event::Notification::HotPlug) carrying
22/// [`HotPlug::CamPresent`](crate::event::HotPlug::CamPresent)/
23/// [`CamRemoved`](crate::event::HotPlug::CamRemoved)) keys off
24/// `module_present`, since that is the field that toggles on physical
25/// insert/removal.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub struct SlotInfo {
28    /// Slot number.
29    pub num: u8,
30    /// `true` once a module is present and ready (`CA_CI_MODULE_READY`).
31    pub module_ready: bool,
32    /// `true` while a module is physically inserted (`CA_CI_MODULE_PRESENT`),
33    /// regardless of whether it has finished initialising.
34    pub module_present: bool,
35}
36
37/// The link-layer device the EN 50221 runtime drives.
38///
39/// All methods mirror the operations a host performs on the CA file descriptor
40/// per EN 50221. Implementations: [`MockCaDevice`] (in-memory, for tests +
41/// differential harness) and the `linux` `CaDevice` over `/dev/dvb/.../ca`.
42pub trait CaDevice {
43    /// Read one link-layer TPDU frame into `buf`; returns the byte count.
44    /// `Ok(0)` means no data available (non-blocking).
45    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
46
47    /// Write one link-layer TPDU frame.
48    fn write(&mut self, buf: &[u8]) -> io::Result<()>;
49
50    /// Reset the interface / slot (ioctl `CA_RESET`).
51    fn reset(&mut self) -> io::Result<()>;
52
53    /// How long the module needs after [`reset`](Self::reset) returns before
54    /// it accepts `Create_T_C` (the DD/cxd2099 class of drivers only
55    /// re-initialise the slot seconds after `CA_RESET`).
56    ///
57    /// `reset` itself must not sleep for this: the sans-IO
58    /// [`Driver`](crate::Driver) reads this value after `reset` and holds the
59    /// transport quiet until it elapses on its injectable clock, so a CAM
60    /// hot-plug edge no longer freezes `pump` (audit r10-O-10). Defaults to
61    /// [`Duration::ZERO`](std::time::Duration::ZERO) — an implementation whose
62    /// `reset` already blocks until the module is usable keeps working
63    /// unchanged.
64    fn reset_settle(&self) -> std::time::Duration {
65        std::time::Duration::ZERO
66    }
67
68    /// Query slot status (ioctl `CA_GET_SLOT_INFO`).
69    fn slot_info(&mut self) -> io::Result<SlotInfo>;
70
71    /// Wait up to `timeout` for the device to become readable; `Ok(true)` if
72    /// readable. The runtime's poll loop calls this between reads.
73    fn poll(&mut self, timeout: std::time::Duration) -> io::Result<bool>;
74}
75
76/// One recorded device operation, for assertions + differential testing.
77#[derive(Debug, Clone, PartialEq, Eq)]
78#[non_exhaustive]
79pub enum DeviceOp {
80    /// A `write()` of these exact bytes.
81    Write(Vec<u8>),
82    /// A `reset()` ioctl.
83    Reset,
84    /// A `slot_info()` ioctl.
85    SlotInfo,
86}
87
88/// In-memory [`CaDevice`] for tests and the differential harness.
89///
90/// - `inbound` is a scripted queue of frames the "module" (mock CAM) sends up;
91///   each [`read`](CaDevice::read) pops one.
92/// - every host-side operation is appended to `ops` so a test (or a differential
93///   comparison against an external reference) can assert the exact emitted
94///   `write`/ioctl sequence.
95#[derive(Debug, Default)]
96pub struct MockCaDevice {
97    /// Scripted frames the module sends to the host (FIFO).
98    pub inbound: std::collections::VecDeque<Vec<u8>>,
99    /// Recorded host-side operations, in order.
100    pub ops: Vec<DeviceOp>,
101    /// Slot status returned by [`slot_info`](CaDevice::slot_info).
102    pub slot: SlotInfo,
103}
104
105impl MockCaDevice {
106    /// New mock with a ready module in slot 0 and the given inbound script.
107    #[must_use]
108    pub fn new(inbound: impl IntoIterator<Item = Vec<u8>>) -> Self {
109        Self {
110            inbound: inbound.into_iter().collect(),
111            ops: Vec::new(),
112            slot: SlotInfo {
113                num: 0,
114                module_ready: true,
115                module_present: true,
116            },
117        }
118    }
119
120    /// The bytes written by the host so far, concatenated (convenience for
121    /// byte-exact differential comparison against the C reference).
122    #[must_use]
123    pub fn written(&self) -> Vec<u8> {
124        self.ops
125            .iter()
126            .filter_map(|o| match o {
127                DeviceOp::Write(b) => Some(b.clone()),
128                _ => None,
129            })
130            .flatten()
131            .collect()
132    }
133}
134
135impl CaDevice for MockCaDevice {
136    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
137        match self.inbound.pop_front() {
138            Some(frame) => {
139                let n = frame.len().min(buf.len());
140                buf[..n].copy_from_slice(&frame[..n]);
141                Ok(n)
142            }
143            None => Ok(0),
144        }
145    }
146
147    fn write(&mut self, buf: &[u8]) -> io::Result<()> {
148        self.ops.push(DeviceOp::Write(buf.to_vec()));
149        Ok(())
150    }
151
152    fn reset(&mut self) -> io::Result<()> {
153        self.ops.push(DeviceOp::Reset);
154        Ok(())
155    }
156
157    fn slot_info(&mut self) -> io::Result<SlotInfo> {
158        self.ops.push(DeviceOp::SlotInfo);
159        Ok(self.slot)
160    }
161
162    fn poll(&mut self, _timeout: std::time::Duration) -> io::Result<bool> {
163        Ok(!self.inbound.is_empty())
164    }
165}
166
167/// One link-layer event for diagnostics, captured in both directions by
168/// [`RecordingCaDevice`].
169#[derive(Debug, Clone, PartialEq, Eq)]
170#[non_exhaustive]
171pub enum LinkEvent {
172    /// Host → module: a frame the host wrote.
173    Tx(Vec<u8>),
174    /// Module → host: a frame the host read.
175    Rx(Vec<u8>),
176    /// A `reset()` ioctl.
177    Reset,
178    /// A `slot_info()` ioctl and the status it returned.
179    SlotInfo(SlotInfo),
180}
181
182/// A [`CaDevice`] decorator that records every frame in **both** directions
183/// (plus ioctls) for live-CAM diagnostics. Wrap a real device, run, then dump
184/// the [`log`](Self::log) — or decode it with
185/// [`trace::decode_log`](crate::trace::decode_log) — to get an annotated byte
186/// trace without hand-instrumenting the device:
187///
188/// ```no_run
189/// # use dvb_ci_runtime::{Driver, device::RecordingCaDevice, trace};
190/// # fn real_device() -> dvb_ci_runtime::MockCaDevice { dvb_ci_runtime::MockCaDevice::new([]) }
191/// let mut driver = Driver::new(RecordingCaDevice::new(real_device()));
192/// driver.init().unwrap();
193/// // ... pump ...
194/// println!("{}", trace::decode_log(driver.device().log()));
195/// ```
196#[derive(Debug, Default)]
197pub struct RecordingCaDevice<D> {
198    inner: D,
199    /// The captured link events, in order.
200    pub log: Vec<LinkEvent>,
201    /// Last logged slot status, so repeated identical `slot_info()` polls
202    /// (the driver now samples every [`pump`](crate::Driver::pump) for
203    /// hot-plug edge detection — #726) don't swamp the trace; only a change
204    /// is recorded, same rationale as `poll()` below.
205    last_slot: Option<SlotInfo>,
206}
207
208impl<D: CaDevice> RecordingCaDevice<D> {
209    /// Wrap `inner`, recording all I/O.
210    pub fn new(inner: D) -> Self {
211        Self {
212            inner,
213            log: Vec::new(),
214            last_slot: None,
215        }
216    }
217
218    /// The recorded link events, in order.
219    #[must_use]
220    pub fn log(&self) -> &[LinkEvent] {
221        &self.log
222    }
223
224    /// Borrow the wrapped device.
225    pub fn inner(&self) -> &D {
226        &self.inner
227    }
228}
229
230impl<D: CaDevice> CaDevice for RecordingCaDevice<D> {
231    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
232        let n = self.inner.read(buf)?;
233        if n > 0 {
234            self.log.push(LinkEvent::Rx(buf[..n].to_vec()));
235        }
236        Ok(n)
237    }
238
239    fn write(&mut self, buf: &[u8]) -> io::Result<()> {
240        self.log.push(LinkEvent::Tx(buf.to_vec()));
241        self.inner.write(buf)
242    }
243
244    fn reset(&mut self) -> io::Result<()> {
245        self.log.push(LinkEvent::Reset);
246        self.inner.reset()
247    }
248
249    fn slot_info(&mut self) -> io::Result<SlotInfo> {
250        let si = self.inner.slot_info()?;
251        if self.last_slot != Some(si) {
252            self.log.push(LinkEvent::SlotInfo(si));
253            self.last_slot = Some(si);
254        }
255        Ok(si)
256    }
257
258    fn poll(&mut self, timeout: std::time::Duration) -> io::Result<bool> {
259        // Polls are not recorded (they would swamp the trace).
260        self.inner.poll(timeout)
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn recording_device_captures_both_directions() {
270        let inner = MockCaDevice::new([vec![0x83, 0x01, 0x01]]);
271        let mut dev = RecordingCaDevice::new(inner);
272        dev.reset().unwrap();
273        dev.write(&[0x82, 0x01, 0x01]).unwrap();
274        let mut buf = [0u8; 16];
275        dev.read(&mut buf).unwrap();
276        assert_eq!(
277            dev.log(),
278            &[
279                LinkEvent::Reset,
280                LinkEvent::Tx(vec![0x82, 0x01, 0x01]),
281                LinkEvent::Rx(vec![0x83, 0x01, 0x01]),
282            ]
283        );
284    }
285
286    #[test]
287    fn mock_records_writes_and_replays_inbound() {
288        let mut dev = MockCaDevice::new([vec![0x01, 0x02], vec![0x03]]);
289        // host writes
290        dev.write(&[0xAA, 0xBB]).unwrap();
291        dev.reset().unwrap();
292        // module frames replay in order
293        let mut buf = [0u8; 16];
294        assert_eq!(dev.read(&mut buf).unwrap(), 2);
295        assert_eq!(&buf[..2], &[0x01, 0x02]);
296        assert_eq!(dev.read(&mut buf).unwrap(), 1);
297        assert_eq!(dev.read(&mut buf).unwrap(), 0); // drained
298        assert_eq!(dev.written(), vec![0xAA, 0xBB]);
299        assert_eq!(dev.ops[1], DeviceOp::Reset);
300    }
301}