Skip to main content

openipc_core/
routes.rs

1use std::collections::HashMap;
2
3use crate::channel::ChannelId;
4use crate::ieee80211::{FrameLayout, WifiFrame};
5use crate::pipeline::{
6    MockPayloadPipeline, PayloadPipeline, PayloadPipelineEvent, RecoveredPayload,
7};
8use crate::wfb::{FecCounters, WfbError, WfbKeypair};
9
10/// Application-defined identifier for a recovered-payload output.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct PayloadRouteId(u64);
13
14impl PayloadRouteId {
15    /// Create a route id from a stable numeric value.
16    pub const fn new(raw: u64) -> Self {
17        Self(raw)
18    }
19
20    /// Return the raw route id value.
21    pub const fn raw(self) -> u64 {
22        self.0
23    }
24}
25
26/// Key for one WFB runtime inside [`PayloadRouteManager`].
27///
28/// Routes with the same `(channel_id, key_slot)` share decryption, FEC state,
29/// and counters.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct PayloadRuntimeKey {
32    channel_id: ChannelId,
33    key_slot: u64,
34}
35
36impl PayloadRuntimeKey {
37    /// Create a runtime key from a channel id and caller-defined key slot.
38    pub const fn new(channel_id: ChannelId, key_slot: u64) -> Self {
39        Self {
40            channel_id,
41            key_slot,
42        }
43    }
44
45    /// Return the WFB/OpenIPC channel id for this runtime.
46    pub const fn channel_id(self) -> ChannelId {
47        self.channel_id
48    }
49
50    /// Return the key slot for this runtime.
51    pub const fn key_slot(self) -> u64 {
52        self.key_slot
53    }
54}
55
56/// Event emitted by route-manager processing.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum PayloadRouteEvent {
59    /// Frame did not match any configured route or usable payload.
60    IgnoredFrame,
61    /// A WFB session packet established or refreshed a runtime session.
62    SessionEstablished {
63        /// Runtime whose WFB session changed.
64        runtime: PayloadRuntimeKey,
65        /// Route ids attached to the runtime.
66        route_ids: Vec<PayloadRouteId>,
67        /// Session epoch accepted from the transmitter.
68        epoch: u64,
69        /// Number of primary fragments in each FEC block.
70        fec_k: usize,
71        /// Total primary plus parity fragments in each FEC block.
72        fec_n: usize,
73    },
74    /// A recovered payload was emitted by a runtime.
75    Payload {
76        /// Runtime that recovered the payload.
77        runtime: PayloadRuntimeKey,
78        /// Route ids that should receive the payload.
79        route_ids: Vec<PayloadRouteId>,
80        /// Recovered payload bytes and packet metadata.
81        payload: RecoveredPayload,
82    },
83}
84
85/// Error returned while routing a WFB frame or decrypted fragment.
86#[derive(Debug, PartialEq, Eq)]
87pub enum PayloadRouteError {
88    /// Caller referenced a runtime key that is not registered.
89    UnknownRuntime(PayloadRuntimeKey),
90    /// Underlying WFB parser/decrypt/FEC error.
91    Wfb(WfbError),
92}
93
94impl std::fmt::Display for PayloadRouteError {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::UnknownRuntime(key) => write!(
98                f,
99                "unknown payload runtime for channel 0x{:08x} key slot {}",
100                key.channel_id().raw(),
101                key.key_slot()
102            ),
103            Self::Wfb(err) => std::fmt::Display::fmt(err, f),
104        }
105    }
106}
107
108impl std::error::Error for PayloadRouteError {}
109
110impl From<WfbError> for PayloadRouteError {
111    fn from(err: WfbError) -> Self {
112        Self::Wfb(err)
113    }
114}
115
116#[derive(Debug, Clone)]
117enum PayloadChannelPipeline {
118    Real(Box<PayloadPipeline>),
119    Mock(MockPayloadPipeline),
120}
121
122impl PayloadChannelPipeline {
123    fn channel_id(&self) -> ChannelId {
124        match self {
125            Self::Real(pipeline) => pipeline.channel_id(),
126            Self::Mock(pipeline) => pipeline.channel_id(),
127        }
128    }
129
130    fn fec_counters(&self) -> FecCounters {
131        match self {
132            Self::Real(pipeline) => pipeline.fec_counters(),
133            Self::Mock(pipeline) => pipeline.fec_counters(),
134        }
135    }
136
137    fn accepts_80211_frame(&self, frame: &[u8]) -> bool {
138        match self {
139            Self::Real(pipeline) => pipeline.accepts_80211_frame(frame),
140            Self::Mock(_) => false,
141        }
142    }
143
144    fn push_matched_payload(
145        &mut self,
146        payload: &[u8],
147    ) -> Result<Vec<PayloadPipelineEvent>, WfbError> {
148        match self {
149            Self::Real(pipeline) => pipeline.push_matched_payload(payload),
150            Self::Mock(_) => Ok(vec![PayloadPipelineEvent::IgnoredFrame]),
151        }
152    }
153
154    fn push_decrypted_80211_frame(
155        &mut self,
156        frame: &[u8],
157        decrypted_fragment: &[u8],
158    ) -> Result<Vec<PayloadPipelineEvent>, WfbError> {
159        match self {
160            Self::Real(pipeline) => pipeline.push_decrypted_80211_frame(frame, decrypted_fragment),
161            Self::Mock(_) => Ok(vec![PayloadPipelineEvent::IgnoredFrame]),
162        }
163    }
164
165    fn push_decrypted_fragment(
166        &mut self,
167        data_nonce: u64,
168        decrypted_fragment: &[u8],
169    ) -> Result<Vec<PayloadPipelineEvent>, WfbError> {
170        match self {
171            Self::Real(pipeline) => {
172                pipeline.push_decrypted_fragment(data_nonce, decrypted_fragment)
173            }
174            Self::Mock(pipeline) => Ok(pipeline.push_payload(data_nonce, decrypted_fragment)),
175        }
176    }
177
178    fn push_mock_payload(&mut self, packet_seq: u64, data: &[u8]) -> Vec<PayloadPipelineEvent> {
179        match self {
180            Self::Real(_) => vec![PayloadPipelineEvent::IgnoredFrame],
181            Self::Mock(pipeline) => pipeline.push_payload(packet_seq, data),
182        }
183    }
184}
185
186/// One route-manager runtime for a single WFB/OpenIPC channel and key slot.
187///
188/// A runtime can be backed by the real WFB [`PayloadPipeline`] or by a fully
189/// synthetic [`MockPayloadPipeline`]. Route IDs attached to the same runtime
190/// share the same recovered payload stream.
191#[derive(Debug, Clone)]
192pub struct PayloadChannelRuntime {
193    pipeline: PayloadChannelPipeline,
194    route_ids: Vec<PayloadRouteId>,
195}
196
197impl PayloadChannelRuntime {
198    fn real(pipeline: PayloadPipeline, route_id: PayloadRouteId) -> Self {
199        Self {
200            pipeline: PayloadChannelPipeline::Real(Box::new(pipeline)),
201            route_ids: vec![route_id],
202        }
203    }
204
205    fn mock(channel_id: ChannelId, route_id: PayloadRouteId) -> Self {
206        Self {
207            pipeline: PayloadChannelPipeline::Mock(MockPayloadPipeline::new(channel_id)),
208            route_ids: vec![route_id],
209        }
210    }
211
212    /// Return this runtime's channel id.
213    pub fn channel_id(&self) -> ChannelId {
214        self.pipeline.channel_id()
215    }
216
217    /// Return the route ids attached to this runtime.
218    pub fn route_ids(&self) -> &[PayloadRouteId] {
219        self.route_ids.as_slice()
220    }
221
222    fn push_route_id(&mut self, route_id: PayloadRouteId) {
223        push_route_id(&mut self.route_ids, route_id);
224    }
225}
226
227/// Fanout manager for one or more OpenIPC/WFB payload routes.
228///
229/// The manager owns one [`PayloadPipeline`] per `(channel_id, key_slot)` and
230/// lets multiple route IDs share that runtime. This is useful for outputs like
231/// video display plus RTP forwarding, or video plus telemetry.
232#[derive(Debug, Clone)]
233pub struct PayloadRouteManager {
234    frame_layout: FrameLayout,
235    runtimes: HashMap<PayloadRuntimeKey, PayloadChannelRuntime>,
236}
237
238impl PayloadRouteManager {
239    /// Create an empty route manager for frames with the given layout.
240    pub fn new(frame_layout: FrameLayout) -> Self {
241        Self {
242            frame_layout,
243            runtimes: HashMap::new(),
244        }
245    }
246
247    /// Return the frame layout used for all registered routes.
248    pub const fn frame_layout(&self) -> FrameLayout {
249        self.frame_layout
250    }
251
252    /// Return the number of distinct WFB runtimes.
253    pub fn runtime_count(&self) -> usize {
254        self.runtimes.len()
255    }
256
257    /// Add a route that receives already-plain WFB fragments.
258    ///
259    /// Routes with the same channel id and key slot share one runtime.
260    pub fn add_plain_route(
261        &mut self,
262        route_id: PayloadRouteId,
263        channel_id: ChannelId,
264        key_slot: u64,
265        fec_k: usize,
266        fec_n: usize,
267    ) -> Result<PayloadRuntimeKey, PayloadRouteError> {
268        let key = PayloadRuntimeKey::new(channel_id, key_slot);
269        if let Some(runtime) = self.runtimes.get_mut(&key) {
270            runtime.push_route_id(route_id);
271            return Ok(key);
272        }
273
274        let pipeline = PayloadPipeline::new(channel_id, self.frame_layout, fec_k, fec_n)?;
275        self.runtimes
276            .insert(key, PayloadChannelRuntime::real(pipeline, route_id));
277        Ok(key)
278    }
279
280    /// Add a route that receives encrypted WFB frames and session packets.
281    ///
282    /// Routes with the same channel id and key slot share one runtime.
283    pub fn add_keyed_route(
284        &mut self,
285        route_id: PayloadRouteId,
286        channel_id: ChannelId,
287        key_slot: u64,
288        keypair: WfbKeypair,
289        minimum_epoch: u64,
290    ) -> Result<PayloadRuntimeKey, PayloadRouteError> {
291        let key = PayloadRuntimeKey::new(channel_id, key_slot);
292        if let Some(runtime) = self.runtimes.get_mut(&key) {
293            runtime.push_route_id(route_id);
294            return Ok(key);
295        }
296
297        let pipeline =
298            PayloadPipeline::with_keypair(channel_id, self.frame_layout, keypair, minimum_epoch)?;
299        self.runtimes
300            .insert(key, PayloadChannelRuntime::real(pipeline, route_id));
301        Ok(key)
302    }
303
304    /// Add a fully synthetic route for no-hardware tests and development.
305    ///
306    /// Mock routes skip 802.11, WFB decrypt, and FEC. Push recovered payload
307    /// bytes with [`Self::push_mock_payload`]; downstream route fanout and RTP
308    /// handling still run exactly like real routes.
309    pub fn add_mock_route(
310        &mut self,
311        route_id: PayloadRouteId,
312        channel_id: ChannelId,
313        key_slot: u64,
314    ) -> PayloadRuntimeKey {
315        let key = PayloadRuntimeKey::new(channel_id, key_slot);
316        if let Some(runtime) = self.runtimes.get_mut(&key) {
317            runtime.push_route_id(route_id);
318            return key;
319        }
320
321        self.runtimes
322            .insert(key, PayloadChannelRuntime::mock(channel_id, route_id));
323        key
324    }
325
326    /// Return route ids attached to a runtime key.
327    pub fn route_ids(&self, key: PayloadRuntimeKey) -> Option<&[PayloadRouteId]> {
328        self.runtimes
329            .get(&key)
330            .map(PayloadChannelRuntime::route_ids)
331    }
332
333    /// Return cumulative FEC counters for a runtime key.
334    pub fn fec_counters(&self, key: PayloadRuntimeKey) -> Option<FecCounters> {
335        self.runtimes
336            .get(&key)
337            .map(|runtime| runtime.pipeline.fec_counters())
338    }
339
340    /// Return true when an 802.11 frame belongs to the selected runtime.
341    pub fn accepts_80211_frame(&self, key: PayloadRuntimeKey, frame: &[u8]) -> bool {
342        self.runtimes
343            .get(&key)
344            .map(|runtime| runtime.pipeline.accepts_80211_frame(frame))
345            .unwrap_or(false)
346    }
347
348    /// Route one raw 802.11 frame to every matching runtime.
349    pub fn push_80211_frame(
350        &mut self,
351        frame: &[u8],
352    ) -> Result<Vec<PayloadRouteEvent>, PayloadRouteError> {
353        let Ok(frame_view) = WifiFrame::parse(frame, self.frame_layout) else {
354            return Ok(vec![PayloadRouteEvent::IgnoredFrame]);
355        };
356        let Some(channel_id) = frame_view.channel_id() else {
357            return Ok(vec![PayloadRouteEvent::IgnoredFrame]);
358        };
359
360        let mut matched = false;
361        let mut route_events = Vec::new();
362        let mut first_error = None;
363
364        for (key, runtime) in self
365            .runtimes
366            .iter_mut()
367            .filter(|(key, _)| key.channel_id() == channel_id)
368        {
369            matched = true;
370            match runtime.pipeline.push_matched_payload(frame_view.payload()) {
371                Ok(events) => {
372                    route_events.extend(map_pipeline_events(*key, runtime.route_ids(), events));
373                }
374                Err(err) => {
375                    if first_error.is_none() {
376                        first_error = Some(err);
377                    }
378                }
379            }
380        }
381
382        if !matched {
383            return Ok(vec![PayloadRouteEvent::IgnoredFrame]);
384        }
385        if route_events.is_empty() {
386            if let Some(err) = first_error {
387                return Err(err.into());
388            }
389        }
390        Ok(route_events)
391    }
392
393    /// Route one 802.11 frame with a caller-supplied decrypted fragment.
394    pub fn push_decrypted_80211_frame(
395        &mut self,
396        key: PayloadRuntimeKey,
397        frame: &[u8],
398        decrypted_fragment: &[u8],
399    ) -> Result<Vec<PayloadRouteEvent>, PayloadRouteError> {
400        let runtime = self
401            .runtimes
402            .get_mut(&key)
403            .ok_or(PayloadRouteError::UnknownRuntime(key))?;
404        let events = runtime
405            .pipeline
406            .push_decrypted_80211_frame(frame, decrypted_fragment)?;
407        Ok(map_pipeline_events(key, runtime.route_ids(), events))
408    }
409
410    /// Push a decrypted fragment directly into one runtime.
411    pub fn push_decrypted_fragment(
412        &mut self,
413        key: PayloadRuntimeKey,
414        data_nonce: u64,
415        decrypted_fragment: &[u8],
416    ) -> Result<Vec<PayloadRouteEvent>, PayloadRouteError> {
417        let runtime = self
418            .runtimes
419            .get_mut(&key)
420            .ok_or(PayloadRouteError::UnknownRuntime(key))?;
421        let events = runtime
422            .pipeline
423            .push_decrypted_fragment(data_nonce, decrypted_fragment)?;
424        Ok(map_pipeline_events(key, runtime.route_ids(), events))
425    }
426
427    /// Push a fully synthetic recovered payload into one mock runtime.
428    pub fn push_mock_payload(
429        &mut self,
430        key: PayloadRuntimeKey,
431        packet_seq: u64,
432        data: &[u8],
433    ) -> Result<Vec<PayloadRouteEvent>, PayloadRouteError> {
434        let runtime = self
435            .runtimes
436            .get_mut(&key)
437            .ok_or(PayloadRouteError::UnknownRuntime(key))?;
438        let events = runtime.pipeline.push_mock_payload(packet_seq, data);
439        Ok(map_pipeline_events(key, runtime.route_ids(), events))
440    }
441}
442
443fn push_route_id(route_ids: &mut Vec<PayloadRouteId>, route_id: PayloadRouteId) {
444    if !route_ids.contains(&route_id) {
445        route_ids.push(route_id);
446    }
447}
448
449fn map_pipeline_events(
450    runtime: PayloadRuntimeKey,
451    route_ids: &[PayloadRouteId],
452    events: Vec<PayloadPipelineEvent>,
453) -> Vec<PayloadRouteEvent> {
454    events
455        .into_iter()
456        .map(|event| match event {
457            PayloadPipelineEvent::IgnoredFrame => PayloadRouteEvent::IgnoredFrame,
458            PayloadPipelineEvent::SessionEstablished {
459                epoch,
460                fec_k,
461                fec_n,
462            } => PayloadRouteEvent::SessionEstablished {
463                runtime,
464                route_ids: route_ids.to_vec(),
465                epoch,
466                fec_k,
467                fec_n,
468            },
469            PayloadPipelineEvent::Payload(payload) => PayloadRouteEvent::Payload {
470                runtime,
471                route_ids: route_ids.to_vec(),
472                payload,
473            },
474        })
475        .collect()
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    fn plain(payload: &[u8]) -> Vec<u8> {
483        let mut out = Vec::new();
484        out.push(0);
485        out.extend_from_slice(&(payload.len() as u16).to_be_bytes());
486        out.extend_from_slice(payload);
487        out
488    }
489
490    #[test]
491    fn routes_share_one_runtime_per_channel_and_key_slot() {
492        let mut manager = PayloadRouteManager::new(FrameLayout::WithFcs);
493        let channel = ChannelId::default_video();
494        let runtime = manager
495            .add_plain_route(PayloadRouteId::new(1), channel, 0, 1, 1)
496            .unwrap();
497        let same_runtime = manager
498            .add_plain_route(PayloadRouteId::new(2), channel, 0, 1, 1)
499            .unwrap();
500
501        assert_eq!(runtime, same_runtime);
502        assert_eq!(manager.runtime_count(), 1);
503
504        let events = manager
505            .push_decrypted_fragment(runtime, 0, &plain(b"rtp bytes"))
506            .unwrap();
507        assert_eq!(
508            events,
509            vec![PayloadRouteEvent::Payload {
510                runtime,
511                route_ids: vec![PayloadRouteId::new(1), PayloadRouteId::new(2)],
512                payload: RecoveredPayload {
513                    channel_id: channel,
514                    packet_seq: 0,
515                    data: b"rtp bytes".to_vec(),
516                },
517            }]
518        );
519    }
520
521    #[test]
522    fn different_channels_get_different_runtimes() {
523        let mut manager = PayloadRouteManager::new(FrameLayout::WithFcs);
524        let video = ChannelId::default_video();
525        let telemetry = ChannelId::from_link_port(
526            crate::channel::DEFAULT_LINK_ID,
527            crate::RadioPort::TelemetryRx,
528        );
529
530        let video_runtime = manager
531            .add_plain_route(PayloadRouteId::new(1), video, 0, 1, 1)
532            .unwrap();
533        let telemetry_runtime = manager
534            .add_plain_route(PayloadRouteId::new(2), telemetry, 0, 1, 1)
535            .unwrap();
536
537        assert_ne!(video_runtime, telemetry_runtime);
538        assert_eq!(manager.runtime_count(), 2);
539    }
540}