ndn_strategy/filters/
rssi.rs

1use crate::context::StrategyContext;
2use crate::cross_layer::LinkQualitySnapshot;
3use crate::filter::StrategyFilter;
4use ndn_transport::ForwardingAction;
5use smallvec::SmallVec;
6
7/// Removes faces with RSSI below a configurable threshold from `Forward` actions.
8///
9/// If all faces in a `Forward` are filtered out, the action is dropped entirely
10/// so the strategy falls through to the next action (typically `Nack` or `Suppress`).
11///
12/// When no `LinkQualitySnapshot` is present in the extensions (e.g. on wired-only
13/// routers), the filter is a no-op — all actions pass through unchanged.
14pub struct RssiFilter {
15    /// Minimum RSSI in dBm. Faces below this threshold are removed.
16    pub min_rssi_dbm: i8,
17}
18
19impl RssiFilter {
20    /// Create a filter that drops faces with RSSI below `min_rssi_dbm`.
21    pub fn new(min_rssi_dbm: i8) -> Self {
22        Self { min_rssi_dbm }
23    }
24}
25
26impl StrategyFilter for RssiFilter {
27    fn name(&self) -> &str {
28        "rssi-filter"
29    }
30
31    fn filter(
32        &self,
33        ctx: &StrategyContext,
34        actions: SmallVec<[ForwardingAction; 2]>,
35    ) -> SmallVec<[ForwardingAction; 2]> {
36        let snapshot = match ctx.extensions.get::<LinkQualitySnapshot>() {
37            Some(s) => s,
38            None => return actions, // no radio data — pass through
39        };
40
41        actions
42            .into_iter()
43            .filter_map(|action| {
44                match action {
45                    ForwardingAction::Forward(faces) => {
46                        let filtered: SmallVec<[_; 4]> = faces
47                            .into_iter()
48                            .filter(|face_id| {
49                                snapshot
50                                    .for_face(*face_id)
51                                    .and_then(|lq| lq.rssi_dbm)
52                                    .is_none_or(|rssi| rssi >= self.min_rssi_dbm)
53                            })
54                            .collect();
55                        if filtered.is_empty() {
56                            None // all faces filtered out — drop this action
57                        } else {
58                            Some(ForwardingAction::Forward(filtered))
59                        }
60                    }
61                    other => Some(other),
62                }
63            })
64            .collect()
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::MeasurementsTable;
72    use crate::cross_layer::FaceLinkQuality;
73    use ndn_packet::Name;
74    use ndn_transport::{AnyMap, FaceId};
75    use smallvec::smallvec;
76    use std::sync::Arc;
77
78    fn make_ctx_with_snapshot<'a>(
79        name: &'a Arc<Name>,
80        measurements: &'a MeasurementsTable,
81        extensions: &'a AnyMap,
82    ) -> StrategyContext<'a> {
83        StrategyContext {
84            name,
85            in_face: FaceId(0),
86            fib_entry: None,
87            pit_token: None,
88            measurements,
89            extensions,
90        }
91    }
92
93    #[test]
94    fn passes_through_when_no_snapshot() {
95        let name = Arc::new(Name::root());
96        let m = MeasurementsTable::new();
97        let ext = AnyMap::new();
98        let ctx = make_ctx_with_snapshot(&name, &m, &ext);
99
100        let filter = RssiFilter::new(-60);
101        let actions = smallvec![ForwardingAction::Forward(smallvec![FaceId(1), FaceId(2)])];
102        let result = filter.filter(&ctx, actions);
103        assert_eq!(result.len(), 1);
104        match &result[0] {
105            ForwardingAction::Forward(faces) => assert_eq!(faces.len(), 2),
106            _ => panic!("expected Forward"),
107        }
108    }
109
110    #[test]
111    fn filters_low_rssi_faces() {
112        let name = Arc::new(Name::root());
113        let m = MeasurementsTable::new();
114        let mut ext = AnyMap::new();
115        ext.insert(LinkQualitySnapshot {
116            per_face: smallvec![
117                FaceLinkQuality {
118                    face_id: FaceId(1),
119                    rssi_dbm: Some(-50),
120                    retransmit_rate: None,
121                    observed_rtt_ms: None,
122                    observed_tput: None
123                },
124                FaceLinkQuality {
125                    face_id: FaceId(2),
126                    rssi_dbm: Some(-70),
127                    retransmit_rate: None,
128                    observed_rtt_ms: None,
129                    observed_tput: None
130                },
131                FaceLinkQuality {
132                    face_id: FaceId(3),
133                    rssi_dbm: None,
134                    retransmit_rate: None,
135                    observed_rtt_ms: None,
136                    observed_tput: None
137                },
138            ],
139        });
140        let ctx = make_ctx_with_snapshot(&name, &m, &ext);
141
142        let filter = RssiFilter::new(-60);
143        let actions = smallvec![ForwardingAction::Forward(smallvec![
144            FaceId(1),
145            FaceId(2),
146            FaceId(3)
147        ])];
148        let result = filter.filter(&ctx, actions);
149        assert_eq!(result.len(), 1);
150        match &result[0] {
151            ForwardingAction::Forward(faces) => {
152                // FaceId(1) passes (-50 >= -60), FaceId(2) fails (-70 < -60), FaceId(3) passes (no RSSI = pass)
153                assert_eq!(faces.as_slice(), &[FaceId(1), FaceId(3)]);
154            }
155            _ => panic!("expected Forward"),
156        }
157    }
158
159    #[test]
160    fn all_filtered_drops_forward_action() {
161        let name = Arc::new(Name::root());
162        let m = MeasurementsTable::new();
163        let mut ext = AnyMap::new();
164        ext.insert(LinkQualitySnapshot {
165            per_face: smallvec![FaceLinkQuality {
166                face_id: FaceId(1),
167                rssi_dbm: Some(-80),
168                retransmit_rate: None,
169                observed_rtt_ms: None,
170                observed_tput: None
171            },],
172        });
173        let ctx = make_ctx_with_snapshot(&name, &m, &ext);
174
175        let filter = RssiFilter::new(-60);
176        let actions = smallvec![
177            ForwardingAction::Forward(smallvec![FaceId(1)]),
178            ForwardingAction::Nack(ndn_transport::NackReason::NoRoute),
179        ];
180        let result = filter.filter(&ctx, actions);
181        // Forward removed, Nack passes through
182        assert_eq!(result.len(), 1);
183        assert!(matches!(result[0], ForwardingAction::Nack(_)));
184    }
185}