From a246679bddf77d590368b4358dc29a5bb3eb68a0 Mon Sep 17 00:00:00 2001 From: Sriman Achanta Date: Thu, 26 Mar 2026 02:08:14 -0400 Subject: [PATCH] alsa-udev: hide audio nodes when wireless headset is disconnected Monitor the wireless_status sysfs attribute on sibling USB interfaces to detect when a wireless headset loses connection to its dongle. When disconnected, suppress the card's nodes from the audio graph; re-emit them when the headset reconnects. --- spa/plugins/alsa/alsa-udev.c | 156 ++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 3 deletions(-) diff --git a/spa/plugins/alsa/alsa-udev.c b/spa/plugins/alsa/alsa-udev.c index fdc255864..9c3e12f20 100644 --- a/spa/plugins/alsa/alsa-udev.c +++ b/spa/plugins/alsa/alsa-udev.c @@ -48,6 +48,7 @@ struct card { unsigned int accessible:1; unsigned int ignored:1; unsigned int emitted:1; + unsigned int wireless_disconnected:1; /* Local SPA object IDs. (Global IDs are produced by PipeWire * out of this using its registry.) Compress-Offload or PCM @@ -59,6 +60,10 @@ struct card { * is used because 0 is a valid ALSA card number. */ uint32_t pcm_device_id; uint32_t compress_offload_device_id; + + /* Syspath of the USB interface that has wireless_status (e.g. + * /sys/devices/.../1-5:1.3). Empty string when not applicable. */ + char wireless_status_syspath[256]; }; static uint32_t calc_pcm_device_id(struct card *card) @@ -92,6 +97,8 @@ struct impl { struct spa_source source; struct spa_source notify; + struct udev_monitor *usb_umonitor; + struct spa_source usb_source; unsigned int use_acp:1; unsigned int expose_busy:1; int use_ucm; @@ -353,6 +360,87 @@ static int check_udev_environment(struct udev *udev, const char *devname) return ret; } +static void check_wireless_status(struct impl *this, struct card *card) +{ + char path[PATH_MAX]; + char buf[32]; + size_t sz; + bool was_disconnected; + + if (card->wireless_status_syspath[0] == '\0') + return; + + was_disconnected = card->wireless_disconnected; + + spa_scnprintf(path, sizeof(path), "%s/wireless_status", card->wireless_status_syspath); + + spa_autoptr(FILE) f = fopen(path, "re"); + if (f == NULL) + return; + + sz = fread(buf, 1, sizeof(buf) - 1, f); + buf[sz] = '\0'; + + card->wireless_disconnected = spa_strstartswith(buf, "disconnected"); + + if (card->wireless_disconnected != was_disconnected) + spa_log_info(this->log, "card %u: wireless headset %s", + card->card_nr, + card->wireless_disconnected ? "disconnected" : "connected"); +} + +static void find_wireless_status(struct impl *this, struct card *card) +{ + const char *bus, *parent_syspath, *parent_sysname; + struct udev_device *parent; + struct dirent *entry; + char path[PATH_MAX]; + size_t sysname_len; + + bus = udev_device_get_property_value(card->udev_device, "ID_BUS"); + if (!spa_streq(bus, "usb")) + return; + + /* udev_device_get_parent_* returns a borrowed reference owned by the child; do not unref it. */ + parent = udev_device_get_parent_with_subsystem_devtype(card->udev_device, "usb", "usb_device"); + if (parent == NULL) + return; + + parent_syspath = udev_device_get_syspath(parent); + parent_sysname = udev_device_get_sysname(parent); + if (parent_syspath == NULL || parent_sysname == NULL) + return; + + sysname_len = strlen(parent_sysname); + + spa_autoptr(DIR) dir = opendir(parent_syspath); + if (dir == NULL) + return; + + while ((entry = readdir(dir)) != NULL) { + /* USB interface directories are named ":." */ + if (strncmp(entry->d_name, parent_sysname, sysname_len) != 0 || + entry->d_name[sysname_len] != ':') + continue; + + spa_scnprintf(path, sizeof(path), "%s/%s/wireless_status", + parent_syspath, entry->d_name); + if (access(path, R_OK) < 0) + continue; + + spa_scnprintf(card->wireless_status_syspath, + sizeof(card->wireless_status_syspath), + "%s/%s", parent_syspath, entry->d_name); + + check_wireless_status(this, card); + + spa_log_debug(this->log, "card %u: found wireless_status at %s (%s)", + card->card_nr, card->wireless_status_syspath, + card->wireless_disconnected ? "disconnected" : "connected"); + return; + } +} + static int check_pcm_device_availability(struct impl *this, struct card *card, int *num_pcm_devices) { @@ -734,7 +822,11 @@ static void process_card(struct impl *this, enum action action, struct card *car return; check_access(this, card); - if (card->accessible && !card->emitted) { + check_wireless_status(this, card); + + bool effective_accessible = card->accessible && !card->wireless_disconnected; + + if (effective_accessible && !card->emitted) { int res = emit_added_object_info(this, card); if (res < 0) { if (card->ignored) @@ -753,7 +845,7 @@ static void process_card(struct impl *this, enum action action, struct card *car card->card_nr); card->unavailable = false; } - } else if (!card->accessible && card->emitted) { + } else if (!effective_accessible && card->emitted) { card->emitted = false; if (card->pcm_device_id != ID_DEVICE_NOT_SUPPORTED) @@ -790,8 +882,11 @@ static void process_udev_device(struct impl *this, enum action action, struct ud return; card = find_card(this, card_nr); - if (action == ACTION_CHANGE && !card) + if (action == ACTION_CHANGE && !card) { card = add_card(this, card_nr, udev_device); + if (card) + find_wireless_status(this, card); + } if (!card) return; @@ -918,6 +1013,41 @@ static void impl_on_fd_events(struct spa_source *source) udev_device_unref(udev_device); } +static void impl_on_usb_events(struct spa_source *source) +{ + struct impl *this = source->data; + struct udev_device *udev_device; + const char *action, *syspath; + unsigned int i; + + udev_device = udev_monitor_receive_device(this->usb_umonitor); + if (udev_device == NULL) + return; + + if ((action = udev_device_get_action(udev_device)) == NULL) + action = "change"; + + if (!spa_streq(action, "change")) + goto done; + + syspath = udev_device_get_syspath(udev_device); + if (syspath == NULL) + goto done; + + for (i = 0; i < this->n_cards; i++) { + struct card *card = &this->cards[i]; + if (card->wireless_status_syspath[0] == '\0') + continue; + if (!spa_streq(card->wireless_status_syspath, syspath)) + continue; + spa_log_debug(this->log, "wireless_status change for card %u", card->card_nr); + process_card(this, ACTION_CHANGE, card); + } + +done: + udev_device_unref(udev_device); +} + static int start_monitor(struct impl *this) { int res; @@ -941,6 +1071,20 @@ static int start_monitor(struct impl *this) spa_log_debug(this->log, "monitor %p", this->umonitor); spa_loop_add_source(this->main_loop, &this->source); + this->usb_umonitor = udev_monitor_new_from_netlink(this->udev, "udev"); + if (this->usb_umonitor != NULL) { + udev_monitor_filter_add_match_subsystem_devtype(this->usb_umonitor, + "usb", "usb_interface"); + udev_monitor_enable_receiving(this->usb_umonitor); + + this->usb_source.func = impl_on_usb_events; + this->usb_source.data = this; + this->usb_source.fd = udev_monitor_get_fd(this->usb_umonitor); + this->usb_source.mask = SPA_IO_IN | SPA_IO_ERR; + + spa_loop_add_source(this->main_loop, &this->usb_source); + } + if ((res = start_inotify(this)) < 0) return res; @@ -958,6 +1102,12 @@ static int stop_monitor(struct impl *this) udev_monitor_unref(this->umonitor); this->umonitor = NULL; + if (this->usb_umonitor != NULL) { + spa_loop_remove_source(this->main_loop, &this->usb_source); + udev_monitor_unref(this->usb_umonitor); + this->usb_umonitor = NULL; + } + stop_inotify(this); return 0;