Skip to main content

wlan_telemetry/processors/
connect_disconnect.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::config::DeviceMobility;
6use crate::convert::{
7    convert_channel_band, convert_is_owe_transition, convert_rssi_bucket, convert_security_type,
8    convert_snr_bucket,
9};
10use crate::processors::toggle_events::ClientConnectionsToggleEvent;
11use crate::util::cobalt_logger::{FilteredCobaltLogger, log_cobalt_batch};
12use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
13use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
14use fidl_fuchsia_wlan_sme as fidl_sme;
15use fuchsia_async as fasync;
16use fuchsia_inspect::Node as InspectNode;
17use fuchsia_inspect_contrib::id_enum::IdEnum;
18use fuchsia_inspect_contrib::inspect_log;
19use fuchsia_inspect_contrib::nodes::{BoundedListNode, LruCacheNode};
20use fuchsia_inspect_derive::Unit;
21use fuchsia_sync::Mutex;
22use ieee80211::OuiFmt;
23use std::collections::HashMap;
24use std::sync::Arc;
25use std::sync::atomic::{AtomicUsize, Ordering};
26use strum_macros::{Display, EnumCount};
27use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
28use windowed_stats::experimental::series::interpolation::{ConstantSample, LastSample};
29use windowed_stats::experimental::series::metadata::{BitsetMap, BitsetNode};
30use windowed_stats::experimental::series::statistic::Union;
31use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
32use wlan_common::bss::BssDescription;
33use wlan_common::channel::Channel;
34use wlan_legacy_metrics_registry as metrics;
35use zx;
36
37const INSPECT_CONNECT_EVENTS_LIMIT: usize = 10;
38const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 20;
39const INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT: usize = 50;
40const INSPECT_CONNECTED_NETWORKS_ID_LIMIT: usize = 16;
41const INSPECT_DISCONNECT_SOURCES_ID_LIMIT: usize = 32;
42const INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT: usize = 32;
43const SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT: zx::BootDuration =
44    zx::BootDuration::from_minutes(2);
45const DAILY_METRICS_LOG_INTERVAL: zx::BootDuration = zx::BootDuration::from_hours(24);
46
47#[derive(Clone, Debug, Display, EnumCount)]
48enum ConnectionState {
49    Idle(IdleState),
50    Connected(ConnectedState),
51    Disconnected(DisconnectedState),
52    ConnectFailed(ConnectFailedState),
53    FailedToStart(FailedToStartState),
54    FailedToStop(FailedToStopState),
55    PnoScanFailedIdle(PnoScanFailedIdleState),
56}
57
58// Update the ConnectDisconnectTimeSeries BitsetMap when making changes to this enum.
59impl IdEnum for ConnectionState {
60    type Id = u8;
61    fn to_id(&self) -> Self::Id {
62        match self {
63            Self::Idle(_) => 0,
64            Self::Disconnected(_) => 1,
65            Self::ConnectFailed(_) => 2,
66            Self::Connected(_) => 3,
67            Self::FailedToStart(_) => 4,
68            Self::FailedToStop(_) => 5,
69            Self::PnoScanFailedIdle(_) => 6,
70        }
71    }
72}
73
74#[derive(Clone, Debug)]
75struct IdleState {}
76
77#[derive(Clone, Debug, PartialEq)]
78struct ConnectedState {
79    bss: Box<BssDescription>,
80    is_owe_transition: bool,
81}
82
83#[derive(Clone, Debug)]
84struct DisconnectedState {}
85
86#[derive(Clone, Debug)]
87struct ConnectFailedState {}
88
89#[derive(Clone, Debug)]
90struct FailedToStartState {}
91
92#[derive(Clone, Debug)]
93struct FailedToStopState {}
94
95#[derive(Clone, Debug)]
96struct PnoScanFailedIdleState {}
97
98#[derive(PartialEq, Eq, Unit, Hash)]
99struct InspectConnectedNetwork {
100    bssid: String,
101    ssid: String,
102    protection: String,
103    ht_cap: Option<Vec<u8>>,
104    vht_cap: Option<Vec<u8>>,
105    is_wmm_assoc: bool,
106    wmm_param: Option<Vec<u8>>,
107}
108
109impl From<&BssDescription> for InspectConnectedNetwork {
110    fn from(bss_description: &BssDescription) -> Self {
111        Self {
112            bssid: bss_description.bssid.to_string(),
113            ssid: bss_description.ssid.to_string(),
114            protection: format!("{:?}", bss_description.protection()),
115            ht_cap: bss_description.raw_ht_cap().map(|cap| cap.bytes.into()),
116            vht_cap: bss_description.raw_vht_cap().map(|cap| cap.bytes.into()),
117            is_wmm_assoc: bss_description.find_wmm_param().is_some(),
118            wmm_param: bss_description.find_wmm_param().map(|bytes| bytes.into()),
119        }
120    }
121}
122
123#[derive(PartialEq, Eq, Unit, Hash)]
124struct InspectConnectAttemptResult {
125    status_code: u16,
126    result: String,
127}
128
129#[derive(PartialEq, Eq, Unit, Hash)]
130struct InspectDisconnectSource {
131    source: String,
132    reason: String,
133    mlme_event_name: Option<String>,
134}
135
136impl From<&fidl_sme::DisconnectSource> for InspectDisconnectSource {
137    fn from(disconnect_source: &fidl_sme::DisconnectSource) -> Self {
138        match disconnect_source {
139            fidl_sme::DisconnectSource::User(reason) => Self {
140                source: "user".to_string(),
141                reason: format!("{reason:?}"),
142                mlme_event_name: None,
143            },
144            fidl_sme::DisconnectSource::Ap(cause) => Self {
145                source: "ap".to_string(),
146                reason: format!("{:?}", cause.reason_code),
147                mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
148            },
149            fidl_sme::DisconnectSource::Mlme(cause) => Self {
150                source: "mlme".to_string(),
151                reason: format!("{:?}", cause.reason_code),
152                mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
153            },
154        }
155    }
156}
157
158#[derive(Clone, Debug, PartialEq)]
159pub struct DisconnectInfo {
160    pub iface_id: u16,
161    pub connected_duration: zx::BootDuration,
162    pub is_sme_reconnecting: bool,
163    pub disconnect_source: fidl_sme::DisconnectSource,
164    pub original_bss_desc: Box<BssDescription>,
165    pub current_rssi_dbm: i8,
166    pub current_snr_db: i8,
167    pub current_channel: Channel,
168}
169
170pub struct ConnectDisconnectLogger {
171    connection_state: Arc<Mutex<ConnectionState>>,
172    cobalt_proxy: Arc<FilteredCobaltLogger>,
173    connect_events_node: Mutex<BoundedListNode>,
174    disconnect_events_node: Mutex<BoundedListNode>,
175    connect_attempt_results_node: Mutex<BoundedListNode>,
176    inspect_metadata_node: Mutex<InspectMetadataNode>,
177    time_series_stats: ConnectDisconnectTimeSeries,
178    successive_connect_attempt_failures: AtomicUsize,
179    last_connect_failure_at: Arc<Mutex<Option<fasync::BootInstant>>>,
180    last_disconnect_at: Arc<Mutex<Option<fasync::MonotonicInstant>>>,
181    daily_connect_stats: Mutex<DailyConnectStats>,
182    device_mobility: DeviceMobility,
183}
184
185impl ConnectDisconnectLogger {
186    pub fn new<S: InspectSender>(
187        cobalt_proxy: Arc<FilteredCobaltLogger>,
188        inspect_node: &InspectNode,
189        inspect_metadata_node: &InspectNode,
190        inspect_metadata_path: &str,
191        time_matrix_client: &S,
192        device_mobility: DeviceMobility,
193    ) -> Self {
194        let connect_events = inspect_node.create_child("connect_events");
195        let disconnect_events = inspect_node.create_child("disconnect_events");
196        let connect_attempt_results = inspect_node.create_child("connect_attempt_results");
197        let this = Self {
198            cobalt_proxy,
199            connection_state: Arc::new(Mutex::new(ConnectionState::Idle(IdleState {}))),
200            connect_events_node: Mutex::new(BoundedListNode::new(
201                connect_events,
202                INSPECT_CONNECT_EVENTS_LIMIT,
203            )),
204            disconnect_events_node: Mutex::new(BoundedListNode::new(
205                disconnect_events,
206                INSPECT_DISCONNECT_EVENTS_LIMIT,
207            )),
208            connect_attempt_results_node: Mutex::new(BoundedListNode::new(
209                connect_attempt_results,
210                INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT,
211            )),
212            inspect_metadata_node: Mutex::new(InspectMetadataNode::new(inspect_metadata_node)),
213            time_series_stats: ConnectDisconnectTimeSeries::new(
214                time_matrix_client,
215                inspect_metadata_path,
216            ),
217            successive_connect_attempt_failures: AtomicUsize::new(0),
218            last_connect_failure_at: Arc::new(Mutex::new(None)),
219            last_disconnect_at: Arc::new(Mutex::new(None)),
220            daily_connect_stats: Mutex::new(DailyConnectStats::new(fasync::BootInstant::now())),
221            device_mobility,
222        };
223        this.log_connection_state();
224        this
225    }
226
227    fn update_connection_state(&self, state: ConnectionState) {
228        *self.connection_state.lock() = state;
229        self.log_connection_state();
230    }
231
232    fn log_connection_state(&self) {
233        let wlan_connectivity_state_id = self.connection_state.lock().to_id() as u64;
234        self.time_series_stats.log_wlan_connectivity_state(1 << wlan_connectivity_state_id);
235    }
236
237    pub fn is_connected(&self) -> bool {
238        matches!(*self.connection_state.lock(), ConnectionState::Connected(_))
239    }
240
241    pub async fn handle_connect_attempt(
242        &self,
243        result: fidl_ieee80211::StatusCode,
244        bss: &BssDescription,
245        is_credential_rejected: bool,
246        is_owe_transition: bool,
247    ) {
248        let mut flushed_successive_failures = None;
249        let mut downtime_duration = None;
250        if result == fidl_ieee80211::StatusCode::Success {
251            self.update_connection_state(ConnectionState::Connected(ConnectedState {
252                bss: Box::new(bss.clone()),
253                is_owe_transition,
254            }));
255            flushed_successive_failures =
256                Some(self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst));
257            downtime_duration =
258                self.last_disconnect_at.lock().map(|t| fasync::MonotonicInstant::now() - t);
259        } else if is_credential_rejected {
260            self.update_connection_state(ConnectionState::Idle(IdleState {}));
261            let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
262            let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
263        } else {
264            self.update_connection_state(ConnectionState::ConnectFailed(ConnectFailedState {}));
265            let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
266            let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
267        }
268
269        self.log_connect_attempt_inspect(result, bss);
270        self.log_connect_attempt_cobalt(result, flushed_successive_failures, downtime_duration)
271            .await;
272        if result == fidl_ieee80211::StatusCode::Success {
273            self.log_device_connected_cobalt_metrics(bss, is_owe_transition).await;
274        }
275
276        let security_type = convert_security_type(&bss.protection());
277        let primary_channel = bss.channel.primary;
278        let channel_band = convert_channel_band(bss.channel.band);
279        let rssi_bucket = convert_rssi_bucket(bss.rssi_dbm);
280        let snr_bucket = convert_snr_bucket(bss.snr_db);
281        let is_owe_transition_dim = convert_is_owe_transition(is_owe_transition);
282
283        let mut daily_stats = self.daily_connect_stats.lock();
284        daily_stats.connect_per_security_type.entry(security_type).or_default().increment(result);
285        daily_stats
286            .connect_per_primary_channel
287            .entry(primary_channel)
288            .or_default()
289            .increment(result);
290        daily_stats.connect_per_channel_band.entry(channel_band).or_default().increment(result);
291        daily_stats.connect_per_rssi_bucket.entry(rssi_bucket).or_default().increment(result);
292        daily_stats.connect_per_snr_bucket.entry(snr_bucket).or_default().increment(result);
293        daily_stats
294            .connect_per_is_owe_transition
295            .entry(is_owe_transition_dim)
296            .or_default()
297            .increment(result);
298    }
299
300    fn log_connect_attempt_inspect(
301        &self,
302        result: fidl_ieee80211::StatusCode,
303        bss: &BssDescription,
304    ) {
305        let mut inspect_metadata_node = self.inspect_metadata_node.lock();
306        let connect_result_id =
307            inspect_metadata_node.connect_attempt_results.insert(InspectConnectAttemptResult {
308                status_code: result.into_primitive(),
309                result: format!("{:?}", result),
310            }) as u64;
311        self.time_series_stats.log_connect_attempt_results(1 << connect_result_id);
312
313        inspect_log!(self.connect_attempt_results_node.lock(), {
314            result: format!("{:?}", result),
315            ssid: bss.ssid.to_string(),
316            bssid: bss.bssid.to_string(),
317            protection: format!("{:?}", bss.protection()),
318        });
319
320        if result == fidl_ieee80211::StatusCode::Success {
321            let connected_network = InspectConnectedNetwork::from(bss);
322            let connected_network_id =
323                inspect_metadata_node.connected_networks.insert(connected_network) as u64;
324
325            self.time_series_stats.log_connected_networks(1 << connected_network_id);
326
327            inspect_log!(self.connect_events_node.lock(), {
328                network_id: connected_network_id,
329            });
330        }
331    }
332
333    #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
334    async fn log_connect_attempt_cobalt(
335        &self,
336        result: fidl_ieee80211::StatusCode,
337        flushed_successive_failures: Option<usize>,
338        downtime_duration: Option<zx::MonotonicDuration>,
339    ) {
340        let mut metric_events = vec![];
341        metric_events.push(MetricEvent {
342            metric_id: metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
343            event_codes: vec![result.into_primitive() as u32],
344            payload: MetricEventPayload::Count(1),
345        });
346
347        if let Some(failures) = flushed_successive_failures {
348            metric_events.push(MetricEvent {
349                metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
350                event_codes: vec![],
351                payload: MetricEventPayload::IntegerValue(failures as i64),
352            });
353        }
354
355        if let Some(duration) = downtime_duration {
356            metric_events.push(MetricEvent {
357                metric_id: metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
358                event_codes: vec![],
359                payload: MetricEventPayload::IntegerValue(duration.into_millis()),
360            });
361        }
362
363        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_connect_attempt_cobalt");
364    }
365
366    async fn log_device_connected_cobalt_metrics(
367        &self,
368        bss: &BssDescription,
369        is_owe_transition: bool,
370    ) {
371        let mut metric_events = vec![];
372        append_device_connected_cobalt_metrics(&mut metric_events, bss, is_owe_transition);
373        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_device_connected_cobalt_metrics");
374    }
375
376    pub async fn handle_channel_switched(&self, channel: Channel) {
377        if let ConnectionState::Connected(ref mut state) = *self.connection_state.lock() {
378            state.bss.channel = channel;
379        }
380        let mut metric_events = vec![];
381        append_device_connected_channel_cobalt_metrics(&mut metric_events, channel);
382        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_channel_switched");
383    }
384
385    pub async fn log_disconnect(&self, info: &DisconnectInfo) {
386        match self.device_mobility {
387            DeviceMobility::Mobile => {
388                // Mobile devices can be considered idle if they disconnect for reasons associated
389                // with going out of range or are commanded to disconnect by upper layers.
390                if !info.disconnect_source.should_log_for_mobile_device() {
391                    self.update_connection_state(ConnectionState::Idle(IdleState {}));
392                } else {
393                    self.update_connection_state(ConnectionState::Disconnected(
394                        DisconnectedState {},
395                    ));
396                }
397            }
398            DeviceMobility::Stationary => {
399                self.update_connection_state(ConnectionState::Disconnected(DisconnectedState {}));
400            }
401        }
402        let _prev = self.last_disconnect_at.lock().replace(fasync::MonotonicInstant::now());
403        self.log_disconnect_inspect(info);
404        self.log_disconnect_cobalt(info).await;
405    }
406
407    fn log_disconnect_inspect(&self, info: &DisconnectInfo) {
408        let mut inspect_metadata_node = self.inspect_metadata_node.lock();
409        let connected_network = InspectConnectedNetwork::from(&*info.original_bss_desc);
410        let connected_network_id =
411            inspect_metadata_node.connected_networks.insert(connected_network) as u64;
412        let disconnect_source = InspectDisconnectSource::from(&info.disconnect_source);
413        let disconnect_source_id =
414            inspect_metadata_node.disconnect_sources.insert(disconnect_source) as u64;
415        inspect_log!(self.disconnect_events_node.lock(), {
416            connected_duration: info.connected_duration.into_nanos(),
417            disconnect_source_id: disconnect_source_id,
418            network_id: connected_network_id,
419            rssi_dbm: info.current_rssi_dbm,
420            snr_db: info.current_snr_db,
421            channel: format!("{}", info.current_channel),
422        });
423
424        self.time_series_stats.log_disconnected_networks(1 << connected_network_id);
425        self.time_series_stats.log_disconnect_sources(1 << disconnect_source_id);
426    }
427
428    async fn log_disconnect_cobalt(&self, info: &DisconnectInfo) {
429        let mut metric_events = vec![];
430        metric_events.push(MetricEvent {
431            metric_id: metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
432            event_codes: vec![],
433            payload: MetricEventPayload::Count(1),
434        });
435
436        if self.device_mobility == DeviceMobility::Mobile
437            && info.disconnect_source.should_log_for_mobile_device()
438        {
439            metric_events.push(MetricEvent {
440                metric_id: metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID,
441                event_codes: vec![],
442                payload: MetricEventPayload::Count(1),
443            });
444        }
445
446        metric_events.push(MetricEvent {
447            metric_id: metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
448            event_codes: vec![],
449            payload: MetricEventPayload::IntegerValue(info.connected_duration.into_millis()),
450        });
451
452        metric_events.push(MetricEvent {
453            metric_id: metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
454            event_codes: vec![
455                u32::from(info.disconnect_source.cobalt_reason_code()),
456                info.disconnect_source.as_cobalt_disconnect_source() as u32,
457            ],
458            payload: MetricEventPayload::Count(1),
459        });
460
461        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_disconnect_cobalt");
462    }
463
464    pub async fn handle_periodic_telemetry(&self) {
465        let mut metric_events = vec![];
466        let now = fasync::BootInstant::now();
467        if let Some(failed_at) = *self.last_connect_failure_at.lock()
468            && now - failed_at >= SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT
469        {
470            let failures = self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
471            if failures > 0 {
472                metric_events.push(MetricEvent {
473                    metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
474                    event_codes: vec![],
475                    payload: MetricEventPayload::IntegerValue(failures as i64),
476                });
477            }
478        }
479
480        {
481            let mut daily_stats = self.daily_connect_stats.lock();
482            if now - daily_stats.last_log_time >= DAILY_METRICS_LOG_INTERVAL {
483                if let ConnectionState::Connected(ref state) = *self.connection_state.lock() {
484                    append_device_connected_cobalt_metrics(
485                        &mut metric_events,
486                        &state.bss,
487                        state.is_owe_transition,
488                    );
489                }
490
491                for (security_type, counter) in daily_stats.connect_per_security_type.drain() {
492                    if counter.total > 0 {
493                        let success_rate = counter.success as f64 / counter.total as f64;
494                        metric_events.push(MetricEvent {
495                            metric_id:
496                                metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
497                            event_codes: vec![security_type as u32],
498                            payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
499                                success_rate,
500                            )),
501                        });
502                    }
503                }
504                for (primary_channel, counter) in daily_stats.connect_per_primary_channel.drain() {
505                    if counter.total > 0 {
506                        let success_rate = counter.success as f64 / counter.total as f64;
507                        metric_events.push(MetricEvent {
508                            metric_id:
509                                metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
510                            event_codes: vec![primary_channel as u32],
511                            payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
512                                success_rate,
513                            )),
514                        });
515                    }
516                }
517                for (channel_band, counter) in daily_stats.connect_per_channel_band.drain() {
518                    if counter.total > 0 {
519                        let success_rate = counter.success as f64 / counter.total as f64;
520                        metric_events.push(MetricEvent {
521                            metric_id:
522                                metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
523                            event_codes: vec![channel_band as u32],
524                            payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
525                                success_rate,
526                            )),
527                        });
528                    }
529                }
530                for (rssi_bucket, counter) in daily_stats.connect_per_rssi_bucket.drain() {
531                    if counter.total > 0 {
532                        let success_rate = counter.success as f64 / counter.total as f64;
533                        metric_events.push(MetricEvent {
534                            metric_id:
535                                metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
536                            event_codes: vec![rssi_bucket as u32],
537                            payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
538                                success_rate,
539                            )),
540                        });
541                    }
542                }
543                for (snr_bucket, counter) in daily_stats.connect_per_snr_bucket.drain() {
544                    if counter.total > 0 {
545                        let success_rate = counter.success as f64 / counter.total as f64;
546                        metric_events.push(MetricEvent {
547                            metric_id:
548                                metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
549                            event_codes: vec![snr_bucket as u32],
550                            payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
551                                success_rate,
552                            )),
553                        });
554                    }
555                }
556                for (is_owe_transition, counter) in
557                    daily_stats.connect_per_is_owe_transition.drain()
558                {
559                    if counter.total > 0 {
560                        let success_rate = counter.success as f64 / counter.total as f64;
561                        metric_events.push(MetricEvent {
562                            metric_id:
563                                metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
564                            event_codes: vec![is_owe_transition as u32],
565                            payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
566                                success_rate,
567                            )),
568                        });
569                    }
570                }
571                daily_stats.last_log_time = now;
572            }
573        }
574
575        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_periodic_telemetry");
576    }
577
578    pub async fn handle_suspend_imminent(&self) {
579        let mut metric_events = vec![];
580
581        let flushed_successive_failures =
582            self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
583        if flushed_successive_failures > 0 {
584            metric_events.push(MetricEvent {
585                metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
586                event_codes: vec![],
587                payload: MetricEventPayload::IntegerValue(flushed_successive_failures as i64),
588            });
589        }
590
591        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_suspend_imminent");
592    }
593
594    pub async fn handle_iface_destroyed(&self) {
595        self.update_connection_state(ConnectionState::Idle(IdleState {}));
596    }
597
598    pub async fn handle_client_connections_toggle(&self, event: &ClientConnectionsToggleEvent) {
599        if event == &ClientConnectionsToggleEvent::Disabled {
600            self.update_connection_state(ConnectionState::Idle(IdleState {}));
601        }
602    }
603
604    pub async fn handle_pno_scan_failure(&self) {
605        let mut metric_events = vec![MetricEvent {
606            metric_id: metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID,
607            event_codes: vec![],
608            payload: MetricEventPayload::Count(1),
609        }];
610
611        let state = self.connection_state.lock().clone();
612        match state {
613            ConnectionState::Idle(_)
614            | ConnectionState::Disconnected(_)
615            | ConnectionState::ConnectFailed(_)
616            | ConnectionState::PnoScanFailedIdle(_) => {
617                metric_events.push(MetricEvent {
618                    metric_id: metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID,
619                    event_codes: vec![],
620                    payload: MetricEventPayload::Count(1),
621                });
622
623                // PNO scan failures while not connected indicate that the system is looking for
624                // networks to connect to but it is unable to.  In this case, we should transition
625                // to the PnoScanFailedIdle state to flag a period of potential connectivity loss.
626                self.update_connection_state(ConnectionState::PnoScanFailedIdle(
627                    PnoScanFailedIdleState {},
628                ));
629            }
630            ConnectionState::Connected(_)
631            | ConnectionState::FailedToStart(_)
632            | ConnectionState::FailedToStop(_) => {
633                // PNO scan failures while connected will not affect the current connectivity state.
634                // If WLAN has already failed to start or failed to stop, the state should remain
635                // unchanged until a different failure or successful connection occurs.
636            }
637        }
638
639        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_pno_scan_failure");
640    }
641    pub async fn handle_client_connections_failed_to_start(&self) {
642        self.update_connection_state(ConnectionState::FailedToStart(FailedToStartState {}));
643    }
644
645    pub async fn handle_client_connections_failed_to_stop(&self) {
646        self.update_connection_state(ConnectionState::FailedToStop(FailedToStopState {}));
647    }
648}
649
650struct InspectMetadataNode {
651    connected_networks: LruCacheNode<InspectConnectedNetwork>,
652    disconnect_sources: LruCacheNode<InspectDisconnectSource>,
653    connect_attempt_results: LruCacheNode<InspectConnectAttemptResult>,
654}
655
656impl InspectMetadataNode {
657    const CONNECTED_NETWORKS: &'static str = "connected_networks";
658    const DISCONNECT_SOURCES: &'static str = "disconnect_sources";
659    const CONNECT_ATTEMPT_RESULTS: &'static str = "connect_attempt_results";
660
661    fn new(inspect_node: &InspectNode) -> Self {
662        let connected_networks = inspect_node.create_child(Self::CONNECTED_NETWORKS);
663        let disconnect_sources = inspect_node.create_child(Self::DISCONNECT_SOURCES);
664        let connect_attempt_results = inspect_node.create_child(Self::CONNECT_ATTEMPT_RESULTS);
665        Self {
666            connected_networks: LruCacheNode::new(
667                connected_networks,
668                INSPECT_CONNECTED_NETWORKS_ID_LIMIT,
669            ),
670            disconnect_sources: LruCacheNode::new(
671                disconnect_sources,
672                INSPECT_DISCONNECT_SOURCES_ID_LIMIT,
673            ),
674            connect_attempt_results: LruCacheNode::new(
675                connect_attempt_results,
676                INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT,
677            ),
678        }
679    }
680}
681
682#[derive(Debug, Clone)]
683struct ConnectDisconnectTimeSeries {
684    wlan_connectivity_states: InspectedTimeMatrix<u64>,
685    connected_networks: InspectedTimeMatrix<u64>,
686    disconnected_networks: InspectedTimeMatrix<u64>,
687    disconnect_sources: InspectedTimeMatrix<u64>,
688    connect_attempt_results: InspectedTimeMatrix<u64>,
689}
690
691impl ConnectDisconnectTimeSeries {
692    pub fn new<S: InspectSender>(client: &S, inspect_metadata_path: &str) -> Self {
693        let wlan_connectivity_states = client.inspect_time_matrix_with_metadata(
694            "wlan_connectivity_states",
695            TimeMatrix::<Union<u64>, LastSample>::new(
696                SamplingProfile::highly_granular(),
697                LastSample::or(0),
698            ),
699            // Update the ConnectionState IdEnum trait when making changes to this list.
700            BitsetMap::from_ordered(Self::wlan_connectivity_states_bitset_map().iter().copied()),
701        );
702        let connected_networks = client.inspect_time_matrix_with_metadata(
703            "connected_networks",
704            TimeMatrix::<Union<u64>, ConstantSample>::new(
705                SamplingProfile::granular(),
706                ConstantSample::default(),
707            ),
708            BitsetNode::from_path(format!(
709                "{}/{}",
710                inspect_metadata_path,
711                InspectMetadataNode::CONNECTED_NETWORKS
712            )),
713        );
714        let disconnected_networks = client.inspect_time_matrix_with_metadata(
715            "disconnected_networks",
716            TimeMatrix::<Union<u64>, ConstantSample>::new(
717                SamplingProfile::granular(),
718                ConstantSample::default(),
719            ),
720            // This time matrix shares its bit labels with `connected_networks`.
721            BitsetNode::from_path(format!(
722                "{}/{}",
723                inspect_metadata_path,
724                InspectMetadataNode::CONNECTED_NETWORKS
725            )),
726        );
727        let disconnect_sources = client.inspect_time_matrix_with_metadata(
728            "disconnect_sources",
729            TimeMatrix::<Union<u64>, ConstantSample>::new(
730                SamplingProfile::granular(),
731                ConstantSample::default(),
732            ),
733            BitsetNode::from_path(format!(
734                "{}/{}",
735                inspect_metadata_path,
736                InspectMetadataNode::DISCONNECT_SOURCES,
737            )),
738        );
739        let connect_attempt_results = client.inspect_time_matrix_with_metadata(
740            "connect_attempt_results",
741            TimeMatrix::<Union<u64>, ConstantSample>::new(
742                SamplingProfile::granular(),
743                ConstantSample::default(),
744            ),
745            BitsetNode::from_path(format!(
746                "{}/{}",
747                inspect_metadata_path,
748                InspectMetadataNode::CONNECT_ATTEMPT_RESULTS,
749            )),
750        );
751        Self {
752            wlan_connectivity_states,
753            connected_networks,
754            disconnected_networks,
755            disconnect_sources,
756            connect_attempt_results,
757        }
758    }
759
760    // TODO(https://fxbug.dev/504712259): Update BitsetMap to accept the enum type
761    // it's associated with rather than constructing bit labels separately like this
762    fn wlan_connectivity_states_bitset_map() -> &'static [&'static str] {
763        &[
764            "idle",
765            "disconnected",
766            "connect_failed",
767            "connected",
768            "start_failure",
769            "stop_failure",
770            "pno_scan_failed",
771        ]
772    }
773
774    fn log_wlan_connectivity_state(&self, data: u64) {
775        self.wlan_connectivity_states.fold_or_log_error(data);
776    }
777    fn log_connected_networks(&self, data: u64) {
778        self.connected_networks.fold_or_log_error(data);
779    }
780    fn log_disconnected_networks(&self, data: u64) {
781        self.disconnected_networks.fold_or_log_error(data);
782    }
783    fn log_disconnect_sources(&self, data: u64) {
784        self.disconnect_sources.fold_or_log_error(data);
785    }
786    fn log_connect_attempt_results(&self, data: u64) {
787        self.connect_attempt_results.fold_or_log_error(data);
788    }
789}
790
791pub trait DisconnectSourceExt {
792    fn should_log_for_mobile_device(&self) -> bool;
793    fn cobalt_reason_code(&self) -> u16;
794    fn as_cobalt_disconnect_source(
795        &self,
796    ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource;
797}
798
799impl DisconnectSourceExt for fidl_sme::DisconnectSource {
800    fn should_log_for_mobile_device(&self) -> bool {
801        match self {
802            fidl_sme::DisconnectSource::Ap(_) => true,
803            fidl_sme::DisconnectSource::Mlme(cause)
804                if cause.reason_code != fidl_ieee80211::ReasonCode::MlmeLinkFailed =>
805            {
806                true
807            }
808            _ => false,
809        }
810    }
811
812    fn cobalt_reason_code(&self) -> u16 {
813        let cobalt_disconnect_reason_code = match self {
814            fidl_sme::DisconnectSource::Ap(cause) | fidl_sme::DisconnectSource::Mlme(cause) => {
815                cause.reason_code.into_primitive()
816            }
817            fidl_sme::DisconnectSource::User(reason) => *reason as u16,
818        };
819        // This `max_event_code: 1000` is set in the metrics registry, but doesn't show up in the
820        // generated bindings.
821        const REASON_CODE_MAX: u16 = 1000;
822        std::cmp::min(cobalt_disconnect_reason_code, REASON_CODE_MAX)
823    }
824
825    fn as_cobalt_disconnect_source(
826        &self,
827    ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource {
828        use metrics::ConnectivityWlanMetricDimensionDisconnectSource as DS;
829        match self {
830            fidl_sme::DisconnectSource::Ap(..) => DS::Ap,
831            fidl_sme::DisconnectSource::User(..) => DS::User,
832            fidl_sme::DisconnectSource::Mlme(..) => DS::Mlme,
833        }
834    }
835}
836
837#[derive(Debug, Default, Copy, Clone, PartialEq)]
838struct ConnectAttemptsCounter {
839    success: u64,
840    total: u64,
841}
842
843impl ConnectAttemptsCounter {
844    fn increment(&mut self, code: fidl_ieee80211::StatusCode) {
845        self.total += 1;
846        if code == fidl_ieee80211::StatusCode::Success {
847            self.success += 1;
848        }
849    }
850}
851
852struct DailyConnectStats {
853    last_log_time: fasync::BootInstant,
854    connect_per_security_type: HashMap<
855        metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType,
856        ConnectAttemptsCounter,
857    >,
858    connect_per_primary_channel: HashMap<u8, ConnectAttemptsCounter>,
859    connect_per_channel_band: HashMap<
860        metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand,
861        ConnectAttemptsCounter,
862    >,
863    connect_per_rssi_bucket:
864        HashMap<metrics::ConnectivityWlanMetricDimensionRssiBucket, ConnectAttemptsCounter>,
865    connect_per_snr_bucket:
866        HashMap<metrics::ConnectivityWlanMetricDimensionSnrBucket, ConnectAttemptsCounter>,
867    connect_per_is_owe_transition: HashMap<
868        metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition,
869        ConnectAttemptsCounter,
870    >,
871}
872
873impl DailyConnectStats {
874    fn new(now: fasync::BootInstant) -> Self {
875        Self {
876            last_log_time: now,
877            connect_per_security_type: HashMap::new(),
878            connect_per_primary_channel: HashMap::new(),
879            connect_per_channel_band: HashMap::new(),
880            connect_per_rssi_bucket: HashMap::new(),
881            connect_per_snr_bucket: HashMap::new(),
882            connect_per_is_owe_transition: HashMap::new(),
883        }
884    }
885}
886
887// Convert float to an integer in "ten thousandth" unit
888// Example: 0.02f64 (i.e. 2%) -> 200 per ten thousand
889fn float_to_ten_thousandth(value: f64) -> i64 {
890    (value * 10000f64) as i64
891}
892
893fn append_device_connected_cobalt_metrics(
894    metric_events: &mut Vec<MetricEvent>,
895    bss: &BssDescription,
896    is_owe_transition: bool,
897) {
898    metric_events.push(MetricEvent {
899        metric_id: metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID,
900        event_codes: vec![],
901        payload: MetricEventPayload::Count(1),
902    });
903
904    let security_type_dim = convert_security_type(&bss.protection());
905    metric_events.push(MetricEvent {
906        metric_id: metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID,
907        event_codes: vec![security_type_dim as u32],
908        payload: MetricEventPayload::Count(1),
909    });
910
911    if bss.supports_uapsd() {
912        metric_events.push(MetricEvent {
913            metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID,
914            event_codes: vec![],
915            payload: MetricEventPayload::Count(1),
916        });
917    }
918
919    if let Some(rm_enabled_cap) = bss.rm_enabled_cap() {
920        if rm_enabled_cap.link_measurement_enabled() {
921            metric_events.push(MetricEvent {
922                metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
923                event_codes: vec![],
924                payload: MetricEventPayload::Count(1),
925            });
926        }
927        if rm_enabled_cap.neighbor_report_enabled() {
928            metric_events.push(MetricEvent {
929                metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
930                event_codes: vec![],
931                payload: MetricEventPayload::Count(1),
932            });
933        }
934    }
935
936    if bss.supports_ft() {
937        metric_events.push(MetricEvent {
938            metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID,
939            event_codes: vec![],
940            payload: MetricEventPayload::Count(1),
941        });
942    }
943
944    if let Some(cap) = bss.ext_cap().and_then(|cap| cap.ext_caps_octet_3)
945        && cap.bss_transition()
946    {
947        metric_events.push(MetricEvent {
948            metric_id:
949                metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
950            event_codes: vec![],
951            payload: MetricEventPayload::Count(1),
952        });
953    }
954
955    append_device_connected_channel_cobalt_metrics(metric_events, bss.channel);
956
957    let oui_string = bss.bssid.to_oui_uppercase("");
958    metric_events.push(MetricEvent {
959        metric_id: metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
960        event_codes: vec![],
961        payload: MetricEventPayload::StringValue(oui_string),
962    });
963
964    let is_owe_transition_dim = convert_is_owe_transition(is_owe_transition);
965    metric_events.push(MetricEvent {
966        metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
967        event_codes: vec![is_owe_transition_dim as u32],
968        payload: MetricEventPayload::Count(1),
969    });
970}
971
972fn append_device_connected_channel_cobalt_metrics(
973    metric_events: &mut Vec<MetricEvent>,
974    channel: Channel,
975) {
976    metric_events.push(MetricEvent {
977        metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
978        event_codes: vec![channel.primary as u32],
979        payload: MetricEventPayload::Count(1),
980    });
981
982    let channel_band_dim = convert_channel_band(channel.band);
983    metric_events.push(MetricEvent {
984        metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
985        event_codes: vec![channel_band_dim as u32],
986        payload: MetricEventPayload::Count(1),
987    });
988}
989
990#[cfg(test)]
991mod tests {
992    use super::*;
993    use crate::testing::*;
994    use assert_matches::assert_matches;
995    use diagnostics_assertions::{
996        AnyBoolProperty, AnyBytesProperty, AnyNumericProperty, AnyStringProperty, assert_data_tree,
997    };
998    use futures::task::Poll;
999    use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
1000    use std::pin::pin;
1001    use strum::EnumCount;
1002    use test_case::test_case;
1003    use windowed_stats::experimental::clock::Timed;
1004    use windowed_stats::experimental::inspect::TimeMatrixClient;
1005    use windowed_stats::experimental::testing::TimeMatrixCall;
1006    use wlan_common::channel::{Bandwidth, Channel};
1007    use wlan_common::ie::IeType;
1008    use wlan_common::test_utils::fake_stas::IesOverrides;
1009    use wlan_common::{fake_bss_description, random_bss_description};
1010
1011    #[fuchsia::test]
1012    fn log_connect_attempt_then_inspect_data_tree_contains_time_matrix_metadata() {
1013        let mut harness = setup_test();
1014
1015        let client =
1016            TimeMatrixClient::new(harness.inspect_node.create_child("wlan_connect_disconnect"));
1017        let logger = ConnectDisconnectLogger::new(
1018            harness.filtered_cobalt_logger(),
1019            &harness.inspect_node,
1020            &harness.inspect_metadata_node,
1021            &harness.inspect_metadata_path,
1022            &client,
1023            DeviceMobility::Mobile,
1024        );
1025        let bss = random_bss_description!();
1026        let mut log_connect_attempt = pin!(logger.handle_connect_attempt(
1027            fidl_ieee80211::StatusCode::Success,
1028            &bss,
1029            false,
1030            false
1031        ));
1032        assert!(
1033            harness.run_until_stalled_drain_cobalt_events(&mut log_connect_attempt).is_ready(),
1034            "`log_connect_attempt` did not complete",
1035        );
1036
1037        let tree = harness.get_inspect_data_tree();
1038        assert_data_tree!(
1039            @executor harness.exec,
1040            tree,
1041            root: contains {
1042                test_stats: contains {
1043                    wlan_connect_disconnect: contains {
1044                        wlan_connectivity_states: {
1045                            "type": "bitset",
1046                            "data": AnyBytesProperty,
1047                            metadata: {
1048                                index: {
1049                                    "0": "idle",
1050                                    "1": "disconnected",
1051                                    "2": "connect_failed",
1052                                    "3": "connected",
1053                                    "4": "start_failure",
1054                                    "5": "stop_failure",
1055                                    "6": "pno_scan_failed",
1056                                },
1057                            },
1058                        },
1059                        connected_networks: {
1060                            "type": "bitset",
1061                            "data": AnyBytesProperty,
1062                            metadata: {
1063                                "index_node_path": "root/test_stats/metadata/connected_networks",
1064                            },
1065                        },
1066                        disconnected_networks: {
1067                            "type": "bitset",
1068                            "data": AnyBytesProperty,
1069                            metadata: {
1070                                "index_node_path": "root/test_stats/metadata/connected_networks",
1071                            },
1072                        },
1073                        disconnect_sources: {
1074                            "type": "bitset",
1075                            "data": AnyBytesProperty,
1076                            metadata: {
1077                                "index_node_path": "root/test_stats/metadata/disconnect_sources",
1078                            },
1079                        },
1080                        connect_attempt_results: {
1081                            "type": "bitset",
1082                            "data": AnyBytesProperty,
1083                            metadata: {
1084                                "index_node_path": "root/test_stats/metadata/connect_attempt_results",
1085                            },
1086                        },
1087                    },
1088                },
1089            }
1090        );
1091    }
1092
1093    #[fuchsia::test]
1094    fn test_log_connect_attempt_inspect() {
1095        let mut test_helper = setup_test();
1096        let logger = ConnectDisconnectLogger::new(
1097            test_helper.filtered_cobalt_logger(),
1098            &test_helper.inspect_node,
1099            &test_helper.inspect_metadata_node,
1100            &test_helper.inspect_metadata_path,
1101            &test_helper.mock_time_matrix_client,
1102            DeviceMobility::Mobile,
1103        );
1104
1105        // Log the event
1106        let bss_description = random_bss_description!();
1107        let mut test_fut = pin!(logger.handle_connect_attempt(
1108            fidl_ieee80211::StatusCode::Success,
1109            &bss_description,
1110            false,
1111            false
1112        ));
1113        assert_eq!(
1114            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1115            Poll::Ready(())
1116        );
1117
1118        // Validate Inspect data
1119        let data = test_helper.get_inspect_data_tree();
1120        assert_data_tree!(@executor test_helper.exec, data, root: contains {
1121            test_stats: contains {
1122                metadata: contains {
1123                    connected_networks: contains {
1124                        "0": {
1125                            "@time": AnyNumericProperty,
1126                            "data": contains {
1127                                bssid: &*BSSID_REGEX,
1128                                ssid: &*SSID_REGEX,
1129                            }
1130                        }
1131                    },
1132                    connect_attempt_results: contains {
1133                        "0": {
1134                            "@time": AnyNumericProperty,
1135                            "data": contains {
1136                                status_code: 0u64,
1137                                result: "Success",
1138                            }
1139                        }
1140                    },
1141                },
1142                connect_events: {
1143                    "0": {
1144                        "@time": AnyNumericProperty,
1145                        network_id: 0u64,
1146                    }
1147                },
1148                connect_attempt_results: {
1149                    "0": {
1150                        "@time": AnyNumericProperty,
1151                        result: "Success",
1152                        ssid: &*SSID_REGEX,
1153                        bssid: &*BSSID_REGEX,
1154                        protection: AnyStringProperty,
1155                    }
1156                }
1157            }
1158        });
1159
1160        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1161        assert_eq!(
1162            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1163            &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 3)),]
1164        );
1165        assert_eq!(
1166            &time_matrix_calls.drain::<u64>("connected_networks")[..],
1167            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1168        );
1169        assert_eq!(
1170            &time_matrix_calls.drain::<u64>("connect_attempt_results")[..],
1171            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1172        );
1173    }
1174
1175    #[fuchsia::test]
1176    fn test_log_connect_attempt_cobalt() {
1177        let mut test_helper = setup_test();
1178        let logger = ConnectDisconnectLogger::new(
1179            test_helper.filtered_cobalt_logger(),
1180            &test_helper.inspect_node,
1181            &test_helper.inspect_metadata_node,
1182            &test_helper.inspect_metadata_path,
1183            &test_helper.mock_time_matrix_client,
1184            DeviceMobility::Mobile,
1185        );
1186
1187        // Generate BSS Description
1188        let bss_description = random_bss_description!(Wpa2,
1189            channel: Channel::new(157, Bandwidth::Cbw40, fidl_ieee80211::WlanBand::FiveGhz),
1190            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
1191        );
1192
1193        // Log the event
1194        let mut test_fut = pin!(logger.handle_connect_attempt(
1195            fidl_ieee80211::StatusCode::Success,
1196            &bss_description,
1197            false,
1198            false
1199        ));
1200        assert_eq!(
1201            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1202            Poll::Ready(())
1203        );
1204
1205        // Validate Cobalt data
1206        let breakdowns_by_status_code = test_helper
1207            .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
1208        assert_eq!(breakdowns_by_status_code.len(), 1);
1209        assert_eq!(
1210            breakdowns_by_status_code[0].event_codes,
1211            vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
1212        );
1213        assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
1214
1215        let metrics_devices =
1216            test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
1217        assert_eq!(metrics_devices.len(), 1);
1218        assert_eq!(metrics_devices[0].payload, MetricEventPayload::Count(1));
1219
1220        let metrics_security =
1221            test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
1222        assert_eq!(metrics_security.len(), 1);
1223        assert_eq!(metrics_security[0].event_codes, vec![5]); // Wpa2Personal
1224
1225        let metrics_channel = test_helper.get_logged_metrics(
1226            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1227        );
1228        assert_eq!(metrics_channel.len(), 1);
1229        assert_eq!(metrics_channel[0].event_codes, vec![157]);
1230
1231        let metrics_band = test_helper.get_logged_metrics(
1232            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
1233        );
1234        assert_eq!(metrics_band.len(), 1);
1235        assert_eq!(metrics_band[0].event_codes, vec![2]); // Band5Ghz
1236
1237        let metrics_oui =
1238            test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
1239        assert_eq!(metrics_oui.len(), 1);
1240        assert_eq!(metrics_oui[0].payload, MetricEventPayload::StringValue("00F620".to_string()));
1241
1242        let metrics_owe_transition = test_helper.get_logged_metrics(
1243            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
1244        );
1245        assert_eq!(metrics_owe_transition.len(), 1);
1246        assert_eq!(
1247            metrics_owe_transition[0].event_codes,
1248            vec![
1249                metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition::No
1250                    as u32
1251            ]
1252        );
1253    }
1254
1255    #[fuchsia::test]
1256    fn test_handle_channel_switched() {
1257        let mut test_helper = setup_test();
1258        let logger = ConnectDisconnectLogger::new(
1259            test_helper.filtered_cobalt_logger(),
1260            &test_helper.inspect_node,
1261            &test_helper.inspect_metadata_node,
1262            &test_helper.inspect_metadata_path,
1263            &test_helper.mock_time_matrix_client,
1264            DeviceMobility::Mobile,
1265        );
1266
1267        let channel = Channel::new(157, Bandwidth::Cbw40, fidl_ieee80211::WlanBand::FiveGhz);
1268        let mut test_fut = pin!(logger.handle_channel_switched(channel));
1269        assert_eq!(
1270            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1271            Poll::Ready(())
1272        );
1273
1274        let metrics_channel = test_helper.get_logged_metrics(
1275            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1276        );
1277        assert_eq!(metrics_channel.len(), 1);
1278        assert_eq!(metrics_channel[0].event_codes, vec![157]);
1279        assert_eq!(metrics_channel[0].payload, MetricEventPayload::Count(1));
1280
1281        let metrics_band = test_helper.get_logged_metrics(
1282            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
1283        );
1284        assert_eq!(metrics_band.len(), 1);
1285        assert_eq!(metrics_band[0].event_codes, vec![2]); // Band5Ghz
1286        assert_eq!(metrics_band[0].payload, MetricEventPayload::Count(1));
1287    }
1288
1289    #[fuchsia::test]
1290    fn test_successive_connect_attempt_failures_cobalt_zero_failures() {
1291        let mut test_helper = setup_test();
1292        let logger = ConnectDisconnectLogger::new(
1293            test_helper.filtered_cobalt_logger(),
1294            &test_helper.inspect_node,
1295            &test_helper.inspect_metadata_node,
1296            &test_helper.inspect_metadata_path,
1297            &test_helper.mock_time_matrix_client,
1298            DeviceMobility::Mobile,
1299        );
1300
1301        let bss_description = random_bss_description!(Wpa2);
1302        let mut test_fut = pin!(logger.handle_connect_attempt(
1303            fidl_ieee80211::StatusCode::Success,
1304            &bss_description,
1305            false,
1306            false
1307        ));
1308        assert_eq!(
1309            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1310            Poll::Ready(())
1311        );
1312
1313        let metrics =
1314            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1315        assert_eq!(metrics.len(), 1);
1316        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
1317    }
1318
1319    #[fuchsia::test]
1320    fn test_log_device_connected_metrics_capabilities() {
1321        let mut test_helper = setup_test();
1322        let logger = ConnectDisconnectLogger::new(
1323            test_helper.filtered_cobalt_logger(),
1324            &test_helper.inspect_node,
1325            &test_helper.inspect_metadata_node,
1326            &test_helper.inspect_metadata_path,
1327            &test_helper.mock_time_matrix_client,
1328            DeviceMobility::Mobile,
1329        );
1330
1331        let wmm_info = vec![0x80]; // U-APSD enabled
1332        #[rustfmt::skip]
1333        let rm_enabled_capabilities = vec![
1334            0x03, // link measurement and neighbor report enabled
1335            0x00, 0x00, 0x00, 0x00,
1336        ];
1337        #[rustfmt::skip]
1338        let ext_capabilities = vec![
1339            0x04, 0x00,
1340            0x08, // BSS transition supported
1341            0x00, 0x00, 0x00, 0x00, 0x40
1342        ];
1343
1344        let bss_description = fake_bss_description!(Wpa2,
1345            ies_overrides: IesOverrides::new()
1346                .remove(IeType::WMM_PARAM)
1347                .set(IeType::WMM_INFO, wmm_info)
1348                .set(IeType::RM_ENABLED_CAPABILITIES, rm_enabled_capabilities)
1349                .set(IeType::MOBILITY_DOMAIN, vec![0x00; 3])
1350                .set(IeType::EXT_CAPABILITIES, ext_capabilities),
1351        );
1352
1353        let mut test_fut = pin!(logger.handle_connect_attempt(
1354            fidl_ieee80211::StatusCode::Success,
1355            &bss_description,
1356            false,
1357            false
1358        ));
1359        assert_eq!(
1360            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1361            Poll::Ready(())
1362        );
1363
1364        let metrics = test_helper
1365            .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
1366        assert_eq!(metrics.len(), 1);
1367        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1368
1369        let metrics = test_helper.get_logged_metrics(
1370            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
1371        );
1372        assert_eq!(metrics.len(), 1);
1373        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1374
1375        let metrics = test_helper.get_logged_metrics(
1376            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
1377        );
1378        assert_eq!(metrics.len(), 1);
1379        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1380
1381        let metrics = test_helper.get_logged_metrics(
1382            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
1383        );
1384        assert_eq!(metrics.len(), 1);
1385        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1386    }
1387
1388    #[test_case(1; "one_failure")]
1389    #[test_case(2; "two_failures")]
1390    #[fuchsia::test(add_test_attr = false)]
1391    fn test_successive_connect_attempt_failures_cobalt_one_failure_then_success(n_failures: usize) {
1392        let mut test_helper = setup_test();
1393        let logger = ConnectDisconnectLogger::new(
1394            test_helper.filtered_cobalt_logger(),
1395            &test_helper.inspect_node,
1396            &test_helper.inspect_metadata_node,
1397            &test_helper.inspect_metadata_path,
1398            &test_helper.mock_time_matrix_client,
1399            DeviceMobility::Mobile,
1400        );
1401
1402        let bss_description = random_bss_description!(Wpa2);
1403        for _i in 0..n_failures {
1404            let mut test_fut = pin!(logger.handle_connect_attempt(
1405                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1406                &bss_description,
1407                false,
1408                false
1409            ));
1410            assert_eq!(
1411                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1412                Poll::Ready(())
1413            );
1414        }
1415
1416        let metrics =
1417            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1418        assert!(metrics.is_empty());
1419
1420        let mut test_fut = pin!(logger.handle_connect_attempt(
1421            fidl_ieee80211::StatusCode::Success,
1422            &bss_description,
1423            false,
1424            false
1425        ));
1426        assert_eq!(
1427            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1428            Poll::Ready(())
1429        );
1430
1431        let metrics =
1432            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1433        assert_eq!(metrics.len(), 1);
1434        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1435
1436        // Verify subsequent successes would report 0 failures
1437        test_helper.clear_cobalt_events();
1438        let mut test_fut = pin!(logger.handle_connect_attempt(
1439            fidl_ieee80211::StatusCode::Success,
1440            &bss_description,
1441            false,
1442            false
1443        ));
1444        assert_eq!(
1445            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1446            Poll::Ready(())
1447        );
1448        let metrics =
1449            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1450        assert_eq!(metrics.len(), 1);
1451        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
1452    }
1453
1454    #[test_case(1; "one_failure")]
1455    #[test_case(2; "two_failures")]
1456    #[fuchsia::test(add_test_attr = false)]
1457    fn test_successive_connect_attempt_failures_cobalt_one_failure_then_timeout(n_failures: usize) {
1458        let mut test_helper = setup_test();
1459        let logger = ConnectDisconnectLogger::new(
1460            test_helper.filtered_cobalt_logger(),
1461            &test_helper.inspect_node,
1462            &test_helper.inspect_metadata_node,
1463            &test_helper.inspect_metadata_path,
1464            &test_helper.mock_time_matrix_client,
1465            DeviceMobility::Mobile,
1466        );
1467
1468        let bss_description = random_bss_description!(Wpa2);
1469        for _i in 0..n_failures {
1470            let mut test_fut = pin!(logger.handle_connect_attempt(
1471                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1472                &bss_description,
1473                false,
1474                false
1475            ));
1476            assert_eq!(
1477                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1478                Poll::Ready(())
1479            );
1480        }
1481
1482        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1483        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1484        assert_eq!(
1485            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1486            Poll::Ready(())
1487        );
1488
1489        // Not enough time has passed, so successive_connect_attempt_failures is not flushed yet
1490        let metrics =
1491            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1492        assert!(metrics.is_empty());
1493
1494        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(120_000_000_000));
1495        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1496        assert_eq!(
1497            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1498            Poll::Ready(())
1499        );
1500
1501        let metrics =
1502            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1503        assert_eq!(metrics.len(), 1);
1504        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1505
1506        // Verify timeout fires only once
1507        test_helper.clear_cobalt_events();
1508        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(240_000_000_000));
1509        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1510        assert_eq!(
1511            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1512            Poll::Ready(())
1513        );
1514        let metrics =
1515            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1516        assert!(metrics.is_empty());
1517    }
1518
1519    #[fuchsia::test]
1520    fn test_daily_connect_success_rate_breakdowns() {
1521        let mut test_helper = setup_test();
1522        let logger = ConnectDisconnectLogger::new(
1523            test_helper.filtered_cobalt_logger(),
1524            &test_helper.inspect_node,
1525            &test_helper.inspect_metadata_node,
1526            &test_helper.inspect_metadata_path,
1527            &test_helper.mock_time_matrix_client,
1528            DeviceMobility::Mobile,
1529        );
1530
1531        let mut bss = random_bss_description!(Wpa2);
1532        bss.channel = Channel::new(6, Bandwidth::Cbw20, fidl_ieee80211::WlanBand::TwoGhz); // primary channel 6 -> Band2Dot4Ghz
1533        bss.rssi_dbm = -50; // rssi -50 -> From50To35 (event code 11)
1534        bss.snr_db = 15; // snr 15 -> From11To15 (event code 3)
1535
1536        // 1 success, 1 failure => 50% success rate
1537        let mut test_fut = pin!(logger.handle_connect_attempt(
1538            fidl_ieee80211::StatusCode::Success,
1539            &bss,
1540            false,
1541            true
1542        ));
1543        assert_eq!(
1544            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1545            Poll::Ready(())
1546        );
1547
1548        let mut test_fut = pin!(logger.handle_connect_attempt(
1549            fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1550            &bss,
1551            false,
1552            true
1553        ));
1554        assert_eq!(
1555            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1556            Poll::Ready(())
1557        );
1558
1559        // Before 24 hours pass, no daily metrics should be logged
1560        test_helper.clear_cobalt_events();
1561        test_helper
1562            .exec
1563            .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000 - 1));
1564        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1565        assert_eq!(
1566            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1567            Poll::Ready(())
1568        );
1569        assert!(
1570            test_helper
1571                .get_logged_metrics(
1572                    metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID
1573                )
1574                .is_empty()
1575        );
1576
1577        // After 24 hours pass, daily metrics should be logged
1578        test_helper
1579            .exec
1580            .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000));
1581        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1582        assert_eq!(
1583            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1584            Poll::Ready(())
1585        );
1586
1587        // Check security type breakdown
1588        let daily_security_metrics = test_helper.get_logged_metrics(
1589            metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
1590        );
1591        assert_eq!(daily_security_metrics.len(), 1);
1592        assert_eq!(
1593            daily_security_metrics[0].event_codes,
1594            vec![
1595                metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
1596                    as u32
1597            ]
1598        );
1599        assert_eq!(daily_security_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1600
1601        // Check primary channel breakdown
1602        let daily_channel_metrics = test_helper.get_logged_metrics(
1603            metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1604        );
1605        assert_eq!(daily_channel_metrics.len(), 1);
1606        assert_eq!(daily_channel_metrics[0].event_codes, vec![6]);
1607        assert_eq!(daily_channel_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1608
1609        // Check channel band breakdown
1610        let daily_band_metrics = test_helper.get_logged_metrics(
1611            metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
1612        );
1613        assert_eq!(daily_band_metrics.len(), 1);
1614        assert_eq!(
1615            daily_band_metrics[0].event_codes,
1616            vec![
1617                metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
1618                    as u32
1619            ]
1620        );
1621        assert_eq!(daily_band_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1622
1623        // Check rssi bucket breakdown
1624        let daily_rssi_metrics = test_helper.get_logged_metrics(
1625            metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
1626        );
1627        assert_eq!(daily_rssi_metrics.len(), 1);
1628        assert_eq!(
1629            daily_rssi_metrics[0].event_codes,
1630            vec![metrics::ConnectivityWlanMetricDimensionRssiBucket::From50To35 as u32]
1631        );
1632        assert_eq!(daily_rssi_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1633
1634        // Check snr bucket breakdown
1635        let daily_snr_metrics = test_helper.get_logged_metrics(
1636            metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
1637        );
1638        assert_eq!(daily_snr_metrics.len(), 1);
1639        assert_eq!(
1640            daily_snr_metrics[0].event_codes,
1641            vec![metrics::ConnectivityWlanMetricDimensionSnrBucket::From11To15 as u32]
1642        );
1643        assert_eq!(daily_snr_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1644
1645        // Check is_owe_transition breakdown
1646        let daily_owe_metrics = test_helper.get_logged_metrics(
1647            metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
1648        );
1649        assert_eq!(daily_owe_metrics.len(), 1);
1650        assert_eq!(
1651            daily_owe_metrics[0].event_codes,
1652            vec![
1653                metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition::Yes
1654                    as u32
1655            ]
1656        );
1657        assert_eq!(daily_owe_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1658    }
1659
1660    #[fuchsia::test]
1661    fn test_log_device_connected_cobalt_metrics_periodically() {
1662        let mut test_helper = setup_test();
1663        let logger = ConnectDisconnectLogger::new(
1664            test_helper.filtered_cobalt_logger(),
1665            &test_helper.inspect_node,
1666            &test_helper.inspect_metadata_node,
1667            &test_helper.inspect_metadata_path,
1668            &test_helper.mock_time_matrix_client,
1669            DeviceMobility::Mobile,
1670        );
1671
1672        let mut bss = random_bss_description!(Wpa2,
1673            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
1674        );
1675        bss.channel = Channel::new(6, Bandwidth::Cbw20, fidl_ieee80211::WlanBand::TwoGhz);
1676
1677        // Connect
1678        let mut test_fut = pin!(logger.handle_connect_attempt(
1679            fidl_ieee80211::StatusCode::Success,
1680            &bss,
1681            false,
1682            false
1683        ));
1684        assert_eq!(
1685            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1686            Poll::Ready(())
1687        );
1688
1689        let connected_metrics =
1690            test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
1691        assert_eq!(connected_metrics.len(), 1);
1692
1693        // Before 24 hours pass, no periodic device connected metrics should be logged
1694        test_helper.clear_cobalt_events();
1695        test_helper
1696            .exec
1697            .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000 - 1));
1698        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1699        assert_eq!(
1700            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1701            Poll::Ready(())
1702        );
1703        assert!(
1704            test_helper
1705                .get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID)
1706                .is_empty()
1707        );
1708
1709        // After 24 hours pass, device connected metrics should be logged again
1710        test_helper
1711            .exec
1712            .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000));
1713        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1714        assert_eq!(
1715            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1716            Poll::Ready(())
1717        );
1718
1719        let connected_metrics =
1720            test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
1721        assert_eq!(connected_metrics.len(), 1);
1722        assert_eq!(connected_metrics[0].payload, MetricEventPayload::Count(1));
1723
1724        let security_metrics =
1725            test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
1726        assert_eq!(security_metrics.len(), 1);
1727
1728        let channel_metrics = test_helper.get_logged_metrics(
1729            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1730        );
1731        assert_eq!(channel_metrics.len(), 1);
1732        assert_eq!(channel_metrics[0].event_codes, vec![6]);
1733
1734        let oui_metrics =
1735            test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
1736        assert_eq!(oui_metrics.len(), 1);
1737        assert_eq!(oui_metrics[0].payload, MetricEventPayload::StringValue("00F620".to_string()));
1738    }
1739
1740    #[fuchsia::test]
1741    fn test_log_device_connected_cobalt_metrics_periodically_channel_switched() {
1742        let mut test_helper = setup_test();
1743        let logger = ConnectDisconnectLogger::new(
1744            test_helper.filtered_cobalt_logger(),
1745            &test_helper.inspect_node,
1746            &test_helper.inspect_metadata_node,
1747            &test_helper.inspect_metadata_path,
1748            &test_helper.mock_time_matrix_client,
1749            DeviceMobility::Mobile,
1750        );
1751
1752        let mut bss = random_bss_description!(Wpa2);
1753        bss.channel = Channel::new(6, Bandwidth::Cbw20, fidl_ieee80211::WlanBand::TwoGhz);
1754
1755        // Connect on channel 6
1756        let mut test_fut = pin!(logger.handle_connect_attempt(
1757            fidl_ieee80211::StatusCode::Success,
1758            &bss,
1759            false,
1760            false
1761        ));
1762        assert_eq!(
1763            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1764            Poll::Ready(())
1765        );
1766
1767        // Switch channel to 36
1768        let new_channel = Channel::new(36, Bandwidth::Cbw20, fidl_ieee80211::WlanBand::FiveGhz);
1769        let mut test_fut = pin!(logger.handle_channel_switched(new_channel));
1770        assert_eq!(
1771            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1772            Poll::Ready(())
1773        );
1774
1775        test_helper.clear_cobalt_events();
1776
1777        // After 24 hours pass, daily device connected metrics should reflect the switched channel
1778        test_helper
1779            .exec
1780            .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000));
1781        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1782        assert_eq!(
1783            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1784            Poll::Ready(())
1785        );
1786
1787        let channel_metrics = test_helper.get_logged_metrics(
1788            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1789        );
1790        assert_eq!(channel_metrics.len(), 1);
1791        assert_eq!(channel_metrics[0].event_codes, vec![36]);
1792    }
1793
1794    #[fuchsia::test]
1795    fn test_log_device_connected_cobalt_metrics_periodically_not_connected() {
1796        let mut test_helper = setup_test();
1797        let logger = ConnectDisconnectLogger::new(
1798            test_helper.filtered_cobalt_logger(),
1799            &test_helper.inspect_node,
1800            &test_helper.inspect_metadata_node,
1801            &test_helper.inspect_metadata_path,
1802            &test_helper.mock_time_matrix_client,
1803            DeviceMobility::Mobile,
1804        );
1805
1806        test_helper.clear_cobalt_events();
1807
1808        // After 24 hours pass, since device is not connected, no device connected metrics are logged
1809        test_helper
1810            .exec
1811            .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000));
1812        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1813        assert_eq!(
1814            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1815            Poll::Ready(())
1816        );
1817
1818        assert!(
1819            test_helper
1820                .get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID)
1821                .is_empty()
1822        );
1823    }
1824
1825    #[fuchsia::test]
1826    fn test_log_connect_attempt_cobalt_owe_transition() {
1827        let mut test_helper = setup_test();
1828        let logger = ConnectDisconnectLogger::new(
1829            test_helper.filtered_cobalt_logger(),
1830            &test_helper.inspect_node,
1831            &test_helper.inspect_metadata_node,
1832            &test_helper.inspect_metadata_path,
1833            &test_helper.mock_time_matrix_client,
1834            DeviceMobility::Mobile,
1835        );
1836
1837        // Generate BSS Description
1838        let bss_description = random_bss_description!(Wpa2,
1839            channel: Channel::new(157, Bandwidth::Cbw40, fidl_ieee80211::WlanBand::FiveGhz),
1840            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
1841        );
1842
1843        // Log the event with is_owe_transition = true
1844        let mut test_fut = pin!(logger.handle_connect_attempt(
1845            fidl_ieee80211::StatusCode::Success,
1846            &bss_description,
1847            false,
1848            true
1849        ));
1850        assert_eq!(
1851            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1852            Poll::Ready(())
1853        );
1854
1855        let metrics_owe_transition = test_helper.get_logged_metrics(
1856            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
1857        );
1858        assert_eq!(metrics_owe_transition.len(), 1);
1859        assert_eq!(
1860            metrics_owe_transition[0].event_codes,
1861            vec![
1862                metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition::Yes
1863                    as u32
1864            ]
1865        );
1866    }
1867
1868    #[fuchsia::test]
1869    fn test_zero_successive_connect_attempt_failures_on_suspend() {
1870        let mut test_helper = setup_test();
1871        let logger = ConnectDisconnectLogger::new(
1872            test_helper.filtered_cobalt_logger(),
1873            &test_helper.inspect_node,
1874            &test_helper.inspect_metadata_node,
1875            &test_helper.inspect_metadata_path,
1876            &test_helper.mock_time_matrix_client,
1877            DeviceMobility::Mobile,
1878        );
1879
1880        let mut test_fut = pin!(logger.handle_suspend_imminent());
1881        assert_eq!(
1882            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1883            Poll::Ready(())
1884        );
1885
1886        let metrics =
1887            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1888        assert!(metrics.is_empty());
1889    }
1890
1891    #[test_case(1; "one_failure")]
1892    #[test_case(2; "two_failures")]
1893    #[fuchsia::test(add_test_attr = false)]
1894    fn test_one_or_more_successive_connect_attempt_failures_on_suspend(n_failures: usize) {
1895        let mut test_helper = setup_test();
1896        let logger = ConnectDisconnectLogger::new(
1897            test_helper.filtered_cobalt_logger(),
1898            &test_helper.inspect_node,
1899            &test_helper.inspect_metadata_node,
1900            &test_helper.inspect_metadata_path,
1901            &test_helper.mock_time_matrix_client,
1902            DeviceMobility::Mobile,
1903        );
1904
1905        let bss_description = random_bss_description!(Wpa2);
1906        for _i in 0..n_failures {
1907            let mut test_fut = pin!(logger.handle_connect_attempt(
1908                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1909                &bss_description,
1910                false,
1911                false
1912            ));
1913            assert_eq!(
1914                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1915                Poll::Ready(())
1916            );
1917        }
1918
1919        let mut test_fut = pin!(logger.handle_suspend_imminent());
1920        assert_eq!(
1921            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1922            Poll::Ready(())
1923        );
1924
1925        let metrics =
1926            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1927        assert_eq!(metrics.len(), 1);
1928        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1929
1930        test_helper.clear_cobalt_events();
1931        let mut test_fut = pin!(logger.handle_suspend_imminent());
1932        assert_eq!(
1933            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1934            Poll::Ready(())
1935        );
1936
1937        // Count of successive failures shouldn't be logged again since it was already logged
1938        let metrics =
1939            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1940        assert!(metrics.is_empty());
1941
1942        // Verify that the connection state has transitioned to ConnectFailed
1943        assert_matches!(*logger.connection_state.lock(), ConnectionState::ConnectFailed(_));
1944    }
1945
1946    #[fuchsia::test]
1947    fn test_log_disconnect_inspect() {
1948        let mut test_helper = setup_test();
1949        let logger = ConnectDisconnectLogger::new(
1950            test_helper.filtered_cobalt_logger(),
1951            &test_helper.inspect_node,
1952            &test_helper.inspect_metadata_node,
1953            &test_helper.inspect_metadata_path,
1954            &test_helper.mock_time_matrix_client,
1955            DeviceMobility::Mobile,
1956        );
1957
1958        // Log the event
1959        let bss_description = fake_bss_description!(Open);
1960        let channel = bss_description.channel;
1961        let disconnect_info = DisconnectInfo {
1962            iface_id: 32,
1963            connected_duration: zx::BootDuration::from_seconds(30),
1964            is_sme_reconnecting: false,
1965            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1966                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1967                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1968            }),
1969            original_bss_desc: Box::new(bss_description),
1970            current_rssi_dbm: -30,
1971            current_snr_db: 25,
1972            current_channel: channel,
1973        };
1974        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1975        assert_eq!(
1976            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1977            Poll::Ready(())
1978        );
1979
1980        // Validate Inspect data
1981        let data = test_helper.get_inspect_data_tree();
1982        assert_data_tree!(@executor test_helper.exec, data, root: contains {
1983            test_stats: contains {
1984                metadata: contains {
1985                    connected_networks: {
1986                        "0": {
1987                            "@time": AnyNumericProperty,
1988                            "data": {
1989                                bssid: &*BSSID_REGEX,
1990                                ssid: &*SSID_REGEX,
1991                                ht_cap: AnyBytesProperty,
1992                                vht_cap: AnyBytesProperty,
1993                                protection: "Open",
1994                                is_wmm_assoc: AnyBoolProperty,
1995                                wmm_param: AnyBytesProperty,
1996                            }
1997                        }
1998                    },
1999                    disconnect_sources: {
2000                        "0": {
2001                            "@time": AnyNumericProperty,
2002                            "data": {
2003                                source: "ap",
2004                                reason: "UnspecifiedReason",
2005                                mlme_event_name: "DeauthenticateIndication",
2006                            }
2007                        }
2008                    },
2009                },
2010                disconnect_events: {
2011                    "0": {
2012                        "@time": AnyNumericProperty,
2013                        connected_duration: zx::BootDuration::from_seconds(30).into_nanos(),
2014                        disconnect_source_id: 0u64,
2015                        network_id: 0u64,
2016                        rssi_dbm: -30i64,
2017                        snr_db: 25i64,
2018                        channel: AnyStringProperty,
2019                    }
2020                }
2021            }
2022        });
2023
2024        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2025        assert_eq!(
2026            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2027            &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 1)),]
2028        );
2029        assert_eq!(
2030            &time_matrix_calls.drain::<u64>("disconnected_networks")[..],
2031            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
2032        );
2033        assert_eq!(
2034            &time_matrix_calls.drain::<u64>("disconnect_sources")[..],
2035            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
2036        );
2037    }
2038
2039    #[fuchsia::test]
2040    fn test_log_disconnect_cobalt() {
2041        let mut test_helper = setup_test();
2042        let logger = ConnectDisconnectLogger::new(
2043            test_helper.filtered_cobalt_logger(),
2044            &test_helper.inspect_node,
2045            &test_helper.inspect_metadata_node,
2046            &test_helper.inspect_metadata_path,
2047            &test_helper.mock_time_matrix_client,
2048            DeviceMobility::Mobile,
2049        );
2050
2051        // Log the event
2052        let disconnect_info = DisconnectInfo {
2053            connected_duration: zx::BootDuration::from_millis(300_000),
2054            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
2055                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2056                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
2057            }),
2058            ..fake_disconnect_info()
2059        };
2060        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
2061        assert_eq!(
2062            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2063            Poll::Ready(())
2064        );
2065
2066        let disconnect_count_metrics =
2067            test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
2068        assert_eq!(disconnect_count_metrics.len(), 1);
2069        assert_eq!(disconnect_count_metrics[0].payload, MetricEventPayload::Count(1));
2070
2071        let connected_duration_metrics =
2072            test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID);
2073        assert_eq!(connected_duration_metrics.len(), 1);
2074        assert_eq!(
2075            connected_duration_metrics[0].payload,
2076            MetricEventPayload::IntegerValue(300_000)
2077        );
2078
2079        let disconnect_by_reason_metrics =
2080            test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
2081        assert_eq!(disconnect_by_reason_metrics.len(), 1);
2082        assert_eq!(disconnect_by_reason_metrics[0].payload, MetricEventPayload::Count(1));
2083        assert_eq!(disconnect_by_reason_metrics[0].event_codes.len(), 2);
2084        assert_eq!(
2085            disconnect_by_reason_metrics[0].event_codes[0],
2086            fidl_ieee80211::ReasonCode::ApInitiated.into_primitive() as u32
2087        );
2088        assert_eq!(
2089            disconnect_by_reason_metrics[0].event_codes[1],
2090            metrics::ConnectivityWlanMetricDimensionDisconnectSource::Ap as u32
2091        );
2092    }
2093
2094    #[test_case(
2095        fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
2096            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2097            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
2098        }),
2099        true;
2100        "ap_disconnect_source"
2101    )]
2102    #[test_case(
2103        fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
2104            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2105            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
2106        }),
2107        true;
2108        "mlme_disconnect_source_not_link_failed"
2109    )]
2110    #[test_case(
2111        fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
2112            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2113            reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
2114        }),
2115        false;
2116        "mlme_link_failed"
2117    )]
2118    #[test_case(
2119        fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown),
2120        false;
2121        "user_disconnect_source"
2122    )]
2123    #[fuchsia::test(add_test_attr = false)]
2124    fn test_log_disconnect_for_mobile_device_cobalt(
2125        disconnect_source: fidl_sme::DisconnectSource,
2126        should_log: bool,
2127    ) {
2128        let mut test_helper = setup_test();
2129        let logger = ConnectDisconnectLogger::new(
2130            test_helper.filtered_cobalt_logger(),
2131            &test_helper.inspect_node,
2132            &test_helper.inspect_metadata_node,
2133            &test_helper.inspect_metadata_path,
2134            &test_helper.mock_time_matrix_client,
2135            DeviceMobility::Mobile,
2136        );
2137
2138        // Log the event
2139        let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
2140        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
2141        assert_eq!(
2142            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2143            Poll::Ready(())
2144        );
2145
2146        let metrics = test_helper
2147            .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
2148        if should_log {
2149            assert_eq!(metrics.len(), 1);
2150            assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
2151            assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
2152        } else {
2153            assert!(metrics.is_empty());
2154            assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
2155        }
2156    }
2157
2158    #[test_case(
2159        fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
2160            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2161            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
2162        });
2163        "mlme_disconnect_source_not_link_failed"
2164    )]
2165    #[test_case(
2166        fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
2167            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2168            reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
2169        });
2170        "mlme_link_failed"
2171    )]
2172    #[test_case(
2173        fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown);
2174        "user_disconnect_source"
2175    )]
2176    #[fuchsia::test(add_test_attr = false)]
2177    fn test_log_disconnect_for_stationary_device(disconnect_source: fidl_sme::DisconnectSource) {
2178        let mut test_helper = setup_test();
2179        let logger = ConnectDisconnectLogger::new(
2180            test_helper.filtered_cobalt_logger(),
2181            &test_helper.inspect_node,
2182            &test_helper.inspect_metadata_node,
2183            &test_helper.inspect_metadata_path,
2184            &test_helper.mock_time_matrix_client,
2185            DeviceMobility::Stationary,
2186        );
2187
2188        // Log the event
2189        let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
2190        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
2191        assert_eq!(
2192            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2193            Poll::Ready(())
2194        );
2195
2196        let metrics = test_helper
2197            .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
2198        assert!(metrics.is_empty());
2199        assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
2200    }
2201
2202    #[fuchsia::test]
2203    fn test_log_downtime_post_disconnect_on_reconnect() {
2204        let mut test_helper = setup_test();
2205        let logger = ConnectDisconnectLogger::new(
2206            test_helper.filtered_cobalt_logger(),
2207            &test_helper.inspect_node,
2208            &test_helper.inspect_metadata_node,
2209            &test_helper.inspect_metadata_path,
2210            &test_helper.mock_time_matrix_client,
2211            DeviceMobility::Mobile,
2212        );
2213
2214        // Connect at 15th second
2215        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(15_000_000_000));
2216        let bss_description = random_bss_description!(Wpa2);
2217        let mut test_fut = pin!(logger.handle_connect_attempt(
2218            fidl_ieee80211::StatusCode::Success,
2219            &bss_description,
2220            false,
2221            false
2222        ));
2223        assert_eq!(
2224            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2225            Poll::Ready(())
2226        );
2227
2228        // Verify no downtime metric is logged on first successful connect
2229        let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
2230        assert!(metrics.is_empty());
2231
2232        // Verify that the connection state has transitioned to Connected
2233        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
2234
2235        // Disconnect at 25th second
2236        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(25_000_000_000));
2237        let disconnect_info = DisconnectInfo {
2238            connected_duration: zx::BootDuration::from_millis(300_000),
2239            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
2240                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2241                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
2242            }),
2243            ..fake_disconnect_info()
2244        };
2245        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
2246        assert_eq!(
2247            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2248            Poll::Ready(())
2249        );
2250
2251        // Verify that the connection state has transitioned to Disconnected
2252        assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
2253
2254        // Reconnect at 60th second
2255        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
2256        let mut test_fut = pin!(logger.handle_connect_attempt(
2257            fidl_ieee80211::StatusCode::Success,
2258            &bss_description,
2259            false,
2260            false
2261        ));
2262        assert_eq!(
2263            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2264            Poll::Ready(())
2265        );
2266
2267        // Verify that downtime metric is logged
2268        let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
2269        assert_eq!(metrics.len(), 1);
2270        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(35_000));
2271
2272        // Verify that the connection state has transitioned to Connected
2273        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
2274    }
2275
2276    #[fuchsia::test]
2277    fn test_log_iface_destroyed() {
2278        let mut test_helper = setup_test();
2279        let logger = ConnectDisconnectLogger::new(
2280            test_helper.filtered_cobalt_logger(),
2281            &test_helper.inspect_node,
2282            &test_helper.inspect_metadata_node,
2283            &test_helper.inspect_metadata_path,
2284            &test_helper.mock_time_matrix_client,
2285            DeviceMobility::Mobile,
2286        );
2287
2288        // Log connect event to move state to connected
2289        let bss_description = random_bss_description!();
2290        let mut test_fut = pin!(logger.handle_connect_attempt(
2291            fidl_ieee80211::StatusCode::Success,
2292            &bss_description,
2293            false,
2294            false
2295        ));
2296        assert_eq!(
2297            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2298            Poll::Ready(())
2299        );
2300
2301        // Verify that the connection state has transitioned to Connected
2302        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
2303
2304        // Log iface destroyed event to move state to idle
2305        let mut test_fut = pin!(logger.handle_iface_destroyed());
2306        assert_eq!(
2307            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2308            Poll::Ready(())
2309        );
2310
2311        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2312        assert_eq!(
2313            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2314            &[
2315                TimeMatrixCall::Fold(Timed::now(1 << 0)),
2316                TimeMatrixCall::Fold(Timed::now(1 << 3)),
2317                TimeMatrixCall::Fold(Timed::now(1 << 0))
2318            ]
2319        );
2320
2321        // Verify that the connection state has transitioned to Idle
2322        assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
2323    }
2324
2325    #[fuchsia::test]
2326    fn test_log_disable_client_connections() {
2327        let mut test_helper = setup_test();
2328        let logger = ConnectDisconnectLogger::new(
2329            test_helper.filtered_cobalt_logger(),
2330            &test_helper.inspect_node,
2331            &test_helper.inspect_metadata_node,
2332            &test_helper.inspect_metadata_path,
2333            &test_helper.mock_time_matrix_client,
2334            DeviceMobility::Mobile,
2335        );
2336
2337        // Log connect event to move state to connected
2338        let bss_description = random_bss_description!();
2339        let mut test_fut = pin!(logger.handle_connect_attempt(
2340            fidl_ieee80211::StatusCode::Success,
2341            &bss_description,
2342            false,
2343            false
2344        ));
2345        assert_eq!(
2346            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2347            Poll::Ready(())
2348        );
2349
2350        // Verify that the connection state has transitioned to Connected
2351        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
2352
2353        // Disable client connections to move state to idle
2354        let mut test_fut =
2355            pin!(logger.handle_client_connections_toggle(&ClientConnectionsToggleEvent::Disabled));
2356        assert_eq!(
2357            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2358            Poll::Ready(())
2359        );
2360
2361        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2362        assert_eq!(
2363            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2364            &[
2365                TimeMatrixCall::Fold(Timed::now(1 << 0)),
2366                TimeMatrixCall::Fold(Timed::now(1 << 3)),
2367                TimeMatrixCall::Fold(Timed::now(1 << 0))
2368            ]
2369        );
2370
2371        // Verify that the connection state has transitioned to Idle
2372        assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
2373    }
2374
2375    #[fuchsia::test]
2376    fn test_wlan_connectivity_states_credential_rejected() {
2377        let mut test_helper = setup_test();
2378        let logger = ConnectDisconnectLogger::new(
2379            test_helper.filtered_cobalt_logger(),
2380            &test_helper.inspect_node,
2381            &test_helper.inspect_metadata_node,
2382            &test_helper.inspect_metadata_path,
2383            &test_helper.mock_time_matrix_client,
2384            DeviceMobility::Mobile,
2385        );
2386
2387        // Log connect failure with credential rejected to move state to idle
2388        let bss_description = random_bss_description!();
2389        let mut test_fut = pin!(logger.handle_connect_attempt(
2390            fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
2391            &bss_description,
2392            true,
2393            false
2394        ));
2395        assert_eq!(
2396            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2397            Poll::Ready(())
2398        );
2399
2400        assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
2401    }
2402
2403    #[fuchsia::test]
2404    fn test_wlan_connectivity_states_failed_to_start() {
2405        let mut test_helper = setup_test();
2406        let logger = ConnectDisconnectLogger::new(
2407            test_helper.filtered_cobalt_logger(),
2408            &test_helper.inspect_node,
2409            &test_helper.inspect_metadata_node,
2410            &test_helper.inspect_metadata_path,
2411            &test_helper.mock_time_matrix_client,
2412            DeviceMobility::Mobile,
2413        );
2414
2415        let mut test_fut = pin!(logger.handle_client_connections_failed_to_start());
2416        assert_eq!(
2417            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2418            Poll::Ready(())
2419        );
2420
2421        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2422        assert_eq!(
2423            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2424            &[
2425                TimeMatrixCall::Fold(Timed::now(1 << 0)), // Initialization
2426                TimeMatrixCall::Fold(Timed::now(1 << 4)), // FailedToStart ID is 4 -> bit 1 << 4
2427            ]
2428        );
2429        assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStart(_));
2430    }
2431
2432    #[fuchsia::test]
2433    fn test_wlan_connectivity_states_failed_to_stop() {
2434        let mut test_helper = setup_test();
2435        let logger = ConnectDisconnectLogger::new(
2436            test_helper.filtered_cobalt_logger(),
2437            &test_helper.inspect_node,
2438            &test_helper.inspect_metadata_node,
2439            &test_helper.inspect_metadata_path,
2440            &test_helper.mock_time_matrix_client,
2441            DeviceMobility::Mobile,
2442        );
2443
2444        let mut test_fut = pin!(logger.handle_client_connections_failed_to_stop());
2445        assert_eq!(
2446            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2447            Poll::Ready(())
2448        );
2449
2450        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2451        assert_eq!(
2452            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2453            &[
2454                TimeMatrixCall::Fold(Timed::now(1 << 0)), // Initialization
2455                TimeMatrixCall::Fold(Timed::now(1 << 5)), // FailedToStop ID is 5 -> bit 1 << 5
2456            ]
2457        );
2458
2459        assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStop(_));
2460    }
2461
2462    #[test_case(ConnectionState::Idle(IdleState {}))]
2463    #[test_case(ConnectionState::Disconnected(DisconnectedState {}))]
2464    #[test_case(ConnectionState::ConnectFailed(ConnectFailedState {}))]
2465    #[test_case(ConnectionState::PnoScanFailedIdle(PnoScanFailedIdleState {}))]
2466    fn test_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
2467        let mut test_helper = setup_test();
2468        let logger = ConnectDisconnectLogger::new(
2469            test_helper.filtered_cobalt_logger(),
2470            &test_helper.inspect_node,
2471            &test_helper.inspect_metadata_node,
2472            &test_helper.inspect_metadata_path,
2473            &test_helper.mock_time_matrix_client,
2474            DeviceMobility::Mobile,
2475        );
2476
2477        // Transition to initial state
2478        *logger.connection_state.lock() = initial_state.clone();
2479
2480        // Log a PNO scan failure
2481        let mut test_fut = pin!(logger.handle_pno_scan_failure());
2482        assert_matches!(
2483            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2484            Poll::Ready(())
2485        );
2486
2487        // Verify the metrics were logged
2488        let metric_events = test_helper
2489            .get_logged_metrics(metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID);
2490        assert_eq!(metric_events.len(), 1);
2491        assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
2492
2493        let metric_events =
2494            test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
2495        assert_eq!(metric_events.len(), 1);
2496        assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
2497
2498        // Verify the time matrix shows the PNO scan failure state
2499        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2500        assert_eq!(
2501            *time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..].last().unwrap(),
2502            TimeMatrixCall::Fold(Timed::now(1 << 6)), // PnoScanFailedIdle ID is 6 -> bit 1 << 6
2503        );
2504
2505        // A PNO scan failure should cause a transition to PnoScanFailedIdle
2506        assert_matches!(*logger.connection_state.lock(), ConnectionState::PnoScanFailedIdle(_));
2507    }
2508
2509    #[test_case(ConnectionState::Connected(ConnectedState {
2510        bss: Box::new(fake_bss_description!(Wpa2)),
2511        is_owe_transition: false,
2512    }))]
2513    #[test_case(ConnectionState::FailedToStart(FailedToStartState {}))]
2514    #[test_case(ConnectionState::FailedToStop(FailedToStopState {}))]
2515    fn test_no_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
2516        let mut test_helper = setup_test();
2517        let logger = ConnectDisconnectLogger::new(
2518            test_helper.filtered_cobalt_logger(),
2519            &test_helper.inspect_node,
2520            &test_helper.inspect_metadata_node,
2521            &test_helper.inspect_metadata_path,
2522            &test_helper.mock_time_matrix_client,
2523            DeviceMobility::Mobile,
2524        );
2525
2526        // Transition to initial state
2527        *logger.connection_state.lock() = initial_state.clone();
2528
2529        // Log a PNO scan failure
2530        let mut test_fut = pin!(logger.handle_pno_scan_failure());
2531        assert_matches!(
2532            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2533            Poll::Ready(())
2534        );
2535
2536        // Verify the metrics were logged
2537        let metric_events =
2538            test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
2539        assert_eq!(metric_events.len(), 1);
2540        assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
2541
2542        // State should not change
2543        assert_eq!(logger.connection_state.lock().to_id(), initial_state.to_id());
2544    }
2545
2546    #[fuchsia::test]
2547    fn test_wlan_connectivity_states_bitset_map_size() {
2548        let enum_variant_count = ConnectionState::COUNT;
2549        let bitset_map_size =
2550            ConnectDisconnectTimeSeries::wlan_connectivity_states_bitset_map().len();
2551        assert_eq!(enum_variant_count, bitset_map_size);
2552    }
2553
2554    fn fake_disconnect_info() -> DisconnectInfo {
2555        let bss_description = random_bss_description!(Wpa2);
2556        let channel = bss_description.channel;
2557        DisconnectInfo {
2558            iface_id: 1,
2559            connected_duration: zx::BootDuration::from_hours(6),
2560            is_sme_reconnecting: false,
2561            disconnect_source: fidl_sme::DisconnectSource::User(
2562                fidl_sme::UserDisconnectReason::Unknown,
2563            ),
2564            original_bss_desc: bss_description.into(),
2565            current_rssi_dbm: -30,
2566            current_snr_db: 25,
2567            current_channel: channel,
2568        }
2569    }
2570}