Skip to main content

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