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#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn mock_records_writes_and_replays_inbound() {
142 let mut dev = MockCaDevice::new([vec![0x01, 0x02], vec![0x03]]);
143 // host writes
144 dev.write(&[0xAA, 0xBB]).unwrap();
145 dev.reset().unwrap();
146 // module frames replay in order
147 let mut buf = [0u8; 16];
148 assert_eq!(dev.read(&mut buf).unwrap(), 2);
149 assert_eq!(&buf[..2], &[0x01, 0x02]);
150 assert_eq!(dev.read(&mut buf).unwrap(), 1);
151 assert_eq!(dev.read(&mut buf).unwrap(), 0); // drained
152 assert_eq!(dev.written(), vec![0xAA, 0xBB]);
153 assert_eq!(dev.ops[1], DeviceOp::Reset);
154 }
155}