Skip to main content

openipc_core/
routes.rs

1use std::{collections::HashMap, sync::Arc};
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: Arc<[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: Arc<[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_into(
145        &mut self,
146        payload: &[u8],
147        events: &mut Vec<PayloadPipelineEvent>,
148    ) -> Result<(), WfbError> {
149        match self {
150            Self::Real(pipeline) => pipeline.push_matched_payload_into(payload, events),
151            Self::Mock(_) => {
152                events.clear();
153                events.push(PayloadPipelineEvent::IgnoredFrame);
154                Ok(())
155            }
156        }
157    }
158
159    fn push_decrypted_80211_frame(
160        &mut self,
161        frame: &[u8],
162        decrypted_fragment: &[u8],
163    ) -> Result<Vec<PayloadPipelineEvent>, WfbError> {
164        match self {
165            Self::Real(pipeline) => pipeline.push_decrypted_80211_frame(frame, decrypted_fragment),
166            Self::Mock(_) => Ok(vec![PayloadPipelineEvent::IgnoredFrame]),
167        }
168    }
169
170    fn push_decrypted_fragment(
171        &mut self,
172        data_nonce: u64,
173        decrypted_fragment: &[u8],
174    ) -> Result<Vec<PayloadPipelineEvent>, WfbError> {
175        match self {
176            Self::Real(pipeline) => {
177                pipeline.push_decrypted_fragment(data_nonce, decrypted_fragment)
178            }
179            Self::Mock(pipeline) => Ok(pipeline.push_payload(data_nonce, decrypted_fragment)),
180        }
181    }
182
183    fn push_mock_payload(&mut self, packet_seq: u64, data: &[u8]) -> Vec<PayloadPipelineEvent> {
184        match self {
185            Self::Real(_) => vec![PayloadPipelineEvent::IgnoredFrame],
186            Self::Mock(pipeline) => pipeline.push_payload(packet_seq, data),
187        }
188    }
189}
190
191/// One route-manager runtime for a single WFB/OpenIPC channel and key slot.
192///
193/// A runtime can be backed by the real WFB [`PayloadPipeline`] or by a fully
194/// synthetic [`MockPayloadPipeline`]. Route IDs attached to the same runtime
195/// share the same recovered payload stream.
196#[derive(Debug, Clone)]
197pub struct PayloadChannelRuntime {
198    pipeline: PayloadChannelPipeline,
199    route_ids: Arc<[PayloadRouteId]>,
200    pipeline_events: Vec<PayloadPipelineEvent>,
201}
202
203impl PayloadChannelRuntime {
204    fn real(pipeline: PayloadPipeline, route_id: PayloadRouteId) -> Self {
205        Self {
206            pipeline: PayloadChannelPipeline::Real(Box::new(pipeline)),
207            route_ids: Arc::from([route_id]),
208            pipeline_events: Vec::with_capacity(1),
209        }
210    }
211
212    fn mock(channel_id: ChannelId, route_id: PayloadRouteId) -> Self {
213        Self {
214            pipeline: PayloadChannelPipeline::Mock(MockPayloadPipeline::new(channel_id)),
215            route_ids: Arc::from([route_id]),
216            pipeline_events: Vec::with_capacity(1),
217        }
218    }
219
220    /// Return this runtime's channel id.
221    pub fn channel_id(&self) -> ChannelId {
222        self.pipeline.channel_id()
223    }
224
225    /// Return the route ids attached to this runtime.
226    pub fn route_ids(&self) -> &[PayloadRouteId] {
227        &self.route_ids
228    }
229
230    fn push_route_id(&mut self, route_id: PayloadRouteId) {
231        if self.route_ids.contains(&route_id) {
232            return;
233        }
234        let mut route_ids = self.route_ids.to_vec();
235        route_ids.push(route_id);
236        self.route_ids = route_ids.into();
237    }
238}
239
240/// Fanout manager for one or more OpenIPC/WFB payload routes.
241///
242/// The manager owns one [`PayloadPipeline`] per `(channel_id, key_slot)` and
243/// lets multiple route IDs share that runtime. This is useful for outputs like
244/// video display plus RTP forwarding, or video plus telemetry.
245#[derive(Debug, Clone)]
246pub struct PayloadRouteManager {
247    frame_layout: FrameLayout,
248    runtimes: HashMap<PayloadRuntimeKey, PayloadChannelRuntime>,
249}
250
251impl PayloadRouteManager {
252    /// Create an empty route manager for frames with the given layout.
253    pub fn new(frame_layout: FrameLayout) -> Self {
254        Self {
255            frame_layout,
256            runtimes: HashMap::new(),
257        }
258    }
259
260    /// Return the frame layout used for all registered routes.
261    pub const fn frame_layout(&self) -> FrameLayout {
262        self.frame_layout
263    }
264
265    /// Return the number of distinct WFB runtimes.
266    pub fn runtime_count(&self) -> usize {
267        self.runtimes.len()
268    }
269
270    /// Add a route that receives already-plain WFB fragments.
271    ///
272    /// Routes with the same channel id and key slot share one runtime.
273    pub fn add_plain_route(
274        &mut self,
275        route_id: PayloadRouteId,
276        channel_id: ChannelId,
277        key_slot: u64,
278        fec_k: usize,
279        fec_n: usize,
280    ) -> Result<PayloadRuntimeKey, PayloadRouteError> {
281        let key = PayloadRuntimeKey::new(channel_id, key_slot);
282        if let Some(runtime) = self.runtimes.get_mut(&key) {
283            runtime.push_route_id(route_id);
284            return Ok(key);
285        }
286
287        let pipeline = PayloadPipeline::new(channel_id, self.frame_layout, fec_k, fec_n)?;
288        self.runtimes
289            .insert(key, PayloadChannelRuntime::real(pipeline, route_id));
290        Ok(key)
291    }
292
293    /// Add a route that receives encrypted WFB frames and session packets.
294    ///
295    /// Routes with the same channel id and key slot share one runtime.
296    pub fn add_keyed_route(
297        &mut self,
298        route_id: PayloadRouteId,
299        channel_id: ChannelId,
300        key_slot: u64,
301        keypair: WfbKeypair,
302        minimum_epoch: u64,
303    ) -> Result<PayloadRuntimeKey, PayloadRouteError> {
304        let key = PayloadRuntimeKey::new(channel_id, key_slot);
305        if let Some(runtime) = self.runtimes.get_mut(&key) {
306            runtime.push_route_id(route_id);
307            return Ok(key);
308        }
309
310        let pipeline =
311            PayloadPipeline::with_keypair(channel_id, self.frame_layout, keypair, minimum_epoch)?;
312        self.runtimes
313            .insert(key, PayloadChannelRuntime::real(pipeline, route_id));
314        Ok(key)
315    }
316
317    /// Add a direct recovered-payload route.
318    ///
319    /// Direct routes skip 802.11, WFB decryption, and FEC. Push payload bytes
320    /// with [`Self::push_direct_payload`]; downstream route fanout and RTP
321    /// handling still run exactly like radio-backed routes. This is suitable
322    /// for RTP arriving from UDP as well as no-hardware tests.
323    pub fn add_direct_route(
324        &mut self,
325        route_id: PayloadRouteId,
326        channel_id: ChannelId,
327        key_slot: u64,
328    ) -> PayloadRuntimeKey {
329        let key = PayloadRuntimeKey::new(channel_id, key_slot);
330        if let Some(runtime) = self.runtimes.get_mut(&key) {
331            runtime.push_route_id(route_id);
332            return key;
333        }
334
335        self.runtimes
336            .insert(key, PayloadChannelRuntime::mock(channel_id, route_id));
337        key
338    }
339
340    /// Add a synthetic route for no-hardware tests and development.
341    ///
342    /// This is an alias for [`Self::add_direct_route`].
343    pub fn add_mock_route(
344        &mut self,
345        route_id: PayloadRouteId,
346        channel_id: ChannelId,
347        key_slot: u64,
348    ) -> PayloadRuntimeKey {
349        self.add_direct_route(route_id, channel_id, key_slot)
350    }
351
352    /// Return route ids attached to a runtime key.
353    pub fn route_ids(&self, key: PayloadRuntimeKey) -> Option<&[PayloadRouteId]> {
354        self.runtimes
355            .get(&key)
356            .map(PayloadChannelRuntime::route_ids)
357    }
358
359    /// Return shared route membership and whether this is a direct runtime.
360    pub(crate) fn route_membership(
361        &self,
362        key: PayloadRuntimeKey,
363    ) -> Option<(Arc<[PayloadRouteId]>, bool)> {
364        self.runtimes.get(&key).map(|runtime| {
365            (
366                Arc::clone(&runtime.route_ids),
367                matches!(runtime.pipeline, PayloadChannelPipeline::Mock(_)),
368            )
369        })
370    }
371
372    /// Return cumulative FEC counters for a runtime key.
373    pub fn fec_counters(&self, key: PayloadRuntimeKey) -> Option<FecCounters> {
374        self.runtimes
375            .get(&key)
376            .map(|runtime| runtime.pipeline.fec_counters())
377    }
378
379    /// Return true when an 802.11 frame belongs to the selected runtime.
380    pub fn accepts_80211_frame(&self, key: PayloadRuntimeKey, frame: &[u8]) -> bool {
381        self.runtimes
382            .get(&key)
383            .map(|runtime| runtime.pipeline.accepts_80211_frame(frame))
384            .unwrap_or(false)
385    }
386
387    /// Route one raw 802.11 frame to every matching runtime.
388    pub fn push_80211_frame(
389        &mut self,
390        frame: &[u8],
391    ) -> Result<Vec<PayloadRouteEvent>, PayloadRouteError> {
392        let Ok(frame_view) = WifiFrame::parse(frame, self.frame_layout) else {
393            return Ok(vec![PayloadRouteEvent::IgnoredFrame]);
394        };
395        self.push_wifi_frame(frame_view)
396    }
397
398    /// Route an already-validated borrowed WiFi frame.
399    pub fn push_wifi_frame(
400        &mut self,
401        frame_view: WifiFrame<'_>,
402    ) -> Result<Vec<PayloadRouteEvent>, PayloadRouteError> {
403        let mut route_events = Vec::new();
404        self.push_wifi_frame_into(frame_view, &mut route_events)?;
405        Ok(route_events)
406    }
407
408    /// Route a validated WiFi frame into a reusable event buffer.
409    pub fn push_wifi_frame_into(
410        &mut self,
411        frame_view: WifiFrame<'_>,
412        route_events: &mut Vec<PayloadRouteEvent>,
413    ) -> Result<(), PayloadRouteError> {
414        route_events.clear();
415        let Some(channel_id) = frame_view.channel_id() else {
416            route_events.push(PayloadRouteEvent::IgnoredFrame);
417            return Ok(());
418        };
419
420        let mut matched = false;
421        let mut first_error = None;
422
423        for (key, runtime) in self
424            .runtimes
425            .iter_mut()
426            .filter(|(key, _)| key.channel_id() == channel_id)
427        {
428            matched = true;
429            match runtime
430                .pipeline
431                .push_matched_payload_into(frame_view.payload(), &mut runtime.pipeline_events)
432            {
433                Ok(()) => {
434                    map_pipeline_events_into(
435                        *key,
436                        Arc::clone(&runtime.route_ids),
437                        runtime.pipeline_events.drain(..),
438                        route_events,
439                    );
440                }
441                Err(err) => {
442                    if first_error.is_none() {
443                        first_error = Some(err);
444                    }
445                }
446            }
447        }
448
449        if !matched {
450            route_events.push(PayloadRouteEvent::IgnoredFrame);
451            return Ok(());
452        }
453        if route_events.is_empty() {
454            if let Some(err) = first_error {
455                return Err(err.into());
456            }
457        }
458        Ok(())
459    }
460
461    /// Route one 802.11 frame with a caller-supplied decrypted fragment.
462    pub fn push_decrypted_80211_frame(
463        &mut self,
464        key: PayloadRuntimeKey,
465        frame: &[u8],
466        decrypted_fragment: &[u8],
467    ) -> Result<Vec<PayloadRouteEvent>, PayloadRouteError> {
468        let runtime = self
469            .runtimes
470            .get_mut(&key)
471            .ok_or(PayloadRouteError::UnknownRuntime(key))?;
472        let events = runtime
473            .pipeline
474            .push_decrypted_80211_frame(frame, decrypted_fragment)?;
475        Ok(map_pipeline_events(key, runtime.route_ids(), events))
476    }
477
478    /// Push a decrypted fragment directly into one runtime.
479    pub fn push_decrypted_fragment(
480        &mut self,
481        key: PayloadRuntimeKey,
482        data_nonce: u64,
483        decrypted_fragment: &[u8],
484    ) -> Result<Vec<PayloadRouteEvent>, PayloadRouteError> {
485        let runtime = self
486            .runtimes
487            .get_mut(&key)
488            .ok_or(PayloadRouteError::UnknownRuntime(key))?;
489        let events = runtime
490            .pipeline
491            .push_decrypted_fragment(data_nonce, decrypted_fragment)?;
492        Ok(map_pipeline_events(key, runtime.route_ids(), events))
493    }
494
495    /// Push an already-recovered payload into one direct runtime.
496    pub fn push_direct_payload(
497        &mut self,
498        key: PayloadRuntimeKey,
499        packet_seq: u64,
500        data: &[u8],
501    ) -> Result<Vec<PayloadRouteEvent>, PayloadRouteError> {
502        let runtime = self
503            .runtimes
504            .get_mut(&key)
505            .ok_or(PayloadRouteError::UnknownRuntime(key))?;
506        let events = runtime.pipeline.push_mock_payload(packet_seq, data);
507        Ok(map_pipeline_events(key, runtime.route_ids(), events))
508    }
509
510    /// Push a synthetic recovered payload into one mock runtime.
511    ///
512    /// This is an alias for [`Self::push_direct_payload`].
513    pub fn push_mock_payload(
514        &mut self,
515        key: PayloadRuntimeKey,
516        packet_seq: u64,
517        data: &[u8],
518    ) -> Result<Vec<PayloadRouteEvent>, PayloadRouteError> {
519        self.push_direct_payload(key, packet_seq, data)
520    }
521}
522
523fn map_pipeline_events(
524    runtime: PayloadRuntimeKey,
525    route_ids: &[PayloadRouteId],
526    events: Vec<PayloadPipelineEvent>,
527) -> Vec<PayloadRouteEvent> {
528    let mut mapped = Vec::with_capacity(events.len());
529    map_pipeline_events_into(runtime, Arc::from(route_ids), events, &mut mapped);
530    mapped
531}
532
533fn map_pipeline_events_into(
534    runtime: PayloadRuntimeKey,
535    route_ids: Arc<[PayloadRouteId]>,
536    events: impl IntoIterator<Item = PayloadPipelineEvent>,
537    mapped: &mut Vec<PayloadRouteEvent>,
538) {
539    mapped.extend(events.into_iter().map(|event| match event {
540        PayloadPipelineEvent::IgnoredFrame => PayloadRouteEvent::IgnoredFrame,
541        PayloadPipelineEvent::SessionEstablished {
542            epoch,
543            fec_k,
544            fec_n,
545        } => PayloadRouteEvent::SessionEstablished {
546            runtime,
547            route_ids: Arc::clone(&route_ids),
548            epoch,
549            fec_k,
550            fec_n,
551        },
552        PayloadPipelineEvent::Payload(payload) => PayloadRouteEvent::Payload {
553            runtime,
554            route_ids: Arc::clone(&route_ids),
555            payload,
556        },
557    }));
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    fn plain(payload: &[u8]) -> Vec<u8> {
565        let mut out = Vec::new();
566        out.push(0);
567        out.extend_from_slice(&(payload.len() as u16).to_be_bytes());
568        out.extend_from_slice(payload);
569        out
570    }
571
572    #[test]
573    fn routes_share_one_runtime_per_channel_and_key_slot() {
574        let mut manager = PayloadRouteManager::new(FrameLayout::WithFcs);
575        let channel = ChannelId::default_video();
576        let runtime = manager
577            .add_plain_route(PayloadRouteId::new(1), channel, 0, 1, 1)
578            .unwrap();
579        let same_runtime = manager
580            .add_plain_route(PayloadRouteId::new(2), channel, 0, 1, 1)
581            .unwrap();
582
583        assert_eq!(runtime, same_runtime);
584        assert_eq!(manager.runtime_count(), 1);
585
586        let events = manager
587            .push_decrypted_fragment(runtime, 0, &plain(b"rtp bytes"))
588            .unwrap();
589        assert_eq!(
590            events,
591            vec![PayloadRouteEvent::Payload {
592                runtime,
593                route_ids: vec![PayloadRouteId::new(1), PayloadRouteId::new(2)].into(),
594                payload: RecoveredPayload {
595                    channel_id: channel,
596                    packet_seq: 0,
597                    data: b"rtp bytes".to_vec(),
598                },
599            }]
600        );
601    }
602
603    #[test]
604    fn different_channels_get_different_runtimes() {
605        let mut manager = PayloadRouteManager::new(FrameLayout::WithFcs);
606        let video = ChannelId::default_video();
607        let telemetry = ChannelId::from_link_port(
608            crate::channel::DEFAULT_LINK_ID,
609            crate::RadioPort::TelemetryRx,
610        );
611
612        let video_runtime = manager
613            .add_plain_route(PayloadRouteId::new(1), video, 0, 1, 1)
614            .unwrap();
615        let telemetry_runtime = manager
616            .add_plain_route(PayloadRouteId::new(2), telemetry, 0, 1, 1)
617            .unwrap();
618
619        assert_ne!(video_runtime, telemetry_runtime);
620        assert_eq!(manager.runtime_count(), 2);
621    }
622}