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::pseudo_energy::EwmaSignalData;
12use async_trait::async_trait;
13use fidl_fuchsia_wlan_common as fidl_common;
14use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
15use fidl_fuchsia_wlan_internal as fidl_internal;
16use fuchsia_async as fasync;
17use futures::lock::Mutex;
18use log::{error, info};
19use std::sync::Arc;
20use wlan_common::historical_list::Timestamped;
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()));
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()));
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 tx_rate_500kbps: 0,
323 });
324 let result =
325 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
326
327 if should_roam_search {
328 assert_matches!(result, RoamTriggerDataOutcome::RoamSearch { .. });
329 } else {
330 assert_matches!(result, RoamTriggerDataOutcome::Noop);
331 }
332 }
333
334 #[fuchsia::test]
335 fn test_stationary_monitor_uses_active_scans() {
336 let mut exec = fasync::TestExecutor::new_with_fake_time();
337 exec.set_fake_time(fasync::MonotonicInstant::now());
338
339 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
342 let connection_data = RoamingConnectionData {
343 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
344 ..generate_random_roaming_connection_data()
345 };
346 let mut test_values = setup_test_with_data(connection_data);
347
348 let trigger_data =
351 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
352 rssi_dbm: rssi as i8,
353 snr_db: TEST_OK_SNR as i8,
354 tx_rate_500kbps: 0,
355 });
356
357 exec.set_fake_time(fasync::MonotonicInstant::after(fasync::MonotonicDuration::from_hours(
359 1,
360 )));
361
362 assert_matches!(
364 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
365 RoamTriggerDataOutcome::RoamSearch { scan_type: fidl_common::ScanType::Active, .. }
366 );
367 }
368
369 #[fuchsia::test]
370 fn test_minimum_time_between_roam_scans() {
371 let mut exec = fasync::TestExecutor::new_with_fake_time();
372 exec.set_fake_time(fasync::MonotonicInstant::now());
373
374 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
377 let connection_data = RoamingConnectionData {
378 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
379 ..generate_random_roaming_connection_data()
380 };
381 let mut test_values = setup_test_with_data(connection_data);
382 let trigger_data =
383 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
384 rssi_dbm: rssi as i8,
385 snr_db: TEST_OK_SNR as i8,
386 tx_rate_500kbps: 0,
387 });
388
389 exec.set_fake_time(fasync::MonotonicInstant::after(
391 MIN_BACKOFF_BETWEEN_ROAM_SCANS - fasync::MonotonicDuration::from_seconds(1),
392 ));
393
394 assert_matches!(
397 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
398 RoamTriggerDataOutcome::Noop
399 );
400
401 exec.set_fake_time(fasync::MonotonicInstant::after(
403 fasync::MonotonicDuration::from_seconds(2),
404 ));
405
406 assert_matches!(
408 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
409 RoamTriggerDataOutcome::RoamSearch { .. }
410 );
411 }
412
413 #[fuchsia::test]
414 fn test_roam_scans_backoff_exponentially() {
415 let mut exec = fasync::TestExecutor::new_with_fake_time();
416 exec.set_fake_time(fasync::MonotonicInstant::now());
417
418 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
421 let connection_data = RoamingConnectionData {
422 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
423 ..generate_random_roaming_connection_data()
424 };
425 let trigger_data =
426 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
427 rssi_dbm: rssi as i8,
428 snr_db: TEST_OK_SNR as i8,
429 tx_rate_500kbps: 0,
430 });
431 let mut test_values = setup_test_with_data(connection_data);
432
433 let mut expected_backoff = MIN_BACKOFF_BETWEEN_ROAM_SCANS;
435 while expected_backoff <= MAX_BACKOFF_BETWEEN_ROAM_SCANS {
436 exec.set_fake_time(fasync::MonotonicInstant::after(
438 expected_backoff - fasync::MonotonicDuration::from_seconds(1),
439 ));
440
441 assert_matches!(
444 run_handle_roam_trigger_data(
445 &mut exec,
446 &mut test_values.monitor,
447 trigger_data.clone()
448 ),
449 RoamTriggerDataOutcome::Noop
450 );
451
452 exec.set_fake_time(fasync::MonotonicInstant::after(
454 fasync::MonotonicDuration::from_seconds(2),
455 ));
456
457 assert_matches!(
459 run_handle_roam_trigger_data(
460 &mut exec,
461 &mut test_values.monitor,
462 trigger_data.clone()
463 ),
464 RoamTriggerDataOutcome::RoamSearch { .. }
465 );
466
467 expected_backoff = expected_backoff * 2;
469 }
470 exec.set_fake_time(fasync::MonotonicInstant::after(
473 MAX_BACKOFF_BETWEEN_ROAM_SCANS + fasync::MonotonicDuration::from_seconds(1),
474 ));
475 assert_matches!(
476 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
477 RoamTriggerDataOutcome::RoamSearch { .. }
478 );
479 }
480
481 #[fuchsia::test]
482 fn test_roam_attempt_resets_backoff() {
483 let mut exec = fasync::TestExecutor::new_with_fake_time();
484 exec.set_fake_time(fasync::MonotonicInstant::now());
485
486 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
489 let connection_data = RoamingConnectionData {
490 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
491 ..generate_random_roaming_connection_data()
492 };
493 let trigger_data =
494 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
495 rssi_dbm: rssi as i8,
496 snr_db: TEST_OK_SNR as i8,
497 tx_rate_500kbps: 0,
498 });
499 let mut test_values = setup_test_with_data(connection_data);
500
501 exec.set_fake_time(
503 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
504 + zx::MonotonicDuration::from_seconds(1),
505 );
506
507 assert_matches!(
509 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
510 RoamTriggerDataOutcome::RoamSearch { .. }
511 );
512
513 exec.set_fake_time(
515 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
516 + zx::MonotonicDuration::from_seconds(1),
517 );
518
519 assert_matches!(
521 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
522 RoamTriggerDataOutcome::Noop
523 );
524
525 test_values.monitor.notify_of_roam_attempt();
527
528 assert_matches!(
531 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
532 RoamTriggerDataOutcome::RoamSearch { .. }
533 );
534 }
535
536 #[fuchsia::test]
537 fn test_rssi_drop_resets_backoff() {
538 let mut exec = fasync::TestExecutor::new_with_fake_time();
539 exec.set_fake_time(fasync::MonotonicInstant::now());
540
541 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
544 let connection_data = RoamingConnectionData {
545 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
546 ..generate_random_roaming_connection_data()
547 };
548 let trigger_data =
549 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
550 rssi_dbm: rssi as i8,
551 snr_db: TEST_OK_SNR as i8,
552 tx_rate_500kbps: 0,
553 });
554 let mut test_values = setup_test_with_data(connection_data);
555
556 exec.set_fake_time(
558 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
559 + zx::MonotonicDuration::from_seconds(1),
560 );
561
562 assert_matches!(
564 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
565 RoamTriggerDataOutcome::RoamSearch { .. }
566 );
567
568 exec.set_fake_time(
570 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
571 + zx::MonotonicDuration::from_seconds(1),
572 );
573
574 assert_matches!(
576 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
577 RoamTriggerDataOutcome::Noop
578 );
579
580 let trigger_data =
583 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
584 rssi_dbm: (rssi - MIN_RSSI_DROP_TO_RESET_BACKOFF) as i8,
585 snr_db: TEST_OK_SNR as i8,
586 tx_rate_500kbps: 0,
587 });
588 assert_matches!(
589 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
590 RoamTriggerDataOutcome::RoamSearch { .. }
591 );
592
593 exec.set_fake_time(
595 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
596 - zx::MonotonicDuration::from_seconds(1),
597 );
598 assert_matches!(
601 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
602 RoamTriggerDataOutcome::Noop
603 );
604 }
605
606 #[fuchsia::test]
607 fn test_should_send_roam_request() {
608 let _exec = fasync::TestExecutor::new();
609 let test_values = setup_test();
610
611 let current_rssi = test_values.monitor.connection_data.signal_data.ewma_rssi.get();
613
614 let candidate = types::ScannedCandidate {
616 bss: types::Bss {
617 signal: types::Signal {
618 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM - 1.0) as i8,
619 snr_db: TEST_OK_SNR as i8,
620 },
621 ..generate_random_bss()
622 },
623 ..generate_random_scanned_candidate()
624 };
625 assert!(
626 !test_values
627 .monitor
628 .should_send_roam_request(PolicyRoamRequest { candidate, reasons: vec![] })
629 .expect("failed to check roam request")
630 );
631
632 let candidate = types::ScannedCandidate {
634 bss: types::Bss {
635 signal: types::Signal {
636 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM) as i8,
637 snr_db: TEST_OK_SNR as i8,
638 },
639 ..generate_random_bss()
640 },
641 ..generate_random_scanned_candidate()
642 };
643 assert!(
644 test_values
645 .monitor
646 .should_send_roam_request(PolicyRoamRequest { candidate, reasons: vec![] })
647 .expect("failed to check roam request")
648 );
649
650 let candidate = types::ScannedCandidate {
653 bss: types::Bss {
654 signal: types::Signal {
655 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM + 1.0) as i8,
656 snr_db: TEST_OK_SNR as i8,
657 },
658 bssid: test_values.monitor.connection_data.ap_state.original().bssid,
659 ..generate_random_bss()
660 },
661 credential: generate_random_password(),
662 ..generate_random_scanned_candidate()
663 };
664 assert!(
665 !test_values
666 .monitor
667 .should_send_roam_request(PolicyRoamRequest { candidate, reasons: vec![] })
668 .expect("failed to check roam reqeust")
669 );
670 }
671
672 #[fuchsia::test]
673 fn test_send_signal_velocity_metric_event() {
674 let mut exec = fasync::TestExecutor::new_with_fake_time();
675 exec.set_fake_time(fasync::MonotonicInstant::now());
676
677 let connection_data = RoamingConnectionData {
678 signal_data: EwmaSignalData::new(-40, 50, 1),
679 ..generate_random_roaming_connection_data()
680 };
681 let mut test_values = setup_test_with_data(connection_data);
682 test_values.saved_networks.set_is_single_bss_response(true);
683
684 let trigger_data =
685 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
686 rssi_dbm: -80,
687 snr_db: TEST_OK_SNR as i8,
688 tx_rate_500kbps: 0,
689 });
690 let _ =
691 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
692
693 assert_matches!(
694 test_values.telemetry_receiver.try_recv(),
695 Ok(TelemetryEvent::OnSignalVelocityUpdate { .. })
696 );
697 }
698
699 #[fuchsia::test]
700 fn test_should_not_roam_scan_single_bss() {
701 let mut exec = fasync::TestExecutor::new_with_fake_time();
702 exec.set_fake_time(fasync::MonotonicInstant::now());
703
704 let rssi = -80;
705 let connection_data = RoamingConnectionData {
706 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
707 ..generate_random_roaming_connection_data()
708 };
709 let mut test_values = setup_test_with_data(connection_data);
710
711 test_values.saved_networks.set_is_single_bss_response(true);
713
714 exec.set_fake_time(fasync::MonotonicInstant::after(fasync::MonotonicDuration::from_hours(
716 1,
717 )));
718
719 let trigger_data =
720 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
721 rssi_dbm: rssi,
722 snr_db: TEST_OK_SNR as i8,
723 tx_rate_500kbps: 0,
724 });
725 let trigger_result =
726 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
727
728 assert_eq!(trigger_result, RoamTriggerDataOutcome::Noop);
729 }
730
731 #[fuchsia::test]
732 fn test_roam_not_considered_if_attempted_too_many_times_today() {
733 let mut exec = fasync::TestExecutor::new_with_fake_time();
734 let first_roam_time = fasync::MonotonicInstant::now();
735 exec.set_fake_time(first_roam_time);
736
737 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 5.0;
739 let connection_data = RoamingConnectionData {
740 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
741 ..generate_random_roaming_connection_data()
742 };
743 let mut test_values = setup_test_with_data(connection_data);
744
745 for _ in 0..NUM_MAX_ROAMS_PER_DAY {
747 test_values.past_roams.try_lock().unwrap().add(RoamEvent::new_roam_now());
748 exec.set_fake_time(fasync::MonotonicInstant::after(MAX_BACKOFF_BETWEEN_ROAM_SCANS));
749 }
750
751 let trigger_data =
752 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
753 rssi_dbm: rssi as i8,
754 snr_db: TEST_OK_SNR as i8,
755 tx_rate_500kbps: 0,
756 });
757
758 exec.set_fake_time(fasync::MonotonicInstant::after(
759 MAX_BACKOFF_BETWEEN_ROAM_SCANS + zx::MonotonicDuration::from_seconds(1),
760 ));
761
762 let should_roam_scan_result =
764 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
765 assert_eq!(should_roam_scan_result, RoamTriggerDataOutcome::Noop);
766
767 exec.set_fake_time(
770 first_roam_time
771 + zx::MonotonicDuration::from_hours(24)
772 + zx::MonotonicDuration::from_seconds(1),
773 );
774 let should_roam_scan_result =
775 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
776 assert_matches!(should_roam_scan_result, RoamTriggerDataOutcome::RoamSearch { .. });
777 }
778
779 #[fuchsia::test]
780 fn test_roaming_disabled_timer_is_set_and_respected() {
781 let mut exec = fasync::TestExecutor::new_with_fake_time();
782 let first_roam_time = fasync::MonotonicInstant::now();
783 exec.set_fake_time(first_roam_time);
784
785 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 5.0;
787 let connection_data = RoamingConnectionData {
788 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
789 ..generate_random_roaming_connection_data()
790 };
791 let mut test_values = setup_test_with_data(connection_data);
792
793 for i in 0..NUM_MAX_ROAMS_PER_DAY {
796 let time_of_roam = first_roam_time + zx::MonotonicDuration::from_minutes(i as i64);
797 exec.set_fake_time(time_of_roam);
798 test_values.past_roams.try_lock().unwrap().add(RoamEvent::new(time_of_roam));
799 }
800
801 let trigger_data =
802 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
803 rssi_dbm: rssi as i8,
804 snr_db: TEST_OK_SNR as i8,
805 tx_rate_500kbps: 0,
806 });
807
808 exec.set_fake_time(fasync::MonotonicInstant::after(zx::MonotonicDuration::from_minutes(
810 NUM_MAX_ROAMS_PER_DAY as i64,
811 )));
812
813 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 let expected_reenabled_time = first_roam_time + TIMESPAN_TO_LIMIT_SCANS;
821 assert_eq!(test_values.monitor.next_roaming_enabled_time, expected_reenabled_time);
822
823 exec.set_fake_time(expected_reenabled_time - zx::MonotonicDuration::from_seconds(1));
825 let should_roam_scan_result =
826 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
827 assert_eq!(should_roam_scan_result, RoamTriggerDataOutcome::Noop);
828
829 exec.set_fake_time(expected_reenabled_time + zx::MonotonicDuration::from_seconds(1));
831 let should_roam_scan_result =
832 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
833 assert_matches!(should_roam_scan_result, RoamTriggerDataOutcome::RoamSearch { .. });
834 }
835}