Skip to main content

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