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 wlan_common::random_fidl_bss_description;
95 use wlan_common::scan::Incompatible;
96
97 struct TestValues {
98 inspector: inspect::Inspector,
99 inspect_node: Arc<Mutex<InspectBoundedListNode>>,
100 telemetry_sender: TelemetrySender,
101 telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
102 }
103
104 fn test_setup() -> TestValues {
105 let inspector = inspect::Inspector::default();
106 let inspect_node =
107 InspectBoundedListNode::new(inspector.root().create_child("bss_select_test"), 10);
108 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
109
110 TestValues {
111 inspector,
112 inspect_node: Arc::new(Mutex::new(inspect_node)),
113 telemetry_sender: TelemetrySender::new(telemetry_sender),
114 telemetry_receiver,
115 }
116 }
117
118 fn generate_candidate_for_scoring(
119 rssi: i8,
120 snr_db: i8,
121 channel: types::WlanChan,
122 ) -> types::ScannedCandidate {
123 let bss = types::Bss {
124 signal: types::Signal { rssi_dbm: rssi, snr_db },
125 channel,
126 bss_description: fidl_fuchsia_wlan_ieee80211::BssDescription {
127 rssi_dbm: rssi,
128 snr_db,
129 primary: channel.into(),
130 ..random_fidl_bss_description!()
131 }
132 .into(),
133 ..generate_random_bss_with_compatibility()
134 };
135 types::ScannedCandidate { bss, ..generate_random_scanned_candidate() }
136 }
137
138 fn connect_failure_with_bssid(bssid: types::Bssid) -> ConnectFailure {
139 ConnectFailure {
140 reason: FailureReason::GeneralFailure,
141 time: fasync::MonotonicInstant::INFINITE,
142 bssid,
143 }
144 }
145
146 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
147 #[fuchsia::test]
148 fn select_bss_sorts_by_score() {
149 let mut exec = fasync::TestExecutor::new();
150 let test_values = test_setup();
151 let mut candidates = vec![];
152
153 candidates.push(generate_candidate_for_scoring(
154 -35,
155 30,
156 generate_channel(36, fidl_fuchsia_wlan_ieee80211::WlanBand::FiveGhz),
157 ));
158 candidates.push(generate_candidate_for_scoring(
159 -30,
160 30,
161 generate_channel(1, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz),
162 ));
163
164 let reason = generate_random_connect_reason();
166 assert_eq!(
167 exec.run_singlethreaded(select_bss(
168 candidates.clone(),
169 reason,
170 test_values.inspect_node.clone(),
171 test_values.telemetry_sender.clone()
172 )),
173 Some(candidates[0].clone())
174 );
175
176 let mut modified_network = candidates[0].clone();
178 let modified_bss = types::Bss {
179 channel: generate_channel(6, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz),
180 ..modified_network.bss.clone()
181 };
182 modified_network.bss = modified_bss;
183 candidates[0] = modified_network;
184
185 assert_eq!(
187 exec.run_singlethreaded(select_bss(
188 candidates.clone(),
189 reason,
190 test_values.inspect_node.clone(),
191 test_values.telemetry_sender.clone()
192 )),
193 Some(candidates[1].clone())
194 );
195 }
196
197 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
198 #[fuchsia::test]
199 fn select_bss_sorts_by_failure_count() {
200 let mut exec = fasync::TestExecutor::new();
201 let test_values = test_setup();
202 let mut candidates = vec![];
203
204 candidates.push(generate_candidate_for_scoring(
205 -30,
206 30,
207 generate_channel(1, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz),
208 ));
209 candidates.push(generate_candidate_for_scoring(
210 -35,
211 30,
212 generate_channel(1, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz),
213 ));
214
215 assert_eq!(
217 exec.run_singlethreaded(select_bss(
218 candidates.clone(),
219 generate_random_connect_reason(),
220 test_values.inspect_node.clone(),
221 test_values.telemetry_sender.clone()
222 )),
223 Some(candidates[0].clone()),
224 );
225
226 let num_failures = 4;
228 candidates[0].saved_network_info.recent_failures =
229 vec![connect_failure_with_bssid(candidates[0].bss.bssid); num_failures];
230
231 assert_eq!(
233 exec.run_singlethreaded(select_bss(
234 candidates.clone(),
235 generate_random_connect_reason(),
236 test_values.inspect_node.clone(),
237 test_values.telemetry_sender.clone()
238 )),
239 Some(candidates[1].clone())
240 );
241
242 candidates[1].saved_network_info.recent_failures =
244 vec![connect_failure_with_bssid(candidates[1].bss.bssid); num_failures];
245
246 assert_eq!(
248 exec.run_singlethreaded(select_bss(
249 candidates.clone(),
250 generate_random_connect_reason(),
251 test_values.inspect_node.clone(),
252 test_values.telemetry_sender.clone()
253 )),
254 Some(candidates[0].clone())
255 );
256 }
257
258 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
259 #[fuchsia::test]
260 fn select_bss_ignore_incompatible() {
261 let mut exec = fasync::TestExecutor::new();
262 let test_values = test_setup();
263 let mut candidates = vec![];
264
265 candidates.push(generate_candidate_for_scoring(
267 -14,
268 30,
269 generate_channel(1, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz),
270 ));
271 candidates.push(generate_candidate_for_scoring(
272 -90,
273 30,
274 generate_channel(1, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz),
275 ));
276
277 assert_eq!(
279 exec.run_singlethreaded(select_bss(
280 candidates.clone(),
281 generate_random_connect_reason(),
282 test_values.inspect_node.clone(),
283 test_values.telemetry_sender.clone()
284 )),
285 Some(candidates[0].clone())
286 );
287
288 candidates[0].bss.compatibility = Incompatible::unknown();
290
291 assert_eq!(
293 exec.run_singlethreaded(select_bss(
294 candidates.clone(),
295 generate_random_connect_reason(),
296 test_values.inspect_node.clone(),
297 test_values.telemetry_sender.clone()
298 )),
299 Some(candidates[1].clone())
300 );
301
302 candidates[1].bss.compatibility = Incompatible::unknown();
306
307 assert_eq!(
309 exec.run_singlethreaded(select_bss(
310 candidates.clone(),
311 generate_random_connect_reason(),
312 test_values.inspect_node.clone(),
313 test_values.telemetry_sender.clone()
314 )),
315 None
316 );
317 }
318
319 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
320 #[fuchsia::test]
321 fn select_bss_logs_to_inspect() {
322 let mut exec = fasync::TestExecutor::new();
323 let test_values = test_setup();
324 let mut candidates = vec![];
325
326 candidates.push(generate_candidate_for_scoring(
327 -50,
328 30,
329 generate_channel(1, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz),
330 ));
331 candidates.push(generate_candidate_for_scoring(
332 -60,
333 30,
334 generate_channel(3, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz),
335 ));
336 candidates.push(generate_candidate_for_scoring(
337 -30,
338 30,
339 generate_channel(6, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz),
340 ));
341
342 assert_eq!(
344 exec.run_singlethreaded(select_bss(
345 candidates.clone(),
346 generate_random_connect_reason(),
347 test_values.inspect_node.clone(),
348 test_values.telemetry_sender.clone()
349 )),
350 Some(candidates[2].clone())
351 );
352
353 assert_data_tree!(@executor exec, test_values.inspector, root: {
354 bss_select_test: {
355 "0": {
356 "@time": AnyNumericProperty,
357 "candidates": {
358 "0": contains {
359 score: AnyNumericProperty,
360 bssid: &*BSSID_REGEX,
361 ssid: &*SSID_REGEX,
362 rssi: AnyNumericProperty,
363 security_type_saved: AnyStringProperty,
364 security_type_scanned: AnyStringProperty,
365 channel: AnyStringProperty,
366 compatible: AnyBoolProperty,
367 incompatibility: AnyStringProperty,
368 recent_failure_count: AnyNumericProperty,
369 saved_network_has_ever_connected: AnyBoolProperty,
370 },
371 "1": contains {
372 score: AnyProperty,
373 },
374 "2": contains {
375 score: AnyProperty,
376 },
377 },
378 "selected": {
379 ssid: candidates[2].network.ssid.to_string(),
380 bssid: candidates[2].bss.bssid.to_string(),
381 rssi: i64::from(candidates[2].bss.signal.rssi_dbm),
382 score: i64::from(scoring_functions::score_bss_scanned_candidate(candidates[2].clone())),
383 security_type_saved: candidates[2].saved_security_type_to_string(),
384 security_type_scanned: format!("{}", wlan_common::bss::Protection::from(candidates[2].security_type_detailed)),
385 channel: AnyStringProperty,
386 compatible: candidates[2].bss.is_compatible(),
387 incompatibility: AnyStringProperty,
388 recent_failure_count: candidates[2].recent_failure_count(),
389 saved_network_has_ever_connected: candidates[2].saved_network_info.has_ever_connected,
390 },
391 }
392 },
393 });
394 }
395
396 #[fuchsia::test]
397 fn select_bss_empty_list_logs_to_inspect() {
398 let mut exec = fasync::TestExecutor::new();
399 let test_values = test_setup();
400 assert_eq!(
401 exec.run_singlethreaded(select_bss(
402 vec![],
403 generate_random_connect_reason(),
404 test_values.inspect_node.clone(),
405 test_values.telemetry_sender.clone()
406 )),
407 None
408 );
409
410 assert_data_tree!(@executor exec, test_values.inspector, root: {
412 bss_select_test: {
413 "0": {
414 "@time": AnyProperty,
415 "candidates": {},
416 }
417 },
418 });
419 }
420
421 #[fuchsia::test]
422 fn select_bss_logs_cobalt_metrics() {
423 let mut exec = fasync::TestExecutor::new();
424 let mut test_values = test_setup();
425
426 let reason_code = generate_random_connect_reason();
427 let candidates =
428 vec![generate_random_scanned_candidate(), generate_random_scanned_candidate()];
429 assert!(
430 exec.run_singlethreaded(select_bss(
431 candidates.clone(),
432 reason_code,
433 test_values.inspect_node.clone(),
434 test_values.telemetry_sender.clone()
435 ))
436 .is_some()
437 );
438
439 assert_matches!(test_values.telemetry_receiver.try_recv(), Ok(event) => {
440 assert_matches!(event, TelemetryEvent::BssSelectionResult {
441 reason,
442 scored_candidates,
443 selected_candidate: _,
444 } => {
445 assert_eq!(reason, reason_code);
446 let mut prior_score = i16::MAX;
447 for (candidate, score) in scored_candidates {
448 assert!(candidates.contains(&candidate));
449 assert!(prior_score >= score);
450 prior_score = score;
451 }
452 })
453 });
454 }
455}