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 if let (Some(rssi), Some(snr)) = (stats.rssi_dbm, stats.snr_db) {
87 self.connection_data.signal_data.update_with_new_measurement(rssi, snr);
88
89 self.connection_data
91 .rssi_velocity
92 .update(self.connection_data.signal_data.ewma_rssi.get());
93
94 self.telemetry_sender.send(TelemetryEvent::OnSignalVelocityUpdate {
95 rssi_velocity: self.connection_data.rssi_velocity.get(),
96 });
97 }
98
99 match self
101 .saved_networks
102 .is_network_single_bss(
103 &self.connection_data.network_identifier,
104 &self.connection_data.credential,
105 )
106 .await
107 {
108 Ok(true) => return Ok(RoamTriggerDataOutcome::Noop),
109 _ => {
110 return Ok(self.should_roam_scan_after_signal_report());
113 }
114 }
115 }
116
117 fn should_roam_scan_after_signal_report(&mut self) -> RoamTriggerDataOutcome {
118 if fasync::MonotonicInstant::now() <= self.next_roaming_enabled_time {
120 return RoamTriggerDataOutcome::Noop;
121 }
122 if let Some(past_roams) = self.past_roams.try_lock() {
125 let recent_roams =
126 past_roams.get_recent(fasync::MonotonicInstant::now() - TIMESPAN_TO_LIMIT_SCANS);
127 if recent_roams.len() >= NUM_MAX_ROAMS_PER_DAY {
128 if let Some(oldest_roam_attempt) = recent_roams.first() {
129 self.next_roaming_enabled_time =
130 oldest_roam_attempt.time() + TIMESPAN_TO_LIMIT_SCANS;
131 info!(
132 "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.",
133 NUM_MAX_ROAMS_PER_DAY, NUM_MAX_ROAMS_PER_DAY
134 );
135 } else {
136 error!("Unexpectedly failed to get the oldest roam attempt.");
137 }
138 return RoamTriggerDataOutcome::Noop;
139 }
140 } else {
141 error!("Unexpectedly failed to get lock on recent roam attempts data");
142 }
143 let mut roam_reasons: Vec<RoamReason> = vec![];
144
145 let rssi_threshold = if self.connection_data.ap_state.tracked.channel.band
147 == fidl_ieee80211::WlanBand::FiveGhz
148 {
149 LOCAL_ROAM_THRESHOLD_RSSI_5G
150 } else {
151 LOCAL_ROAM_THRESHOLD_RSSI_2G
152 };
153 let rssi = self.connection_data.signal_data.ewma_rssi.get();
154 if rssi <= rssi_threshold {
155 roam_reasons.push(RoamReason::RssiBelowThreshold);
156
157 if rssi
159 <= self.connection_data.previous_roam_scan_data.rssi
160 - MIN_RSSI_DROP_TO_RESET_BACKOFF
161 {
162 self.scan_backoff = MIN_BACKOFF_BETWEEN_ROAM_SCANS;
163 }
164 }
165
166 let now = fasync::MonotonicInstant::now();
168 if roam_reasons.is_empty()
169 || now < self.connection_data.previous_roam_scan_data.time + self.scan_backoff
170 {
171 return RoamTriggerDataOutcome::Noop;
172 }
173
174 self.scan_backoff =
176 std::cmp::min(self.scan_backoff * 2_i64, MAX_BACKOFF_BETWEEN_ROAM_SCANS);
177
178 self.connection_data.previous_roam_scan_data.time = fasync::MonotonicInstant::now();
180 self.connection_data.previous_roam_scan_data.rssi = rssi;
181 info!("Initiating roam search for roam reasons: {:?}", roam_reasons);
182
183 RoamTriggerDataOutcome::RoamSearch {
184 scan_type: fidl_common::ScanType::Active,
187 network_identifier: self.connection_data.network_identifier.clone(),
188 credential: self.connection_data.credential.clone(),
189 current_security: self.connection_data.ap_state.original().protection().into(),
190 reasons: roam_reasons,
191 }
192 }
193}
194
195#[async_trait(?Send)]
196impl RoamMonitorApi for StationaryMonitor {
197 async fn handle_roam_trigger_data(
198 &mut self,
199 data: RoamTriggerData,
200 ) -> Result<RoamTriggerDataOutcome, anyhow::Error> {
201 match data {
202 RoamTriggerData::SignalReportInd(stats) => self.handle_signal_report(stats).await,
203 }
204 }
205
206 fn should_send_roam_request(&self, request: PolicyRoamRequest) -> Result<bool, anyhow::Error> {
207 if request.candidate.bss.bssid == self.connection_data.ap_state.original().bssid {
208 info!("Selected roam candidate is the currently connected candidate, ignoring");
209 return Ok(false);
210 }
211 let latest_rssi = self.connection_data.signal_data.ewma_rssi.get();
214 if (request.candidate.bss.signal.rssi_dbm as f64)
215 < latest_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM
216 {
217 info!(
218 "Selected roam candidate ({:?}) is not enough of an improvement. Ignoring.",
219 request.candidate.to_string_without_pii()
220 );
221 return Ok(false);
222 }
223 Ok(true)
224 }
225
226 fn notify_of_roam_attempt(&mut self) {
227 self.scan_backoff = MIN_BACKOFF_BETWEEN_ROAM_SCANS;
230 }
231}
232
233#[cfg(test)]
234mod test {
235 use super::*;
236 use crate::util::testing::{
237 FakeSavedNetworksManager, generate_random_bss, generate_random_password,
238 generate_random_roaming_connection_data, generate_random_scanned_candidate,
239 };
240 use assert_matches::assert_matches;
241 use fidl_fuchsia_wlan_internal as fidl_internal;
242 use futures::channel::mpsc;
243 use futures::task::Poll;
244 use test_case::test_case;
245
246 struct TestValues {
247 monitor: StationaryMonitor,
248 telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
249 saved_networks: Arc<FakeSavedNetworksManager>,
250 past_roams: Arc<Mutex<PastRoamList>>,
251 }
252
253 const TEST_OK_SNR: f64 = 40.0;
254
255 fn setup_test() -> TestValues {
256 let connection_data = generate_random_roaming_connection_data();
257 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
258 let telemetry_sender = TelemetrySender::new(telemetry_sender);
259 let saved_networks = Arc::new(FakeSavedNetworksManager::new());
260 let past_roams = Arc::new(Mutex::new(PastRoamList::new()));
261 saved_networks.set_is_single_bss_response(false);
264 let monitor = StationaryMonitor {
265 connection_data,
266 telemetry_sender,
267 saved_networks: saved_networks.clone(),
268 scan_backoff: MIN_BACKOFF_BETWEEN_ROAM_SCANS,
269 past_roams: past_roams.clone(),
270 next_roaming_enabled_time: fasync::MonotonicInstant::now(),
271 };
272 TestValues { monitor, telemetry_receiver, saved_networks, past_roams }
273 }
274
275 fn setup_test_with_data(connection_data: RoamingConnectionData) -> TestValues {
276 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
277 let telemetry_sender = TelemetrySender::new(telemetry_sender);
278 let saved_networks = Arc::new(FakeSavedNetworksManager::new());
279 let past_roams = Arc::new(Mutex::new(PastRoamList::new()));
280 let monitor = StationaryMonitor {
281 connection_data,
282 telemetry_sender,
283 saved_networks: saved_networks.clone(),
284 scan_backoff: MIN_BACKOFF_BETWEEN_ROAM_SCANS,
285 past_roams: past_roams.clone(),
286 next_roaming_enabled_time: fasync::MonotonicInstant::now(),
287 };
288 TestValues { monitor, telemetry_receiver, saved_networks, past_roams }
289 }
290
291 fn run_handle_roam_trigger_data(
294 exec: &mut fasync::TestExecutor,
295 monitor: &mut StationaryMonitor,
296 trigger_data: RoamTriggerData,
297 ) -> RoamTriggerDataOutcome {
298 return assert_matches!(exec.run_until_stalled(&mut monitor.handle_roam_trigger_data(trigger_data)), Poll::Ready(Ok(should_roam)) => {should_roam});
299 }
300
301 #[test_case(-80, true; "bad rssi")]
302 #[test_case(-40, false; "good rssi")]
303 #[fuchsia::test(add_test_attr = false)]
304 fn test_handle_signal_report_trigger_data(rssi: i8, should_roam_search: bool) {
305 let mut exec = fasync::TestExecutor::new_with_fake_time();
306 exec.set_fake_time(fasync::MonotonicInstant::now());
307
308 let connection_data = RoamingConnectionData {
310 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
311 ..generate_random_roaming_connection_data()
312 };
313 let mut test_values = setup_test_with_data(connection_data);
314
315 exec.set_fake_time(fasync::MonotonicInstant::after(fasync::MonotonicDuration::from_hours(
317 1,
318 )));
319
320 let trigger_data =
323 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
324 rssi_dbm: Some(rssi),
325 snr_db: Some(TEST_OK_SNR as i8),
326 tx_rate_500kbps: Some(0),
327 ..Default::default()
328 });
329 let result =
330 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
331
332 if should_roam_search {
333 assert_matches!(result, RoamTriggerDataOutcome::RoamSearch { .. });
334 } else {
335 assert_matches!(result, RoamTriggerDataOutcome::Noop);
336 }
337 }
338
339 #[fuchsia::test]
340 fn test_stationary_monitor_uses_active_scans() {
341 let mut exec = fasync::TestExecutor::new_with_fake_time();
342 exec.set_fake_time(fasync::MonotonicInstant::now());
343
344 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
347 let connection_data = RoamingConnectionData {
348 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
349 ..generate_random_roaming_connection_data()
350 };
351 let mut test_values = setup_test_with_data(connection_data);
352
353 let trigger_data =
356 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
357 rssi_dbm: Some(rssi as i8),
358 snr_db: Some(TEST_OK_SNR as i8),
359 tx_rate_500kbps: Some(0),
360 ..Default::default()
361 });
362
363 exec.set_fake_time(fasync::MonotonicInstant::after(fasync::MonotonicDuration::from_hours(
365 1,
366 )));
367
368 assert_matches!(
370 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
371 RoamTriggerDataOutcome::RoamSearch { scan_type: fidl_common::ScanType::Active, .. }
372 );
373 }
374
375 #[fuchsia::test]
376 fn test_minimum_time_between_roam_scans() {
377 let mut exec = fasync::TestExecutor::new_with_fake_time();
378 exec.set_fake_time(fasync::MonotonicInstant::now());
379
380 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
383 let connection_data = RoamingConnectionData {
384 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
385 ..generate_random_roaming_connection_data()
386 };
387 let mut test_values = setup_test_with_data(connection_data);
388 let trigger_data =
389 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
390 rssi_dbm: Some(rssi as i8),
391 snr_db: Some(TEST_OK_SNR as i8),
392 tx_rate_500kbps: Some(0),
393 ..Default::default()
394 });
395
396 exec.set_fake_time(fasync::MonotonicInstant::after(
398 MIN_BACKOFF_BETWEEN_ROAM_SCANS - fasync::MonotonicDuration::from_seconds(1),
399 ));
400
401 assert_matches!(
404 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
405 RoamTriggerDataOutcome::Noop
406 );
407
408 exec.set_fake_time(fasync::MonotonicInstant::after(
410 fasync::MonotonicDuration::from_seconds(2),
411 ));
412
413 assert_matches!(
415 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
416 RoamTriggerDataOutcome::RoamSearch { .. }
417 );
418 }
419
420 #[fuchsia::test]
421 fn test_roam_scans_backoff_exponentially() {
422 let mut exec = fasync::TestExecutor::new_with_fake_time();
423 exec.set_fake_time(fasync::MonotonicInstant::now());
424
425 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
428 let connection_data = RoamingConnectionData {
429 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
430 ..generate_random_roaming_connection_data()
431 };
432 let trigger_data =
433 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
434 rssi_dbm: Some(rssi as i8),
435 snr_db: Some(TEST_OK_SNR as i8),
436 tx_rate_500kbps: Some(0),
437 ..Default::default()
438 });
439 let mut test_values = setup_test_with_data(connection_data);
440
441 let mut expected_backoff = MIN_BACKOFF_BETWEEN_ROAM_SCANS;
443 while expected_backoff <= MAX_BACKOFF_BETWEEN_ROAM_SCANS {
444 exec.set_fake_time(fasync::MonotonicInstant::after(
446 expected_backoff - fasync::MonotonicDuration::from_seconds(1),
447 ));
448
449 assert_matches!(
452 run_handle_roam_trigger_data(
453 &mut exec,
454 &mut test_values.monitor,
455 trigger_data.clone()
456 ),
457 RoamTriggerDataOutcome::Noop
458 );
459
460 exec.set_fake_time(fasync::MonotonicInstant::after(
462 fasync::MonotonicDuration::from_seconds(2),
463 ));
464
465 assert_matches!(
467 run_handle_roam_trigger_data(
468 &mut exec,
469 &mut test_values.monitor,
470 trigger_data.clone()
471 ),
472 RoamTriggerDataOutcome::RoamSearch { .. }
473 );
474
475 expected_backoff = expected_backoff * 2;
477 }
478 exec.set_fake_time(fasync::MonotonicInstant::after(
481 MAX_BACKOFF_BETWEEN_ROAM_SCANS + fasync::MonotonicDuration::from_seconds(1),
482 ));
483 assert_matches!(
484 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
485 RoamTriggerDataOutcome::RoamSearch { .. }
486 );
487 }
488
489 #[fuchsia::test]
490 fn test_roam_attempt_resets_backoff() {
491 let mut exec = fasync::TestExecutor::new_with_fake_time();
492 exec.set_fake_time(fasync::MonotonicInstant::now());
493
494 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
497 let connection_data = RoamingConnectionData {
498 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
499 ..generate_random_roaming_connection_data()
500 };
501 let trigger_data =
502 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
503 rssi_dbm: Some(rssi as i8),
504 snr_db: Some(TEST_OK_SNR as i8),
505 tx_rate_500kbps: Some(0),
506 ..Default::default()
507 });
508 let mut test_values = setup_test_with_data(connection_data);
509
510 exec.set_fake_time(
512 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
513 + zx::MonotonicDuration::from_seconds(1),
514 );
515
516 assert_matches!(
518 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
519 RoamTriggerDataOutcome::RoamSearch { .. }
520 );
521
522 exec.set_fake_time(
524 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
525 + zx::MonotonicDuration::from_seconds(1),
526 );
527
528 assert_matches!(
530 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
531 RoamTriggerDataOutcome::Noop
532 );
533
534 test_values.monitor.notify_of_roam_attempt();
536
537 assert_matches!(
540 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
541 RoamTriggerDataOutcome::RoamSearch { .. }
542 );
543 }
544
545 #[fuchsia::test]
546 fn test_rssi_drop_resets_backoff() {
547 let mut exec = fasync::TestExecutor::new_with_fake_time();
548 exec.set_fake_time(fasync::MonotonicInstant::now());
549
550 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 1.0;
553 let connection_data = RoamingConnectionData {
554 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 1),
555 ..generate_random_roaming_connection_data()
556 };
557 let trigger_data =
558 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
559 rssi_dbm: Some(rssi as i8),
560 snr_db: Some(TEST_OK_SNR as i8),
561 tx_rate_500kbps: Some(0),
562 ..Default::default()
563 });
564 let mut test_values = setup_test_with_data(connection_data);
565
566 exec.set_fake_time(
568 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
569 + zx::MonotonicDuration::from_seconds(1),
570 );
571
572 assert_matches!(
574 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
575 RoamTriggerDataOutcome::RoamSearch { .. }
576 );
577
578 exec.set_fake_time(
580 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
581 + zx::MonotonicDuration::from_seconds(1),
582 );
583
584 assert_matches!(
586 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
587 RoamTriggerDataOutcome::Noop
588 );
589
590 let trigger_data =
593 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
594 rssi_dbm: Some((rssi - MIN_RSSI_DROP_TO_RESET_BACKOFF) as i8),
595 snr_db: Some(TEST_OK_SNR as i8),
596 tx_rate_500kbps: Some(0),
597 ..Default::default()
598 });
599 assert_matches!(
600 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
601 RoamTriggerDataOutcome::RoamSearch { .. }
602 );
603
604 exec.set_fake_time(
606 fasync::MonotonicInstant::after(MIN_BACKOFF_BETWEEN_ROAM_SCANS)
607 - zx::MonotonicDuration::from_seconds(1),
608 );
609 assert_matches!(
612 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone()),
613 RoamTriggerDataOutcome::Noop
614 );
615 }
616
617 #[fuchsia::test]
618 fn test_should_send_roam_request() {
619 let _exec = fasync::TestExecutor::new();
620 let test_values = setup_test();
621
622 let current_rssi = test_values.monitor.connection_data.signal_data.ewma_rssi.get();
624
625 let candidate = types::ScannedCandidate {
627 bss: types::Bss {
628 signal: types::Signal {
629 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM - 1.0) 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 {
645 bss: types::Bss {
646 signal: types::Signal {
647 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM) as i8,
648 snr_db: TEST_OK_SNR as i8,
649 },
650 ..generate_random_bss()
651 },
652 ..generate_random_scanned_candidate()
653 };
654 assert!(
655 test_values
656 .monitor
657 .should_send_roam_request(PolicyRoamRequest { candidate, reasons: vec![] })
658 .expect("failed to check roam request")
659 );
660
661 let candidate = types::ScannedCandidate {
664 bss: types::Bss {
665 signal: types::Signal {
666 rssi_dbm: (current_rssi + MIN_RSSI_IMPROVEMENT_TO_ROAM + 1.0) as i8,
667 snr_db: TEST_OK_SNR as i8,
668 },
669 bssid: test_values.monitor.connection_data.ap_state.original().bssid,
670 ..generate_random_bss()
671 },
672 credential: generate_random_password(),
673 ..generate_random_scanned_candidate()
674 };
675 assert!(
676 !test_values
677 .monitor
678 .should_send_roam_request(PolicyRoamRequest { candidate, reasons: vec![] })
679 .expect("failed to check roam reqeust")
680 );
681 }
682
683 #[fuchsia::test]
684 fn test_send_signal_velocity_metric_event() {
685 let mut exec = fasync::TestExecutor::new_with_fake_time();
686 exec.set_fake_time(fasync::MonotonicInstant::now());
687
688 let connection_data = RoamingConnectionData {
689 signal_data: EwmaSignalData::new(-40, 50, 1),
690 ..generate_random_roaming_connection_data()
691 };
692 let mut test_values = setup_test_with_data(connection_data);
693 test_values.saved_networks.set_is_single_bss_response(true);
694
695 let trigger_data =
696 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
697 rssi_dbm: Some(-80),
698 snr_db: Some(TEST_OK_SNR as i8),
699 tx_rate_500kbps: Some(0),
700 ..Default::default()
701 });
702 let _ =
703 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
704
705 assert_matches!(
706 test_values.telemetry_receiver.try_recv(),
707 Ok(TelemetryEvent::OnSignalVelocityUpdate { .. })
708 );
709 }
710
711 #[fuchsia::test]
712 fn test_should_not_roam_scan_single_bss() {
713 let mut exec = fasync::TestExecutor::new_with_fake_time();
714 exec.set_fake_time(fasync::MonotonicInstant::now());
715
716 let rssi = -80;
717 let connection_data = RoamingConnectionData {
718 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
719 ..generate_random_roaming_connection_data()
720 };
721 let mut test_values = setup_test_with_data(connection_data);
722
723 test_values.saved_networks.set_is_single_bss_response(true);
725
726 exec.set_fake_time(fasync::MonotonicInstant::after(fasync::MonotonicDuration::from_hours(
728 1,
729 )));
730
731 let trigger_data =
732 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
733 rssi_dbm: Some(rssi),
734 snr_db: Some(TEST_OK_SNR as i8),
735 tx_rate_500kbps: Some(0),
736 ..Default::default()
737 });
738 let trigger_result =
739 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
740
741 assert_eq!(trigger_result, RoamTriggerDataOutcome::Noop);
742 }
743
744 #[fuchsia::test]
745 fn test_roam_not_considered_if_attempted_too_many_times_today() {
746 let mut exec = fasync::TestExecutor::new_with_fake_time();
747 let first_roam_time = fasync::MonotonicInstant::now();
748 exec.set_fake_time(first_roam_time);
749
750 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 5.0;
752 let connection_data = RoamingConnectionData {
753 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
754 ..generate_random_roaming_connection_data()
755 };
756 let mut test_values = setup_test_with_data(connection_data);
757
758 for _ in 0..NUM_MAX_ROAMS_PER_DAY {
760 test_values.past_roams.try_lock().unwrap().add(RoamEvent::new_roam_now());
761 exec.set_fake_time(fasync::MonotonicInstant::after(MAX_BACKOFF_BETWEEN_ROAM_SCANS));
762 }
763
764 let trigger_data =
765 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
766 rssi_dbm: Some(rssi as i8),
767 snr_db: Some(TEST_OK_SNR as i8),
768 tx_rate_500kbps: Some(0),
769 ..Default::default()
770 });
771
772 exec.set_fake_time(fasync::MonotonicInstant::after(
773 MAX_BACKOFF_BETWEEN_ROAM_SCANS + zx::MonotonicDuration::from_seconds(1),
774 ));
775
776 let should_roam_scan_result =
778 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
779 assert_eq!(should_roam_scan_result, RoamTriggerDataOutcome::Noop);
780
781 exec.set_fake_time(
784 first_roam_time
785 + zx::MonotonicDuration::from_hours(24)
786 + zx::MonotonicDuration::from_seconds(1),
787 );
788 let should_roam_scan_result =
789 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
790 assert_matches!(should_roam_scan_result, RoamTriggerDataOutcome::RoamSearch { .. });
791 }
792
793 #[fuchsia::test]
794 fn test_roaming_disabled_timer_is_set_and_respected() {
795 let mut exec = fasync::TestExecutor::new_with_fake_time();
796 let first_roam_time = fasync::MonotonicInstant::now();
797 exec.set_fake_time(first_roam_time);
798
799 let rssi = LOCAL_ROAM_THRESHOLD_RSSI_5G - 5.0;
801 let connection_data = RoamingConnectionData {
802 signal_data: EwmaSignalData::new(rssi, TEST_OK_SNR, 10),
803 ..generate_random_roaming_connection_data()
804 };
805 let mut test_values = setup_test_with_data(connection_data);
806
807 for i in 0..NUM_MAX_ROAMS_PER_DAY {
810 let time_of_roam = first_roam_time + zx::MonotonicDuration::from_minutes(i as i64);
811 exec.set_fake_time(time_of_roam);
812 test_values.past_roams.try_lock().unwrap().add(RoamEvent::new(time_of_roam));
813 }
814
815 let trigger_data =
816 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
817 rssi_dbm: Some(rssi as i8),
818 snr_db: Some(TEST_OK_SNR as i8),
819 tx_rate_500kbps: Some(0),
820 ..Default::default()
821 });
822
823 exec.set_fake_time(fasync::MonotonicInstant::after(zx::MonotonicDuration::from_minutes(
825 NUM_MAX_ROAMS_PER_DAY as i64,
826 )));
827
828 let should_roam_scan_result =
830 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
831 assert_eq!(should_roam_scan_result, RoamTriggerDataOutcome::Noop);
832
833 let expected_reenabled_time = first_roam_time + TIMESPAN_TO_LIMIT_SCANS;
836 assert_eq!(test_values.monitor.next_roaming_enabled_time, expected_reenabled_time);
837
838 exec.set_fake_time(expected_reenabled_time - zx::MonotonicDuration::from_seconds(1));
840 let should_roam_scan_result =
841 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
842 assert_eq!(should_roam_scan_result, RoamTriggerDataOutcome::Noop);
843
844 exec.set_fake_time(expected_reenabled_time + zx::MonotonicDuration::from_seconds(1));
846 let should_roam_scan_result =
847 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data.clone());
848 assert_matches!(should_roam_scan_result, RoamTriggerDataOutcome::RoamSearch { .. });
849 }
850
851 #[fuchsia::test]
852 fn test_stationary_monitor_ignores_missing_signal_fields() {
853 let mut exec = fasync::TestExecutor::new_with_fake_time();
854 exec.set_fake_time(fasync::MonotonicInstant::now());
855
856 let initial_rssi = -60.0;
857 let initial_snr = 25.0;
858 let connection_data = RoamingConnectionData {
859 signal_data: EwmaSignalData::new(initial_rssi, initial_snr, 1),
860 ..generate_random_roaming_connection_data()
861 };
862 let mut test_values = setup_test_with_data(connection_data);
863
864 let trigger_data =
866 RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
867 rssi_dbm: None,
868 snr_db: None,
869 tx_rate_500kbps: Some(54),
870 ..Default::default()
871 });
872
873 let result =
874 run_handle_roam_trigger_data(&mut exec, &mut test_values.monitor, trigger_data);
875 assert_eq!(result, RoamTriggerDataOutcome::Noop);
876
877 assert_eq!(test_values.monitor.connection_data.signal_data.ewma_rssi.get(), initial_rssi);
879 assert_eq!(test_values.monitor.connection_data.signal_data.ewma_snr.get(), initial_snr);
880 }
881}