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/// A failed device operation, with enough context to pinpoint it from a log
38/// line alone: **which operation** (`write`, `read`, `poll`, `ioctl
39/// CA_RESET`, ...), **which device** (the `/dev/dvb/adapterN/...` path, or
40/// the fd role for a `from_file` wrapper) and the **errno**. A bare
41/// `Invalid argument (os error 22)` from `Driver::pump` (seen on a DD CXD2099
42/// during init) names none of those, and the same errno means different
43/// things per operation: the kernel's `dvb_ca_en50221` returns `EINVAL` from
44/// `write` while the slot is not yet `RUNNING`, from `read` when the buffer is
45/// smaller than the pending frame, and from `CA_GET_SLOT_INFO` for a slot it
46/// does not have.
47///
48/// It is carried as the payload of an [`io::Error`] of the *same*
49/// [`io::ErrorKind`] as the original, so `kind()`-based handling is
50/// unchanged; the errno stays reachable through [`errno`](Self::errno) /
51/// [`DeviceOpError::errno_of`] (a wrapped error no longer answers
52/// `raw_os_error()` itself).
53#[derive(Debug)]
54pub struct DeviceOpError {
55 /// The operation that failed, e.g. `"write (17 bytes)"`.
56 pub op: String,
57 /// The device, e.g. `"/dev/dvb/adapter3/ca0"`.
58 pub device: String,
59 source: io::Error,
60}
61
62impl DeviceOpError {
63 /// Wrap `source` (an error from `op` on `device`) into an [`io::Error`] of
64 /// the same kind that displays all three.
65 #[must_use]
66 pub fn wrap(op: impl Into<String>, device: &str, source: io::Error) -> io::Error {
67 let kind = source.kind();
68 io::Error::new(
69 kind,
70 DeviceOpError {
71 op: op.into(),
72 device: device.to_owned(),
73 source,
74 },
75 )
76 }
77
78 /// The OS errno of the underlying failure, if it had one.
79 #[must_use]
80 pub fn errno(&self) -> Option<i32> {
81 self.source.raw_os_error()
82 }
83
84 /// The OS errno of `err` whether or not it is wrapped in a
85 /// [`DeviceOpError`] (a plain OS error answers for itself).
86 #[must_use]
87 pub fn errno_of(err: &io::Error) -> Option<i32> {
88 err.raw_os_error().or_else(|| {
89 err.get_ref()
90 .and_then(|inner| inner.downcast_ref::<DeviceOpError>())
91 .and_then(DeviceOpError::errno)
92 })
93 }
94}
95
96impl std::fmt::Display for DeviceOpError {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 write!(f, "{}: {} failed: {}", self.device, self.op, self.source)
99 }
100}
101
102impl std::error::Error for DeviceOpError {
103 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
104 Some(&self.source)
105 }
106}
107
108/// The link-layer device the EN 50221 runtime drives.
109///
110/// All methods mirror the operations a host performs on the CA file descriptor
111/// per EN 50221. Implementations: [`MockCaDevice`] (in-memory, for tests +
112/// differential harness) and the `linux` `CaDevice` over `/dev/dvb/.../ca`.
113pub trait CaDevice {
114 /// Read one link-layer TPDU frame into `buf`; returns the byte count.
115 /// `Ok(0)` means no data available (non-blocking).
116 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
117
118 /// Write one link-layer TPDU frame.
119 fn write(&mut self, buf: &[u8]) -> io::Result<()>;
120
121 /// Reset the interface / slot (ioctl `CA_RESET`).
122 fn reset(&mut self) -> io::Result<()>;
123
124 /// How long the module needs after [`reset`](Self::reset) returns before
125 /// it accepts `Create_T_C` (the DD/cxd2099 class of drivers only
126 /// re-initialise the slot seconds after `CA_RESET`).
127 ///
128 /// `reset` itself must not sleep for this: the sans-IO
129 /// [`Driver`](crate::Driver) reads this value after `reset` and holds the
130 /// transport quiet until it elapses on its injectable clock, so a CAM
131 /// hot-plug edge no longer freezes `pump` (audit r10-O-10). Defaults to
132 /// [`Duration::ZERO`](std::time::Duration::ZERO) — an implementation whose
133 /// `reset` already blocks until the module is usable keeps working
134 /// unchanged.
135 fn reset_settle(&self) -> std::time::Duration {
136 std::time::Duration::ZERO
137 }
138
139 /// Query slot status (ioctl `CA_GET_SLOT_INFO`).
140 fn slot_info(&mut self) -> io::Result<SlotInfo>;
141
142 /// Wait up to `timeout` for the device to become readable; `Ok(true)` if
143 /// readable. The runtime's poll loop calls this between reads.
144 fn poll(&mut self, timeout: std::time::Duration) -> io::Result<bool>;
145}
146
147/// One recorded device operation, for assertions + differential testing.
148#[derive(Debug, Clone, PartialEq, Eq)]
149#[non_exhaustive]
150pub enum DeviceOp {
151 /// A `write()` of these exact bytes.
152 Write(Vec<u8>),
153 /// A `reset()` ioctl.
154 Reset,
155 /// A `slot_info()` ioctl.
156 SlotInfo,
157}
158
159/// In-memory [`CaDevice`] for tests and the differential harness.
160///
161/// - `inbound` is a scripted queue of frames the "module" (mock CAM) sends up;
162/// each [`read`](CaDevice::read) pops one.
163/// - every host-side operation is appended to `ops` so a test (or a differential
164/// comparison against an external reference) can assert the exact emitted
165/// `write`/ioctl sequence.
166#[derive(Debug, Default)]
167pub struct MockCaDevice {
168 /// Scripted frames the module sends to the host (FIFO).
169 pub inbound: std::collections::VecDeque<Vec<u8>>,
170 /// Recorded host-side operations, in order.
171 pub ops: Vec<DeviceOp>,
172 /// Slot status returned by [`slot_info`](CaDevice::slot_info).
173 pub slot: SlotInfo,
174}
175
176impl MockCaDevice {
177 /// New mock with a ready module in slot 0 and the given inbound script.
178 #[must_use]
179 pub fn new(inbound: impl IntoIterator<Item = Vec<u8>>) -> Self {
180 Self {
181 inbound: inbound.into_iter().collect(),
182 ops: Vec::new(),
183 slot: SlotInfo {
184 num: 0,
185 module_ready: true,
186 module_present: true,
187 },
188 }
189 }
190
191 /// The bytes written by the host so far, concatenated (convenience for
192 /// byte-exact differential comparison against the C reference).
193 #[must_use]
194 pub fn written(&self) -> Vec<u8> {
195 self.ops
196 .iter()
197 .filter_map(|o| match o {
198 DeviceOp::Write(b) => Some(b.clone()),
199 _ => None,
200 })
201 .flatten()
202 .collect()
203 }
204}
205
206impl CaDevice for MockCaDevice {
207 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
208 match self.inbound.pop_front() {
209 Some(frame) => {
210 let n = frame.len().min(buf.len());
211 buf[..n].copy_from_slice(&frame[..n]);
212 Ok(n)
213 }
214 None => Ok(0),
215 }
216 }
217
218 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
219 self.ops.push(DeviceOp::Write(buf.to_vec()));
220 Ok(())
221 }
222
223 fn reset(&mut self) -> io::Result<()> {
224 self.ops.push(DeviceOp::Reset);
225 Ok(())
226 }
227
228 fn slot_info(&mut self) -> io::Result<SlotInfo> {
229 self.ops.push(DeviceOp::SlotInfo);
230 Ok(self.slot)
231 }
232
233 fn poll(&mut self, _timeout: std::time::Duration) -> io::Result<bool> {
234 Ok(!self.inbound.is_empty())
235 }
236}
237
238/// One link-layer event for diagnostics, captured in both directions by
239/// [`RecordingCaDevice`].
240#[derive(Debug, Clone, PartialEq, Eq)]
241#[non_exhaustive]
242pub enum LinkEvent {
243 /// Host → module: a frame the host wrote.
244 Tx(Vec<u8>),
245 /// Module → host: a frame the host read.
246 Rx(Vec<u8>),
247 /// A `reset()` ioctl.
248 Reset,
249 /// A `slot_info()` ioctl and the status it returned.
250 SlotInfo(SlotInfo),
251}
252
253/// A [`CaDevice`] decorator that records every frame in **both** directions
254/// (plus ioctls) for live-CAM diagnostics. Wrap a real device, run, then dump
255/// the [`log`](Self::log) — or decode it with
256/// [`trace::decode_log`](crate::trace::decode_log) — to get an annotated byte
257/// trace without hand-instrumenting the device:
258///
259/// ```no_run
260/// # use dvb_ci_runtime::{Driver, device::RecordingCaDevice, trace};
261/// # fn real_device() -> dvb_ci_runtime::MockCaDevice { dvb_ci_runtime::MockCaDevice::new([]) }
262/// let mut driver = Driver::new(RecordingCaDevice::new(real_device()));
263/// driver.init().unwrap();
264/// // ... pump ...
265/// println!("{}", trace::decode_log(driver.device().log()));
266/// ```
267#[derive(Debug, Default)]
268pub struct RecordingCaDevice<D> {
269 inner: D,
270 /// The captured link events, in order.
271 pub log: Vec<LinkEvent>,
272 /// Last logged slot status, so repeated identical `slot_info()` polls
273 /// (the driver now samples every [`pump`](crate::Driver::pump) for
274 /// hot-plug edge detection — #726) don't swamp the trace; only a change
275 /// is recorded, same rationale as `poll()` below.
276 last_slot: Option<SlotInfo>,
277}
278
279impl<D: CaDevice> RecordingCaDevice<D> {
280 /// Wrap `inner`, recording all I/O.
281 pub fn new(inner: D) -> Self {
282 Self {
283 inner,
284 log: Vec::new(),
285 last_slot: None,
286 }
287 }
288
289 /// The recorded link events, in order.
290 #[must_use]
291 pub fn log(&self) -> &[LinkEvent] {
292 &self.log
293 }
294
295 /// Borrow the wrapped device.
296 pub fn inner(&self) -> &D {
297 &self.inner
298 }
299}
300
301impl<D: CaDevice> CaDevice for RecordingCaDevice<D> {
302 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
303 let n = self.inner.read(buf)?;
304 if n > 0 {
305 self.log.push(LinkEvent::Rx(buf[..n].to_vec()));
306 }
307 Ok(n)
308 }
309
310 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
311 self.log.push(LinkEvent::Tx(buf.to_vec()));
312 self.inner.write(buf)
313 }
314
315 fn reset(&mut self) -> io::Result<()> {
316 self.log.push(LinkEvent::Reset);
317 self.inner.reset()
318 }
319
320 /// Forwarded: without this the wrapper falls back to the trait default
321 /// (`Duration::ZERO`) and `Driver::init` writes the first TPDU straight
322 /// after `CA_RESET` (EINVAL on a CXD2099). Guarded by
323 /// `tests/wrapper_forwards_defaults.rs`.
324 fn reset_settle(&self) -> std::time::Duration {
325 self.inner.reset_settle()
326 }
327
328 fn slot_info(&mut self) -> io::Result<SlotInfo> {
329 let si = self.inner.slot_info()?;
330 if self.last_slot != Some(si) {
331 self.log.push(LinkEvent::SlotInfo(si));
332 self.last_slot = Some(si);
333 }
334 Ok(si)
335 }
336
337 fn poll(&mut self, timeout: std::time::Duration) -> io::Result<bool> {
338 // Polls are not recorded (they would swamp the trace).
339 self.inner.poll(timeout)
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn recording_device_captures_both_directions() {
349 let inner = MockCaDevice::new([vec![0x83, 0x01, 0x01]]);
350 let mut dev = RecordingCaDevice::new(inner);
351 dev.reset().unwrap();
352 dev.write(&[0x82, 0x01, 0x01]).unwrap();
353 let mut buf = [0u8; 16];
354 dev.read(&mut buf).unwrap();
355 assert_eq!(
356 dev.log(),
357 &[
358 LinkEvent::Reset,
359 LinkEvent::Tx(vec![0x82, 0x01, 0x01]),
360 LinkEvent::Rx(vec![0x83, 0x01, 0x01]),
361 ]
362 );
363 }
364
365 #[test]
366 fn mock_records_writes_and_replays_inbound() {
367 let mut dev = MockCaDevice::new([vec![0x01, 0x02], vec![0x03]]);
368 // host writes
369 dev.write(&[0xAA, 0xBB]).unwrap();
370 dev.reset().unwrap();
371 // module frames replay in order
372 let mut buf = [0u8; 16];
373 assert_eq!(dev.read(&mut buf).unwrap(), 2);
374 assert_eq!(&buf[..2], &[0x01, 0x02]);
375 assert_eq!(dev.read(&mut buf).unwrap(), 1);
376 assert_eq!(dev.read(&mut buf).unwrap(), 0); // drained
377 assert_eq!(dev.written(), vec![0xAA, 0xBB]);
378 assert_eq!(dev.ops[1], DeviceOp::Reset);
379 }
380}