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