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