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