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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub struct SlotInfo {
17    /// Slot number.
18    pub num: u8,
19    /// `true` once a module is present and ready (CA_CI_MODULE_READY).
20    pub module_ready: bool,
21}
22
23/// The link-layer device the EN 50221 runtime drives.
24///
25/// All methods mirror the operations a host performs on the CA file descriptor
26/// per EN 50221. Implementations: [`MockCaDevice`] (in-memory, for tests +
27/// differential harness) and the `linux` `CaDevice` over `/dev/dvb/.../ca`.
28pub trait CaDevice {
29    /// Read one link-layer TPDU frame into `buf`; returns the byte count.
30    /// `Ok(0)` means no data available (non-blocking).
31    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
32
33    /// Write one link-layer TPDU frame.
34    fn write(&mut self, buf: &[u8]) -> io::Result<()>;
35
36    /// Reset the interface / slot (ioctl `CA_RESET`).
37    fn reset(&mut self) -> io::Result<()>;
38
39    /// Query slot status (ioctl `CA_GET_SLOT_INFO`).
40    fn slot_info(&mut self) -> io::Result<SlotInfo>;
41
42    /// Wait up to `timeout` for the device to become readable; `Ok(true)` if
43    /// readable. The runtime's poll loop calls this between reads.
44    fn poll(&mut self, timeout: std::time::Duration) -> io::Result<bool>;
45}
46
47/// One recorded device operation, for assertions + differential testing.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum DeviceOp {
50    /// A `write()` of these exact bytes.
51    Write(Vec<u8>),
52    /// A `reset()` ioctl.
53    Reset,
54    /// A `slot_info()` ioctl.
55    SlotInfo,
56}
57
58/// In-memory [`CaDevice`] for tests and the differential harness.
59///
60/// - `inbound` is a scripted queue of frames the "module" (mock CAM) sends up;
61///   each [`read`](CaDevice::read) pops one.
62/// - every host-side operation is appended to `ops` so a test (or a differential
63///   comparison against an external reference) can assert the exact emitted
64///   `write`/ioctl sequence.
65#[derive(Debug, Default)]
66pub struct MockCaDevice {
67    /// Scripted frames the module sends to the host (FIFO).
68    pub inbound: std::collections::VecDeque<Vec<u8>>,
69    /// Recorded host-side operations, in order.
70    pub ops: Vec<DeviceOp>,
71    /// Slot status returned by [`slot_info`](CaDevice::slot_info).
72    pub slot: SlotInfo,
73}
74
75impl MockCaDevice {
76    /// New mock with a ready module in slot 0 and the given inbound script.
77    #[must_use]
78    pub fn new(inbound: impl IntoIterator<Item = Vec<u8>>) -> Self {
79        Self {
80            inbound: inbound.into_iter().collect(),
81            ops: Vec::new(),
82            slot: SlotInfo {
83                num: 0,
84                module_ready: true,
85            },
86        }
87    }
88
89    /// The bytes written by the host so far, concatenated (convenience for
90    /// byte-exact differential comparison against the C reference).
91    #[must_use]
92    pub fn written(&self) -> Vec<u8> {
93        self.ops
94            .iter()
95            .filter_map(|o| match o {
96                DeviceOp::Write(b) => Some(b.clone()),
97                _ => None,
98            })
99            .flatten()
100            .collect()
101    }
102}
103
104impl CaDevice for MockCaDevice {
105    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
106        match self.inbound.pop_front() {
107            Some(frame) => {
108                let n = frame.len().min(buf.len());
109                buf[..n].copy_from_slice(&frame[..n]);
110                Ok(n)
111            }
112            None => Ok(0),
113        }
114    }
115
116    fn write(&mut self, buf: &[u8]) -> io::Result<()> {
117        self.ops.push(DeviceOp::Write(buf.to_vec()));
118        Ok(())
119    }
120
121    fn reset(&mut self) -> io::Result<()> {
122        self.ops.push(DeviceOp::Reset);
123        Ok(())
124    }
125
126    fn slot_info(&mut self) -> io::Result<SlotInfo> {
127        self.ops.push(DeviceOp::SlotInfo);
128        Ok(self.slot)
129    }
130
131    fn poll(&mut self, _timeout: std::time::Duration) -> io::Result<bool> {
132        Ok(!self.inbound.is_empty())
133    }
134}
135
136/// One link-layer event for diagnostics, captured in both directions by
137/// [`RecordingCaDevice`].
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum LinkEvent {
140    /// Host → module: a frame the host wrote.
141    Tx(Vec<u8>),
142    /// Module → host: a frame the host read.
143    Rx(Vec<u8>),
144    /// A `reset()` ioctl.
145    Reset,
146    /// A `slot_info()` ioctl and the status it returned.
147    SlotInfo(SlotInfo),
148}
149
150/// A [`CaDevice`] decorator that records every frame in **both** directions
151/// (plus ioctls) for live-CAM diagnostics. Wrap a real device, run, then dump
152/// the [`log`](Self::log) — or decode it with
153/// [`trace::decode_log`](crate::trace::decode_log) — to get an annotated byte
154/// trace without hand-instrumenting the device:
155///
156/// ```no_run
157/// # use dvb_ci_runtime::{Driver, device::RecordingCaDevice, trace};
158/// # fn real_device() -> dvb_ci_runtime::MockCaDevice { dvb_ci_runtime::MockCaDevice::new([]) }
159/// let mut driver = Driver::new(RecordingCaDevice::new(real_device()));
160/// driver.init().unwrap();
161/// // ... pump ...
162/// println!("{}", trace::decode_log(driver.device().log()));
163/// ```
164#[derive(Debug, Default)]
165pub struct RecordingCaDevice<D> {
166    inner: D,
167    /// The captured link events, in order.
168    pub log: Vec<LinkEvent>,
169}
170
171impl<D: CaDevice> RecordingCaDevice<D> {
172    /// Wrap `inner`, recording all I/O.
173    pub fn new(inner: D) -> Self {
174        Self {
175            inner,
176            log: Vec::new(),
177        }
178    }
179
180    /// The recorded link events, in order.
181    #[must_use]
182    pub fn log(&self) -> &[LinkEvent] {
183        &self.log
184    }
185
186    /// Borrow the wrapped device.
187    pub fn inner(&self) -> &D {
188        &self.inner
189    }
190}
191
192impl<D: CaDevice> CaDevice for RecordingCaDevice<D> {
193    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
194        let n = self.inner.read(buf)?;
195        if n > 0 {
196            self.log.push(LinkEvent::Rx(buf[..n].to_vec()));
197        }
198        Ok(n)
199    }
200
201    fn write(&mut self, buf: &[u8]) -> io::Result<()> {
202        self.log.push(LinkEvent::Tx(buf.to_vec()));
203        self.inner.write(buf)
204    }
205
206    fn reset(&mut self) -> io::Result<()> {
207        self.log.push(LinkEvent::Reset);
208        self.inner.reset()
209    }
210
211    fn slot_info(&mut self) -> io::Result<SlotInfo> {
212        let si = self.inner.slot_info()?;
213        self.log.push(LinkEvent::SlotInfo(si));
214        Ok(si)
215    }
216
217    fn poll(&mut self, timeout: std::time::Duration) -> io::Result<bool> {
218        // Polls are not recorded (they would swamp the trace).
219        self.inner.poll(timeout)
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn recording_device_captures_both_directions() {
229        let inner = MockCaDevice::new([vec![0x83, 0x01, 0x01]]);
230        let mut dev = RecordingCaDevice::new(inner);
231        dev.reset().unwrap();
232        dev.write(&[0x82, 0x01, 0x01]).unwrap();
233        let mut buf = [0u8; 16];
234        dev.read(&mut buf).unwrap();
235        assert_eq!(
236            dev.log(),
237            &[
238                LinkEvent::Reset,
239                LinkEvent::Tx(vec![0x82, 0x01, 0x01]),
240                LinkEvent::Rx(vec![0x83, 0x01, 0x01]),
241            ]
242        );
243    }
244
245    #[test]
246    fn mock_records_writes_and_replays_inbound() {
247        let mut dev = MockCaDevice::new([vec![0x01, 0x02], vec![0x03]]);
248        // host writes
249        dev.write(&[0xAA, 0xBB]).unwrap();
250        dev.reset().unwrap();
251        // module frames replay in order
252        let mut buf = [0u8; 16];
253        assert_eq!(dev.read(&mut buf).unwrap(), 2);
254        assert_eq!(&buf[..2], &[0x01, 0x02]);
255        assert_eq!(dev.read(&mut buf).unwrap(), 1);
256        assert_eq!(dev.read(&mut buf).unwrap(), 0); // drained
257        assert_eq!(dev.written(), vec![0xAA, 0xBB]);
258        assert_eq!(dev.ops[1], DeviceOp::Reset);
259    }
260}