1use crate::client::{ClientSmeStatus, ServingApInfo};
6use fuchsia_inspect::{
7 BoolProperty, BytesProperty, Inspector, IntProperty, Node, Property, StringProperty,
8 UintProperty,
9};
10use fuchsia_inspect_contrib::inspect_insert;
11use fuchsia_inspect_contrib::log::{InspectListClosure, InspectUintArray};
12use fuchsia_inspect_contrib::nodes::{BoundedListNode, MonotonicTimeProperty, NodeTimeExt};
13use fuchsia_sync::Mutex;
14use ieee80211::Ssid;
15use wlan_common::ie::{self, wsc};
16use {
17 fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211,
18 fidl_fuchsia_wlan_mlme as fidl_mlme,
19};
20
21const STATE_EVENTS_LIMIT: usize = 50;
25const RSN_EVENTS_LIMIT: usize = 50;
26
27const JOIN_SCAN_EVENTS_LIMIT: usize = 10;
29
30const IDLE_STR: &str = "idle";
32
33pub struct SmeTree {
35 pub inspector: Inspector,
37 pub _root_node: Node,
39 pub state_events: Mutex<BoundedListNode>,
42 pub rsn_events: Mutex<BoundedListNode>,
44 #[allow(dead_code)]
47 pub join_scan_events: Mutex<BoundedListNode>,
48 pub last_pulse: Mutex<PulseNode>,
55
56 pub scan_discard_fidl_bss: UintProperty,
58
59 pub scan_merge_ie_failures: UintProperty,
62}
63
64impl SmeTree {
65 pub fn new(
66 inspector: Inspector,
67 node: Node,
68 device_info: &fidl_mlme::DeviceInfo,
69 spectrum_management_support: &fidl_common::SpectrumManagementSupport,
70 ) -> Self {
71 let state_events =
72 BoundedListNode::new(node.create_child("state_events"), STATE_EVENTS_LIMIT);
73 let rsn_events = BoundedListNode::new(node.create_child("rsn_events"), RSN_EVENTS_LIMIT);
74 let join_scan_events =
75 BoundedListNode::new(node.create_child("join_scan_events"), JOIN_SCAN_EVENTS_LIMIT);
76 let pulse = PulseNode::new(node.create_child("last_pulse"));
77 let scan_discard_fidl_bss = node.create_uint("scan_discard_fidl_bss", 0);
78 let scan_merge_ie_failures = node.create_uint("scan_merge_ie_failures", 0);
79 inspect_insert!(node, device_support: {
80 device_info: {
81 bands: InspectListClosure(&device_info.bands, |node, key, band| {
82 inspect_insert!(node, var key: {
83 band: match band.band {
84 fidl_ieee80211::WlanBand::TwoGhz => "2.4Ghz",
85 fidl_ieee80211::WlanBand::FiveGhz => "5Ghz",
86 _ => "Unknown",
87 },
88 operating_channels: InspectUintArray::new(&band.operating_channels),
89 });
90 }),
91 },
92 spectrum_management_support: {
93 dfs: {
94 supported: spectrum_management_support.dfs.supported,
95 }
96 }
97 });
98 Self {
99 inspector,
100 _root_node: node,
101 state_events: Mutex::new(state_events),
102 rsn_events: Mutex::new(rsn_events),
103 join_scan_events: Mutex::new(join_scan_events),
104 last_pulse: Mutex::new(pulse),
105 scan_discard_fidl_bss,
106 scan_merge_ie_failures,
107 }
108 }
109
110 pub fn update_pulse(&self, new_status: ClientSmeStatus) {
111 self.last_pulse.lock().update(new_status)
112 }
113
114 pub fn clone_vmo_data(&self) -> Option<fidl::Vmo> {
115 self.inspector.copy_vmo()
116 }
117}
118
119pub struct PulseNode {
120 node: Node,
121 _started: MonotonicTimeProperty,
122 last_updated: MonotonicTimeProperty,
123 last_link_up: Option<MonotonicTimeProperty>,
124 status_node: Option<ClientSmeStatusNode>,
125
126 status: Option<ClientSmeStatus>,
128}
129
130impl PulseNode {
131 fn new(node: Node) -> Self {
132 let now = zx::MonotonicInstant::get();
133 let started = node.create_time_at("started", now);
134 let last_updated = node.create_time_at("last_updated", now);
135 Self {
136 node,
137 _started: started,
138 last_updated,
139 last_link_up: None,
140 status_node: None,
141 status: None,
142 }
143 }
144
145 pub fn update(&mut self, new_status: ClientSmeStatus) {
146 let now = zx::MonotonicInstant::get();
147 self.last_updated.set_at(now);
148
149 if new_status.is_connected()
153 || self.status.as_ref().map(|s| s.is_connected()).unwrap_or(false)
154 {
155 match &self.last_link_up {
156 Some(last_link_up) => last_link_up.set_at(now),
157 None => self.last_link_up = Some(self.node.create_time_at("last_link_up", now)),
158 }
159 }
160
161 if let Some(status) = &self.status {
163 if *status == new_status {
164 return;
165 }
166 }
167
168 match self.status_node.as_mut() {
169 Some(status_node) => status_node.update(self.status.as_ref(), &new_status),
170 None => {
171 self.status_node =
172 Some(ClientSmeStatusNode::new(self.node.create_child("status"), &new_status))
173 }
174 }
175 self.status = Some(new_status);
176 }
177}
178
179pub struct ClientSmeStatusNode {
180 node: Node,
181 status_str: StringProperty,
182 prev_connected_to: Option<ServingApInfoNode>,
183 connected_to: Option<ServingApInfoNode>,
184 connecting_to: Option<ConnectingToNode>,
185}
186
187impl ClientSmeStatusNode {
188 fn new(node: Node, status: &ClientSmeStatus) -> Self {
189 let status_str = node.create_string("status_str", IDLE_STR);
190 let mut status_node = Self {
191 node,
192 status_str,
193 prev_connected_to: None,
194 connected_to: None,
195 connecting_to: None,
196 };
197 status_node.update(None, status);
198 status_node
199 }
200
201 pub fn update(&mut self, old_status: Option<&ClientSmeStatus>, new_status: &ClientSmeStatus) {
202 let status_str = match new_status {
203 ClientSmeStatus::Connected(_) => "connected",
204 ClientSmeStatus::Connecting(_) => "connecting",
205 ClientSmeStatus::Roaming(_) => "roaming",
206 ClientSmeStatus::Idle => IDLE_STR,
207 };
208 self.status_str.set(status_str);
209
210 if status_str == IDLE_STR {
211 if let Some(ClientSmeStatus::Connected(serving_ap_info)) = old_status {
212 match self.prev_connected_to.as_mut() {
213 Some(prev_connected_to) => prev_connected_to.update(serving_ap_info),
214 None => {
215 self.prev_connected_to = Some(ServingApInfoNode::new(
216 self.node.create_child("prev_connected_to"),
217 serving_ap_info,
218 ));
219 }
220 }
221 }
222 }
223
224 match &new_status {
225 ClientSmeStatus::Connected(serving_ap_info) => match self.connected_to.as_mut() {
226 Some(connected_to) => connected_to.update(serving_ap_info),
227 None => {
228 self.connected_to = Some(ServingApInfoNode::new(
229 self.node.create_child("connected_to"),
230 serving_ap_info,
231 ));
232 }
233 },
234 ClientSmeStatus::Connecting(_)
235 | ClientSmeStatus::Roaming(_)
236 | ClientSmeStatus::Idle => {
237 self.connected_to = None;
238 }
239 }
240
241 match &new_status {
242 ClientSmeStatus::Connecting(ssid) => match self.connecting_to.as_mut() {
243 Some(connecting_to) => connecting_to.update(ssid),
244 None => {
245 self.connecting_to =
246 Some(ConnectingToNode::new(self.node.create_child("connecting_to"), ssid));
247 }
248 },
249 ClientSmeStatus::Connected(_) | ClientSmeStatus::Roaming(_) | ClientSmeStatus::Idle => {
250 self.connecting_to = None;
251 }
252 }
253 }
254}
255
256pub struct ServingApInfoNode {
257 node: Node,
258 bssid: StringProperty,
259 ssid: StringProperty,
260
261 rssi_dbm: IntProperty,
262 snr_db: IntProperty,
263 signal_report_time: MonotonicTimeProperty,
264 channel: ChannelNode,
265 protection: StringProperty,
266 is_wmm_assoc: BoolProperty,
267 wmm_param: Option<BssWmmParamNode>,
268 ht_cap: Option<BytesProperty>,
269 vht_cap: Option<BytesProperty>,
270 wsc: Option<BssWscNode>,
271}
272
273impl ServingApInfoNode {
274 fn new(node: Node, ap: &ServingApInfo) -> Self {
275 let mut serving_ap_info_node = Self {
276 bssid: node.create_string("bssid", ap.bssid.to_string()),
277 ssid: node.create_string("ssid", ap.ssid.to_string()),
278 rssi_dbm: node.create_int("rssi_dbm", ap.rssi_dbm as i64),
279 snr_db: node.create_int("snr_db", ap.snr_db as i64),
280 signal_report_time: node.create_time_at("signal_report_time", ap.signal_report_time),
281 channel: ChannelNode::new(node.create_child("channel"), ap.channel.into()),
282 protection: node.create_string("protection", format!("{}", ap.protection)),
283
284 ht_cap: None,
286 vht_cap: None,
287 is_wmm_assoc: node.create_bool("is_wmm_assoc", false),
288 wmm_param: None,
289 wsc: None,
290
291 node,
292 };
293 serving_ap_info_node.update_ht_cap_node(ap);
294 serving_ap_info_node.update_vht_cap_node(ap);
295 serving_ap_info_node.update_wmm_node(ap);
296 serving_ap_info_node.update_wsc_node(ap);
297
298 serving_ap_info_node
299 }
300
301 fn update(&mut self, ap: &ServingApInfo) {
302 self.bssid.set(&ap.bssid.to_string());
303 self.ssid.set(&ap.ssid.to_string());
304 self.rssi_dbm.set(ap.rssi_dbm as i64);
305 self.snr_db.set(ap.snr_db as i64);
306 self.signal_report_time.set_at(ap.signal_report_time);
307 self.channel.update(ap.channel.into());
308 self.protection.set(&format!("{}", ap.protection));
309
310 self.update_ht_cap_node(ap);
311 self.update_vht_cap_node(ap);
312 self.update_wmm_node(ap);
313 self.update_wsc_node(ap);
314 }
315
316 fn update_ht_cap_node(&mut self, ap: &ServingApInfo) {
317 match &ap.ht_cap {
318 Some(ht_cap) => match self.ht_cap.as_mut() {
319 Some(ht_cap_prop) => ht_cap_prop.set(&ht_cap.bytes),
320 None => self.ht_cap = Some(self.node.create_bytes("ht_cap", ht_cap.bytes)),
321 },
322 None => {
323 self.ht_cap = None;
324 }
325 }
326 }
327
328 fn update_vht_cap_node(&mut self, ap: &ServingApInfo) {
329 match &ap.vht_cap {
330 Some(vht_cap) => match self.vht_cap.as_mut() {
331 Some(vht_cap_prop) => vht_cap_prop.set(&vht_cap.bytes),
332 None => self.vht_cap = Some(self.node.create_bytes("vht_cap", vht_cap.bytes)),
333 },
334 None => {
335 self.vht_cap = None;
336 }
337 }
338 }
339
340 fn update_wmm_node(&mut self, ap: &ServingApInfo) {
341 match &ap.wmm_param {
342 Some(wmm_param) => {
343 self.is_wmm_assoc.set(true);
344 match self.wmm_param.as_mut() {
345 Some(wmm_param_node) => wmm_param_node.update(wmm_param),
346 None => {
347 self.wmm_param = Some(BssWmmParamNode::new(
348 self.node.create_child("wmm_param"),
349 wmm_param,
350 ))
351 }
352 }
353 }
354 None => {
355 self.is_wmm_assoc.set(false);
356 self.wmm_param = None;
357 }
358 }
359 }
360
361 fn update_wsc_node(&mut self, ap: &ServingApInfo) {
362 match &ap.probe_resp_wsc {
363 Some(wsc) => match self.wsc.as_mut() {
364 Some(wsc_node) => wsc_node.update(wsc),
365 None => self.wsc = Some(BssWscNode::new(self.node.create_child("wsc"), wsc)),
366 },
367 None => {
368 self.wsc = None;
369 }
370 }
371 }
372}
373
374pub struct ChannelNode {
375 _node: Node,
376 primary: UintProperty,
377 cbw: StringProperty,
378 secondary80: UintProperty,
379}
380
381impl ChannelNode {
382 pub fn new(node: Node, channel: fidl_common::WlanChannel) -> Self {
383 let primary = node.create_uint("primary", channel.primary as u64);
384 let cbw = node.create_string("cbw", format!("{:?}", channel.cbw));
385 let secondary80 = node.create_uint("secondary80", channel.secondary80 as u64);
386 Self { _node: node, primary, cbw, secondary80 }
387 }
388
389 pub fn update(&mut self, channel: fidl_common::WlanChannel) {
390 self.primary.set(channel.primary as u64);
391 self.cbw.set(&format!("{:?}", channel.cbw));
392 self.secondary80.set(channel.secondary80 as u64);
393 }
394}
395
396pub struct BssWmmParamNode {
397 _node: Node,
398 wmm_info: BssWmmInfoNode,
399 ac_be: BssWmmAcParamsNode,
400 ac_bk: BssWmmAcParamsNode,
401 ac_vi: BssWmmAcParamsNode,
402 ac_vo: BssWmmAcParamsNode,
403}
404
405impl BssWmmParamNode {
406 fn new(node: Node, wmm_param: &ie::WmmParam) -> Self {
407 let wmm_info =
408 BssWmmInfoNode::new(node.create_child("wmm_info"), wmm_param.wmm_info.ap_wmm_info());
409 let ac_be = BssWmmAcParamsNode::new(node.create_child("ac_be"), wmm_param.ac_be_params);
410 let ac_bk = BssWmmAcParamsNode::new(node.create_child("ac_bk"), wmm_param.ac_bk_params);
411 let ac_vi = BssWmmAcParamsNode::new(node.create_child("ac_vi"), wmm_param.ac_vi_params);
412 let ac_vo = BssWmmAcParamsNode::new(node.create_child("ac_vo"), wmm_param.ac_vo_params);
413 Self { _node: node, wmm_info, ac_be, ac_bk, ac_vi, ac_vo }
414 }
415
416 fn update(&mut self, wmm_param: &ie::WmmParam) {
417 self.wmm_info.update(&wmm_param.wmm_info.ap_wmm_info());
418 self.ac_be.update(&wmm_param.ac_be_params);
419 self.ac_bk.update(&wmm_param.ac_bk_params);
420 self.ac_vi.update(&wmm_param.ac_vi_params);
421 self.ac_vo.update(&wmm_param.ac_vo_params);
422 }
423}
424
425pub struct BssWmmInfoNode {
426 _node: Node,
427 param_set_count: UintProperty,
428 uapsd: BoolProperty,
429}
430
431impl BssWmmInfoNode {
432 fn new(node: Node, info: ie::ApWmmInfo) -> Self {
433 let param_set_count =
434 node.create_uint("param_set_count", info.parameter_set_count() as u64);
435 let uapsd = node.create_bool("uapsd", info.uapsd());
436 Self { _node: node, param_set_count, uapsd }
437 }
438
439 fn update(&mut self, info: &ie::ApWmmInfo) {
440 self.param_set_count.set(info.parameter_set_count() as u64);
441 self.uapsd.set(info.uapsd());
442 }
443}
444
445pub struct BssWmmAcParamsNode {
446 _node: Node,
447 aifsn: UintProperty,
448 acm: BoolProperty,
449 ecw_min: UintProperty,
450 ecw_max: UintProperty,
451 txop_limit: UintProperty,
452}
453
454impl BssWmmAcParamsNode {
455 fn new(node: Node, ac_params: ie::WmmAcParams) -> Self {
456 let aifsn = node.create_uint("aifsn", ac_params.aci_aifsn.aifsn() as u64);
457 let acm = node.create_bool("acm", ac_params.aci_aifsn.acm());
458 let ecw_min = node.create_uint("ecw_min", ac_params.ecw_min_max.ecw_min() as u64);
459 let ecw_max = node.create_uint("ecw_max", ac_params.ecw_min_max.ecw_max() as u64);
460 let txop_limit = node.create_uint("txop_limit", ac_params.txop_limit as u64);
461 Self { _node: node, aifsn, acm, ecw_min, ecw_max, txop_limit }
462 }
463
464 fn update(&self, ac_params: &ie::WmmAcParams) {
465 self.aifsn.set(ac_params.aci_aifsn.aifsn() as u64);
466 self.acm.set(ac_params.aci_aifsn.acm());
467 self.ecw_min.set(ac_params.ecw_min_max.ecw_min() as u64);
468 self.ecw_max.set(ac_params.ecw_min_max.ecw_max() as u64);
469 self.txop_limit.set(ac_params.txop_limit as u64);
470 }
471}
472
473pub struct BssWscNode {
474 _node: Node,
475 manufacturer: StringProperty,
476 model_name: StringProperty,
477 model_number: StringProperty,
478 device_name: StringProperty,
479}
480
481impl BssWscNode {
482 fn new(node: Node, wsc: &wsc::ProbeRespWsc) -> Self {
483 let manufacturer =
484 node.create_string("manufacturer", String::from_utf8_lossy(&wsc.manufacturer[..]));
485 let model_name =
486 node.create_string("model_name", String::from_utf8_lossy(&wsc.model_name[..]));
487 let model_number =
488 node.create_string("model_number", String::from_utf8_lossy(&wsc.model_number[..]));
489 let device_name =
490 node.create_string("device_name", String::from_utf8_lossy(&wsc.device_name[..]));
491
492 Self { _node: node, manufacturer, model_name, model_number, device_name }
493 }
494
495 fn update(&mut self, wsc: &wsc::ProbeRespWsc) {
496 self.manufacturer.set(&String::from_utf8_lossy(&wsc.manufacturer[..]));
497 self.model_name.set(&String::from_utf8_lossy(&wsc.model_name[..]));
498 self.model_number.set(&String::from_utf8_lossy(&wsc.model_number[..]));
499 self.device_name.set(&String::from_utf8_lossy(&wsc.device_name[..]));
500 }
501}
502
503pub struct ConnectingToNode {
504 _node: Node,
505 ssid: StringProperty,
506}
507
508impl ConnectingToNode {
509 fn new(node: Node, ssid: &Ssid) -> Self {
510 let ssid = node.create_string("ssid", ssid.to_string());
511 Self { _node: node, ssid }
512 }
513
514 fn update(&mut self, ssid: &Ssid) {
515 self.ssid.set(&ssid.to_string());
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use crate::client::test_utils;
523 use diagnostics_assertions::{assert_data_tree, AnyProperty};
524 use fuchsia_inspect::Inspector;
525
526 #[test]
527 fn test_inspect_update_pulse_connect_disconnect() {
528 let inspector = Inspector::default();
529 let root = inspector.root();
530 let mut pulse = PulseNode::new(root.create_child("last_pulse"));
531
532 let status = ClientSmeStatus::Idle;
534 pulse.update(status);
535 assert_data_tree!(inspector, root: {
536 last_pulse: {
537 started: AnyProperty,
538 last_updated: AnyProperty,
539 status: { status_str: "idle" }
540 }
541 });
542
543 let status = ClientSmeStatus::Connecting(Ssid::try_from("foo").unwrap());
546 pulse.update(status);
547 assert_data_tree!(inspector, root: {
548 last_pulse: {
549 started: AnyProperty,
550 last_updated: AnyProperty,
551 status: {
552 status_str: "connecting",
553 connecting_to: { ssid: "<ssid-666f6f>" }
554 },
555 }
556 });
557
558 let status = ClientSmeStatus::Connected(test_utils::fake_serving_ap_info());
562 pulse.update(status);
563 assert_data_tree!(inspector, root: {
564 last_pulse: {
565 started: AnyProperty,
566 last_updated: AnyProperty,
567 last_link_up: AnyProperty,
568 status: {
569 status_str: "connected",
570 connected_to: contains {
571 ssid: "<ssid-666f6f>",
572 bssid: "37:0a:16:03:09:46",
573 },
574 },
575 }
576 });
577
578 let status = ClientSmeStatus::Idle;
581 pulse.update(status);
582 assert_data_tree!(inspector, root: {
583 last_pulse: {
584 started: AnyProperty,
585 last_updated: AnyProperty,
586 last_link_up: AnyProperty,
587 status: {
588 status_str: "idle",
589 prev_connected_to: contains {
590 ssid: "<ssid-666f6f>",
591 bssid: "37:0a:16:03:09:46",
592 },
593 },
594 }
595 });
596 }
597
598 #[test]
599 fn test_inspect_update_pulse_wmm_status_changed() {
600 let inspector = Inspector::default();
601 let root = inspector.root();
602 let mut pulse = PulseNode::new(root.create_child("last_pulse"));
603
604 let mut serving_ap_info = test_utils::fake_serving_ap_info();
605 serving_ap_info.wmm_param = None;
606 let status = ClientSmeStatus::Connected(serving_ap_info.clone());
607 pulse.update(status);
608 assert_data_tree!(inspector, root: {
609 last_pulse: contains {
610 status: contains {
611 connected_to: contains {
612 is_wmm_assoc: false,
613 },
614 },
615 }
616 });
617
618 let mut wmm_param =
619 *ie::parse_wmm_param(&test_utils::fake_wmm_param().bytes[..]).expect("parse wmm");
620 serving_ap_info.wmm_param = Some(wmm_param);
621 let status = ClientSmeStatus::Connected(serving_ap_info.clone());
622 pulse.update(status);
623 assert_data_tree!(inspector, root: {
624 last_pulse: contains {
625 status: contains {
626 connected_to: contains {
627 is_wmm_assoc: true,
628 wmm_param: contains {
629 ac_be: {
630 aifsn: 3u64,
631 acm: false,
632 ecw_min: 4u64,
633 ecw_max: 10u64,
634 txop_limit: 0u64,
635 },
636 ac_bk: {
637 aifsn: 7u64,
638 acm: false,
639 ecw_min: 4u64,
640 ecw_max: 10u64,
641 txop_limit: 0u64,
642 },
643 ac_vi: {
644 aifsn: 2u64,
645 acm: false,
646 ecw_min: 3u64,
647 ecw_max: 4u64,
648 txop_limit: 0x5eu64,
649 },
650 ac_vo: {
651 aifsn: 2u64,
652 acm: false,
653 ecw_min: 2u64,
654 ecw_max: 3u64,
655 txop_limit: 0x2fu64,
656 },
657 wmm_info: contains {
658 uapsd: true,
659 },
660 }
661 },
662 },
663 }
664 });
665
666 let mut wmm_info = wmm_param.wmm_info.ap_wmm_info();
667 wmm_info.set_uapsd(false);
668 wmm_param.wmm_info.0 = wmm_info.0;
669 wmm_param.ac_be_params.aci_aifsn.set_aifsn(9);
670 wmm_param.ac_bk_params.aci_aifsn.set_acm(true);
671 wmm_param.ac_vi_params.ecw_min_max.set_ecw_min(11);
672 wmm_param.ac_vi_params.ecw_min_max.set_ecw_max(14);
673 wmm_param.ac_vo_params.txop_limit = 0xaa;
674 serving_ap_info.wmm_param = Some(wmm_param);
675 let status = ClientSmeStatus::Connected(serving_ap_info.clone());
676 pulse.update(status);
677 assert_data_tree!(inspector, root: {
678 last_pulse: contains {
679 status: contains {
680 connected_to: contains {
681 is_wmm_assoc: true,
682 wmm_param: contains {
683 ac_be: {
684 aifsn: 9u64,
685 acm: false,
686 ecw_min: 4u64,
687 ecw_max: 10u64,
688 txop_limit: 0u64,
689 },
690 ac_bk: {
691 aifsn: 7u64,
692 acm: true,
693 ecw_min: 4u64,
694 ecw_max: 10u64,
695 txop_limit: 0u64,
696 },
697 ac_vi: {
698 aifsn: 2u64,
699 acm: false,
700 ecw_min: 11u64,
701 ecw_max: 14u64,
702 txop_limit: 0x5eu64,
703 },
704 ac_vo: {
705 aifsn: 2u64,
706 acm: false,
707 ecw_min: 2u64,
708 ecw_max: 3u64,
709 txop_limit: 0xaau64,
710 },
711 wmm_info: contains {
712 uapsd: false,
713 },
714 }
715 },
716 },
717 }
718 });
719
720 serving_ap_info.wmm_param = None;
721 let status = ClientSmeStatus::Connected(serving_ap_info.clone());
722 pulse.update(status);
723 assert_data_tree!(inspector, root: {
724 last_pulse: contains {
725 status: contains {
726 connected_to: contains {
727 is_wmm_assoc: false,
728 },
729 },
730 }
731 });
732 }
733}