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