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 /// Query slot status (ioctl `CA_GET_SLOT_INFO`).
54 fn slot_info(&mut self) -> io::Result<SlotInfo>;
55
56 /// Wait up to `timeout` for the device to become readable; `Ok(true)` if
57 /// readable. The runtime's poll loop calls this between reads.
58 fn poll(&mut self, timeout: std::time::Duration) -> io::Result<bool>;
59}
60
61/// One recorded device operation, for assertions + differential testing.
62#[derive(Debug, Clone, PartialEq, Eq)]
63#[non_exhaustive]
64pub enum DeviceOp {
65 /// A `write()` of these exact bytes.
66 Write(Vec<u8>),
67 /// A `reset()` ioctl.
68 Reset,
69 /// A `slot_info()` ioctl.
70 SlotInfo,
71}
72
73/// In-memory [`CaDevice`] for tests and the differential harness.
74///
75/// - `inbound` is a scripted queue of frames the "module" (mock CAM) sends up;
76/// each [`read`](CaDevice::read) pops one.
77/// - every host-side operation is appended to `ops` so a test (or a differential
78/// comparison against an external reference) can assert the exact emitted
79/// `write`/ioctl sequence.
80#[derive(Debug, Default)]
81pub struct MockCaDevice {
82 /// Scripted frames the module sends to the host (FIFO).
83 pub inbound: std::collections::VecDeque<Vec<u8>>,
84 /// Recorded host-side operations, in order.
85 pub ops: Vec<DeviceOp>,
86 /// Slot status returned by [`slot_info`](CaDevice::slot_info).
87 pub slot: SlotInfo,
88}
89
90impl MockCaDevice {
91 /// New mock with a ready module in slot 0 and the given inbound script.
92 #[must_use]
93 pub fn new(inbound: impl IntoIterator<Item = Vec<u8>>) -> Self {
94 Self {
95 inbound: inbound.into_iter().collect(),
96 ops: Vec::new(),
97 slot: SlotInfo {
98 num: 0,
99 module_ready: true,
100 module_present: true,
101 },
102 }
103 }
104
105 /// The bytes written by the host so far, concatenated (convenience for
106 /// byte-exact differential comparison against the C reference).
107 #[must_use]
108 pub fn written(&self) -> Vec<u8> {
109 self.ops
110 .iter()
111 .filter_map(|o| match o {
112 DeviceOp::Write(b) => Some(b.clone()),
113 _ => None,
114 })
115 .flatten()
116 .collect()
117 }
118}
119
120impl CaDevice for MockCaDevice {
121 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
122 match self.inbound.pop_front() {
123 Some(frame) => {
124 let n = frame.len().min(buf.len());
125 buf[..n].copy_from_slice(&frame[..n]);
126 Ok(n)
127 }
128 None => Ok(0),
129 }
130 }
131
132 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
133 self.ops.push(DeviceOp::Write(buf.to_vec()));
134 Ok(())
135 }
136
137 fn reset(&mut self) -> io::Result<()> {
138 self.ops.push(DeviceOp::Reset);
139 Ok(())
140 }
141
142 fn slot_info(&mut self) -> io::Result<SlotInfo> {
143 self.ops.push(DeviceOp::SlotInfo);
144 Ok(self.slot)
145 }
146
147 fn poll(&mut self, _timeout: std::time::Duration) -> io::Result<bool> {
148 Ok(!self.inbound.is_empty())
149 }
150}
151
152/// One link-layer event for diagnostics, captured in both directions by
153/// [`RecordingCaDevice`].
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[non_exhaustive]
156pub enum LinkEvent {
157 /// Host → module: a frame the host wrote.
158 Tx(Vec<u8>),
159 /// Module → host: a frame the host read.
160 Rx(Vec<u8>),
161 /// A `reset()` ioctl.
162 Reset,
163 /// A `slot_info()` ioctl and the status it returned.
164 SlotInfo(SlotInfo),
165}
166
167/// A [`CaDevice`] decorator that records every frame in **both** directions
168/// (plus ioctls) for live-CAM diagnostics. Wrap a real device, run, then dump
169/// the [`log`](Self::log) — or decode it with
170/// [`trace::decode_log`](crate::trace::decode_log) — to get an annotated byte
171/// trace without hand-instrumenting the device:
172///
173/// ```no_run
174/// # use dvb_ci_runtime::{Driver, device::RecordingCaDevice, trace};
175/// # fn real_device() -> dvb_ci_runtime::MockCaDevice { dvb_ci_runtime::MockCaDevice::new([]) }
176/// let mut driver = Driver::new(RecordingCaDevice::new(real_device()));
177/// driver.init().unwrap();
178/// // ... pump ...
179/// println!("{}", trace::decode_log(driver.device().log()));
180/// ```
181#[derive(Debug, Default)]
182pub struct RecordingCaDevice<D> {
183 inner: D,
184 /// The captured link events, in order.
185 pub log: Vec<LinkEvent>,
186 /// Last logged slot status, so repeated identical `slot_info()` polls
187 /// (the driver now samples every [`pump`](crate::Driver::pump) for
188 /// hot-plug edge detection — #726) don't swamp the trace; only a change
189 /// is recorded, same rationale as `poll()` below.
190 last_slot: Option<SlotInfo>,
191}
192
193impl<D: CaDevice> RecordingCaDevice<D> {
194 /// Wrap `inner`, recording all I/O.
195 pub fn new(inner: D) -> Self {
196 Self {
197 inner,
198 log: Vec::new(),
199 last_slot: None,
200 }
201 }
202
203 /// The recorded link events, in order.
204 #[must_use]
205 pub fn log(&self) -> &[LinkEvent] {
206 &self.log
207 }
208
209 /// Borrow the wrapped device.
210 pub fn inner(&self) -> &D {
211 &self.inner
212 }
213}
214
215impl<D: CaDevice> CaDevice for RecordingCaDevice<D> {
216 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
217 let n = self.inner.read(buf)?;
218 if n > 0 {
219 self.log.push(LinkEvent::Rx(buf[..n].to_vec()));
220 }
221 Ok(n)
222 }
223
224 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
225 self.log.push(LinkEvent::Tx(buf.to_vec()));
226 self.inner.write(buf)
227 }
228
229 fn reset(&mut self) -> io::Result<()> {
230 self.log.push(LinkEvent::Reset);
231 self.inner.reset()
232 }
233
234 fn slot_info(&mut self) -> io::Result<SlotInfo> {
235 let si = self.inner.slot_info()?;
236 if self.last_slot != Some(si) {
237 self.log.push(LinkEvent::SlotInfo(si));
238 self.last_slot = Some(si);
239 }
240 Ok(si)
241 }
242
243 fn poll(&mut self, timeout: std::time::Duration) -> io::Result<bool> {
244 // Polls are not recorded (they would swamp the trace).
245 self.inner.poll(timeout)
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn recording_device_captures_both_directions() {
255 let inner = MockCaDevice::new([vec![0x83, 0x01, 0x01]]);
256 let mut dev = RecordingCaDevice::new(inner);
257 dev.reset().unwrap();
258 dev.write(&[0x82, 0x01, 0x01]).unwrap();
259 let mut buf = [0u8; 16];
260 dev.read(&mut buf).unwrap();
261 assert_eq!(
262 dev.log(),
263 &[
264 LinkEvent::Reset,
265 LinkEvent::Tx(vec![0x82, 0x01, 0x01]),
266 LinkEvent::Rx(vec![0x83, 0x01, 0x01]),
267 ]
268 );
269 }
270
271 #[test]
272 fn mock_records_writes_and_replays_inbound() {
273 let mut dev = MockCaDevice::new([vec![0x01, 0x02], vec![0x03]]);
274 // host writes
275 dev.write(&[0xAA, 0xBB]).unwrap();
276 dev.reset().unwrap();
277 // module frames replay in order
278 let mut buf = [0u8; 16];
279 assert_eq!(dev.read(&mut buf).unwrap(), 2);
280 assert_eq!(&buf[..2], &[0x01, 0x02]);
281 assert_eq!(dev.read(&mut buf).unwrap(), 1);
282 assert_eq!(dev.read(&mut buf).unwrap(), 0); // drained
283 assert_eq!(dev.written(), vec![0xAA, 0xBB]);
284 assert_eq!(dev.ops[1], DeviceOp::Reset);
285 }
286}