settings_light/
light_controller.rs

1// Copyright 2020 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::light_fidl_handler::{GroupPublisher, InfoPublisher};
6use crate::light_hardware_configuration::{DisableConditions, LightHardwareConfiguration};
7use crate::types::{LightGroup, LightInfo, LightState, LightType, LightValue};
8use anyhow::{Context, Error};
9use fidl_fuchsia_hardware_light::{Info, LightMarker, LightProxy};
10use fidl_fuchsia_settings_storage::LightGroups;
11use fuchsia_async as fasync;
12use futures::channel::mpsc::UnboundedReceiver;
13use futures::channel::oneshot::Sender;
14use futures::lock::Mutex;
15use futures::StreamExt;
16use settings_common::call_async;
17use settings_common::config::default_settings::DefaultSetting;
18use settings_common::inspect::event::{
19    ExternalEventPublisher, ResponseType, SettingValuePublisher,
20};
21use settings_common::service_context::{ExternalServiceProxy, ServiceContext};
22use settings_media_buttons::{Event, MediaButtons};
23use settings_storage::fidl_storage::{FidlStorage, FidlStorageConvertible};
24use settings_storage::storage_factory::{NoneT, StorageAccess, StorageFactory};
25use settings_storage::UpdateState;
26use std::borrow::Cow;
27use std::collections::hash_map::Entry;
28use std::collections::HashMap;
29use std::rc::Rc;
30
31/// Used as the argument field in a LightError::InvalidArgument to signal the FIDL handler to
32/// signal that a fidl LightError::INVALID_NAME should be returned to the client.
33pub(crate) const ARG_NAME: &str = "name";
34
35/// Hardware path used to connect to light devices.
36pub(crate) const DEVICE_PATH: &str = "/dev/class/light/*";
37
38impl FidlStorageConvertible for LightInfo {
39    type Storable = LightGroups;
40    type Loader = NoneT;
41    const KEY: &'static str = "light_info";
42
43    #[allow(clippy::redundant_closure)]
44    fn to_storable(self) -> Self::Storable {
45        LightGroups {
46            groups: self
47                .light_groups
48                .into_values()
49                .map(|group| fidl_fuchsia_settings::LightGroup::from(group))
50                .collect(),
51        }
52    }
53
54    fn from_storable(storable: Self::Storable) -> Self {
55        // Unwrap ok since validation would ensure non-None name before writing to storage.
56        let light_groups = storable
57            .groups
58            .into_iter()
59            .map(|group| (group.name.clone().unwrap(), group.into()))
60            .collect();
61        Self { light_groups }
62    }
63}
64
65#[derive(thiserror::Error, Debug)]
66pub(crate) enum LightError {
67    #[error("Invalid input argument for Light setting: argument:{0:?} value:{1:?}")]
68    InvalidArgument(&'static str, String),
69    #[error(
70        "Call to an external dependency {0:?} for Light setting failed. \
71         Request:{1:?}: Error:{2}"
72    )]
73    ExternalFailure(&'static str, Cow<'static, str>, Cow<'static, str>),
74    #[error("Write failed for Light setting: {0:?}")]
75    WriteFailure(Error),
76    #[error("Unexpected error: {0:?}")]
77    UnexpectedError(&'static str),
78}
79
80impl From<&LightError> for ResponseType {
81    fn from(error: &LightError) -> Self {
82        match error {
83            LightError::InvalidArgument(..) => ResponseType::InvalidArgument,
84            LightError::ExternalFailure(..) => ResponseType::ExternalFailure,
85            LightError::WriteFailure(..) => ResponseType::StorageFailure,
86            LightError::UnexpectedError(..) => ResponseType::UnexpectedError,
87        }
88    }
89}
90
91pub struct LightController {
92    /// Proxy for interacting with light hardware.
93    light_proxy: ExternalServiceProxy<LightProxy, ExternalEventPublisher>,
94
95    /// inspect_event_tx.clone() configuration that determines what lights to return to the client.
96    ///
97    /// If present, overrides the lights from the underlying fuchsia.hardware.light API.
98    light_hardware_config: Option<LightHardwareConfiguration>,
99
100    /// Cache of data that includes hardware values. The data stored on disk does not persist the
101    /// hardware values, so restoring does not bring the values back into memory. The data needs to
102    /// be cached at this layer so we don't lose track of them.
103    data_cache: Rc<Mutex<Option<LightInfo>>>,
104
105    /// Disk storage for light setting. Stores in fidl format.
106    store: Rc<FidlStorage>,
107
108    /// HangingGet publisher for WatchLightGroups fidl api.
109    publisher: Option<InfoPublisher>,
110
111    /// HangingGet publisher for WatchLightGroup (singular) fidl api.
112    group_publishers: HashMap<String, GroupPublisher>,
113
114    /// Publisher for updates to the setting value.
115    setting_value_publisher: SettingValuePublisher<LightInfo>,
116}
117
118pub(crate) enum Request {
119    SetLightGroupValue(String, Vec<LightState>, Sender<Result<(), LightError>>),
120}
121
122impl StorageAccess for LightController {
123    type Storage = FidlStorage;
124    type Data = LightInfo;
125    const STORAGE_KEY: &'static str = LightInfo::KEY;
126}
127
128/// Controller for processing requests surrounding the Light protocol.
129impl LightController {
130    pub(crate) async fn new<F>(
131        service_context: Rc<ServiceContext>,
132        default_setting: &mut DefaultSetting<LightHardwareConfiguration, &'static str>,
133        storage_factory: Rc<F>,
134        setting_value_publisher: SettingValuePublisher<LightInfo>,
135        external_publisher: ExternalEventPublisher,
136    ) -> Result<Self, Error>
137    where
138        F: StorageFactory<Storage = FidlStorage>,
139    {
140        let light_hardware_config =
141            default_setting.load_default_value().context("loading default value")?;
142
143        LightController::create_with_config(
144            service_context,
145            light_hardware_config,
146            &*storage_factory,
147            setting_value_publisher,
148            external_publisher,
149        )
150        .await
151    }
152
153    /// Alternate constructor that allows specifying a configuration.
154    async fn create_with_config<F>(
155        service_context: Rc<ServiceContext>,
156        light_hardware_config: Option<LightHardwareConfiguration>,
157        storage_factory: &F,
158        setting_value_publisher: SettingValuePublisher<LightInfo>,
159        external_publisher: ExternalEventPublisher,
160    ) -> Result<Self, Error>
161    where
162        F: StorageFactory<Storage = FidlStorage>,
163    {
164        let light_proxy = service_context
165            .connect_device_path::<LightMarker, _>(DEVICE_PATH, external_publisher)
166            .await
167            .context("connecting to fuchsia.hardware.light")?;
168
169        Ok(LightController {
170            light_proxy,
171            light_hardware_config,
172            data_cache: Rc::new(Mutex::new(None)),
173            store: storage_factory.get_store().await,
174            publisher: None,
175            group_publishers: HashMap::new(),
176            setting_value_publisher,
177        })
178    }
179
180    pub(crate) fn register_publishers(
181        &mut self,
182        publisher: InfoPublisher,
183        group_publishers: HashMap<String, GroupPublisher>,
184    ) {
185        self.publisher = Some(publisher);
186        self.group_publishers = group_publishers;
187    }
188
189    fn publish(&self, info: LightInfo) {
190        let _ = self.setting_value_publisher.publish(&info);
191        let pg = info.light_groups.iter().filter_map(|(key, group)| {
192            self.group_publishers.get(key).map(|publisher| (publisher, group))
193        });
194        for (publisher, group) in pg {
195            publisher.update(|old_group| {
196                let Some(old_group) = old_group.as_mut() else {
197                    *old_group = Some(group.clone());
198                    return true;
199                };
200
201                if *old_group != *group {
202                    *old_group = group.clone();
203                    return true;
204                }
205                false
206            });
207        }
208
209        if let Some(publisher) = self.publisher.as_ref() {
210            publisher.set(info);
211        }
212    }
213
214    pub(crate) async fn handle(
215        self,
216        mut event_rx: UnboundedReceiver<(Event, super::ResultSender)>,
217        mut request_rx: UnboundedReceiver<Request>,
218    ) -> Result<fasync::Task<()>, LightError> {
219        Ok(fasync::Task::local(async move {
220            let mut next_event = event_rx.next();
221            let mut next_request = request_rx.next();
222            'request: loop {
223                futures::select! {
224                    event = next_event => {
225                        let Some((Event::OnButton(media_buttons), response_tx)) = event else {
226                            continue;
227                        };
228                        next_event = event_rx.next();
229                        let res = if let MediaButtons { mic_mute: Some(mic_mute), .. } = media_buttons {
230                            self.on_mic_mute(mic_mute).await.map(|res| res.map(|info| self.publish(info)))
231                        } else {
232                            Ok(None)
233                        };
234                        let _ = response_tx.send(res);
235                    }
236                    request = next_request => {
237                        let Some(Request::SetLightGroupValue(name, state, tx)) = request else {
238                            continue;
239                        };
240                        next_request = request_rx.next();
241                        // Validate state contains valid float numbers.
242                        for light_state in &state {
243                            if !light_state.is_finite() {
244                                let _ = tx.send(Err(LightError::InvalidArgument(
245                                    "state",
246                                    format!("{light_state:?}"),
247                                )));
248                                continue 'request;
249                            }
250                        }
251
252                        match self.set(name, state).await {
253                            Ok(info) => {
254                                if let Some(info) = info {
255                                    self.publish(info);
256                                }
257                                let _ = tx.send(Ok(()));
258                            }
259                            Err(e) => {
260                                let _ = tx.send(Err(e));
261                            }
262                        }
263                    }
264                }
265            }
266        }))
267    }
268
269    async fn set(
270        &self,
271        name: String,
272        state: Vec<LightState>,
273    ) -> Result<Option<LightInfo>, LightError> {
274        let mut light_info = self.data_cache.lock().await;
275        // TODO(https://fxbug.dev/42058901) Deduplicate the code here and in mic_mute if possible.
276        if light_info.is_none() {
277            drop(light_info);
278            let _ = self.restore().await?;
279            light_info = self.data_cache.lock().await;
280        }
281
282        let current =
283            light_info.as_mut().ok_or_else(|| LightError::UnexpectedError("missing data cache"))?;
284        let mut entry = match current.light_groups.entry(name.clone()) {
285            Entry::Vacant(_) => {
286                // Reject sets if the light name is not known.
287                return Err(LightError::InvalidArgument(ARG_NAME, name));
288            }
289            Entry::Occupied(entry) => entry,
290        };
291
292        let group = entry.get_mut();
293
294        if state.len() != group.lights.len() {
295            // If the number of light states provided doesn't match the number of lights,
296            // return an error.
297            return Err(LightError::InvalidArgument("state", format!("{state:?}")));
298        }
299
300        if !state.iter().filter_map(|state| state.value.clone()).all(|value| {
301            match group.light_type {
302                LightType::Brightness => matches!(value, LightValue::Brightness(_)),
303                LightType::Rgb => matches!(value, LightValue::Rgb(_)),
304                LightType::Simple => matches!(value, LightValue::Simple(_)),
305            }
306        }) {
307            // If not all the light values match the light type of this light group, return an
308            // error.
309            return Err(LightError::InvalidArgument("state", format!("{state:?}")));
310        }
311
312        // After the main validations, write the state to the hardware.
313        self.write_light_group_to_hardware(group, &state).await?;
314
315        self.store
316            .write(current.clone())
317            .await
318            .map(|state| match state {
319                UpdateState::Unchanged => None,
320                UpdateState::Updated => Some(current.clone()),
321            })
322            .map_err(|e| LightError::WriteFailure(e.context("writing light on set")))
323    }
324
325    /// Writes the given list of light states for a light group to the actual hardware.
326    ///
327    /// [LightState::None] elements in the vector are ignored and not written to the hardware.
328    async fn write_light_group_to_hardware(
329        &self,
330        group: &mut LightGroup,
331        state: &[LightState],
332    ) -> Result<(), LightError> {
333        for (i, (light, hardware_index)) in
334            state.iter().zip(group.hardware_index.iter()).enumerate()
335        {
336            let (set_result, method_name) = match light.clone().value {
337                // No value provided for this index, just skip it and don't update the
338                // stored value.
339                None => continue,
340                Some(LightValue::Brightness(brightness)) => (
341                    call_async!(self.light_proxy =>
342                        set_brightness_value(*hardware_index, brightness))
343                    .await,
344                    "set_brightness_value",
345                ),
346                Some(LightValue::Rgb(rgb)) => {
347                    let value = rgb
348                        .clone()
349                        .try_into()
350                        .map_err(|_| LightError::InvalidArgument("value", format!("{rgb:?}")))?;
351                    (
352                        call_async!(self.light_proxy =>
353                            set_rgb_value(*hardware_index, & value))
354                        .await,
355                        "set_rgb_value",
356                    )
357                }
358                Some(LightValue::Simple(on)) => (
359                    call_async!(self.light_proxy => set_simple_value(*hardware_index, on)).await,
360                    "set_simple_value",
361                ),
362            };
363            set_result
364                .map_err(|e| format!("{e:?}"))
365                .and_then(|res| res.map_err(|e| format!("{e:?}")))
366                .map_err(|e| {
367                    LightError::ExternalFailure(
368                        "fuchsia.hardware.light",
369                        Cow::Owned(format!("{method_name} for light {hardware_index}")),
370                        Cow::Owned(e),
371                    )
372                })?;
373
374            // Set was successful, save this light value.
375            group.lights[i] = light.clone();
376        }
377        Ok(())
378    }
379
380    async fn on_mic_mute(&self, mic_mute: bool) -> Result<Option<LightInfo>, LightError> {
381        let mut light_info = self.data_cache.lock().await;
382        if light_info.is_none() {
383            drop(light_info);
384            let _ = self.restore().await?;
385            light_info = self.data_cache.lock().await;
386        }
387
388        let current =
389            light_info.as_mut().ok_or_else(|| LightError::UnexpectedError("missing data cache"))?;
390        for light in current
391            .light_groups
392            .values_mut()
393            .filter(|l| l.disable_conditions.contains(&DisableConditions::MicSwitch))
394        {
395            // This condition means that the LED is hard-wired to the mute switch and will only be
396            // on when the mic is disabled.
397            light.enabled = mic_mute;
398        }
399
400        self.store
401            .write(current.clone())
402            .await
403            .map(|state| match state {
404                UpdateState::Unchanged => None,
405                UpdateState::Updated => Some(current.clone()),
406            })
407            .map_err(|e| LightError::WriteFailure(e.context("writing light on mic mute")))
408    }
409
410    pub(crate) async fn restore(&self) -> Result<LightInfo, LightError> {
411        let light_info = if let Some(config) = self.light_hardware_config.clone() {
412            // Configuration is specified, restore from the configuration.
413            self.restore_from_configuration(config).await
414        } else {
415            // Read light info from hardware.
416            self.restore_from_hardware().await
417        }?;
418        let mut guard = self.data_cache.lock().await;
419        *guard = Some(light_info.clone());
420        Ok(light_info)
421    }
422
423    /// Restores the light information from a pre-defined hardware configuration. Individual light
424    /// states are read from the underlying fuchsia.hardware.Light API, but the structure of the
425    /// light groups is determined by the given `config`.
426    async fn restore_from_configuration(
427        &self,
428        config: LightHardwareConfiguration,
429    ) -> Result<LightInfo, LightError> {
430        let current = self.store.get::<LightInfo>().await;
431        let mut light_groups: HashMap<String, LightGroup> = HashMap::new();
432        for group_config in config.light_groups {
433            let mut light_state: Vec<LightState> = Vec::new();
434
435            // TODO(https://fxbug.dev/42134045): once all clients go through setui, restore state from hardware
436            // only if not found in persistent storage.
437            for light_index in group_config.hardware_index.iter() {
438                light_state.push(
439                    self.light_state_from_hardware_index(*light_index, group_config.light_type)
440                        .await?,
441                );
442            }
443
444            // Restore previous state.
445            let enabled = current
446                .light_groups
447                .get(&group_config.name)
448                .map(|found_group| found_group.enabled)
449                .unwrap_or(true);
450
451            let _ = light_groups.insert(
452                group_config.name.clone(),
453                LightGroup {
454                    name: group_config.name,
455                    enabled,
456                    light_type: group_config.light_type,
457                    lights: light_state,
458                    hardware_index: group_config.hardware_index,
459                    disable_conditions: group_config.disable_conditions,
460                },
461            );
462        }
463
464        Ok(LightInfo { light_groups })
465    }
466
467    /// Restores the light information when no hardware configuration is specified by reading from
468    /// the underlying fuchsia.hardware.Light API and turning each light into a [`LightGroup`].
469    ///
470    /// [`LightGroup`]: ../../light/types/struct.LightGroup.html
471    async fn restore_from_hardware(&self) -> Result<LightInfo, LightError> {
472        let num_lights = call_async!(self.light_proxy => get_num_lights()).await.map_err(|e| {
473            LightError::ExternalFailure(
474                "fuchsia.hardware.light",
475                Cow::Borrowed("get_num_lights"),
476                Cow::Owned(format!("{e:?}")),
477            )
478        })?;
479
480        let mut current = self.store.get::<LightInfo>().await;
481        for i in 0..num_lights {
482            let info = call_async!(self.light_proxy => get_info(i))
483                .await
484                .map_err(|e| format!("{e:?}"))
485                .and_then(|res| res.map_err(|e| format!("{e:?}")))
486                .map_err(|e| {
487                    LightError::ExternalFailure(
488                        "fuchsia.hardware.light",
489                        Cow::Owned(format!("get_info for light {i}")),
490                        Cow::Owned(e),
491                    )
492                })?;
493            let (name, group) = self.light_info_to_group(i, info).await?;
494            let _ = current.light_groups.insert(name, group);
495        }
496
497        Ok(current)
498    }
499
500    /// Converts an Info object from the fuchsia.hardware.Light API into a LightGroup, the internal
501    /// representation used for our service.
502    async fn light_info_to_group(
503        &self,
504        index: u32,
505        info: Info,
506    ) -> Result<(String, LightGroup), LightError> {
507        let light_type: LightType = info.capability.into();
508
509        let light_state = self.light_state_from_hardware_index(index, light_type).await?;
510
511        Ok((
512            info.name.clone(),
513            LightGroup {
514                name: info.name,
515                // When there's no config, lights are assumed to be always enabled.
516                enabled: true,
517                light_type,
518                lights: vec![light_state],
519                hardware_index: vec![index],
520                disable_conditions: vec![],
521            },
522        ))
523    }
524
525    /// Reads light state from the underlying fuchsia.hardware.Light API for the given hardware
526    /// index and light type.
527    async fn light_state_from_hardware_index(
528        &self,
529        index: u32,
530        light_type: LightType,
531    ) -> Result<LightState, LightError> {
532        // Read the proper value depending on the light type.
533        let value = match light_type {
534            LightType::Brightness => {
535                call_async!(self.light_proxy => get_current_brightness_value(index))
536                    .await
537                    .map_err(|e| format!("{e:?}"))
538                    .and_then(|res| res.map_err(|e| format!("{e:?}")))
539                    .map(LightValue::Brightness)
540                    .map_err(|e| {
541                        LightError::ExternalFailure(
542                            "fuchsia.hardware.light",
543                            Cow::Owned(format!("get_current_brightness_value for light {index}")),
544                            Cow::Owned(e),
545                        )
546                    })?
547            }
548            LightType::Rgb => call_async!(self.light_proxy => get_current_rgb_value(index))
549                .await
550                .map_err(|e| format!("{e:?}"))
551                .and_then(|res| res.map_err(|e| format!("{e:?}")))
552                .map(LightValue::from)
553                .map_err(|e| {
554                    LightError::ExternalFailure(
555                        "fuchsia.hardware.light",
556                        Cow::Owned(format!("get_current_rgb_value for light {index}")),
557                        Cow::Owned(e),
558                    )
559                })?,
560            LightType::Simple => call_async!(self.light_proxy => get_current_simple_value(index))
561                .await
562                .map_err(|e| format!("{e:?}"))
563                .and_then(|res| res.map_err(|e| format!("{e:?}")))
564                .map(LightValue::Simple)
565                .map_err(|e| {
566                    LightError::ExternalFailure(
567                        "fuchsia.hardware.light",
568                        Cow::Owned(format!("get_current_simple_value for light {index}")),
569                        Cow::Owned(e),
570                    )
571                })?,
572        };
573
574        Ok(LightState { value: Some(value) })
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    use futures::channel::mpsc;
581
582    use super::*;
583    use crate::light_fidl_handler::LightFidlHandler;
584    use crate::test_fakes::hardware_light_service::HardwareLightService;
585    use settings_test_common::fakes::service::ServiceRegistry;
586    use settings_test_common::storage::InMemoryFidlStorageFactory;
587
588    // Verify that a set call without a restore call succeeds. This can happen when the controller
589    // is shutdown after inactivity and is brought up again to handle the set call.
590    #[fuchsia::test()]
591    async fn test_set_before_restore() {
592        // Create a fake hardware light service that responds to FIDL calls and add it to the
593        // service registry so that FIDL calls are routed to this fake service.
594        let service_registry = ServiceRegistry::create();
595        let light_service_handle = Rc::new(Mutex::new(HardwareLightService::new()));
596        service_registry.lock().await.register_service(light_service_handle.clone());
597
598        let service_context = ServiceContext::new(Some(ServiceRegistry::serve(service_registry)));
599
600        // Add a light to the fake service.
601        light_service_handle
602            .lock()
603            .await
604            .insert_light(0, "light_1".to_string(), LightType::Simple, LightValue::Simple(false))
605            .await;
606
607        let storage_factory = InMemoryFidlStorageFactory::new();
608        storage_factory.initialize_storage::<LightInfo>().await;
609
610        let (tx, _rx) = mpsc::unbounded();
611        let setting_value_publisher = SettingValuePublisher::new(tx);
612        let (tx, _rx) = mpsc::unbounded();
613        let event_publisher = ExternalEventPublisher::new(tx);
614
615        // Create the light controller.
616        let mut light_controller = LightController::create_with_config(
617            Rc::new(service_context),
618            None,
619            &storage_factory,
620            setting_value_publisher,
621            event_publisher,
622        )
623        .await
624        .expect("Failed to create light controller");
625        let info = light_controller.restore().await.unwrap();
626        let (info_hanging_get, group_hanging_gets) = LightFidlHandler::build_hanging_gets(info);
627        light_controller.register_publishers(
628            info_hanging_get.new_publisher(),
629            group_hanging_gets
630                .iter()
631                .map(|(key, hanging_get)| (key.clone(), hanging_get.new_publisher()))
632                .collect(),
633        );
634
635        // Call set and verify it succeeds.
636        let _ = light_controller
637            .set("light_1".to_string(), vec![LightState { value: Some(LightValue::Simple(true)) }])
638            .await
639            .expect("Set call failed");
640
641        // Verify the data cache is populated after the set call.
642        let _ =
643            light_controller.data_cache.lock().await.as_ref().expect("Data cache is not populated");
644    }
645
646    // Verify that an on_mic_mute event without a restore call succeeds. This can happen when the
647    // controller is shutdown after inactivity and is brought up again to handle the set call.
648    #[fuchsia::test()]
649    async fn test_on_mic_mute_before_restore() {
650        // Create a fake hardware light service that responds to FIDL calls and add it to the
651        // service registry so that FIDL calls are routed to this fake service.
652        let service_registry = ServiceRegistry::create();
653        let light_service_handle = Rc::new(Mutex::new(HardwareLightService::new()));
654        service_registry.lock().await.register_service(light_service_handle.clone());
655
656        let service_context = ServiceContext::new(Some(ServiceRegistry::serve(service_registry)));
657
658        // Add a light to the fake service.
659        light_service_handle
660            .lock()
661            .await
662            .insert_light(0, "light_1".to_string(), LightType::Simple, LightValue::Simple(false))
663            .await;
664
665        let storage_factory = InMemoryFidlStorageFactory::new();
666        storage_factory.initialize_storage::<LightInfo>().await;
667
668        let (tx, _rx) = mpsc::unbounded();
669        let setting_value_publisher = SettingValuePublisher::new(tx);
670        let (tx, _rx) = mpsc::unbounded();
671        let event_publisher = ExternalEventPublisher::new(tx);
672
673        // Create the light controller.
674        let mut light_controller = LightController::create_with_config(
675            Rc::new(service_context),
676            None,
677            &storage_factory,
678            setting_value_publisher,
679            event_publisher,
680        )
681        .await
682        .expect("Failed to create light controller");
683        let info = light_controller.restore().await.unwrap();
684        let (info_hanging_get, group_hanging_gets) = LightFidlHandler::build_hanging_gets(info);
685        light_controller.register_publishers(
686            info_hanging_get.new_publisher(),
687            group_hanging_gets
688                .iter()
689                .map(|(key, hanging_get)| (key.clone(), hanging_get.new_publisher()))
690                .collect(),
691        );
692
693        // Call on_mic_mute and verify it succeeds.
694        let _ = light_controller.on_mic_mute(false).await.expect("Set call failed");
695
696        // Verify the data cache is populated after the set call.
697        let _ =
698            light_controller.data_cache.lock().await.as_ref().expect("Data cache is not populated");
699    }
700}