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