1use std::cmp::max;
6
7use crate::client::types;
8use crate::config_management::FailureReason::CredentialRejected;
9use crate::util::pseudo_energy::*;
10
11const RSSI_AND_VELOCITY_SCORE_WEIGHT: f32 = 0.6;
13const SNR_SCORE_WEIGHT: f32 = 0.4;
14
15const LOWER_RSSI_BOUND_FOR_5G_BONUS: i16 = -64; const UPPER_RSSI_BOUND_FOR_5G_BONUS: i16 = -25; const MAX_5G_PREFERENCE_BOOST: i16 = 20;
19const TAPER_AMOUNT_FOR_5G_BONUS_PER_DBM_OUTSIDE_RANGE: i16 = 2;
20
21const SCORE_PENALTY_FOR_RECENT_CONNECT_FAILURE: i16 = 5;
23const THRESHOLD_EXCESSIVE_RECENT_CONNECT_FAILURES: usize = 5; const SCORE_PENALTY_FOR_EXCESSIVE_RECENT_CONNECT_FAILURES: i16 = 10;
25const SCORE_PENALTY_FOR_RECENT_CREDENTIAL_REJECTED: i16 = 30; const SCORE_PENALTY_FOR_SHORT_CONNECTION: i16 = 20;
27
28pub fn score_bss_scanned_candidate(bss_candidate: types::ScannedCandidate) -> i16 {
29 let mut score = calculate_base_signal_score(bss_candidate.bss.signal.rssi_dbm as i16);
30 let channel = bss_candidate.bss.channel;
31
32 if channel.is_5ghz() {
34 score = score.saturating_add(calculate_5g_bonus(score));
35 }
36
37 let matching_failures = bss_candidate
39 .saved_network_info
40 .recent_failures
41 .iter()
42 .filter(|failure| failure.bssid == bss_candidate.bss.bssid);
43 let mut connect_failure_count: usize = 0;
44 for failure in matching_failures {
45 if failure.reason == CredentialRejected {
48 score = score.saturating_sub(SCORE_PENALTY_FOR_RECENT_CREDENTIAL_REJECTED);
49 } else {
50 connect_failure_count += 1;
51 if connect_failure_count <= THRESHOLD_EXCESSIVE_RECENT_CONNECT_FAILURES {
52 score = score.saturating_sub(SCORE_PENALTY_FOR_RECENT_CONNECT_FAILURE);
53 } else {
54 score = score.saturating_sub(SCORE_PENALTY_FOR_EXCESSIVE_RECENT_CONNECT_FAILURES);
56 }
57 }
58 }
59 let short_connection_score: i16 = bss_candidate
61 .recent_short_connections()
62 .try_into()
63 .unwrap_or(i16::MAX)
64 .saturating_mul(SCORE_PENALTY_FOR_SHORT_CONNECTION);
65
66 score.saturating_sub(short_connection_score)
67}
68
69fn calculate_base_signal_score(rssi: i16) -> i16 {
76 if rssi <= -30 {
77 rssi
78 } else {
79 let m = -2.7735;
80 let b = -113.2;
81 let y = m * rssi as f64 + b;
82 y as i16
83 }
84}
85
86fn calculate_5g_bonus(rssi: i16) -> i16 {
87 let taper_rate = max(
89 max(LOWER_RSSI_BOUND_FOR_5G_BONUS - rssi, 0),
90 max(rssi - UPPER_RSSI_BOUND_FOR_5G_BONUS, 0),
91 );
92 max(0, MAX_5G_PREFERENCE_BOOST - (taper_rate * TAPER_AMOUNT_FOR_5G_BONUS_PER_DBM_OUTSIDE_RANGE))
94}
95
96pub fn score_current_connection_signal_data(
97 data: EwmaSignalData,
98 rssi_velocity: impl Into<f64> + std::cmp::PartialOrd<f64>,
99) -> u8 {
100 let rssi_velocity_score = match data.ewma_rssi.get() {
101 r if r <= -81.0 => match rssi_velocity {
102 v if v < -2.7 => 0,
103 v if v < -1.8 => 0,
104 v if v < -0.9 => 0,
105 v if v <= 0.9 => 0,
106 v if v <= 1.8 => 20,
107 v if v <= 2.7 => 18,
108 _ => 10,
109 },
110 r if r <= -76.0 => match rssi_velocity {
111 v if v < -2.7 => 0,
112 v if v < -1.8 => 0,
113 v if v < -0.9 => 0,
114 v if v <= 0.9 => 15,
115 v if v <= 1.8 => 28,
116 v if v <= 2.7 => 25,
117 _ => 15,
118 },
119 r if r <= -71.0 => match rssi_velocity {
120 v if v < -2.7 => 0,
121 v if v < -1.8 => 5,
122 v if v < -0.9 => 15,
123 v if v <= 0.9 => 30,
124 v if v <= 1.8 => 45,
125 v if v <= 2.7 => 38,
126 _ => 4,
127 },
128 r if r <= -66.0 => match rssi_velocity {
129 v if v < -2.7 => 10,
130 v if v < -1.8 => 18,
131 v if v < -0.9 => 30,
132 v if v <= 0.9 => 48,
133 v if v <= 1.8 => 60,
134 v if v <= 2.7 => 50,
135 _ => 38,
136 },
137 r if r <= -61.0 => match rssi_velocity {
138 v if v < -2.7 => 20,
139 v if v < -1.8 => 30,
140 v if v < -0.9 => 45,
141 v if v <= 0.9 => 70,
142 v if v <= 1.8 => 75,
143 v if v <= 2.7 => 60,
144 _ => 55,
145 },
146 r if r <= -56.0 => match rssi_velocity {
147 v if v < -2.7 => 40,
148 v if v < -1.8 => 50,
149 v if v < -0.9 => 63,
150 v if v <= 0.9 => 85,
151 v if v <= 1.8 => 85,
152 v if v <= 2.7 => 70,
153 _ => 65,
154 },
155 r if r <= -51.0 => match rssi_velocity {
156 v if v < -2.7 => 55,
157 v if v < -1.8 => 65,
158 v if v < -0.9 => 75,
159 v if v <= 0.9 => 95,
160 v if v <= 1.8 => 90,
161 v if v <= 2.7 => 80,
162 _ => 75,
163 },
164 _ => match rssi_velocity {
165 v if v < -2.7 => 60,
166 v if v < -1.8 => 70,
167 v if v < -0.9 => 80,
168 v if v <= 0.9 => 100,
169 v if v <= 1.8 => 95,
170 v if v <= 2.7 => 90,
171 _ => 80,
172 },
173 };
174
175 let snr_score = match data.ewma_snr.get() {
176 s if s <= 10.0 => 0,
177 s if s <= 15.0 => 15,
178 s if s <= 20.0 => 37,
179 s if s <= 25.0 => 53,
180 s if s <= 30.0 => 68,
181 s if s <= 35.0 => 80,
182 s if s <= 40.0 => 95,
183 _ => 100,
184 };
185
186 ((rssi_velocity_score as f32 * RSSI_AND_VELOCITY_SCORE_WEIGHT)
187 + (snr_score as f32 * SNR_SCORE_WEIGHT)) as u8
188}
189
190#[cfg(test)]
191mod test {
192 use super::*;
193 use crate::config_management::{ConnectFailure, FailureReason, PastConnectionData};
194 use crate::util::testing::{
195 generate_channel, generate_random_bss, generate_random_saved_network_data,
196 generate_random_scanned_candidate, random_connection_data,
197 };
198 use fuchsia_async as fasync;
199 use test_util::assert_gt;
200
201 fn connect_failure_with_bssid(bssid: types::Bssid) -> ConnectFailure {
202 ConnectFailure {
203 reason: FailureReason::GeneralFailure,
204 time: fasync::MonotonicInstant::INFINITE,
205 bssid,
206 }
207 }
208
209 fn past_connection_with_bssid_uptime(
210 bssid: types::Bssid,
211 uptime: zx::MonotonicDuration,
212 ) -> PastConnectionData {
213 PastConnectionData {
214 bssid,
215 connection_uptime: uptime,
216 disconnect_time: fasync::MonotonicInstant::INFINITE, ..random_connection_data()
218 }
219 }
220
221 #[fuchsia::test]
222 fn test_weights_sum_to_one() {
223 assert_eq!(RSSI_AND_VELOCITY_SCORE_WEIGHT + SNR_SCORE_WEIGHT, 1.0);
224 }
225
226 #[fasync::run_singlethreaded(test)]
227 async fn test_score_bss_prefers_less_short_connections() {
228 let bss_worse = types::Bss {
229 signal: types::Signal { rssi_dbm: -60, snr_db: 0 },
230 channel: generate_channel(3),
231 ..generate_random_bss()
232 };
233 let bss_better = types::Bss {
234 signal: types::Signal { rssi_dbm: -60, snr_db: 0 },
235 channel: generate_channel(3),
236 ..generate_random_bss()
237 };
238 let mut internal_data = generate_random_saved_network_data();
239 let short_uptime = zx::MonotonicDuration::from_seconds(30);
240 let okay_uptime = zx::MonotonicDuration::from_minutes(100);
241 let short_uptime_data = past_connection_with_bssid_uptime(bss_worse.bssid, short_uptime);
243 let okay_uptime_data = past_connection_with_bssid_uptime(bss_better.bssid, okay_uptime);
244 internal_data.past_connections.add(bss_worse.bssid, short_uptime_data);
245 internal_data.past_connections.add(bss_better.bssid, okay_uptime_data);
246 let shared_candidate_data = types::ScannedCandidate {
247 saved_network_info: internal_data,
248 ..generate_random_scanned_candidate()
249 };
250 let bss_worse = types::ScannedCandidate { bss: bss_worse, ..shared_candidate_data.clone() };
251 let bss_better =
252 types::ScannedCandidate { bss: bss_better, ..shared_candidate_data.clone() };
253
254 assert!(score_bss_scanned_candidate(bss_better) > score_bss_scanned_candidate(bss_worse));
256 }
257
258 #[fasync::run_singlethreaded(test)]
259 async fn test_score_bss_prefers_less_failures() {
260 let bss_worse = types::Bss {
261 signal: types::Signal { rssi_dbm: -60, snr_db: 0 },
262 channel: generate_channel(3),
263 ..generate_random_bss()
264 };
265 let bss_better = types::Bss {
266 signal: types::Signal { rssi_dbm: -60, snr_db: 0 },
267 channel: generate_channel(3),
268 ..generate_random_bss()
269 };
270 let mut internal_data = generate_random_saved_network_data();
271 let mut failures = vec![connect_failure_with_bssid(bss_worse.bssid); 12];
273 failures.push(connect_failure_with_bssid(bss_better.bssid));
274 internal_data.recent_failures = failures;
275 let shared_candidate_data = types::ScannedCandidate {
276 saved_network_info: internal_data,
277 ..generate_random_scanned_candidate()
278 };
279 let bss_worse = types::ScannedCandidate { bss: bss_worse, ..shared_candidate_data.clone() };
280 let bss_better =
281 types::ScannedCandidate { bss: bss_better, ..shared_candidate_data.clone() };
282 assert!(score_bss_scanned_candidate(bss_better) > score_bss_scanned_candidate(bss_worse));
284 }
285
286 #[fasync::run_singlethreaded(test)]
287 async fn test_score_bss_prefers_strong_5ghz_with_failures() {
288 let bss_worse = types::Bss {
291 signal: types::Signal { rssi_dbm: -35, snr_db: 0 },
292 channel: generate_channel(3),
293 ..generate_random_bss()
294 };
295 let bss_better = types::Bss {
296 signal: types::Signal { rssi_dbm: -35, snr_db: 0 },
297 channel: generate_channel(36),
298 ..generate_random_bss()
299 };
300 let mut internal_data = generate_random_saved_network_data();
301 internal_data.recent_failures = vec![connect_failure_with_bssid(bss_better.bssid); 2];
304 let shared_candidate_data = types::ScannedCandidate {
305 saved_network_info: internal_data,
306 ..generate_random_scanned_candidate()
307 };
308 let bss_worse = types::ScannedCandidate { bss: bss_worse, ..shared_candidate_data.clone() };
309 let bss_better =
310 types::ScannedCandidate { bss: bss_better, ..shared_candidate_data.clone() };
311 assert!(score_bss_scanned_candidate(bss_better) > score_bss_scanned_candidate(bss_worse));
312 }
313
314 #[fasync::run_singlethreaded(test)]
315 async fn test_score_credentials_rejected_worse() {
316 let bss_worse = types::Bss {
320 signal: types::Signal { rssi_dbm: -30, snr_db: 0 },
321 channel: generate_channel(44),
322 ..generate_random_bss()
323 };
324 let bss_better = types::Bss {
325 signal: types::Signal { rssi_dbm: -30, snr_db: 0 },
326 channel: generate_channel(44),
327 ..generate_random_bss()
328 };
329 let mut internal_data = generate_random_saved_network_data();
330 let mut failures = vec![connect_failure_with_bssid(bss_better.bssid); 4];
332 failures.push(ConnectFailure {
333 bssid: bss_worse.bssid,
334 time: fasync::MonotonicInstant::now(),
335 reason: FailureReason::CredentialRejected,
336 });
337 internal_data.recent_failures = failures;
338 let shared_candidate_data = types::ScannedCandidate {
339 saved_network_info: internal_data,
340 ..generate_random_scanned_candidate()
341 };
342 let bss_worse = types::ScannedCandidate { bss: bss_worse, ..shared_candidate_data.clone() };
343 let bss_better =
344 types::ScannedCandidate { bss: bss_better, ..shared_candidate_data.clone() };
345
346 assert!(score_bss_scanned_candidate(bss_better) > score_bss_scanned_candidate(bss_worse));
347 }
348
349 #[fasync::run_singlethreaded(test)]
350 async fn score_many_penalties_do_not_cause_panic() {
351 let bss = types::Bss {
352 signal: types::Signal { rssi_dbm: -80, snr_db: 0 },
353 channel: generate_channel(1),
354 ..generate_random_bss()
355 };
356 let mut internal_data = generate_random_saved_network_data();
357 internal_data.recent_failures = vec![connect_failure_with_bssid(bss.bssid); 10];
359 for _ in 0..1200 {
360 internal_data.recent_failures.push(ConnectFailure {
361 bssid: bss.bssid,
362 time: fasync::MonotonicInstant::now(),
363 reason: FailureReason::CredentialRejected,
364 });
365 }
366 let short_uptime = zx::MonotonicDuration::from_seconds(30);
367 let data = past_connection_with_bssid_uptime(bss.bssid, short_uptime);
368 for _ in 0..10 {
369 internal_data.past_connections.add(bss.bssid, data);
370 }
371 let scanned_candidate = types::ScannedCandidate {
372 bss,
373 saved_network_info: internal_data,
374 ..generate_random_scanned_candidate()
375 };
376
377 assert_eq!(score_bss_scanned_candidate(scanned_candidate), i16::MIN);
378 }
379
380 #[fuchsia::test]
383 fn high_rssi_scores_higher_than_low_rssi() {
384 let strong_clear_signal = EwmaSignalData::new(-50, 35, 10);
385 let weak_clear_signal = EwmaSignalData::new(-85, 35, 10);
386 assert_gt!(
387 score_current_connection_signal_data(strong_clear_signal, 0.0),
388 score_current_connection_signal_data(weak_clear_signal, 0.0)
389 );
390
391 let strong_noisy_signal = EwmaSignalData::new(-50, 5, 10);
392 let weak_noisy_signal = EwmaSignalData::new(-85, 55, 10);
393 assert_gt!(
394 score_current_connection_signal_data(strong_noisy_signal, 0.0),
395 score_current_connection_signal_data(weak_noisy_signal, 0.0)
396 );
397 }
398
399 #[fuchsia::test]
400 fn high_snr_scores_higher_than_low_snr() {
401 let strong_clear_signal = EwmaSignalData::new(-50, 35, 10);
402 let strong_noisy_signal = EwmaSignalData::new(-50, 5, 10);
403 assert_gt!(
404 score_current_connection_signal_data(strong_clear_signal, 0.0),
405 score_current_connection_signal_data(strong_noisy_signal, 0.0)
406 );
407
408 let weak_clear_signal = EwmaSignalData::new(-85, 35, 10);
409 let weak_noisy_signal = EwmaSignalData::new(-85, 5, 10);
410 assert_gt!(
411 score_current_connection_signal_data(weak_clear_signal, 0.0),
412 score_current_connection_signal_data(weak_noisy_signal, 0.0)
413 );
414 }
415
416 #[fuchsia::test]
417 fn positive_velocity_scores_higher_than_negative_velocity() {
418 let signal = EwmaSignalData::new(-50, 35, 10);
419 assert_gt!(
420 score_current_connection_signal_data(signal, 3.0),
421 score_current_connection_signal_data(signal, -3.0)
422 );
423 }
424
425 #[fuchsia::test]
426 fn stable_high_rssi_scores_higher_than_volatile_high_rssi() {
427 let strong_signal = EwmaSignalData::new(-50, 35, 10);
428 assert_gt!(
429 score_current_connection_signal_data(strong_signal, 0.0),
430 score_current_connection_signal_data(strong_signal, 3.0)
431 );
432 assert_gt!(
433 score_current_connection_signal_data(strong_signal, 0.0),
434 score_current_connection_signal_data(strong_signal, -3.0)
435 );
436 }
437
438 #[fuchsia::test]
439 fn improving_weak_rssi_scores_higher_than_stable_weak_rssi() {
440 let weak_signal = EwmaSignalData::new(-85, 10, 10);
441 assert_gt!(
442 score_current_connection_signal_data(weak_signal, 3.0),
443 score_current_connection_signal_data(weak_signal, 0.0)
444 );
445 }
446
447 #[fuchsia::test]
448 fn test_calculate_5g_bonus_max_bonus_between_cutoffs() {
449 assert_eq!(calculate_5g_bonus(LOWER_RSSI_BOUND_FOR_5G_BONUS), MAX_5G_PREFERENCE_BOOST);
450 assert_eq!(calculate_5g_bonus(LOWER_RSSI_BOUND_FOR_5G_BONUS + 1), MAX_5G_PREFERENCE_BOOST);
451 assert_eq!(calculate_5g_bonus(UPPER_RSSI_BOUND_FOR_5G_BONUS - 1), MAX_5G_PREFERENCE_BOOST);
452 assert_eq!(calculate_5g_bonus(UPPER_RSSI_BOUND_FOR_5G_BONUS), MAX_5G_PREFERENCE_BOOST);
453 }
454
455 #[fuchsia::test]
456 fn test_calculate_5g_bonus_linear_decrease_below_lower_cutoff() {
457 assert_eq!(
458 calculate_5g_bonus(LOWER_RSSI_BOUND_FOR_5G_BONUS - 1),
459 MAX_5G_PREFERENCE_BOOST - TAPER_AMOUNT_FOR_5G_BONUS_PER_DBM_OUTSIDE_RANGE
460 );
461 assert_eq!(
462 calculate_5g_bonus(LOWER_RSSI_BOUND_FOR_5G_BONUS - 2),
463 MAX_5G_PREFERENCE_BOOST - (2 * TAPER_AMOUNT_FOR_5G_BONUS_PER_DBM_OUTSIDE_RANGE)
464 );
465 assert_eq!(calculate_5g_bonus(LOWER_RSSI_BOUND_FOR_5G_BONUS - 10), 0);
466 assert_eq!(calculate_5g_bonus(LOWER_RSSI_BOUND_FOR_5G_BONUS - 20), 0);
467 }
468
469 #[fuchsia::test]
470 fn test_calculate_5g_bonus_linear_decrease_above_upper_cutoff() {
471 assert_eq!(
472 calculate_5g_bonus(UPPER_RSSI_BOUND_FOR_5G_BONUS + 1),
473 MAX_5G_PREFERENCE_BOOST - TAPER_AMOUNT_FOR_5G_BONUS_PER_DBM_OUTSIDE_RANGE
474 );
475 assert_eq!(
476 calculate_5g_bonus(UPPER_RSSI_BOUND_FOR_5G_BONUS + 2),
477 MAX_5G_PREFERENCE_BOOST - (2 * TAPER_AMOUNT_FOR_5G_BONUS_PER_DBM_OUTSIDE_RANGE)
478 );
479 assert_eq!(calculate_5g_bonus(UPPER_RSSI_BOUND_FOR_5G_BONUS + 10), 0);
480 assert_eq!(calculate_5g_bonus(UPPER_RSSI_BOUND_FOR_5G_BONUS + 20), 0);
481 }
482
483 #[fuchsia::test]
484 fn test_calculate_base_signal_score() {
485 assert_eq!(calculate_base_signal_score(-30), -30);
487 assert_eq!(calculate_base_signal_score(-50), -50);
488
489 assert_eq!(calculate_base_signal_score(-25), -43);
491 assert_eq!(calculate_base_signal_score(-20), -57);
492 }
493}