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