1use crate::client::config_management::Credential;
6use crate::client::roaming::lib::*;
7use crate::client::roaming::roam_monitor::RoamMonitorApi;
8use crate::client::types;
9use crate::config_management::SavedNetworksManagerApi;
10use crate::telemetry::{TelemetryEvent, TelemetrySender};
11use crate::util::historical_list::Timestamped;
12use crate::util::pseudo_energy::EwmaSignalData;
13use async_trait::async_trait;
14use futures::lock::Mutex;
15use log::{error, info};
16use std::sync::Arc;
17use {
18 fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_internal as fidl_internal,
19 fuchsia_async as fasync,
20};
21
22pub const MIN_BACKOFF_BETWEEN_ROAM_SCANS: zx::MonotonicDuration =
23 zx::MonotonicDuration::from_minutes(1);
24pub const MAX_BACKOFF_BETWEEN_ROAM_SCANS: zx::MonotonicDuration =
25 zx::MonotonicDuration::from_minutes(32);
26
27const LOCAL_ROAM_THRESHOLD_RSSI_2G: f64 = -72.0;
28const LOCAL_ROAM_THRESHOLD_RSSI_5G: f64 = -75.0;
29
30const MIN_RSSI_IMPROVEMENT_TO_ROAM: f64 = 3.0;
31const MIN_RSSI_DROP_TO_RESET_BACKOFF: f64 = 3.0;
32
33pub const STATIONARY_ROAMING_EWMA_SMOOTHING_FACTOR: usize = 10;
36
37pub const NUM_MAX_ROAMS_PER_DAY: usize = 5;
39
40pub struct StationaryMonitor {
41 pub connection_data: RoamingConnectionData,
42 pub telemetry_sender: TelemetrySender,
43 saved_networks: Arc<dyn SavedNetworksManagerApi>,
44 scan_backoff: zx::MonotonicDuration,
45 past_roams: Arc<Mutex<PastRoamList>>,
47 next_roaming_enabled_time: fasync::MonotonicInstant,
48}
49
50impl StationaryMonitor {
51 pub fn new(
52 ap_state: types::ApState,
53 network_identifier: types::NetworkIdentifier,
54 credential: Credential,
55 telemetry_sender: TelemetrySender,
56 saved_networks: Arc<dyn SavedNetworksManagerApi>,
57 past_roams: Arc<Mutex<PastRoamList>>,
58 ) -> Self {
59 let connection_data = RoamingConnectionData::new(
60 ap_state.clone(),
61 network_identifier,
62 credential,
63 EwmaSignalData::new(
64 ap_state.tracked.signal.rssi_dbm,
65 ap_state.tracked.signal.snr_db,
66 STATIONARY_ROAMING_EWMA_SMOOTHING_FACTOR,
67 ),
68 );
69 Self {
70 connection_data,
71 telemetry_sender,
72 saved_networks,
73 scan_backoff: MIN_BACKOFF_BETWEEN_ROAM_SCANS,
74 past_roams,
75 next_roaming_enabled_time: fasync::MonotonicInstant::now(),
76 }
77 }
78
79 #[allow(clippy::needless_return, reason = "mass allow for https://fxbug.dev/381896734")]
82 async fn handle_signal_report(
83 &mut self,
84 stats: fidl_internal::SignalReportIndication,
85 ) -> Result<RoamTriggerDataOutcome, anyhow::Error> {
86 self.connection_data.signal_data.update_with_new_measurement(stats.rssi_dbm, stats.snr_db);
87
88 self.connection_data.rssi_velocity.update(self.connection_data.signal_data.ewma_rssi.get());
90
91 self.telemetry_sender.send(TelemetryEvent::OnSignalVelocityUpdate {
92 rssi_velocity: self.connection_data.rssi_velocity.get(),
93 });
94
95 match self
97 .saved_networks
98 .is_network_single_bss(
99 &self.connection_data.network_identifier,
100 &self.connection_data.credential,
101 )
102 .await
103 {
104 Ok(true) => return Ok(RoamTriggerDataOutcome::Noop),
105 _ => {
106 return Ok(self.should_roam_scan_after_signal_report());
109 }
110 }
111 }
112
113 fn should_roam_scan_after_signal_report(&mut self) -> RoamTriggerDataOutcome {
114 if fasync::MonotonicInstant::now() <= self.next_roaming_enabled_time {
116 return RoamTriggerDataOutcome::Noop;
117 }
118 if let Some(past_roams) = self.past_roams.try_lock() {
121 let recent_roams =
122 past_roams.get_recent(fasync::MonotonicInstant::now() - TIMESPAN_TO_LIMIT_SCANS);
123 if recent_roams.len() >= NUM_MAX_ROAMS_PER_DAY {
124 if let Some(oldest_roam_attempt) = recent_roams.first() {
125 self.next_roaming_enabled_time =
126 oldest_roam_attempt.time() + TIMESPAN_TO_LIMIT_SCANS;
127 info!("Maximum number of roam attempts per day ({}) has been reached. Roam scanning is disabled until a reboot, or until fewer than {} roam attempts have occured in the past 24 hours.", NUM_MAX_ROAMS_PER_DAY, NUM_MAX_ROAMS_PER_DAY);
128 } else {
129 error!("Unexpectedly failed to get the oldest roam attempt.");
130 }
131 return RoamTriggerDataOutcome::Noop;
132 }
133 } else {
134 error!("Unexpectedly failed to get lock on recent roam attempts data");
135 }
136 let mut roam_reasons: Vec<RoamReason> = vec![];
137
138 let rssi_threshold = if self.connection_data.ap_state.tracked.channel.is_5ghz() {
140 LOCAL_ROAM_THRESHOLD_RSSI_5G
141 } else {
142 LOCAL_ROAM_THRESHOLD_RSSI_2G
143 };
144 let rssi = self.connection_data.signal_data.ewma_rssi.get();
145 if rssi <= rssi_threshold {
146 roam_reasons.push(RoamReason::RssiBelowThreshold);
147
148 if rssi
150 <= self.connection_data.previous_roam_scan_data.rssi
151 - MIN_RSSI_DROP_TO_RESET_BACKOFF
152 {
153 self.scan_backoff = MIN_BACKOFF_BETWEEN_ROAM_SCANS;
154 }
155 }
156
157 let now = fasync::MonotonicInstant::now();
159 if roam_reasons.is_empty()
160 || now < self.connection_data.previous_roam_scan_data.time + self.scan_backoff
161 {
162 return RoamTriggerDataOutcome::Noop;
163 }
164
165 self.scan_backoff =
167 std::cmp::min(self.scan_backoff * 2_i64, MAX_BACKOFF_BETWEEN_ROAM_SCANS);
168
169 self.connection_data.previous_roam_scan_data.time = fasync::MonotonicInstant::now();
171 self.connection_data.previous_roam_scan_data.rssi = rssi;
172 info!("Initiating roam search for roam reasons: {:?}", &roam_reasons);
173
174 RoamTriggerDataOutcome::RoamSearch {
175 scan_type: fidl_common::ScanType::Active,
178 network_identifier: self.connection_data.network_identifier.clone(),
179 credential: self.connection_data.credential.clone(),
180 current_security: self.connection_data.ap_state.original().protection().into(),
181 reasons: roam_reasons,
182 }
183 }
184}
185
186#[async_trait(?Send)]
187impl RoamMonitorApi for StationaryMonitor {
188 async fn handle_roam_trigger_data(
189 &mut self,
190 data: RoamTriggerData,
191 ) -> Result<RoamTriggerDataOutcome, anyhow::Error> {
192 match data {
193 RoamTriggerData::SignalReportInd(stats) => self.handle_signal_report(stats).await,
194 }
195 }
196
197 fn should_send_roam_request(&self, request: PolicyRoamRequest) -> Result<bool, anyhow::Error> {
198 if request.candidate.bss.bssid == self.connection_data.ap_state.original().bssid {
199 info!("Selected roam candidate is the currently connected candidate, ignoring");
200 return Ok(false);
201 }
202 let latest_rssi = self.connection_data.signal_data.ewma_rssi.get();
205 if (request.candidate.bss.signal.rssi_dbm as f64)
206 < latest_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM
207 {
208 info!(
209 "Selected roam candidate ({:?}) is not enough of an improvement. Ignoring.",
210 request.candidate.to_string_without_pii()
211 );
212 return Ok(false);
213 }
214 Ok(true)
215 }
216
217 fn notify_of_roam_attempt(&mut self) {
218 self.scan_backoff = MIN_BACKOFF_BETWEEN_ROAM_SCANS;
221 }
222}
223
224#[cfg(test)]
225mod test {
226 use super::*;
227 use crate::util::testing::{
228 generate_random_bss, generate_random_password, generate_random_roaming_connection_data,
229 generate_random_scanned_candidate, FakeSavedNetworksManager,
230 };
231 use assert_matches::assert_matches;
232 use fidl_fuchsia_wlan_internal as fidl_internal;
233 use futures::channel::mpsc;
234 use futures::task::Poll;
235 use test_case::test_case;
236
237 struct TestValues {
238 monitor: StationaryMonitor,
239 telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
240 saved_networks: Arc<FakeSavedNetworksManager>,
241 past_roams: Arc<Mutex<PastRoamList>>,
242 }
243
244 const TEST_OK_SNR: f64 = 40.0;
245
246 fn setup_test() -> TestValues {
247 let connection_data = generate_random_roaming_connection_data();
248 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
249 let telemetry_sender = TelemetrySender::new(telemetry_sender);
250 let saved_networks = Arc::new(FakeSavedNetworksManager::new());
251 let past_roams = Arc::new(Mutex::new(PastRoamList::new(NUM_MAX_ROAMS_PER_DAY)));
252 saved_networks.set_is_single_bss_response(false);
255 let monitor = StationaryMonitor {
256 connection_data,
257 telemetry_sender,
258 saved_networks: saved_networks.clone(),
259 scan_backoff: MIN_BACKOFF_BETWEEN_ROAM_SCANS,
260 past_roams: past_roams.clone(),
261 next_roaming_enabled_time: fasync::MonotonicInstant::now(),
262 };
263 TestValues { monitor, telemetry_receiver, saved_networks, past_roams }
264 }
265
266 fn setup_test_with_data(connection_data: RoamingConnectionData) -> TestValues {
267 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
268 let telemetry_sender = TelemetrySender::new(telemetry_sender);
269 let saved_networks = Arc::new(FakeSavedNetworksManager::new());
270 let past_roams = Arc::new(Mutex::new(PastRoamList::new(NUM_MAX_ROAMS_PER_DAY)));
271 let monitor = StationaryMonitor {
272 connection_data,
273 telemetry_sender,
274 saved_networks: saved_networks.clone(),
275 scan_backoff: MIN_BACKOFF_BETWEEN_ROAM_SCANS,
276 past_roams: past_roams.clone(),
277 next_roaming_enabled_time: fasync::MonotonicInstant::now(),
278 };
279 TestValues { monitor, telemetry_receiver, saved_networks, past_roams }
280 }
281
282 fn run_handle_roam_trigger_data(
285 exec: &mut fasync::TestExecutor,
286 monitor: &mut StationaryMonitor,
287 trigger_data: RoamTriggerData,
288 ) -> RoamTriggerDataOutcome {
289 return assert_matches!(exec.run_until_stalled(&mut monitor.handle_roam_trigger_data(trigger_data)), Poll::Ready(Ok(should_roam)) => {should_roam});
290 }
291
292 #[test_case(-80, true; "bad rssi")]
293 #[test_case(-40, false; "good rssi")]
294 #[fuchsia::test(add_test_attr = false)]
295 fn test_handle_signal_report_trigger_data(rssi: i8, should_roam_search: bool) {
296 let mut exec = fasync::TestExecutor::new_with_fake_time();
297 exec.set_fake_time(fasync::MonotonicInstant::now());
298
299 let connection_data = RoamingConnectionData {
301 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
302 ..generate_random_roaming_connection_data()
303 };
304 let mut test_values = setup_test_with_data(connection_data);
305
306 exec.set_fake_time(fasync::MonotonicInstant::after(fasync::MonotonicDuration::from_hours(
308 1,
309 )));
310
311 let trigger_data =
314 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
315 rssi_dbm: rssi,
316 snr_db: TEST_OK_SNR as i8,
317 });
318 let result =
319 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
320
321 if should_roam_search {
322 assert_matches!(result, RoamTriggerDataOutcome::RoamSearch { .. });
323 } else {
324 assert_matches!(result, RoamTriggerDataOutcome::Noop);
325 }
326 }
327
328 #[fuchsia::test]
329 fn test_stationary_monitor_uses_active_scans() {
330 let mut exec = fasync::TestExecutor::new_with_fake_time();
331 exec.set_fake_time(fasync::MonotonicInstant::now());
332
333 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
336 let connection_data = RoamingConnectionData {
337 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
338 ..generate_random_roaming_connection_data()
339 };
340 let mut test_values = setup_test_with_data(connection_data);
341
342 let trigger_data =
345 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
346 rssi_dbm: rssi as i8,
347 snr_db: TEST_OK_SNR as i8,
348 });
349
350 exec.set_fake_time(fasync::MonotonicInstant::after(fasync::MonotonicDuration::from_hours(
352 1,
353 )));
354
355 assert_matches!(
357 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
358 RoamTriggerDataOutcome::RoamSearch { scan_type: fidl_common::ScanType::Active, .. }
359 );
360 }
361
362 #[fuchsia::test]
363 fn test_minimum_time_between_roam_scans() {
364 let mut exec = fasync::TestExecutor::new_with_fake_time();
365 exec.set_fake_time(fasync::MonotonicInstant::now());
366
367 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
370 let connection_data = RoamingConnectionData {
371 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
372 ..generate_random_roaming_connection_data()
373 };
374 let mut test_values = setup_test_with_data(connection_data);
375 let trigger_data =
376 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
377 rssi_dbm: rssi as i8,
378 snr_db: TEST_OK_SNR as i8,
379 });
380
381 exec.set_fake_time(fasync::MonotonicInstant::after(
383 MIN_BACKOFF_BETWEEN_ROAM_SCANS - fasync::MonotonicDuration::from_seconds(1),
384 ));
385
386 assert_matches!(
389 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
390 RoamTriggerDataOutcome::Noop
391 );
392
393 exec.set_fake_time(fasync::MonotonicInstant::after(
395 fasync::MonotonicDuration::from_seconds(2),
396 ));
397
398 assert_matches!(
400 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
401 RoamTriggerDataOutcome::RoamSearch { .. }
402 );
403 }
404
405 #[fuchsia::test]
406 fn test_roam_scans_backoff_exponentially() {
407 let mut exec = fasync::TestExecutor::new_with_fake_time();
408 exec.set_fake_time(fasync::MonotonicInstant::now());
409
410 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
413 let connection_data = RoamingConnectionData {
414 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
415 ..generate_random_roaming_connection_data()
416 };
417 let trigger_data =
418 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
419 rssi_dbm: rssi as i8,
420 snr_db: TEST_OK_SNR as i8,
421 });
422 let mut test_values = setup_test_with_data(connection_data);
423
424 let mut expected_backoff = MIN_BACKOFF_BETWEEN_ROAM_SCANS;
426 while expected_backoff <= MAX_BACKOFF_BETWEEN_ROAM_SCANS {
427 exec.set_fake_time(fasync::MonotonicInstant::after(
429 expected_backoff - fasync::MonotonicDuration::from_seconds(1),
430 ));
431
432 assert_matches!(
435 run_handle_roam_trigger_data(
436 &mut exec,
437 &mut test_values.monitor,
438 trigger_data.clone()
439 ),
440 RoamTriggerDataOutcome::Noop
441 );
442
443 exec.set_fake_time(fasync::MonotonicInstant::after(
445 fasync::MonotonicDuration::from_seconds(2),
446 ));
447
448 assert_matches!(
450 run_handle_roam_trigger_data(
451 &mut exec,
452 &mut test_values.monitor,
453 trigger_data.clone()
454 ),
455 RoamTriggerDataOutcome::RoamSearch { .. }
456 );
457
458 expected_backoff = expected_backoff * 2;
460 }
461 exec.set_fake_time(fasync::MonotonicInstant::after(
464 MAX_BACKOFF_BETWEEN_ROAM_SCANS + fasync::MonotonicDuration::from_seconds(1),
465 ));
466 assert_matches!(
467 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
468 RoamTriggerDataOutcome::RoamSearch { .. }
469 );
470 }
471
472 #[fuchsia::test]
473 fn test_roam_attempt_resets_backoff() {
474 let mut exec = fasync::TestExecutor::new_with_fake_time();
475 exec.set_fake_time(fasync::MonotonicInstant::now());
476
477 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
480 let connection_data = RoamingConnectionData {
481 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
482 ..generate_random_roaming_connection_data()
483 };
484 let trigger_data =
485 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
486 rssi_dbm: rssi as i8,
487 snr_db: TEST_OK_SNR as i8,
488 });
489 let mut test_values = setup_test_with_data(connection_data);
490
491 exec.set_fake_time(
493 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
494 + zx::MonotonicDuration::from_seconds(1),
495 );
496
497 assert_matches!(
499 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
500 RoamTriggerDataOutcome::RoamSearch { .. }
501 );
502
503 exec.set_fake_time(
505 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
506 + zx::MonotonicDuration::from_seconds(1),
507 );
508
509 assert_matches!(
511 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
512 RoamTriggerDataOutcome::Noop
513 );
514
515 test_values.monitor.notify_of_roam_attempt();
517
518 assert_matches!(
521 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
522 RoamTriggerDataOutcome::RoamSearch { .. }
523 );
524 }
525
526 #[fuchsia::test]
527 fn test_rssi_drop_resets_backoff() {
528 let mut exec = fasync::TestExecutor::new_with_fake_time();
529 exec.set_fake_time(fasync::MonotonicInstant::now());
530
531 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
534 let connection_data = RoamingConnectionData {
535 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
536 ..generate_random_roaming_connection_data()
537 };
538 let trigger_data =
539 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
540 rssi_dbm: rssi as i8,
541 snr_db: TEST_OK_SNR as i8,
542 });
543 let mut test_values = setup_test_with_data(connection_data);
544
545 exec.set_fake_time(
547 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
548 + zx::MonotonicDuration::from_seconds(1),
549 );
550
551 assert_matches!(
553 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
554 RoamTriggerDataOutcome::RoamSearch { .. }
555 );
556
557 exec.set_fake_time(
559 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
560 + zx::MonotonicDuration::from_seconds(1),
561 );
562
563 assert_matches!(
565 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
566 RoamTriggerDataOutcome::Noop
567 );
568
569 let trigger_data =
572 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
573 rssi_dbm: (rssi - MIN_RSSI_DROP_TO_RESET_BACKOFF) as i8,
574 snr_db: TEST_OK_SNR as i8,
575 });
576 assert_matches!(
577 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
578 RoamTriggerDataOutcome::RoamSearch { .. }
579 );
580
581 exec.set_fake_time(
583 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
584 - zx::MonotonicDuration::from_seconds(1),
585 );
586 assert_matches!(
589 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
590 RoamTriggerDataOutcome::Noop
591 );
592 }
593
594 #[fuchsia::test]
595 fn test_should_send_roam_request() {
596 let _exec = fasync::TestExecutor::new();
597 let test_values = setup_test();
598
599 let current_rssi = test_values.monitor.connection_data.signal_data.ewma_rssi.get();
601
602 let candidate = types::ScannedCandidate {
604 bss: types::Bss {
605 signal: types::Signal {
606 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM - 1.0) as i8,
607 snr_db: TEST_OK_SNR as i8,
608 },
609 ..generate_random_bss()
610 },
611 ..generate_random_scanned_candidate()
612 };
613 assert!(!test_values
614 .monitor
615 .should_send_roam_request(PolicyRoamRequest { candidate, reasons: vec![] })
616 .expect("failed to check roam request"));
617
618 let candidate = types::ScannedCandidate {
620 bss: types::Bss {
621 signal: types::Signal {
622 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM) as i8,
623 snr_db: TEST_OK_SNR as i8,
624 },
625 ..generate_random_bss()
626 },
627 ..generate_random_scanned_candidate()
628 };
629 assert!(test_values
630 .monitor
631 .should_send_roam_request(PolicyRoamRequest { candidate, reasons: vec![] })
632 .expect("failed to check roam request"));
633
634 let candidate = types::ScannedCandidate {
637 bss: types::Bss {
638 signal: types::Signal {
639 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM + 1.0) as i8,
640 snr_db: TEST_OK_SNR as i8,
641 },
642 bssid: test_values.monitor.connection_data.ap_state.original().bssid,
643 ..generate_random_bss()
644 },
645 credential: generate_random_password(),
646 ..generate_random_scanned_candidate()
647 };
648 assert!(!test_values
649 .monitor
650 .should_send_roam_request(PolicyRoamRequest { candidate, reasons: vec![] })
651 .expect("failed to check roam reqeust"));
652 }
653
654 #[fuchsia::test]
655 fn test_send_signal_velocity_metric_event() {
656 let mut exec = fasync::TestExecutor::new_with_fake_time();
657 exec.set_fake_time(fasync::MonotonicInstant::now());
658
659 let connection_data = RoamingConnectionData {
660 signal_data: EwmaSignalData::new(-40, 50, 1),
661 ..generate_random_roaming_connection_data()
662 };
663 let mut test_values = setup_test_with_data(connection_data);
664 test_values.saved_networks.set_is_single_bss_response(true);
665
666 let trigger_data =
667 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
668 rssi_dbm: -80,
669 snr_db: TEST_OK_SNR as i8,
670 });
671 let _ =
672 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
673
674 assert_matches!(
675 test_values.telemetry_receiver.try_next(),
676 Ok(Some(TelemetryEvent::OnSignalVelocityUpdate { .. }))
677 );
678 }
679
680 #[fuchsia::test]
681 fn test_should_not_roam_scan_single_bss() {
682 let mut exec = fasync::TestExecutor::new_with_fake_time();
683 exec.set_fake_time(fasync::MonotonicInstant::now());
684
685 let rssi = -80;
686 let connection_data = RoamingConnectionData {
687 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
688 ..generate_random_roaming_connection_data()
689 };
690 let mut test_values = setup_test_with_data(connection_data);
691
692 test_values.saved_networks.set_is_single_bss_response(true);
694
695 exec.set_fake_time(fasync::MonotonicInstant::after(fasync::MonotonicDuration::from_hours(
697 1,
698 )));
699
700 let trigger_data =
701 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
702 rssi_dbm: rssi,
703 snr_db: TEST_OK_SNR as i8,
704 });
705 let trigger_result =
706 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
707
708 assert_eq!(trigger_result, RoamTriggerDataOutcome::Noop);
709 }
710
711 #[fuchsia::test]
712 fn test_roam_not_considered_if_attempted_too_many_times_today() {
713 let mut exec = fasync::TestExecutor::new_with_fake_time();
714 let first_roam_time = fasync::MonotonicInstant::now();
715 exec.set_fake_time(first_roam_time);
716
717 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 5.0;
719 let connection_data = RoamingConnectionData {
720 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
721 ..generate_random_roaming_connection_data()
722 };
723 let mut test_values = setup_test_with_data(connection_data);
724
725 for _ in 0..NUM_MAX_ROAMS_PER_DAY {
727 test_values.past_roams.try_lock().unwrap().add(RoamEvent::new_roam_now());
728 exec.set_fake_time(fasync::MonotonicInstant::after(MAX_BACKOFF_BETWEEN_ROAM_SCANS));
729 }
730
731 let trigger_data =
732 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
733 rssi_dbm: rssi as i8,
734 snr_db: TEST_OK_SNR as i8,
735 });
736
737 exec.set_fake_time(fasync::MonotonicInstant::after(
738 MAX_BACKOFF_BETWEEN_ROAM_SCANS + zx::MonotonicDuration::from_seconds(1),
739 ));
740
741 let should_roam_scan_result =
743 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
744 assert_eq!(should_roam_scan_result, RoamTriggerDataOutcome::Noop);
745
746 exec.set_fake_time(
749 first_roam_time
750 + zx::MonotonicDuration::from_hours(24)
751 + zx::MonotonicDuration::from_seconds(1),
752 );
753 let should_roam_scan_result =
754 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
755 assert_matches!(should_roam_scan_result, RoamTriggerDataOutcome::RoamSearch { .. });
756 }
757
758 #[fuchsia::test]
759 fn test_roaming_disabled_timer_is_set_and_respected() {
760 let mut exec = fasync::TestExecutor::new_with_fake_time();
761 let first_roam_time = fasync::MonotonicInstant::now();
762 exec.set_fake_time(first_roam_time);
763
764 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 5.0;
766 let connection_data = RoamingConnectionData {
767 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
768 ..generate_random_roaming_connection_data()
769 };
770 let mut test_values = setup_test_with_data(connection_data);
771
772 for i in 0..NUM_MAX_ROAMS_PER_DAY {
775 let time_of_roam = first_roam_time + zx::MonotonicDuration::from_minutes(i as i64);
776 exec.set_fake_time(time_of_roam);
777 test_values.past_roams.try_lock().unwrap().add(RoamEvent::new(time_of_roam));
778 }
779
780 let trigger_data =
781 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
782 rssi_dbm: rssi as i8,
783 snr_db: TEST_OK_SNR as i8,
784 });
785
786 exec.set_fake_time(fasync::MonotonicInstant::after(zx::MonotonicDuration::from_minutes(
788 NUM_MAX_ROAMS_PER_DAY as i64,
789 )));
790
791 let should_roam_scan_result =
793 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
794 assert_eq!(should_roam_scan_result, RoamTriggerDataOutcome::Noop);
795
796 let expected_reenabled_time = first_roam_time + TIMESPAN_TO_LIMIT_SCANS;
799 assert_eq!(test_values.monitor.next_roaming_enabled_time, expected_reenabled_time);
800
801 exec.set_fake_time(expected_reenabled_time - zx::MonotonicDuration::from_seconds(1));
803 let should_roam_scan_result =
804 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
805 assert_eq!(should_roam_scan_result, RoamTriggerDataOutcome::Noop);
806
807 exec.set_fake_time(expected_reenabled_time + zx::MonotonicDuration::from_seconds(1));
809 let should_roam_scan_result =
810 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
811 assert_matches!(should_roam_scan_result, RoamTriggerDataOutcome::RoamSearch { .. });
812 }
813}