1use crate::client::connection_selection::scoring_functions;
6use crate::client::types;
7use crate::telemetry::{TelemetryEvent, TelemetrySender};
8use fuchsia_inspect_auto_persist::AutoPersist;
9use fuchsia_inspect_contrib::inspect_log;
10use fuchsia_inspect_contrib::log::InspectList;
11use fuchsia_inspect_contrib::nodes::BoundedListNode as InspectBoundedListNode;
12use futures::lock::Mutex;
13use log::{error, info};
14use std::cmp::Reverse;
15use std::sync::Arc;
16
17pub async fn select_bss(
20 allowed_candidate_list: Vec<types::ScannedCandidate>,
21 reason: types::ConnectReason,
22 inspect_node: Arc<Mutex<AutoPersist<InspectBoundedListNode>>>,
23 telemetry_sender: TelemetrySender,
24) -> Option<types::ScannedCandidate> {
25 if allowed_candidate_list.is_empty() {
26 info!("No BSSs available to select from.");
27 } else {
28 info!("Selecting from {} BSSs found for allowed networks", allowed_candidate_list.len());
29 }
30
31 let mut inspect_node = inspect_node.lock().await;
32
33 let mut scored_candidates = allowed_candidate_list
34 .iter()
35 .inspect(|&candidate| {
36 info!("{}", candidate.to_string_without_pii());
37 })
38 .filter(|&candidate| {
39 if !candidate.bss.is_compatible() {
43 error!("BSS is unexpectedly incompatible: {}", candidate.to_string_without_pii());
44 false
45 } else {
46 true
47 }
48 })
49 .map(|candidate| {
50 (candidate.clone(), scoring_functions::score_bss_scanned_candidate(candidate.clone()))
51 })
52 .collect::<Vec<(types::ScannedCandidate, i16)>>();
53
54 scored_candidates.sort_by_key(|(_, score)| Reverse(*score));
55 let selected_candidate = scored_candidates.first();
56
57 inspect_log!(
59 inspect_node.get_mut(),
60 candidates: InspectList(&allowed_candidate_list),
61 selected?: selected_candidate.map(|(candidate, _)| candidate)
62 );
63
64 telemetry_sender.send(TelemetryEvent::BssSelectionResult {
65 reason,
66 scored_candidates: scored_candidates.clone(),
67 selected_candidate: selected_candidate.cloned(),
68 });
69
70 if let Some((candidate, _)) = selected_candidate {
71 info!("Selected BSS:");
72 info!("{}", candidate.to_string_without_pii());
73 Some(candidate.clone())
74 } else {
75 None
76 }
77}
78
79#[cfg(test)]
80mod test {
81 use super::*;
82 use crate::config_management::{ConnectFailure, FailureReason};
83 use crate::util::testing::{
84 create_inspect_persistence_channel, generate_channel,
85 generate_random_bss_with_compatibility, generate_random_connect_reason,
86 generate_random_scanned_candidate,
87 };
88 use assert_matches::assert_matches;
89 use diagnostics_assertions::{
90 AnyBoolProperty, AnyNumericProperty, AnyProperty, AnyStringProperty, assert_data_tree,
91 };
92 use futures::channel::mpsc;
93 use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
94 use rand::Rng;
95 use wlan_common::random_fidl_bss_description;
96 use wlan_common::scan::Incompatible;
97 use {
98 fidl_fuchsia_wlan_common as fidl_common, fuchsia_async as fasync,
99 fuchsia_inspect as inspect,
100 };
101
102 struct TestValues {
103 inspector: inspect::Inspector,
104 inspect_node: Arc<Mutex<AutoPersist<InspectBoundedListNode>>>,
105 telemetry_sender: TelemetrySender,
106 telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
107 }
108
109 fn test_setup() -> TestValues {
110 let inspector = inspect::Inspector::default();
111 let inspect_node =
112 InspectBoundedListNode::new(inspector.root().create_child("bss_select_test"), 10);
113 let (persistence_req_sender, _persistence_stream) = create_inspect_persistence_channel();
114 let inspect_node = AutoPersist::new(inspect_node, "test", persistence_req_sender);
115 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
116
117 TestValues {
118 inspector,
119 inspect_node: Arc::new(Mutex::new(inspect_node)),
120 telemetry_sender: TelemetrySender::new(telemetry_sender),
121 telemetry_receiver,
122 }
123 }
124
125 fn generate_candidate_for_scoring(
126 rssi: i8,
127 snr_db: i8,
128 channel: types::WlanChan,
129 ) -> types::ScannedCandidate {
130 let bss = types::Bss {
131 signal: types::Signal { rssi_dbm: rssi, snr_db },
132 channel,
133 bss_description: fidl_common::BssDescription {
134 rssi_dbm: rssi,
135 snr_db,
136 channel: channel.into(),
137 ..random_fidl_bss_description!()
138 }
139 .into(),
140 ..generate_random_bss_with_compatibility()
141 };
142 types::ScannedCandidate { bss, ..generate_random_scanned_candidate() }
143 }
144
145 fn connect_failure_with_bssid(bssid: types::Bssid) -> ConnectFailure {
146 ConnectFailure {
147 reason: FailureReason::GeneralFailure,
148 time: fasync::MonotonicInstant::INFINITE,
149 bssid,
150 }
151 }
152
153 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
154 #[fuchsia::test]
155 fn select_bss_sorts_by_score() {
156 let mut exec = fasync::TestExecutor::new();
157 let test_values = test_setup();
158 let mut candidates = vec![];
159
160 candidates.push(generate_candidate_for_scoring(-35, 30, generate_channel(36)));
161 candidates.push(generate_candidate_for_scoring(-30, 30, generate_channel(1)));
162
163 let reason = generate_random_connect_reason();
165 assert_eq!(
166 exec.run_singlethreaded(select_bss(
167 candidates.clone(),
168 reason,
169 test_values.inspect_node.clone(),
170 test_values.telemetry_sender.clone()
171 )),
172 Some(candidates[0].clone())
173 );
174
175 let mut modified_network = candidates[0].clone();
177 let modified_bss =
178 types::Bss { channel: generate_channel(6), ..modified_network.bss.clone() };
179 modified_network.bss = modified_bss;
180 candidates[0] = modified_network;
181
182 assert_eq!(
184 exec.run_singlethreaded(select_bss(
185 candidates.clone(),
186 reason,
187 test_values.inspect_node.clone(),
188 test_values.telemetry_sender.clone()
189 )),
190 Some(candidates[1].clone())
191 );
192 }
193
194 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
195 #[fuchsia::test]
196 fn select_bss_sorts_by_failure_count() {
197 let mut exec = fasync::TestExecutor::new();
198 let test_values = test_setup();
199 let mut candidates = vec![];
200
201 candidates.push(generate_candidate_for_scoring(-30, 30, generate_channel(1)));
202 candidates.push(generate_candidate_for_scoring(-35, 30, generate_channel(1)));
203
204 assert_eq!(
206 exec.run_singlethreaded(select_bss(
207 candidates.clone(),
208 generate_random_connect_reason(),
209 test_values.inspect_node.clone(),
210 test_values.telemetry_sender.clone()
211 )),
212 Some(candidates[0].clone()),
213 );
214
215 let num_failures = 4;
217 candidates[0].saved_network_info.recent_failures =
218 vec![connect_failure_with_bssid(candidates[0].bss.bssid); num_failures];
219
220 assert_eq!(
222 exec.run_singlethreaded(select_bss(
223 candidates.clone(),
224 generate_random_connect_reason(),
225 test_values.inspect_node.clone(),
226 test_values.telemetry_sender.clone()
227 )),
228 Some(candidates[1].clone())
229 );
230
231 candidates[1].saved_network_info.recent_failures =
233 vec![connect_failure_with_bssid(candidates[1].bss.bssid); num_failures];
234
235 assert_eq!(
237 exec.run_singlethreaded(select_bss(
238 candidates.clone(),
239 generate_random_connect_reason(),
240 test_values.inspect_node.clone(),
241 test_values.telemetry_sender.clone()
242 )),
243 Some(candidates[0].clone())
244 );
245 }
246
247 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
248 #[fuchsia::test]
249 fn select_bss_ignore_incompatible() {
250 let mut exec = fasync::TestExecutor::new();
251 let test_values = test_setup();
252 let mut candidates = vec![];
253
254 candidates.push(generate_candidate_for_scoring(-14, 30, generate_channel(1)));
256 candidates.push(generate_candidate_for_scoring(-90, 30, generate_channel(1)));
257
258 assert_eq!(
260 exec.run_singlethreaded(select_bss(
261 candidates.clone(),
262 generate_random_connect_reason(),
263 test_values.inspect_node.clone(),
264 test_values.telemetry_sender.clone()
265 )),
266 Some(candidates[0].clone())
267 );
268
269 candidates[0].bss.compatibility = Incompatible::unknown();
271
272 assert_eq!(
274 exec.run_singlethreaded(select_bss(
275 candidates.clone(),
276 generate_random_connect_reason(),
277 test_values.inspect_node.clone(),
278 test_values.telemetry_sender.clone()
279 )),
280 Some(candidates[1].clone())
281 );
282
283 candidates[1].bss.compatibility = Incompatible::unknown();
287
288 assert_eq!(
290 exec.run_singlethreaded(select_bss(
291 candidates.clone(),
292 generate_random_connect_reason(),
293 test_values.inspect_node.clone(),
294 test_values.telemetry_sender.clone()
295 )),
296 None
297 );
298 }
299
300 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
301 #[fuchsia::test]
302 fn select_bss_logs_to_inspect() {
303 let mut exec = fasync::TestExecutor::new();
304 let test_values = test_setup();
305 let mut candidates = vec![];
306
307 candidates.push(generate_candidate_for_scoring(-50, 30, generate_channel(1)));
308 candidates.push(generate_candidate_for_scoring(-60, 30, generate_channel(3)));
309 candidates.push(generate_candidate_for_scoring(-30, 30, generate_channel(6)));
310
311 assert_eq!(
313 exec.run_singlethreaded(select_bss(
314 candidates.clone(),
315 generate_random_connect_reason(),
316 test_values.inspect_node.clone(),
317 test_values.telemetry_sender.clone()
318 )),
319 Some(candidates[2].clone())
320 );
321
322 assert_data_tree!(@executor exec, test_values.inspector, root: {
323 bss_select_test: {
324 "0": {
325 "@time": AnyNumericProperty,
326 "candidates": {
327 "0": contains {
328 score: AnyNumericProperty,
329 bssid: &*BSSID_REGEX,
330 ssid: &*SSID_REGEX,
331 rssi: AnyNumericProperty,
332 security_type_saved: AnyStringProperty,
333 security_type_scanned: AnyStringProperty,
334 channel: AnyStringProperty,
335 compatible: AnyBoolProperty,
336 incompatibility: AnyStringProperty,
337 recent_failure_count: AnyNumericProperty,
338 saved_network_has_ever_connected: AnyBoolProperty,
339 },
340 "1": contains {
341 score: AnyProperty,
342 },
343 "2": contains {
344 score: AnyProperty,
345 },
346 },
347 "selected": {
348 ssid: candidates[2].network.ssid.to_string(),
349 bssid: candidates[2].bss.bssid.to_string(),
350 rssi: i64::from(candidates[2].bss.signal.rssi_dbm),
351 score: i64::from(scoring_functions::score_bss_scanned_candidate(candidates[2].clone())),
352 security_type_saved: candidates[2].saved_security_type_to_string(),
353 security_type_scanned: format!("{}", wlan_common::bss::Protection::from(candidates[2].security_type_detailed)),
354 channel: AnyStringProperty,
355 compatible: candidates[2].bss.is_compatible(),
356 incompatibility: AnyStringProperty,
357 recent_failure_count: candidates[2].recent_failure_count(),
358 saved_network_has_ever_connected: candidates[2].saved_network_info.has_ever_connected,
359 },
360 }
361 },
362 });
363 }
364
365 #[fuchsia::test]
366 fn select_bss_empty_list_logs_to_inspect() {
367 let mut exec = fasync::TestExecutor::new();
368 let test_values = test_setup();
369 assert_eq!(
370 exec.run_singlethreaded(select_bss(
371 vec![],
372 generate_random_connect_reason(),
373 test_values.inspect_node.clone(),
374 test_values.telemetry_sender.clone()
375 )),
376 None
377 );
378
379 assert_data_tree!(@executor exec, test_values.inspector, root: {
381 bss_select_test: {
382 "0": {
383 "@time": AnyProperty,
384 "candidates": {},
385 }
386 },
387 });
388 }
389
390 #[fuchsia::test]
391 fn select_bss_logs_cobalt_metrics() {
392 let mut exec = fasync::TestExecutor::new();
393 let mut test_values = test_setup();
394
395 let reason_code = generate_random_connect_reason();
396 let candidates =
397 vec![generate_random_scanned_candidate(), generate_random_scanned_candidate()];
398 assert!(
399 exec.run_singlethreaded(select_bss(
400 candidates.clone(),
401 reason_code,
402 test_values.inspect_node.clone(),
403 test_values.telemetry_sender.clone()
404 ))
405 .is_some()
406 );
407
408 assert_matches!(test_values.telemetry_receiver.try_next(), Ok(Some(event)) => {
409 assert_matches!(event, TelemetryEvent::BssSelectionResult {
410 reason,
411 scored_candidates,
412 selected_candidate: _,
413 } => {
414 assert_eq!(reason, reason_code);
415 let mut prior_score = i16::MAX;
416 for (candidate, score) in scored_candidates {
417 assert!(candidates.contains(&candidate));
418 assert!(prior_score >= score);
419 prior_score = score;
420 }
421 })
422 });
423 }
424}