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 channel: 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(-35, 30, generate_channel(36)));
155 candidates.push(generate_candidate_for_scoring(-30, 30, generate_channel(1)));
156
157 let reason = generate_random_connect_reason();
159 assert_eq!(
160 exec.run_singlethreaded(select_bss(
161 candidates.clone(),
162 reason,
163 test_values.inspect_node.clone(),
164 test_values.telemetry_sender.clone()
165 )),
166 Some(candidates[0].clone())
167 );
168
169 let mut modified_network = candidates[0].clone();
171 let modified_bss =
172 types::Bss { channel: generate_channel(6), ..modified_network.bss.clone() };
173 modified_network.bss = modified_bss;
174 candidates[0] = modified_network;
175
176 assert_eq!(
178 exec.run_singlethreaded(select_bss(
179 candidates.clone(),
180 reason,
181 test_values.inspect_node.clone(),
182 test_values.telemetry_sender.clone()
183 )),
184 Some(candidates[1].clone())
185 );
186 }
187
188 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
189 #[fuchsia::test]
190 fn select_bss_sorts_by_failure_count() {
191 let mut exec = fasync::TestExecutor::new();
192 let test_values = test_setup();
193 let mut candidates = vec![];
194
195 candidates.push(generate_candidate_for_scoring(-30, 30, generate_channel(1)));
196 candidates.push(generate_candidate_for_scoring(-35, 30, generate_channel(1)));
197
198 assert_eq!(
200 exec.run_singlethreaded(select_bss(
201 candidates.clone(),
202 generate_random_connect_reason(),
203 test_values.inspect_node.clone(),
204 test_values.telemetry_sender.clone()
205 )),
206 Some(candidates[0].clone()),
207 );
208
209 let num_failures = 4;
211 candidates[0].saved_network_info.recent_failures =
212 vec![connect_failure_with_bssid(candidates[0].bss.bssid); num_failures];
213
214 assert_eq!(
216 exec.run_singlethreaded(select_bss(
217 candidates.clone(),
218 generate_random_connect_reason(),
219 test_values.inspect_node.clone(),
220 test_values.telemetry_sender.clone()
221 )),
222 Some(candidates[1].clone())
223 );
224
225 candidates[1].saved_network_info.recent_failures =
227 vec![connect_failure_with_bssid(candidates[1].bss.bssid); num_failures];
228
229 assert_eq!(
231 exec.run_singlethreaded(select_bss(
232 candidates.clone(),
233 generate_random_connect_reason(),
234 test_values.inspect_node.clone(),
235 test_values.telemetry_sender.clone()
236 )),
237 Some(candidates[0].clone())
238 );
239 }
240
241 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
242 #[fuchsia::test]
243 fn select_bss_ignore_incompatible() {
244 let mut exec = fasync::TestExecutor::new();
245 let test_values = test_setup();
246 let mut candidates = vec![];
247
248 candidates.push(generate_candidate_for_scoring(-14, 30, generate_channel(1)));
250 candidates.push(generate_candidate_for_scoring(-90, 30, generate_channel(1)));
251
252 assert_eq!(
254 exec.run_singlethreaded(select_bss(
255 candidates.clone(),
256 generate_random_connect_reason(),
257 test_values.inspect_node.clone(),
258 test_values.telemetry_sender.clone()
259 )),
260 Some(candidates[0].clone())
261 );
262
263 candidates[0].bss.compatibility = Incompatible::unknown();
265
266 assert_eq!(
268 exec.run_singlethreaded(select_bss(
269 candidates.clone(),
270 generate_random_connect_reason(),
271 test_values.inspect_node.clone(),
272 test_values.telemetry_sender.clone()
273 )),
274 Some(candidates[1].clone())
275 );
276
277 candidates[1].bss.compatibility = Incompatible::unknown();
281
282 assert_eq!(
284 exec.run_singlethreaded(select_bss(
285 candidates.clone(),
286 generate_random_connect_reason(),
287 test_values.inspect_node.clone(),
288 test_values.telemetry_sender.clone()
289 )),
290 None
291 );
292 }
293
294 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
295 #[fuchsia::test]
296 fn select_bss_logs_to_inspect() {
297 let mut exec = fasync::TestExecutor::new();
298 let test_values = test_setup();
299 let mut candidates = vec![];
300
301 candidates.push(generate_candidate_for_scoring(-50, 30, generate_channel(1)));
302 candidates.push(generate_candidate_for_scoring(-60, 30, generate_channel(3)));
303 candidates.push(generate_candidate_for_scoring(-30, 30, generate_channel(6)));
304
305 assert_eq!(
307 exec.run_singlethreaded(select_bss(
308 candidates.clone(),
309 generate_random_connect_reason(),
310 test_values.inspect_node.clone(),
311 test_values.telemetry_sender.clone()
312 )),
313 Some(candidates[2].clone())
314 );
315
316 assert_data_tree!(@executor exec, test_values.inspector, root: {
317 bss_select_test: {
318 "0": {
319 "@time": AnyNumericProperty,
320 "candidates": {
321 "0": contains {
322 score: AnyNumericProperty,
323 bssid: &*BSSID_REGEX,
324 ssid: &*SSID_REGEX,
325 rssi: AnyNumericProperty,
326 security_type_saved: AnyStringProperty,
327 security_type_scanned: AnyStringProperty,
328 channel: AnyStringProperty,
329 compatible: AnyBoolProperty,
330 incompatibility: AnyStringProperty,
331 recent_failure_count: AnyNumericProperty,
332 saved_network_has_ever_connected: AnyBoolProperty,
333 },
334 "1": contains {
335 score: AnyProperty,
336 },
337 "2": contains {
338 score: AnyProperty,
339 },
340 },
341 "selected": {
342 ssid: candidates[2].network.ssid.to_string(),
343 bssid: candidates[2].bss.bssid.to_string(),
344 rssi: i64::from(candidates[2].bss.signal.rssi_dbm),
345 score: i64::from(scoring_functions::score_bss_scanned_candidate(candidates[2].clone())),
346 security_type_saved: candidates[2].saved_security_type_to_string(),
347 security_type_scanned: format!("{}", wlan_common::bss::Protection::from(candidates[2].security_type_detailed)),
348 channel: AnyStringProperty,
349 compatible: candidates[2].bss.is_compatible(),
350 incompatibility: AnyStringProperty,
351 recent_failure_count: candidates[2].recent_failure_count(),
352 saved_network_has_ever_connected: candidates[2].saved_network_info.has_ever_connected,
353 },
354 }
355 },
356 });
357 }
358
359 #[fuchsia::test]
360 fn select_bss_empty_list_logs_to_inspect() {
361 let mut exec = fasync::TestExecutor::new();
362 let test_values = test_setup();
363 assert_eq!(
364 exec.run_singlethreaded(select_bss(
365 vec![],
366 generate_random_connect_reason(),
367 test_values.inspect_node.clone(),
368 test_values.telemetry_sender.clone()
369 )),
370 None
371 );
372
373 assert_data_tree!(@executor exec, test_values.inspector, root: {
375 bss_select_test: {
376 "0": {
377 "@time": AnyProperty,
378 "candidates": {},
379 }
380 },
381 });
382 }
383
384 #[fuchsia::test]
385 fn select_bss_logs_cobalt_metrics() {
386 let mut exec = fasync::TestExecutor::new();
387 let mut test_values = test_setup();
388
389 let reason_code = generate_random_connect_reason();
390 let candidates =
391 vec![generate_random_scanned_candidate(), generate_random_scanned_candidate()];
392 assert!(
393 exec.run_singlethreaded(select_bss(
394 candidates.clone(),
395 reason_code,
396 test_values.inspect_node.clone(),
397 test_values.telemetry_sender.clone()
398 ))
399 .is_some()
400 );
401
402 assert_matches!(test_values.telemetry_receiver.try_next(), Ok(Some(event)) => {
403 assert_matches!(event, TelemetryEvent::BssSelectionResult {
404 reason,
405 scored_candidates,
406 selected_candidate: _,
407 } => {
408 assert_eq!(reason, reason_code);
409 let mut prior_score = i16::MAX;
410 for (candidate, score) in scored_candidates {
411 assert!(candidates.contains(&candidate));
412 assert!(prior_score >= score);
413 prior_score = score;
414 }
415 })
416 });
417 }
418}