fuchsia_audio/
device.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::dai::DaiFormatSet;
6use crate::format_set::PcmFormatSet;
7use camino::Utf8PathBuf;
8use std::collections::BTreeMap;
9use std::fmt::Display;
10use std::str::FromStr;
11use thiserror::Error;
12use {
13    fidl_fuchsia_audio_controller as fac, fidl_fuchsia_audio_device as fadevice,
14    fidl_fuchsia_hardware_audio as fhaudio, fidl_fuchsia_io as fio, zx_types,
15};
16// Separate this to a distinct alias, to clarify when various 'DeviceType's are used.
17use fadevice::DeviceType as AdrDevType;
18
19/// The type of an audio device.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct Type(fac::DeviceType);
22
23impl Type {
24    /// Returns the devfs class for this device.
25    ///
26    /// e.g. /dev/class/{class}/some_device
27    pub fn devfs_class(&self) -> &str {
28        match self.0 {
29            fac::DeviceType::Codec => "codec",
30            fac::DeviceType::Composite => "audio-composite",
31            fac::DeviceType::Dai => "dai",
32            fac::DeviceType::Input => "audio-input",
33            fac::DeviceType::Output => "audio-output",
34            _ => panic!("Unexpected device type"),
35        }
36    }
37}
38
39impl From<Type> for fac::DeviceType {
40    fn from(value: Type) -> Self {
41        value.0
42    }
43}
44
45impl From<AdrDevType> for Type {
46    fn from(value: AdrDevType) -> Self {
47        let device_type = match value {
48            AdrDevType::Codec => fac::DeviceType::Codec,
49            AdrDevType::Composite => fac::DeviceType::Composite,
50            _ => panic!("Unexpected device type"),
51        };
52        Self(device_type)
53    }
54}
55
56impl From<fac::DeviceType> for Type {
57    fn from(value: fac::DeviceType) -> Self {
58        Self(value)
59    }
60}
61
62impl From<Type> for AdrDevType {
63    fn from(value: Type) -> Self {
64        match value.0 {
65            fac::DeviceType::Codec => AdrDevType::Codec,
66            fac::DeviceType::Composite => AdrDevType::Composite,
67            _ => panic!("Unexpected device type"),
68        }
69    }
70}
71
72impl FromStr for Type {
73    type Err = String;
74
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        let device_type = match s.to_lowercase().as_str() {
77            "codec" => Ok(fac::DeviceType::Codec),
78            "composite" => Ok(fac::DeviceType::Composite),
79            "dai" => Ok(fac::DeviceType::Dai),
80            "input" => Ok(fac::DeviceType::Input),
81            "output" => Ok(fac::DeviceType::Output),
82            _ => Err(format!(
83                "Invalid device type: {}. Expected one of: Codec, Composite, Dai, Input, Output",
84                s
85            )),
86        }?;
87
88        Ok(Self(device_type))
89    }
90}
91
92impl Display for Type {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        let s = match self.0 {
95            fac::DeviceType::Codec => "Codec",
96            fac::DeviceType::Composite => "Composite",
97            fac::DeviceType::Dai => "Dai",
98            fac::DeviceType::Input => "Input",
99            fac::DeviceType::Output => "Output",
100            _ => "<unknown>",
101        };
102        f.write_str(s)
103    }
104}
105
106impl TryFrom<(HardwareType, Option<Direction>)> for Type {
107    type Error = String;
108
109    fn try_from(value: (HardwareType, Option<Direction>)) -> Result<Self, Self::Error> {
110        let (type_, direction) = value;
111        let device_type = match type_.0 {
112            fhaudio::DeviceType::Codec => Ok(fac::DeviceType::Codec),
113            fhaudio::DeviceType::Composite => Ok(fac::DeviceType::Composite),
114            fhaudio::DeviceType::Dai => Ok(fac::DeviceType::Dai),
115            fhaudio::DeviceType::StreamConfig => Ok(
116                match direction
117                    .ok_or_else(|| format!("direction is missing for StreamConfig type"))?
118                {
119                    Direction::Input => fac::DeviceType::Input,
120                    Direction::Output => fac::DeviceType::Output,
121                },
122            ),
123            _ => Err(format!("unknown device type")),
124        }?;
125        Ok(Self(device_type))
126    }
127}
128
129/// The type of an audio device driver.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct HardwareType(pub fhaudio::DeviceType);
132
133impl FromStr for HardwareType {
134    type Err = String;
135
136    fn from_str(s: &str) -> Result<Self, Self::Err> {
137        let device_type = match s.to_lowercase().as_str() {
138            "codec" => Ok(fhaudio::DeviceType::Codec),
139            "composite" => Ok(fhaudio::DeviceType::Composite),
140            "dai" => Ok(fhaudio::DeviceType::Dai),
141            "streamconfig" => Ok(fhaudio::DeviceType::StreamConfig),
142            _ => Err(format!(
143                "Invalid type: {}. Expected one of: Codec, Composite, Dai, StreamConfig",
144                s
145            )),
146        }?;
147        Ok(Self(device_type))
148    }
149}
150
151impl Display for HardwareType {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        let s = match self.0 {
154            fhaudio::DeviceType::Codec => "Codec",
155            fhaudio::DeviceType::Composite => "Composite",
156            fhaudio::DeviceType::Dai => "Dai",
157            fhaudio::DeviceType::StreamConfig => "StreamConfig",
158            _ => "<unknown>",
159        };
160        f.write_str(s)
161    }
162}
163
164impl From<Type> for HardwareType {
165    fn from(value: Type) -> Self {
166        let hw_type = match value.0 {
167            fac::DeviceType::Codec => fhaudio::DeviceType::Codec,
168            fac::DeviceType::Composite => fhaudio::DeviceType::Composite,
169            fac::DeviceType::Dai => fhaudio::DeviceType::Dai,
170            fac::DeviceType::Input | fac::DeviceType::Output => fhaudio::DeviceType::StreamConfig,
171            _ => panic!("Unexpected device type"),
172        };
173        Self(hw_type)
174    }
175}
176
177/// The direction in which audio flows through a device.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum Direction {
180    /// Device is a source of streamed audio.
181    Input,
182
183    /// Device is a destination for streamed audio.
184    Output,
185}
186
187impl FromStr for Direction {
188    type Err = String;
189
190    fn from_str(s: &str) -> Result<Self, Self::Err> {
191        match s.to_lowercase().as_str() {
192            "input" => Ok(Self::Input),
193            "output" => Ok(Self::Output),
194            _ => Err(format!("Invalid direction: {}. Expected one of: input, output", s)),
195        }
196    }
197}
198
199/// Identifies a single audio device.
200#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
201pub enum Selector {
202    Devfs(DevfsSelector),
203    Registry(RegistrySelector),
204}
205
206impl TryFrom<fac::DeviceSelector> for Selector {
207    type Error = String;
208
209    fn try_from(value: fac::DeviceSelector) -> Result<Self, Self::Error> {
210        match value {
211            fac::DeviceSelector::Devfs(devfs) => Ok(Self::Devfs(devfs.into())),
212            fac::DeviceSelector::Registry(token_id) => Ok(Self::Registry(token_id.into())),
213            _ => Err("unknown selector variant".to_string()),
214        }
215    }
216}
217
218impl From<fac::Devfs> for Selector {
219    fn from(value: fac::Devfs) -> Self {
220        Self::Devfs(value.into())
221    }
222}
223
224impl From<Selector> for fac::DeviceSelector {
225    fn from(value: Selector) -> Self {
226        match value {
227            Selector::Devfs(devfs_selector) => devfs_selector.into(),
228            Selector::Registry(registry_selector) => registry_selector.into(),
229        }
230    }
231}
232
233/// Identifies a device backed by a hardware driver protocol in devfs.
234#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
235pub struct DevfsSelector(pub fac::Devfs);
236
237impl DevfsSelector {
238    /// Returns the full devfs path for this device.
239    pub fn path(&self) -> Utf8PathBuf {
240        Utf8PathBuf::from("/dev/class").join(self.relative_path())
241    }
242
243    /// Returns the path for this device relative to the /dev/class directory root.
244    pub fn relative_path(&self) -> Utf8PathBuf {
245        Utf8PathBuf::from(self.device_type().devfs_class()).join(self.0.name.clone())
246    }
247
248    /// Returns the type of this device.
249    pub fn device_type(&self) -> Type {
250        Type(self.0.device_type)
251    }
252}
253
254impl Display for DevfsSelector {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        f.write_str(self.path().as_str())
257    }
258}
259
260impl TryFrom<fac::DeviceSelector> for DevfsSelector {
261    type Error = String;
262
263    fn try_from(value: fac::DeviceSelector) -> Result<Self, Self::Error> {
264        match value {
265            fac::DeviceSelector::Devfs(devfs) => Ok(Self(devfs.into())),
266            _ => Err("unknown selector type".to_string()),
267        }
268    }
269}
270
271impl From<fac::Devfs> for DevfsSelector {
272    fn from(value: fac::Devfs) -> Self {
273        Self(value)
274    }
275}
276
277impl From<DevfsSelector> for fac::DeviceSelector {
278    fn from(value: DevfsSelector) -> Self {
279        Self::Devfs(value.0)
280    }
281}
282
283/// Identifies a device available through the `fuchsia.audio.device/Registry` protocol.
284#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
285pub struct RegistrySelector(pub fadevice::TokenId);
286
287impl RegistrySelector {
288    pub fn token_id(&self) -> fadevice::TokenId {
289        self.0
290    }
291}
292
293impl Display for RegistrySelector {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        write!(f, "{}", self.0)
296    }
297}
298
299impl TryFrom<fac::DeviceSelector> for RegistrySelector {
300    type Error = String;
301
302    fn try_from(value: fac::DeviceSelector) -> Result<Self, Self::Error> {
303        match value {
304            fac::DeviceSelector::Registry(token_id) => Ok(Self(token_id)),
305            _ => Err("unknown selector type".to_string()),
306        }
307    }
308}
309
310impl From<fadevice::TokenId> for RegistrySelector {
311    fn from(value: fadevice::TokenId) -> Self {
312        Self(value)
313    }
314}
315
316impl From<RegistrySelector> for fac::DeviceSelector {
317    fn from(value: RegistrySelector) -> Self {
318        Self::Registry(value.0)
319    }
320}
321
322/// Device info from the `fuchsia.audio.device/Registry` protocol.
323#[derive(Debug, Clone, PartialEq)]
324pub struct Info(pub fadevice::Info);
325
326impl Info {
327    pub fn token_id(&self) -> fadevice::TokenId {
328        self.0.token_id.expect("missing 'token_id'")
329    }
330
331    pub fn registry_selector(&self) -> RegistrySelector {
332        RegistrySelector(self.token_id())
333    }
334
335    pub fn device_type(&self) -> Type {
336        Type::from(self.0.device_type.expect("missing 'device_type'"))
337    }
338
339    pub fn device_name(&self) -> &str {
340        self.0.device_name.as_ref().expect("missing 'device_name'")
341    }
342
343    pub fn unique_instance_id(&self) -> Option<UniqueInstanceId> {
344        self.0.unique_instance_id.map(UniqueInstanceId)
345    }
346
347    pub fn plug_detect_capabilities(&self) -> Option<PlugDetectCapabilities> {
348        self.0.plug_detect_caps.map(PlugDetectCapabilities::from)
349    }
350
351    pub fn gain_capabilities(&self) -> Option<GainCapabilities> {
352        None
353    }
354
355    pub fn clock_domain(&self) -> Option<ClockDomain> {
356        self.0.clock_domain.map(ClockDomain)
357    }
358
359    pub fn supported_ring_buffer_formats(
360        &self,
361    ) -> Result<BTreeMap<fadevice::ElementId, Vec<PcmFormatSet>>, String> {
362        self.0
363            .ring_buffer_format_sets
364            .as_ref()
365            .map_or_else(
366                || Ok(BTreeMap::new()),
367                |element_rb_format_sets| {
368                    element_rb_format_sets
369                        .iter()
370                        .cloned()
371                        .map(|element_rb_format_set| {
372                            let element_id = element_rb_format_set
373                                .element_id
374                                .ok_or_else(|| "missing element_id".to_string())?;
375                            let fidl_format_sets = element_rb_format_set
376                                .format_sets
377                                .ok_or_else(|| "missing format_sets".to_string())?;
378
379                            let format_sets: Vec<PcmFormatSet> = fidl_format_sets
380                                .into_iter()
381                                .map(TryInto::try_into)
382                                .collect::<Result<Vec<_>, _>>()
383                                .map_err(|err| format!("invalid format set: {}", err))?;
384
385                            Ok((element_id, format_sets))
386                        })
387                        .collect::<Result<BTreeMap<_, _>, String>>()
388                },
389            )
390            .map_err(|err| format!("invalid ring buffer format sets: {}", err))
391    }
392
393    pub fn supported_dai_formats(
394        &self,
395    ) -> Result<BTreeMap<fadevice::ElementId, Vec<DaiFormatSet>>, String> {
396        self.0
397            .dai_format_sets
398            .as_ref()
399            .map_or_else(
400                || Ok(BTreeMap::new()),
401                |element_dai_format_sets| {
402                    element_dai_format_sets
403                        .iter()
404                        .cloned()
405                        .map(|element_dai_format_set| {
406                            let element_id = element_dai_format_set
407                                .element_id
408                                .ok_or_else(|| "missing element_id".to_string())?;
409                            let fidl_format_sets = element_dai_format_set
410                                .format_sets
411                                .ok_or_else(|| "missing format_sets".to_string())?;
412
413                            let dai_format_sets: Vec<DaiFormatSet> = fidl_format_sets
414                                .into_iter()
415                                .map(TryInto::try_into)
416                                .collect::<Result<Vec<_>, _>>()
417                                .map_err(|err| format!("invalid DAI format set: {}", err))?;
418
419                            Ok((element_id, dai_format_sets))
420                        })
421                        .collect::<Result<BTreeMap<_, _>, String>>()
422                },
423            )
424            .map_err(|err| format!("invalid ring buffer format sets: {}", err))
425    }
426}
427
428impl From<fadevice::Info> for Info {
429    fn from(value: fadevice::Info) -> Self {
430        Self(value)
431    }
432}
433
434#[derive(Debug, Clone, PartialEq)]
435pub struct UniqueInstanceId(pub [u8; fadevice::UNIQUE_INSTANCE_ID_SIZE as usize]);
436
437impl Display for UniqueInstanceId {
438    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439        for byte in self.0 {
440            write!(f, "{:02x}", byte)?;
441        }
442        Ok(())
443    }
444}
445
446impl From<[u8; fadevice::UNIQUE_INSTANCE_ID_SIZE as usize]> for UniqueInstanceId {
447    fn from(value: [u8; fadevice::UNIQUE_INSTANCE_ID_SIZE as usize]) -> Self {
448        Self(value)
449    }
450}
451
452#[derive(Debug, Clone, PartialEq)]
453pub struct PlugDetectCapabilities(pub fadevice::PlugDetectCapabilities);
454
455impl Display for PlugDetectCapabilities {
456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        let s = match self.0 {
458            fadevice::PlugDetectCapabilities::Hardwired => "Hardwired",
459            fadevice::PlugDetectCapabilities::Pluggable => "Pluggable (can async notify)",
460            _ => "<unknown>",
461        };
462        f.write_str(s)
463    }
464}
465
466impl From<fadevice::PlugDetectCapabilities> for PlugDetectCapabilities {
467    fn from(value: fadevice::PlugDetectCapabilities) -> Self {
468        Self(value)
469    }
470}
471
472impl From<PlugDetectCapabilities> for fadevice::PlugDetectCapabilities {
473    fn from(value: PlugDetectCapabilities) -> Self {
474        value.0
475    }
476}
477
478impl From<fhaudio::PlugDetectCapabilities> for PlugDetectCapabilities {
479    fn from(value: fhaudio::PlugDetectCapabilities) -> Self {
480        let plug_detect_caps = match value {
481            fhaudio::PlugDetectCapabilities::Hardwired => {
482                fadevice::PlugDetectCapabilities::Hardwired
483            }
484            fhaudio::PlugDetectCapabilities::CanAsyncNotify => {
485                fadevice::PlugDetectCapabilities::Pluggable
486            }
487        };
488        Self(plug_detect_caps)
489    }
490}
491
492impl TryFrom<PlugDetectCapabilities> for fhaudio::PlugDetectCapabilities {
493    type Error = String;
494
495    fn try_from(value: PlugDetectCapabilities) -> Result<Self, Self::Error> {
496        match value.0 {
497            fadevice::PlugDetectCapabilities::Hardwired => Ok(Self::Hardwired),
498            fadevice::PlugDetectCapabilities::Pluggable => Ok(Self::CanAsyncNotify),
499            _ => Err("unsupported PlugDetectCapabilities value".to_string()),
500        }
501    }
502}
503
504/// Describes the plug state of a device or endpoint, and when it changed.
505#[derive(Debug, Clone, Copy, PartialEq)]
506pub struct PlugEvent {
507    pub state: PlugState,
508
509    /// The Zircon monotonic time when the plug state changed.
510    pub time: zx_types::zx_time_t,
511}
512
513impl From<(fadevice::PlugState, i64 /* time */)> for PlugEvent {
514    fn from(value: (fadevice::PlugState, i64)) -> Self {
515        let (state, time) = value;
516        Self { state: state.into(), time }
517    }
518}
519
520impl TryFrom<fhaudio::PlugState> for PlugEvent {
521    type Error = String;
522
523    fn try_from(value: fhaudio::PlugState) -> Result<Self, Self::Error> {
524        let plugged = value.plugged.ok_or_else(|| "missing 'plugged'".to_string())?;
525        let time = value.plug_state_time.ok_or_else(|| "missing 'plug_state_time'".to_string())?;
526        let state = PlugState(if plugged {
527            fadevice::PlugState::Plugged
528        } else {
529            fadevice::PlugState::Unplugged
530        });
531        Ok(Self { state, time })
532    }
533}
534
535#[derive(Debug, Clone, Copy, PartialEq)]
536pub struct PlugState(pub fadevice::PlugState);
537
538impl Display for PlugState {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        let s = match self.0 {
541            fadevice::PlugState::Plugged => "Plugged",
542            fadevice::PlugState::Unplugged => "Unplugged",
543            _ => "<unknown>",
544        };
545        f.write_str(s)
546    }
547}
548
549impl From<fadevice::PlugState> for PlugState {
550    fn from(value: fadevice::PlugState) -> Self {
551        Self(value)
552    }
553}
554
555impl From<PlugState> for fadevice::PlugState {
556    fn from(value: PlugState) -> Self {
557        value.0
558    }
559}
560
561#[derive(Debug, Clone, Copy, PartialEq)]
562pub struct GainState {
563    pub gain_db: f32,
564    pub muted: Option<bool>,
565    pub agc_enabled: Option<bool>,
566}
567
568impl TryFrom<fhaudio::GainState> for GainState {
569    type Error = String;
570
571    fn try_from(value: fhaudio::GainState) -> Result<Self, Self::Error> {
572        Ok(Self {
573            gain_db: value.gain_db.ok_or_else(|| "missing 'gain_db'".to_string())?,
574            muted: value.muted,
575            agc_enabled: value.agc_enabled,
576        })
577    }
578}
579
580#[derive(Debug, Clone, Copy, PartialEq)]
581pub struct GainCapabilities {
582    pub min_gain_db: f32,
583    pub max_gain_db: f32,
584    pub gain_step_db: f32,
585    pub can_mute: Option<bool>,
586    pub can_agc: Option<bool>,
587}
588
589impl TryFrom<&fhaudio::StreamProperties> for GainCapabilities {
590    type Error = String;
591
592    fn try_from(value: &fhaudio::StreamProperties) -> Result<Self, Self::Error> {
593        Ok(Self {
594            min_gain_db: value.min_gain_db.ok_or_else(|| "missing 'min_gain_db'".to_string())?,
595            max_gain_db: value.max_gain_db.ok_or_else(|| "missing 'max_gain_db'".to_string())?,
596            gain_step_db: value.gain_step_db.ok_or_else(|| "missing 'gain_step_db'".to_string())?,
597            can_mute: value.can_mute,
598            can_agc: value.can_agc,
599        })
600    }
601}
602
603#[derive(Debug, Clone, Copy, PartialEq, Eq)]
604pub struct ClockDomain(pub fhaudio::ClockDomain);
605
606impl Display for ClockDomain {
607    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608        write!(f, "{}", self.0)?;
609        match self.0 {
610            fhaudio::CLOCK_DOMAIN_MONOTONIC => f.write_str(" (monotonic)"),
611            fhaudio::CLOCK_DOMAIN_EXTERNAL => f.write_str(" (external)"),
612            _ => Ok(()),
613        }
614    }
615}
616
617impl From<fhaudio::ClockDomain> for ClockDomain {
618    fn from(value: fhaudio::ClockDomain) -> Self {
619        Self(value)
620    }
621}
622
623impl From<ClockDomain> for fhaudio::ClockDomain {
624    fn from(value: ClockDomain) -> Self {
625        value.0
626    }
627}
628
629#[derive(Error, Debug)]
630pub enum ListDevfsError {
631    #[error("Failed to open directory {}: {:?}", name, err)]
632    Open {
633        name: String,
634        #[source]
635        err: fuchsia_fs::node::OpenError,
636    },
637
638    #[error("Failed to read directory {} entries: {:?}", name, err)]
639    Readdir {
640        name: String,
641        #[source]
642        err: fuchsia_fs::directory::EnumerateError,
643    },
644}
645
646/// Returns selectors for all audio devices in devfs.
647///
648/// `dev_class` should be a proxy to the `/dev/class` directory.
649pub async fn list_devfs(
650    dev_class: &fio::DirectoryProxy,
651) -> Result<Vec<DevfsSelector>, ListDevfsError> {
652    const TYPES: &[Type] = &[
653        Type(fac::DeviceType::Codec),
654        Type(fac::DeviceType::Composite),
655        Type(fac::DeviceType::Dai),
656        Type(fac::DeviceType::Input),
657        Type(fac::DeviceType::Output),
658    ];
659
660    let mut selectors = vec![];
661
662    for device_type in TYPES {
663        let subdir_name = device_type.devfs_class();
664        let subdir =
665            fuchsia_fs::directory::open_directory(dev_class, subdir_name, fio::Flags::empty())
666                .await
667                .map_err(|err| ListDevfsError::Open { name: subdir_name.to_string(), err })?;
668        let entries = fuchsia_fs::directory::readdir(&subdir)
669            .await
670            .map_err(|err| ListDevfsError::Readdir { name: subdir_name.to_string(), err })?;
671        selectors.extend(entries.into_iter().map(|entry| {
672            DevfsSelector(fac::Devfs { name: entry.name, device_type: device_type.0 })
673        }));
674    }
675
676    Ok(selectors)
677}
678
679#[derive(Error, Debug)]
680pub enum ListRegistryError {
681    #[error(transparent)]
682    Fidl(#[from] fidl::Error),
683
684    #[error("failed to get devices: {:?}", .0)]
685    WatchDevicesAdded(fadevice::RegistryWatchDevicesAddedError),
686}
687
688/// Returns info for all audio devices in the `fuchsia.audio.device` registry.
689pub async fn list_registry(
690    registry: &fadevice::RegistryProxy,
691) -> Result<Vec<Info>, ListRegistryError> {
692    Ok(registry
693        .watch_devices_added()
694        .await
695        .map_err(ListRegistryError::Fidl)?
696        .map_err(ListRegistryError::WatchDevicesAdded)?
697        .devices
698        .expect("missing devices")
699        .into_iter()
700        .map(Info::from)
701        .collect())
702}
703
704#[cfg(test)]
705mod test {
706    use super::*;
707    use fidl::endpoints::spawn_stream_handler;
708    use std::sync::Arc;
709    use test_case::test_case;
710    use vfs::pseudo_directory;
711
712    #[test_case("composite", fac::DeviceType::Composite; "composite")]
713    #[test_case("input", fac::DeviceType::Input; "input")]
714    #[test_case("output", fac::DeviceType::Output; "output")]
715    fn test_parse_type(s: &str, expected_type: fac::DeviceType) {
716        assert_eq!(Type(expected_type), s.parse::<Type>().unwrap());
717    }
718
719    #[test]
720    fn test_parse_type_invalid() {
721        assert!("not a valid device type".parse::<Type>().is_err());
722    }
723
724    #[test_case("Codec", fhaudio::DeviceType::Codec; "Codec")]
725    #[test_case("Composite", fhaudio::DeviceType::Composite; "Composite")]
726    #[test_case("Dai", fhaudio::DeviceType::Dai; "Dai")]
727    #[test_case("StreamConfig", fhaudio::DeviceType::StreamConfig; "StreamConfig")]
728    fn test_parse_hardware_type(s: &str, expected_type: fhaudio::DeviceType) {
729        assert_eq!(HardwareType(expected_type), s.parse::<HardwareType>().unwrap());
730    }
731
732    #[test]
733    fn test_parse_hardware_type_invalid() {
734        assert!("not a valid hardware device type".parse::<Type>().is_err());
735    }
736
737    #[test_case(fhaudio::DeviceType::Codec, None, fac::DeviceType::Codec; "Codec")]
738    #[test_case(fhaudio::DeviceType::Composite, None, fac::DeviceType::Composite; "Composite")]
739    #[test_case(fhaudio::DeviceType::Dai, None, fac::DeviceType::Dai; "Dai")]
740    #[test_case(
741        fhaudio::DeviceType::StreamConfig,
742        Some(Direction::Input),
743        fac::DeviceType::Input;
744        "StreamConfig input"
745    )]
746    #[test_case(
747        fhaudio::DeviceType::StreamConfig,
748        Some(Direction::Output),
749        fac::DeviceType::Output;
750        "StreamConfig output"
751    )]
752    fn test_from_hardware_type_with_direction(
753        hardware_type: fhaudio::DeviceType,
754        direction: Option<Direction>,
755        expected_type: fac::DeviceType,
756    ) {
757        assert_eq!(
758            Type(expected_type),
759            (HardwareType(hardware_type), direction).try_into().unwrap()
760        )
761    }
762
763    #[test_case(
764        fac::Devfs { name: "3d99d780".to_string(), device_type: fac::DeviceType::Codec },
765        "/dev/class/codec/3d99d780";
766        "codec"
767    )]
768    #[test_case(
769        fac::Devfs { name: "3d99d780".to_string(), device_type: fac::DeviceType::Composite },
770        "/dev/class/audio-composite/3d99d780";
771        "composite"
772    )]
773    #[test_case(
774        fac::Devfs { name: "3d99d780".to_string(), device_type: fac::DeviceType::Dai },
775        "/dev/class/dai/3d99d780";
776        "dai"
777    )]
778    #[test_case(
779        fac::Devfs { name: "3d99d780".to_string(), device_type: fac::DeviceType::Input },
780        "/dev/class/audio-input/3d99d780";
781        "input"
782    )]
783    #[test_case(
784        fac::Devfs { name: "3d99d780".to_string(), device_type: fac::DeviceType::Output },
785        "/dev/class/audio-output/3d99d780";
786        "output"
787    )]
788    fn test_devfs_selector_path(devfs: fac::Devfs, expected_path: &str) {
789        assert_eq!(expected_path, DevfsSelector(devfs).path());
790    }
791
792    fn placeholder_node() -> Arc<vfs::service::Service> {
793        vfs::service::endpoint(move |_scope, _channel| {
794            // Just drop the channel.
795        })
796    }
797
798    #[fuchsia::test]
799    async fn test_list_devfs() {
800        // Placeholder for serving the device protocol.
801        // list_devfs doesn't connect to it, so we don't serve it.
802        let placeholder = placeholder_node();
803
804        let dev_class_vfs = pseudo_directory! {
805            "codec" => pseudo_directory! {
806                "codec-0" => placeholder.clone(),
807            },
808            "audio-composite" => pseudo_directory! {
809                "composite-0" => placeholder.clone(),
810            },
811            "dai" => pseudo_directory! {
812                "dai-0" => placeholder.clone(),
813            },
814            "audio-input" => pseudo_directory! {
815                "input-0" => placeholder.clone(),
816                "input-1" => placeholder.clone(),
817            },
818            "audio-output" => pseudo_directory! {
819                "output-0" => placeholder.clone(),
820            },
821        };
822
823        let dev_class = vfs::directory::serve_read_only(dev_class_vfs);
824        let selectors = list_devfs(&dev_class).await.unwrap();
825
826        assert_eq!(
827            vec![
828                DevfsSelector(fac::Devfs {
829                    name: "codec-0".to_string(),
830                    device_type: fac::DeviceType::Codec,
831                }),
832                DevfsSelector(fac::Devfs {
833                    name: "composite-0".to_string(),
834                    device_type: fac::DeviceType::Composite,
835                }),
836                DevfsSelector(fac::Devfs {
837                    name: "dai-0".to_string(),
838                    device_type: fac::DeviceType::Dai,
839                }),
840                DevfsSelector(fac::Devfs {
841                    name: "input-0".to_string(),
842                    device_type: fac::DeviceType::Input,
843                }),
844                DevfsSelector(fac::Devfs {
845                    name: "input-1".to_string(),
846                    device_type: fac::DeviceType::Input,
847                }),
848                DevfsSelector(fac::Devfs {
849                    name: "output-0".to_string(),
850                    device_type: fac::DeviceType::Output,
851                }),
852            ],
853            selectors
854        );
855    }
856
857    fn serve_registry(devices: Vec<fadevice::Info>) -> fadevice::RegistryProxy {
858        let devices = Arc::new(devices);
859        spawn_stream_handler(move |request| {
860            let devices = devices.clone();
861            async move {
862                match request {
863                    fadevice::RegistryRequest::WatchDevicesAdded { responder } => responder
864                        .send(Ok(&fadevice::RegistryWatchDevicesAddedResponse {
865                            devices: Some((*devices).clone()),
866                            ..Default::default()
867                        }))
868                        .unwrap(),
869                    _ => unimplemented!(),
870                }
871            }
872        })
873    }
874
875    #[fuchsia::test]
876    async fn test_list_registry() {
877        let devices = vec![
878            fadevice::Info { token_id: Some(1), ..Default::default() },
879            fadevice::Info { token_id: Some(2), ..Default::default() },
880            fadevice::Info { token_id: Some(3), ..Default::default() },
881        ];
882
883        let registry = serve_registry(devices);
884
885        let infos = list_registry(&registry).await.unwrap();
886
887        assert_eq!(
888            infos,
889            vec![
890                Info::from(fadevice::Info { token_id: Some(1), ..Default::default() }),
891                Info::from(fadevice::Info { token_id: Some(2), ..Default::default() }),
892                Info::from(fadevice::Info { token_id: Some(3), ..Default::default() }),
893            ]
894        );
895    }
896
897    #[test]
898    fn test_unique_instance_id_display() {
899        let id = UniqueInstanceId([
900            0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
901            0x0e, 0x0f,
902        ]);
903        let expected = "000102030405060708090a0b0c0d0e0f";
904        assert_eq!(id.to_string(), expected);
905    }
906}