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 = match fuchsia_fs::directory::open_directory(
669            dev_class,
670            subdir_name,
671            fio::Flags::empty(),
672        )
673        .await
674        {
675            Ok(d) => d,
676            // NOT_FOUND is expected: some device classes are absent on certain boards.
677            // (e.g. /dev/class/codec is missing on VIM3 running UAC2 USB audio.)
678            Err(err) if err.is_not_found_error() => continue,
679            Err(err) => return Err(ListDevfsError::Open { name: subdir_name.to_string(), err }),
680        };
681        let entries = fuchsia_fs::directory::readdir(&subdir)
682            .await
683            .map_err(|err| ListDevfsError::Readdir { name: subdir_name.to_string(), err })?;
684        selectors.extend(entries.into_iter().map(|entry| {
685            DevfsSelector(fac::Devfs { name: entry.name, device_type: device_type.0 })
686        }));
687    }
688
689    Ok(selectors)
690}
691
692#[derive(Error, Debug)]
693pub enum ListRegistryError {
694    #[error(transparent)]
695    Fidl(#[from] fidl::Error),
696
697    #[error("failed to get devices: {:?}", .0)]
698    WatchDevicesAdded(fadevice::RegistryWatchDevicesAddedError),
699}
700
701/// Returns info for all audio devices in the `fuchsia.audio.device` registry.
702pub async fn list_registry(
703    registry: &fadevice::RegistryProxy,
704) -> Result<Vec<Info>, ListRegistryError> {
705    Ok(registry
706        .watch_devices_added()
707        .await
708        .map_err(ListRegistryError::Fidl)?
709        .map_err(ListRegistryError::WatchDevicesAdded)?
710        .devices
711        .expect("missing devices")
712        .into_iter()
713        .map(Info::from)
714        .collect())
715}
716
717#[cfg(test)]
718mod test {
719    use super::*;
720    #[cfg(feature = "fdomain")]
721    use fidl_test_util::fdomain::spawn_stream_handler;
722    #[cfg(not(feature = "fdomain"))]
723    use fidl_test_util::spawn_stream_handler;
724    use std::sync::Arc;
725    use test_case::test_case;
726    #[cfg(not(feature = "fdomain"))]
727    use vfs::pseudo_directory;
728
729    #[test_case("composite", fac::DeviceType::Composite; "composite")]
730    #[test_case("input", fac::DeviceType::Input; "input")]
731    #[test_case("output", fac::DeviceType::Output; "output")]
732    fn test_parse_type(s: &str, expected_type: fac::DeviceType) {
733        assert_eq!(Type(expected_type), s.parse::<Type>().unwrap());
734    }
735
736    #[test]
737    fn test_parse_type_invalid() {
738        assert!("not a valid device type".parse::<Type>().is_err());
739    }
740
741    #[test_case("Codec", fhaudio::DeviceType::Codec; "Codec")]
742    #[test_case("Composite", fhaudio::DeviceType::Composite; "Composite")]
743    #[test_case("Dai", fhaudio::DeviceType::Dai; "Dai")]
744    #[test_case("StreamConfig", fhaudio::DeviceType::StreamConfig; "StreamConfig")]
745    fn test_parse_hardware_type(s: &str, expected_type: fhaudio::DeviceType) {
746        assert_eq!(HardwareType(expected_type), s.parse::<HardwareType>().unwrap());
747    }
748
749    #[test]
750    fn test_parse_hardware_type_invalid() {
751        assert!("not a valid hardware device type".parse::<Type>().is_err());
752    }
753
754    #[test_case(fhaudio::DeviceType::Codec, None, fac::DeviceType::Codec; "Codec")]
755    #[test_case(fhaudio::DeviceType::Composite, None, fac::DeviceType::Composite; "Composite")]
756    #[test_case(fhaudio::DeviceType::Dai, None, fac::DeviceType::Dai; "Dai")]
757    #[test_case(
758        fhaudio::DeviceType::StreamConfig,
759        Some(Direction::Input),
760        fac::DeviceType::Input;
761        "StreamConfig input"
762    )]
763    #[test_case(
764        fhaudio::DeviceType::StreamConfig,
765        Some(Direction::Output),
766        fac::DeviceType::Output;
767        "StreamConfig output"
768    )]
769    fn test_from_hardware_type_with_direction(
770        hardware_type: fhaudio::DeviceType,
771        direction: Option<Direction>,
772        expected_type: fac::DeviceType,
773    ) {
774        assert_eq!(
775            Type(expected_type),
776            (HardwareType(hardware_type), direction).try_into().unwrap()
777        )
778    }
779
780    #[test_case(
781        fac::Devfs { name: "3d99d780".to_string(), device_type: fac::DeviceType::Codec },
782        "/dev/class/codec/3d99d780";
783        "codec"
784    )]
785    #[test_case(
786        fac::Devfs { name: "3d99d780".to_string(), device_type: fac::DeviceType::Composite },
787        "/dev/class/audio-composite/3d99d780";
788        "composite"
789    )]
790    #[test_case(
791        fac::Devfs { name: "3d99d780".to_string(), device_type: fac::DeviceType::Dai },
792        "/dev/class/dai/3d99d780";
793        "dai"
794    )]
795    #[test_case(
796        fac::Devfs { name: "3d99d780".to_string(), device_type: fac::DeviceType::Input },
797        "/dev/class/audio-input/3d99d780";
798        "input"
799    )]
800    #[test_case(
801        fac::Devfs { name: "3d99d780".to_string(), device_type: fac::DeviceType::Output },
802        "/dev/class/audio-output/3d99d780";
803        "output"
804    )]
805    fn test_devfs_selector_path(devfs: fac::Devfs, expected_path: &str) {
806        assert_eq!(expected_path, DevfsSelector(devfs).path());
807    }
808
809    #[cfg(not(feature = "fdomain"))]
810    fn placeholder_node() -> Arc<vfs::service::Service> {
811        vfs::service::endpoint(move |_scope, _channel| {
812            // Just drop the channel.
813        })
814    }
815
816    #[cfg(not(feature = "fdomain"))]
817    #[fuchsia::test]
818    async fn test_list_devfs() {
819        // Placeholder for serving the device protocol.
820        // list_devfs doesn't connect to it, so we don't serve it.
821        let placeholder = placeholder_node();
822
823        let dev_class_vfs = pseudo_directory! {
824            "codec" => pseudo_directory! {
825                "codec-0" => placeholder.clone(),
826            },
827            "audio-composite" => pseudo_directory! {
828                "composite-0" => placeholder.clone(),
829            },
830            "dai" => pseudo_directory! {
831                "dai-0" => placeholder.clone(),
832            },
833            "audio-input" => pseudo_directory! {
834                "input-0" => placeholder.clone(),
835                "input-1" => placeholder.clone(),
836            },
837            "audio-output" => pseudo_directory! {
838                "output-0" => placeholder.clone(),
839            },
840        };
841
842        let execution_scope = vfs::execution_scope::ExecutionScope::new();
843        let dev_class = vfs::directory::serve_read_only(dev_class_vfs, execution_scope);
844        let selectors = list_devfs(&dev_class).await.unwrap();
845
846        assert_eq!(
847            vec![
848                DevfsSelector(fac::Devfs {
849                    name: "codec-0".to_string(),
850                    device_type: fac::DeviceType::Codec,
851                }),
852                DevfsSelector(fac::Devfs {
853                    name: "composite-0".to_string(),
854                    device_type: fac::DeviceType::Composite,
855                }),
856                DevfsSelector(fac::Devfs {
857                    name: "dai-0".to_string(),
858                    device_type: fac::DeviceType::Dai,
859                }),
860                DevfsSelector(fac::Devfs {
861                    name: "input-0".to_string(),
862                    device_type: fac::DeviceType::Input,
863                }),
864                DevfsSelector(fac::Devfs {
865                    name: "input-1".to_string(),
866                    device_type: fac::DeviceType::Input,
867                }),
868                DevfsSelector(fac::Devfs {
869                    name: "output-0".to_string(),
870                    device_type: fac::DeviceType::Output,
871                }),
872            ],
873            selectors
874        );
875    }
876
877    #[cfg(not(feature = "fdomain"))]
878    #[fuchsia::test]
879    async fn test_list_devfs_missing_dirs() {
880        // Verify that list_devfs succeeds and skips device classes whose
881        // directories are absent from devfs (NOT_FOUND), while still
882        // returning entries for the classes that are present.
883        let placeholder = placeholder_node();
884
885        // Omit "audio-input" and "codec" to simulate a board (e.g. VIM3 with
886        // UAC2 USB audio) where those devfs class directories do not exist.
887        let dev_class_vfs = pseudo_directory! {
888            "audio-composite" => pseudo_directory! {
889                "composite-0" => placeholder.clone(),
890            },
891            "dai" => pseudo_directory! {
892                "dai-0" => placeholder.clone(),
893            },
894            "audio-output" => pseudo_directory! {
895                "output-0" => placeholder.clone(),
896            },
897        };
898
899        let execution_scope = vfs::execution_scope::ExecutionScope::new();
900        let dev_class = vfs::directory::serve_read_only(dev_class_vfs, execution_scope);
901        let selectors = list_devfs(&dev_class).await.unwrap();
902
903        assert_eq!(
904            vec![
905                DevfsSelector(fac::Devfs {
906                    name: "composite-0".to_string(),
907                    device_type: fac::DeviceType::Composite,
908                }),
909                DevfsSelector(fac::Devfs {
910                    name: "dai-0".to_string(),
911                    device_type: fac::DeviceType::Dai,
912                }),
913                DevfsSelector(fac::Devfs {
914                    name: "output-0".to_string(),
915                    device_type: fac::DeviceType::Output,
916                }),
917            ],
918            selectors
919        );
920    }
921
922    #[cfg(not(feature = "fdomain"))]
923    #[fuchsia::test]
924    async fn test_list_devfs_all_missing() {
925        // Verify that list_devfs succeeds with an empty result when none of
926        // the known device class directories exist, e.g. a board that
927        // exposes no audio devices at all.
928        let dev_class_vfs = pseudo_directory! {};
929
930        let execution_scope = vfs::execution_scope::ExecutionScope::new();
931        let dev_class = vfs::directory::serve_read_only(dev_class_vfs, execution_scope);
932        let selectors = list_devfs(&dev_class).await.unwrap();
933
934        assert_eq!(Vec::<DevfsSelector>::new(), selectors);
935    }
936
937    #[cfg(not(feature = "fdomain"))]
938    #[fuchsia::test]
939    async fn test_list_devfs_one_class_present() {
940        // The opposite extreme from test_list_devfs: only a single known
941        // device class directory exists, everything else is absent.
942        let placeholder = placeholder_node();
943
944        let dev_class_vfs = pseudo_directory! {
945            "dai" => pseudo_directory! {
946                "dai-0" => placeholder.clone(),
947            },
948        };
949
950        let execution_scope = vfs::execution_scope::ExecutionScope::new();
951        let dev_class = vfs::directory::serve_read_only(dev_class_vfs, execution_scope);
952        let selectors = list_devfs(&dev_class).await.unwrap();
953
954        assert_eq!(
955            vec![DevfsSelector(fac::Devfs {
956                name: "dai-0".to_string(),
957                device_type: fac::DeviceType::Dai,
958            })],
959            selectors
960        );
961    }
962
963    #[cfg(not(feature = "fdomain"))]
964    #[fuchsia::test]
965    async fn test_list_devfs_present_but_empty() {
966        // A device class directory can exist but contain no devices yet
967        // (e.g. a hot-pluggable class before anything is plugged in). This
968        // is a distinct code path from a directory being entirely absent
969        // (NOT_FOUND): the open() succeeds and readdir() returns no
970        // entries. Both should be harmless to list_devfs.
971        let placeholder = placeholder_node();
972
973        let dev_class_vfs = pseudo_directory! {
974            "codec" => pseudo_directory! {},
975            "dai" => pseudo_directory! {
976                "dai-0" => placeholder.clone(),
977            },
978        };
979
980        let execution_scope = vfs::execution_scope::ExecutionScope::new();
981        let dev_class = vfs::directory::serve_read_only(dev_class_vfs, execution_scope);
982        let selectors = list_devfs(&dev_class).await.unwrap();
983
984        assert_eq!(
985            vec![DevfsSelector(fac::Devfs {
986                name: "dai-0".to_string(),
987                device_type: fac::DeviceType::Dai,
988            })],
989            selectors
990        );
991    }
992
993    #[cfg(not(feature = "fdomain"))]
994    #[fuchsia::test]
995    async fn test_list_devfs_unknown_class_ignored() {
996        // list_devfs only opens the fixed set of known device class names;
997        // an unrelated /dev/class subdirectory (e.g. from an unrelated
998        // driver) should be ignored rather than surfacing an error or
999        // being included in the result.
1000        let placeholder = placeholder_node();
1001
1002        let dev_class_vfs = pseudo_directory! {
1003            "dai" => pseudo_directory! {
1004                "dai-0" => placeholder.clone(),
1005            },
1006            "thermal" => pseudo_directory! {
1007                "thermal-0" => placeholder.clone(),
1008            },
1009        };
1010
1011        let execution_scope = vfs::execution_scope::ExecutionScope::new();
1012        let dev_class = vfs::directory::serve_read_only(dev_class_vfs, execution_scope);
1013        let selectors = list_devfs(&dev_class).await.unwrap();
1014
1015        assert_eq!(
1016            vec![DevfsSelector(fac::Devfs {
1017                name: "dai-0".to_string(),
1018                device_type: fac::DeviceType::Dai,
1019            })],
1020            selectors
1021        );
1022    }
1023
1024    fn serve_registry(
1025        #[cfg(feature = "fdomain")] client: &Arc<fdomain_client::Client>,
1026        devices: Vec<fadevice::Info>,
1027    ) -> fadevice::RegistryProxy {
1028        let devices = Arc::new(devices);
1029        spawn_stream_handler(
1030            #[cfg(feature = "fdomain")]
1031            client,
1032            move |request| {
1033                let devices = devices.clone();
1034                async move {
1035                    match request {
1036                        fadevice::RegistryRequest::WatchDevicesAdded { responder } => responder
1037                            .send(Ok(&fadevice::RegistryWatchDevicesAddedResponse {
1038                                devices: Some((*devices).clone()),
1039                                ..Default::default()
1040                            }))
1041                            .unwrap(),
1042                        _ => unimplemented!(),
1043                    }
1044                }
1045            },
1046        )
1047    }
1048
1049    #[fuchsia::test]
1050    async fn test_list_registry() {
1051        #[cfg(feature = "fdomain")]
1052        let client = fdomain_local::local_client_empty();
1053        let devices = vec![
1054            fadevice::Info { token_id: Some(1), ..Default::default() },
1055            fadevice::Info { token_id: Some(2), ..Default::default() },
1056            fadevice::Info { token_id: Some(3), ..Default::default() },
1057        ];
1058
1059        let registry = serve_registry(
1060            #[cfg(feature = "fdomain")]
1061            &client,
1062            devices,
1063        );
1064
1065        let infos = list_registry(&registry).await.unwrap();
1066
1067        assert_eq!(
1068            infos,
1069            vec![
1070                Info::from(fadevice::Info { token_id: Some(1), ..Default::default() }),
1071                Info::from(fadevice::Info { token_id: Some(2), ..Default::default() }),
1072                Info::from(fadevice::Info { token_id: Some(3), ..Default::default() }),
1073            ]
1074        );
1075    }
1076
1077    #[test]
1078    fn test_unique_instance_id_display() {
1079        let id = UniqueInstanceId([
1080            0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
1081            0x0e, 0x0f,
1082        ]);
1083        let expected = "000102030405060708090a0b0c0d0e0f";
1084        assert_eq!(id.to_string(), expected);
1085    }
1086}