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 fidl_fuchsia_wlan_common as fidl_common;
15use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
16use fidl_fuchsia_wlan_internal as fidl_internal;
17use fuchsia_async as fasync;
18use futures::lock::Mutex;
19use log::{error, info};
20use std::sync::Arc;
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!(
128 "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.",
129 NUM_MAX_ROAMS_PER_DAY, NUM_MAX_ROAMS_PER_DAY
130 );
131 } else {
132 error!("Unexpectedly failed to get the oldest roam attempt.");
133 }
134 return RoamTriggerDataOutcome::Noop;
135 }
136 } else {
137 error!("Unexpectedly failed to get lock on recent roam attempts data");
138 }
139 let mut roam_reasons: Vec<RoamReason> = vec![];
140
141 let rssi_threshold = if self.connection_data.ap_state.tracked.channel.band
143 == fidl_ieee80211::WlanBand::FiveGhz
144 {
145 LOCAL_ROAM_THRESHOLD_RSSI_5G
146 } else {
147 LOCAL_ROAM_THRESHOLD_RSSI_2G
148 };
149 let rssi = self.connection_data.signal_data.ewma_rssi.get();
150 if rssi <= rssi_threshold {
151 roam_reasons.push(RoamReason::RssiBelowThreshold);
152
153 if rssi
155 <= self.connection_data.previous_roam_scan_data.rssi
156 - MIN_RSSI_DROP_TO_RESET_BACKOFF
157 {
158 self.scan_backoff = MIN_BACKOFF_BETWEEN_ROAM_SCANS;
159 }
160 }
161
162 let now = fasync::MonotonicInstant::now();
164 if roam_reasons.is_empty()
165 || now < self.connection_data.previous_roam_scan_data.time + self.scan_backoff
166 {
167 return RoamTriggerDataOutcome::Noop;
168 }
169
170 self.scan_backoff =
172 std::cmp::min(self.scan_backoff * 2_i64, MAX_BACKOFF_BETWEEN_ROAM_SCANS);
173
174 self.connection_data.previous_roam_scan_data.time = fasync::MonotonicInstant::now();
176 self.connection_data.previous_roam_scan_data.rssi = rssi;
177 info!("Initiating roam search for roam reasons: {:?}", roam_reasons);
178
179 RoamTriggerDataOutcome::RoamSearch {
180 scan_type: fidl_common::ScanType::Active,
183 network_identifier: self.connection_data.network_identifier.clone(),
184 credential: self.connection_data.credential.clone(),
185 current_security: self.connection_data.ap_state.original().protection().into(),
186 reasons: roam_reasons,
187 }
188 }
189}
190
191#[async_trait(?Send)]
192impl RoamMonitorApi for StationaryMonitor {
193 async fn handle_roam_trigger_data(
194 &mut self,
195 data: RoamTriggerData,
196 ) -> Result<RoamTriggerDataOutcome, anyhow::Error> {
197 match data {
198 RoamTriggerData::SignalReportInd(stats) => self.handle_signal_report(stats).await,
199 }
200 }
201
202 fn should_send_roam_request(&self, request: PolicyRoamRequest) -> Result<bool, anyhow::Error> {
203 if request.candidate.bss.bssid == self.connection_data.ap_state.original().bssid {
204 info!("Selected roam candidate is the currently connected candidate, ignoring");
205 return Ok(false);
206 }
207 let latest_rssi = self.connection_data.signal_data.ewma_rssi.get();
210 if (request.candidate.bss.signal.rssi_dbm as f64)
211 < latest_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM
212 {
213 info!(
214 "Selected roam candidate ({:?}) is not enough of an improvement. Ignoring.",
215 request.candidate.to_string_without_pii()
216 );
217 return Ok(false);
218 }
219 Ok(true)
220 }
221
222 fn notify_of_roam_attempt(&mut self) {
223 self.scan_backoff = MIN_BACKOFF_BETWEEN_ROAM_SCANS;
226 }
227}
228
229#[cfg(test)]
230mod test {
231 use super::*;
232 use crate::util::testing::{
233 FakeSavedNetworksManager, generate_random_bss, generate_random_password,
234 generate_random_roaming_connection_data, generate_random_scanned_candidate,
235 };
236 use assert_matches::assert_matches;
237 use fidl_fuchsia_wlan_internal as fidl_internal;
238 use futures::channel::mpsc;
239 use futures::task::Poll;
240 use test_case::test_case;
241
242 struct TestValues {
243 monitor: StationaryMonitor,
244 telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
245 saved_networks: Arc<FakeSavedNetworksManager>,
246 past_roams: Arc<Mutex<PastRoamList>>,
247 }
248
249 const TEST_OK_SNR: f64 = 40.0;
250
251 fn setup_test() -> TestValues {
252 let connection_data = generate_random_roaming_connection_data();
253 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
254 let telemetry_sender = TelemetrySender::new(telemetry_sender);
255 let saved_networks = Arc::new(FakeSavedNetworksManager::new());
256 let past_roams = Arc::new(Mutex::new(PastRoamList::new(NUM_MAX_ROAMS_PER_DAY)));
257 saved_networks.set_is_single_bss_response(false);
260 let monitor = StationaryMonitor {
261 connection_data,
262 telemetry_sender,
263 saved_networks: saved_networks.clone(),
264 scan_backoff: MIN_BACKOFF_BETWEEN_ROAM_SCANS,
265 past_roams: past_roams.clone(),
266 next_roaming_enabled_time: fasync::MonotonicInstant::now(),
267 };
268 TestValues { monitor, telemetry_receiver, saved_networks, past_roams }
269 }
270
271 fn setup_test_with_data(connection_data: RoamingConnectionData) -> TestValues {
272 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
273 let telemetry_sender = TelemetrySender::new(telemetry_sender);
274 let saved_networks = Arc::new(FakeSavedNetworksManager::new());
275 let past_roams = Arc::new(Mutex::new(PastRoamList::new(NUM_MAX_ROAMS_PER_DAY)));
276 let monitor = StationaryMonitor {
277 connection_data,
278 telemetry_sender,
279 saved_networks: saved_networks.clone(),
280 scan_backoff: MIN_BACKOFF_BETWEEN_ROAM_SCANS,
281 past_roams: past_roams.clone(),
282 next_roaming_enabled_time: fasync::MonotonicInstant::now(),
283 };
284 TestValues { monitor, telemetry_receiver, saved_networks, past_roams }
285 }
286
287 fn run_handle_roam_trigger_data(
290 exec: &mut fasync::TestExecutor,
291 monitor: &mut StationaryMonitor,
292 trigger_data: RoamTriggerData,
293 ) -> RoamTriggerDataOutcome {
294 return assert_matches!(exec.run_until_stalled(&mut monitor.handle_roam_trigger_data(trigger_data)), Poll::Ready(Ok(should_roam)) => {should_roam});
295 }
296
297 #[test_case(-80, true; "bad rssi")]
298 #[test_case(-40, false; "good rssi")]
299 #[fuchsia::test(add_test_attr = false)]
300 fn test_handle_signal_report_trigger_data(rssi: i8, should_roam_search: bool) {
301 let mut exec = fasync::TestExecutor::new_with_fake_time();
302 exec.set_fake_time(fasync::MonotonicInstant::now());
303
304 let connection_data = RoamingConnectionData {
306 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
307 ..generate_random_roaming_connection_data()
308 };
309 let mut test_values = setup_test_with_data(connection_data);
310
311 exec.set_fake_time(fasync::MonotonicInstant::after(fasync::MonotonicDuration::from_hours(
313 1,
314 )));
315
316 let trigger_data =
319 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
320 rssi_dbm: rssi,
321 snr_db: TEST_OK_SNR as i8,
322 });
323 let result =
324 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
325
326 if should_roam_search {
327 assert_matches!(result, RoamTriggerDataOutcome::RoamSearch { .. });
328 } else {
329 assert_matches!(result, RoamTriggerDataOutcome::Noop);
330 }
331 }
332
333 #[fuchsia::test]
334 fn test_stationary_monitor_uses_active_scans() {
335 let mut exec = fasync::TestExecutor::new_with_fake_time();
336 exec.set_fake_time(fasync::MonotonicInstant::now());
337
338 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
341 let connection_data = RoamingConnectionData {
342 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
343 ..generate_random_roaming_connection_data()
344 };
345 let mut test_values = setup_test_with_data(connection_data);
346
347 let trigger_data =
350 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
351 rssi_dbm: rssi as i8,
352 snr_db: TEST_OK_SNR as i8,
353 });
354
355 exec.set_fake_time(fasync::MonotonicInstant::after(fasync::MonotonicDuration::from_hours(
357 1,
358 )));
359
360 assert_matches!(
362 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
363 RoamTriggerDataOutcome::RoamSearch { scan_type: fidl_common::ScanType::Active, .. }
364 );
365 }
366
367 #[fuchsia::test]
368 fn test_minimum_time_between_roam_scans() {
369 let mut exec = fasync::TestExecutor::new_with_fake_time();
370 exec.set_fake_time(fasync::MonotonicInstant::now());
371
372 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
375 let connection_data = RoamingConnectionData {
376 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
377 ..generate_random_roaming_connection_data()
378 };
379 let mut test_values = setup_test_with_data(connection_data);
380 let trigger_data =
381 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
382 rssi_dbm: rssi as i8,
383 snr_db: TEST_OK_SNR as i8,
384 });
385
386 exec.set_fake_time(fasync::MonotonicInstant::after(
388 MIN_BACKOFF_BETWEEN_ROAM_SCANS - fasync::MonotonicDuration::from_seconds(1),
389 ));
390
391 assert_matches!(
394 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
395 RoamTriggerDataOutcome::Noop
396 );
397
398 exec.set_fake_time(fasync::MonotonicInstant::after(
400 fasync::MonotonicDuration::from_seconds(2),
401 ));
402
403 assert_matches!(
405 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
406 RoamTriggerDataOutcome::RoamSearch { .. }
407 );
408 }
409
410 #[fuchsia::test]
411 fn test_roam_scans_backoff_exponentially() {
412 let mut exec = fasync::TestExecutor::new_with_fake_time();
413 exec.set_fake_time(fasync::MonotonicInstant::now());
414
415 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
418 let connection_data = RoamingConnectionData {
419 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
420 ..generate_random_roaming_connection_data()
421 };
422 let trigger_data =
423 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
424 rssi_dbm: rssi as i8,
425 snr_db: TEST_OK_SNR as i8,
426 });
427 let mut test_values = setup_test_with_data(connection_data);
428
429 let mut expected_backoff = MIN_BACKOFF_BETWEEN_ROAM_SCANS;
431 while expected_backoff <= MAX_BACKOFF_BETWEEN_ROAM_SCANS {
432 exec.set_fake_time(fasync::MonotonicInstant::after(
434 expected_backoff - fasync::MonotonicDuration::from_seconds(1),
435 ));
436
437 assert_matches!(
440 run_handle_roam_trigger_data(
441 &mut exec,
442 &mut test_values.monitor,
443 trigger_data.clone()
444 ),
445 RoamTriggerDataOutcome::Noop
446 );
447
448 exec.set_fake_time(fasync::MonotonicInstant::after(
450 fasync::MonotonicDuration::from_seconds(2),
451 ));
452
453 assert_matches!(
455 run_handle_roam_trigger_data(
456 &mut exec,
457 &mut test_values.monitor,
458 trigger_data.clone()
459 ),
460 RoamTriggerDataOutcome::RoamSearch { .. }
461 );
462
463 expected_backoff = expected_backoff * 2;
465 }
466 exec.set_fake_time(fasync::MonotonicInstant::after(
469 MAX_BACKOFF_BETWEEN_ROAM_SCANS + fasync::MonotonicDuration::from_seconds(1),
470 ));
471 assert_matches!(
472 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
473 RoamTriggerDataOutcome::RoamSearch { .. }
474 );
475 }
476
477 #[fuchsia::test]
478 fn test_roam_attempt_resets_backoff() {
479 let mut exec = fasync::TestExecutor::new_with_fake_time();
480 exec.set_fake_time(fasync::MonotonicInstant::now());
481
482 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
485 let connection_data = RoamingConnectionData {
486 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
487 ..generate_random_roaming_connection_data()
488 };
489 let trigger_data =
490 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
491 rssi_dbm: rssi as i8,
492 snr_db: TEST_OK_SNR as i8,
493 });
494 let mut test_values = setup_test_with_data(connection_data);
495
496 exec.set_fake_time(
498 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
499 + zx::MonotonicDuration::from_seconds(1),
500 );
501
502 assert_matches!(
504 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
505 RoamTriggerDataOutcome::RoamSearch { .. }
506 );
507
508 exec.set_fake_time(
510 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
511 + zx::MonotonicDuration::from_seconds(1),
512 );
513
514 assert_matches!(
516 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
517 RoamTriggerDataOutcome::Noop
518 );
519
520 test_values.monitor.notify_of_roam_attempt();
522
523 assert_matches!(
526 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
527 RoamTriggerDataOutcome::RoamSearch { .. }
528 );
529 }
530
531 #[fuchsia::test]
532 fn test_rssi_drop_resets_backoff() {
533 let mut exec = fasync::TestExecutor::new_with_fake_time();
534 exec.set_fake_time(fasync::MonotonicInstant::now());
535
536 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
539 let connection_data = RoamingConnectionData {
540 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
541 ..generate_random_roaming_connection_data()
542 };
543 let trigger_data =
544 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
545 rssi_dbm: rssi as i8,
546 snr_db: TEST_OK_SNR as i8,
547 });
548 let mut test_values = setup_test_with_data(connection_data);
549
550 exec.set_fake_time(
552 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
553 + zx::MonotonicDuration::from_seconds(1),
554 );
555
556 assert_matches!(
558 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
559 RoamTriggerDataOutcome::RoamSearch { .. }
560 );
561
562 exec.set_fake_time(
564 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
565 + zx::MonotonicDuration::from_seconds(1),
566 );
567
568 assert_matches!(
570 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
571 RoamTriggerDataOutcome::Noop
572 );
573
574 let trigger_data =
577 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
578 rssi_dbm: (rssi - MIN_RSSI_DROP_TO_RESET_BACKOFF) as i8,
579 snr_db: TEST_OK_SNR as i8,
580 });
581 assert_matches!(
582 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
583 RoamTriggerDataOutcome::RoamSearch { .. }
584 );
585
586 exec.set_fake_time(
588 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
589 - zx::MonotonicDuration::from_seconds(1),
590 );
591 assert_matches!(
594 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
595 RoamTriggerDataOutcome::Noop
596 );
597 }
598
599 #[fuchsia::test]
600 fn test_should_send_roam_request() {
601 let _exec = fasync::TestExecutor::new();
602 let test_values = setup_test();
603
604 let current_rssi = test_values.monitor.connection_data.signal_data.ewma_rssi.get();
606
607 let candidate = types::ScannedCandidate {
609 bss: types::Bss {
610 signal: types::Signal {
611 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM - 1.0) as i8,
612 snr_db: TEST_OK_SNR as i8,
613 },
614 ..generate_random_bss()
615 },
616 ..generate_random_scanned_candidate()
617 };
618 assert!(
619 !test_values
620 .monitor
621 .should_send_roam_request(PolicyRoamRequest { candidate, reasons: vec![] })
622 .expect("failed to check roam request")
623 );
624
625 let candidate = types::ScannedCandidate {
627 bss: types::Bss {
628 signal: types::Signal {
629 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM) as i8,
630 snr_db: TEST_OK_SNR as i8,
631 },
632 ..generate_random_bss()
633 },
634 ..generate_random_scanned_candidate()
635 };
636 assert!(
637 test_values
638 .monitor
639 .should_send_roam_request(PolicyRoamRequest { candidate, reasons: vec![] })
640 .expect("failed to check roam request")
641 );
642
643 let candidate = types::ScannedCandidate {
646 bss: types::Bss {
647 signal: types::Signal {
648 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM + 1.0) as i8,
649 snr_db: TEST_OK_SNR as i8,
650 },
651 bssid: test_values.monitor.connection_data.ap_state.original().bssid,
652 ..generate_random_bss()
653 },
654 credential: generate_random_password(),
655 ..generate_random_scanned_candidate()
656 };
657 assert!(
658 !test_values
659 .monitor
660 .should_send_roam_request(PolicyRoamRequest { candidate, reasons: vec![] })
661 .expect("failed to check roam reqeust")
662 );
663 }
664
665 #[fuchsia::test]
666 fn test_send_signal_velocity_metric_event() {
667 let mut exec = fasync::TestExecutor::new_with_fake_time();
668 exec.set_fake_time(fasync::MonotonicInstant::now());
669
670 let connection_data = RoamingConnectionData {
671 signal_data: EwmaSignalData::new(-40, 50, 1),
672 ..generate_random_roaming_connection_data()
673 };
674 let mut test_values = setup_test_with_data(connection_data);
675 test_values.saved_networks.set_is_single_bss_response(true);
676
677 let trigger_data =
678 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
679 rssi_dbm: -80,
680 snr_db: TEST_OK_SNR as i8,
681 });
682 let _ =
683 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
684
685 assert_matches!(
686 test_values.telemetry_receiver.try_next(),
687 Ok(Some(TelemetryEvent::OnSignalVelocityUpdate { .. }))
688 );
689 }
690
691 #[fuchsia::test]
692 fn test_should_not_roam_scan_single_bss() {
693 let mut exec = fasync::TestExecutor::new_with_fake_time();
694 exec.set_fake_time(fasync::MonotonicInstant::now());
695
696 let rssi = -80;
697 let connection_data = RoamingConnectionData {
698 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
699 ..generate_random_roaming_connection_data()
700 };
701 let mut test_values = setup_test_with_data(connection_data);
702
703 test_values.saved_networks.set_is_single_bss_response(true);
705
706 exec.set_fake_time(fasync::MonotonicInstant::after(fasync::MonotonicDuration::from_hours(
708 1,
709 )));
710
711 let trigger_data =
712 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
713 rssi_dbm: rssi,
714 snr_db: TEST_OK_SNR as i8,
715 });
716 let trigger_result =
717 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
718
719 assert_eq!(trigger_result, RoamTriggerDataOutcome::Noop);
720 }
721
722 #[fuchsia::test]
723 fn test_roam_not_considered_if_attempted_too_many_times_today() {
724 let mut exec = fasync::TestExecutor::new_with_fake_time();
725 let first_roam_time = fasync::MonotonicInstant::now();
726 exec.set_fake_time(first_roam_time);
727
728 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 5.0;
730 let connection_data = RoamingConnectionData {
731 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
732 ..generate_random_roaming_connection_data()
733 };
734 let mut test_values = setup_test_with_data(connection_data);
735
736 for _ in 0..NUM_MAX_ROAMS_PER_DAY {
738 test_values.past_roams.try_lock().unwrap().add(RoamEvent::new_roam_now());
739 exec.set_fake_time(fasync::MonotonicInstant::after(MAX_BACKOFF_BETWEEN_ROAM_SCANS));
740 }
741
742 let trigger_data =
743 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
744 rssi_dbm: rssi as i8,
745 snr_db: TEST_OK_SNR as i8,
746 });
747
748 exec.set_fake_time(fasync::MonotonicInstant::after(
749 MAX_BACKOFF_BETWEEN_ROAM_SCANS + zx::MonotonicDuration::from_seconds(1),
750 ));
751
752 let should_roam_scan_result =
754 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
755 assert_eq!(should_roam_scan_result, RoamTriggerDataOutcome::Noop);
756
757 exec.set_fake_time(
760 first_roam_time
761 + zx::MonotonicDuration::from_hours(24)
762 + zx::MonotonicDuration::from_seconds(1),
763 );
764 let should_roam_scan_result =
765 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
766 assert_matches!(should_roam_scan_result, RoamTriggerDataOutcome::RoamSearch { .. });
767 }
768
769 #[fuchsia::test]
770 fn test_roaming_disabled_timer_is_set_and_respected() {
771 let mut exec = fasync::TestExecutor::new_with_fake_time();
772 let first_roam_time = fasync::MonotonicInstant::now();
773 exec.set_fake_time(first_roam_time);
774
775 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 5.0;
777 let connection_data = RoamingConnectionData {
778 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
779 ..generate_random_roaming_connection_data()
780 };
781 let mut test_values = setup_test_with_data(connection_data);
782
783 for i in 0..NUM_MAX_ROAMS_PER_DAY {
786 let time_of_roam = first_roam_time + zx::MonotonicDuration::from_minutes(i as i64);
787 exec.set_fake_time(time_of_roam);
788 test_values.past_roams.try_lock().unwrap().add(RoamEvent::new(time_of_roam));
789 }
790
791 let trigger_data =
792 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
793 rssi_dbm: rssi as i8,
794 snr_db: TEST_OK_SNR as i8,
795 });
796
797 exec.set_fake_time(fasync::MonotonicInstant::after(zx::MonotonicDuration::from_minutes(
799 NUM_MAX_ROAMS_PER_DAY as i64,
800 )));
801
802 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 let expected_reenabled_time = first_roam_time + TIMESPAN_TO_LIMIT_SCANS;
810 assert_eq!(test_values.monitor.next_roaming_enabled_time, expected_reenabled_time);
811
812 exec.set_fake_time(expected_reenabled_time - zx::MonotonicDuration::from_seconds(1));
814 let should_roam_scan_result =
815 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
816 assert_eq!(should_roam_scan_result, RoamTriggerDataOutcome::Noop);
817
818 exec.set_fake_time(expected_reenabled_time + zx::MonotonicDuration::from_seconds(1));
820 let should_roam_scan_result =
821 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
822 assert_matches!(should_roam_scan_result, RoamTriggerDataOutcome::RoamSearch { .. });
823 }
824}