dvb_ci_runtime/event.rs
1//! The sans-IO event/action model.
2//!
3//! The protocol core is pure: it consumes [`Event`]s and produces [`Action`]s,
4//! with no device, threads, or clock of its own. The driver loop executes the
5//! actions against a [`CaDevice`](crate::CaDevice) and feeds events back. This
6//! keeps every state machine deterministic and testable without
7//! hardware — a test (or a differential comparison against an external
8//! reference) drives a sequence of events and asserts the emitted action
9//! sequence.
10
11use std::time::Duration;
12
13use dvb_ci::objects::ca_pmt_reply::CaEnable;
14use dvb_ci::resource::ResourceId;
15
16/// An input to the protocol core.
17#[derive(Debug, Clone, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum Event<'a> {
20 /// One link-layer frame was read from the device.
21 Readable(&'a [u8]),
22 /// Logical time advanced by `elapsed` since the last tick (drives poll
23 /// cadence and resource timers without a real clock in the core).
24 Tick {
25 /// Time since the previous tick.
26 elapsed: Duration,
27 },
28 /// A request from the host application.
29 Host(HostRequest<'a>),
30}
31
32/// A request the host application makes of the stack.
33#[derive(Debug, Clone, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum HostRequest<'a> {
36 /// Bring the interface up: reset the slot and open the transport connection.
37 Init,
38 /// Send a serialized `ca_pmt` APDU body to the CAM's conditional-access
39 /// resource (descrambling request).
40 SendCaPmt(&'a [u8]),
41 /// Answer an MMI `menu`/`list` by 1-based `choice_ref` (0 = "back"/cancel),
42 /// sent as `menu_answ` to the module.
43 MmiMenuAnswer(u8),
44 /// Answer an MMI `enquiry` with the user's input text (EN 300 468 Annex A
45 /// bytes), sent as `answ` (`answ_id = answer`).
46 MmiEnquiryAnswer(&'a [u8]),
47 /// Abort the current MMI enquiry (`answ` with `answ_id = cancel`).
48 MmiCancel,
49 /// Ask the module to open its MMI menu (`enter_menu` on the
50 /// application_information session) — e.g. to read card / entitlement info.
51 EnterMenu,
52 /// Descramble the services in a PMT section (raw `dvb-si` PMT bytes). The
53 /// stack filters the PMT's `CA_descriptor`s to the CAM's advertised CAIDs
54 /// (from its `ca_info`) and sends a `ca_pmt` with `list_management = only`,
55 /// `cmd_id = ok_descrambling`. The reply outcome surfaces as
56 /// [`Notification::CaPmtReply`].
57 Descramble(&'a [u8]),
58 /// Descramble a **set** of programmes in one CA-PMT list (`first`/`more`/
59 /// `last`, all `ok_descrambling`), replacing any prior set. Each element is a
60 /// raw PMT section.
61 DescramblePrograms(&'a [&'a [u8]]),
62 /// Add one programme to the descrambled set (`list_management = add`).
63 AddProgram(&'a [u8]),
64 /// Remove one programme from the descrambled set (`list_management = update`,
65 /// `cmd_id = not_selected`).
66 RemoveProgram(&'a [u8]),
67 /// Tear the interface down (close sessions + transport connection).
68 Shutdown,
69}
70
71/// An output the driver loop must perform.
72#[derive(Debug, Clone, PartialEq, Eq)]
73#[non_exhaustive]
74pub enum Action {
75 /// Write one link-layer frame to the device.
76 Write(Vec<u8>),
77 /// Issue the `CA_RESET` ioctl.
78 Reset,
79 /// Issue the `CA_GET_SLOT_INFO` ioctl.
80 QuerySlot,
81 /// Arm the poll/timer to fire after `after` (coalesced: the latest wins).
82 SetTimer {
83 /// Delay before the next [`Event::Tick`] should be delivered.
84 after: Duration,
85 },
86 /// Surface a host-facing [`Notification`].
87 Notify(Notification),
88}
89
90/// A host-facing event surfaced by the stack (the useful outputs of a CI
91/// session — what an application reacts to).
92#[derive(Debug, Clone, PartialEq, Eq)]
93#[non_exhaustive]
94pub enum Notification {
95 /// The module is present and the resource-manager handshake completed.
96 CamReady,
97 /// `application_information` was received.
98 ApplicationInfo {
99 /// `application_type` (0x01 = CA).
100 application_type: u8,
101 /// `application_manufacturer`.
102 manufacturer: u16,
103 /// `manufacturer_code`.
104 code: u16,
105 /// The decoded menu string.
106 menu: String,
107 },
108 /// `ca_info` was received — the CA system ids the module supports.
109 CaInfo {
110 /// `CA_system_id` values the CAM can descramble.
111 ca_system_ids: Vec<u16>,
112 },
113 /// A `ca_pmt_reply` was received for a prior CA_PMT.
114 CaPmtReply {
115 /// `program_number` the reply pertains to.
116 program_number: u16,
117 /// Programme-level `CA_enable` (EN 50221 §8.4.3.5 Table 26). `None`
118 /// iff the programme `CA_enable_flag` bit was clear (no
119 /// programme-level status given) — plumbed straight through from the
120 /// `dvb_ci` `CaPmtReply` object's own `Option<CaEnable>`, never
121 /// collapsed to a sentinel.
122 ca_enable: Option<CaEnable>,
123 /// Whether descrambling is (or was) possible, derived from
124 /// `ca_enable`. Kept for back-compat with the pre-#763 boolean-only
125 /// surface.
126 descrambling_ok: bool,
127 },
128 /// A per-programme entitlement status transition (#763). Edge-triggered:
129 /// fires once per `program_number` only when the programme-level
130 /// `CA_enable` status (EN 50221 §8.4.3.5, Table 26) changes versus the
131 /// last observed `ca_pmt_reply` for that programme. The transition is
132 /// detected by the periodic re-query (`Driver::set_requery_interval`),
133 /// which re-sends the active `ca_pmt`s with `ca_pmt_cmd_id = query` so
134 /// the CAM re-evaluates and replies. Complements the coarse #726
135 /// `HotPlug` module/card layer with the fine-grained per-service layer.
136 /// (Programme-level status only; the ES-level `CA_enable` entries are
137 /// not evaluated for this event.)
138 Entitlement {
139 /// `program_number` the status pertains to.
140 program_number: u16,
141 /// Programme-level `CA_enable` status.
142 ca_enable: CaEnable,
143 /// Whether descrambling is possible per the current status.
144 descrambling_ok: bool,
145 },
146 /// An MMI menu/enquiry the host should display.
147 Mmi(MmiEvent),
148 /// A `host_control` request the CAM made of the host (EN 50221 §8.5.1). The
149 /// host acts on it out-of-band (retune / PID replace); the runtime only
150 /// surfaces the decoded request.
151 HostControl(HostControlEvent),
152 /// A session for `resource` was opened.
153 SessionOpened {
154 /// The resource the session serves.
155 resource: ResourceId,
156 },
157 /// A session closed.
158 SessionClosed {
159 /// The `session_nb` that closed.
160 session_nb: u16,
161 },
162 /// A protocol error surfaced by the stack (non-fatal; informational).
163 Error {
164 /// Human-readable detail.
165 detail: String,
166 },
167 /// A CAM/card hot-plug transition (#726). See [`HotPlug`].
168 HotPlug(HotPlug),
169}
170
171impl Notification {
172 /// This notification's [`HotPlug`] transition, if it is one — a cheap
173 /// classifier for poll-mode consumers that only care about hot-plug
174 /// edges.
175 #[must_use]
176 pub fn hotplug(&self) -> Option<HotPlug> {
177 if let Notification::HotPlug(h) = self {
178 Some(*h)
179 } else {
180 None
181 }
182 }
183}
184
185/// A CAM/card hot-plug transition. `Cam*` are real DVB-CA slot-status edges;
186/// `Card*` are best-effort EN 50221 app-layer inference (no card-detect line).
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188#[non_exhaustive]
189pub enum HotPlug {
190 /// The module transitioned absent → present *and* ready (DVB-CA slot
191 /// status: `CA_CI_MODULE_PRESENT` and `CA_CI_MODULE_READY` both set —
192 /// Linux uapi `linux/dvb/ca.h` `ca_slot_info.flags`). A real hardware
193 /// signal from [`SlotInfo`](crate::device::SlotInfo), surfaced once on the
194 /// edge (not on every poll); the driver re-drives the handshake (a fresh
195 /// [`Init`](crate::event::HostRequest::Init)) so the newly-inserted module
196 /// gets a clean resource-manager session.
197 CamPresent,
198 /// The module transitioned present → absent (DVB-CA slot status:
199 /// `CA_CI_MODULE_PRESENT` clear). A real hardware signal; the driver tears
200 /// down all session/handshake state so a later re-insert re-handshakes
201 /// cleanly rather than reusing stale session numbers.
202 CamRemoved,
203 /// A smart card was inferred to have been inserted into the module.
204 ///
205 /// **Best-effort app-layer inference** — EN 50221 slots are module-level
206 /// only; there is no card-detect line (verified against DD ddbridge /
207 /// cxd2099 driver behaviour). Raised from one of: an `ca_info` CAID-set
208 /// transition from empty to non-empty, or a `ca_pmt_reply`
209 /// `descrambling_ok` transition from `false` to `true`. Some CAMs instead
210 /// give a strong signal by resetting the module on card change, which
211 /// surfaces as [`CamPresent`](HotPlug::CamPresent) rather than this
212 /// variant.
213 CardInserted,
214 /// A smart card was inferred to have been removed from the module.
215 ///
216 /// **Best-effort app-layer inference** (see [`CardInserted`](HotPlug::CardInserted)
217 /// for why no hardware signal exists). Raised from one of: an `ca_info`
218 /// CAID-set transition from non-empty to empty, a `ca_pmt_reply`
219 /// `descrambling_ok` transition from `true` to `false`, or MMI menu/list/
220 /// enquiry text matching a "no card" style keyword.
221 CardRemoved,
222 /// The inserted smart card was inferred to have changed (swapped without
223 /// an intervening removal the runtime observed).
224 ///
225 /// **Best-effort app-layer inference** (see [`CardInserted`](HotPlug::CardInserted)
226 /// for why no hardware signal exists). Raised when a later `ca_info`
227 /// reports a different non-empty CAID set than the last one seen for this
228 /// module.
229 CardChanged,
230}
231
232impl HotPlug {
233 /// Stable lowercase spec-ish token for this transition (#204 label
234 /// convention).
235 #[must_use]
236 pub fn name(&self) -> &'static str {
237 match self {
238 Self::CamPresent => "cam-present",
239 Self::CamRemoved => "cam-removed",
240 Self::CardInserted => "card-inserted",
241 Self::CardRemoved => "card-removed",
242 Self::CardChanged => "card-changed",
243 }
244 }
245}
246
247broadcast_common::impl_spec_display!(HotPlug);
248
249/// A decoded high-level MMI `menu()` / `list()` ready for display (§8.6.5,
250/// Tables 49/51). The three header lines and the choice list are kept separate
251/// so a UI can render them directly — a title bar, two sub-lines, and a list of
252/// selectable rows — without re-parsing.
253#[derive(Debug, Clone, PartialEq, Eq, Default)]
254pub struct MmiMenu {
255 /// Title line.
256 pub title: String,
257 /// Sub-title line (often a section heading).
258 pub subtitle: String,
259 /// Bottom line (often a hint such as "Select item and press OK").
260 pub bottom: String,
261 /// The selectable choices, in wire order. Answer the Nth (1-based) with
262 /// [`Driver::mmi_menu_answer`](crate::Driver::mmi_menu_answer)`(N)`; `0`
263 /// cancels / goes back.
264 pub choices: Vec<String>,
265}
266
267/// MMI (man-machine interface) host events.
268#[derive(Debug, Clone, PartialEq, Eq)]
269#[non_exhaustive]
270pub enum MmiEvent {
271 /// A `menu()` to display — the user picks one choice. Answer via
272 /// [`Driver::mmi_menu_answer`](crate::Driver::mmi_menu_answer).
273 Menu(MmiMenu),
274 /// A `list()` to display — informational (e.g. an entitlement listing); the
275 /// host typically dismisses it with
276 /// [`Driver::mmi_menu_answer`](crate::Driver::mmi_menu_answer)`(0)`.
277 List(MmiMenu),
278 /// An `enquiry` (text prompt) expecting an answer. Reply via
279 /// [`Driver::mmi_enquiry_answer`](crate::Driver::mmi_enquiry_answer) or
280 /// [`Driver::mmi_cancel`](crate::Driver::mmi_cancel).
281 Enquiry {
282 /// Prompt text.
283 prompt: String,
284 /// Whether the answer should be hidden (e.g. PIN).
285 blind: bool,
286 /// Expected answer length.
287 answer_len: u8,
288 },
289 /// The module closed the MMI dialog.
290 Close,
291}
292
293/// A `host_control` request the CAM makes of the host (ETSI EN 50221 §8.5.1,
294/// Tables 27-30). The runtime surfaces the decoded request; the host performs
295/// the retune / PID replacement itself (out of band) — there is no re-tune
296/// logic in the stack.
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298#[non_exhaustive]
299pub enum HostControlEvent {
300 /// `tune()` (Table 27): retune to the identified service.
301 Tune {
302 /// `network_id`.
303 network_id: u16,
304 /// `original_network_id`.
305 original_network_id: u16,
306 /// `transport_stream_id`.
307 transport_stream_id: u16,
308 /// `service_id`.
309 service_id: u16,
310 },
311 /// `replace()` (Table 28): temporarily replace one component PID with
312 /// another from the same multiplex.
313 Replace {
314 /// `replacement_ref` — matched later by a Clear Replace.
315 replacement_ref: u8,
316 /// 13-bit `replaced_PID`.
317 replaced_pid: u16,
318 /// 13-bit `replacement_PID`.
319 replacement_pid: u16,
320 },
321 /// `clear_replace()` (Table 29): undo all Replace operations sharing this
322 /// `replacement_ref`.
323 ClearReplace {
324 /// `replacement_ref` shared with one or more prior Replace requests.
325 replacement_ref: u8,
326 },
327 /// `ask_release()` (Table 30): the CAM asks the host to release any
328 /// replacements it holds (header-only request).
329 AskRelease,
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn notification_entitlement_construction() {
338 let note = Notification::Entitlement {
339 program_number: 1234,
340 ca_enable: CaEnable::Possible,
341 descrambling_ok: true,
342 };
343 match note {
344 Notification::Entitlement {
345 program_number,
346 ca_enable,
347 descrambling_ok,
348 } => {
349 assert_eq!(program_number, 1234);
350 assert_eq!(ca_enable, CaEnable::Possible);
351 assert!(descrambling_ok);
352 }
353 _ => panic!("expected Entitlement variant"),
354 }
355 }
356}