Skip to content

Bluetooth web tab, event-driven hotplug, and quieter device logging - #58

Merged
bbangert merged 6 commits into
mainfrom
feat/bluetooth-ui-tab
Jun 13, 2026
Merged

Bluetooth web tab, event-driven hotplug, and quieter device logging#58
bbangert merged 6 commits into
mainfrom
feat/bluetooth-ui-tab

Conversation

@bbangert

Copy link
Copy Markdown
Owner

Builds the Bluetooth UI on top of the merged backend API (#56), then fixes a CPU regression and the device log level found during validation. Four commits, kept on one branch because the final review-polish commit edits files across all three concerns — splitting would leave the pieces interdependent.

What's here

1. Bluetooth web tab + Overview USB peripherals (612cf64)

  • New /bluetooth LiveView (nav tab after Audio): proxy status card, master + active-connections toggles, live stats, and radio selection — wired to the UniversalProxy.Bluetooth API and its bluetooth:state / :stats / :radios PubSub topics. Controls disable while a setter is in flight.
  • Overview's Connected-hardware table now lists USB sound cards and USB Bluetooth radios as peripheral rows that route to their managing tab. Onboard SoC audio / UART Bluetooth stay off the physical-slot list (they aren't on a USB slot). New bluetooth/chip/usb glyphs.

2. Event-driven hotplug — CPU regression fix (8ae9361)

  • RadioMonitor polled adapters_info every 5s, which ran a full org.bluez GetManagedObjects (whole device tree, ~49ms, scaling with discovered BLE devices) just to read adapter Name/Address. That was the CPU climb since the BT-UI-API work.
  • adapters_info now does a cheap per-adapter Adapter1.GetAll over paths the Client tracks from InterfacesAdded/Removed. RadioMonitor drops its timer and re-enumerates only on the Client's adapter-change broadcast (claim at setup, hotplug). Audio.Server's identical 5s poll likewise moves to NervesUEvent (sound subsystem, debounced), keeping the timer only as a host/dev fallback.
  • Hardware-measured: adapters_info 49ms → 3ms and no longer polled; 15-min loadavg 0.14 → 0.04.

3. Device Logger defaults to :info (5b4b348)

  • Nerves firmware builds in MIX_ENV=dev, where target.exs left the level at Elixir's :debug default — capturing espex's per-advertisement debug logging into the ring buffer. prod.exs already set :info, so production was unaffected; this makes dev-build firmware match.

4. Review polish (d633d2f)

  • Server-side busy guards on the mutating LiveView handlers (a scripted client can't stack espex restarts); robust subscribe_uevents (rescue ArgumentError + catch :exit); drop a duplicated with_last/1 helper for Tailwind last:border-b-0.

Verification

485 tests + 5 doctests, strict compile, format, and dialyzer all pass. Reviewed by parallel specialist agents (Elixir/OTP, LiveView, security) — PASS with two minor warnings, both fixed in d633d2f. Hardware-validated on the rpi3 across the full cycle (tab renders, toggles, radio status, CPU before/after, debug logging off).

Known gaps (tracked, not blocking)

  • Dual-radio switching and USB-dongle hotplug can't be hardware-validated until the ASUS BT500 is physically attached (host-tested only).
  • Overview peripheral rows are appended, not interleaved by physical USB slot — the backend has no USB-slot mapping for audio/BT devices.
  • Follow-up issue Bluez.Client mode-transition Task should run supervised #57 (supervise the Bluez mode-transition Task) is pre-existing, unrelated.

🤖 Generated with Claude Code

bbangert and others added 4 commits June 13, 2026 14:42
New BluetoothLive (route + nav tab after Audio) renders the proxy
status, master/active-connections toggles, live stats, and radio
selection, wired to the UniversalProxy.Bluetooth API and its three
PubSub topics. Controls disable while a setter is in flight so a
click-storm can't stack espex restarts.

Overview's Connected hardware table now also lists USB sound cards and
USB Bluetooth radios as peripheral rows that route to their managing
tab; onboard SoC audio and UART Bluetooth stay off the physical-slot
list. Adds bluetooth/chip/usb icon glyphs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The RadioMonitor polled adapters_info every 5s, and adapters_info did a
full org.bluez GetManagedObjects (whole device tree, ~49ms, scaling with
discovered BLE devices) just to read adapter Name/Address — the CPU
regression since the BT-UI-API work.

adapters_info now does a cheap per-adapter Adapter1.GetAll over paths the
Client tracks from InterfacesAdded/Removed. RadioMonitor drops its timer
and re-enumerates only when the Client broadcasts an adapter change
(claim at setup, hotplug add/remove). Audio.Server likewise switches its
5s hotplug poll to NervesUEvent (sound subsystem, debounced), keeping the
timer only as a host/dev fallback where uevents aren't available.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nerves firmware builds in MIX_ENV=dev, where target.exs left the Logger
level at Elixir's :debug default — capturing espex's per-advertisement
debug logging (and other debug noise) into the ring buffer. prod.exs
already sets :info, so production was unaffected; this makes dev-build
firmware match. Re-enable at runtime with Logger.configure(level: :debug).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…last

- BluetoothLive: server-side `busy` early-return on the three mutating
  handlers so a scripted client can't stack espex restarts mid-write (the
  HTML disabled attribute is UX-only).
- Audio.Server: subscribe_uevents rescues ArgumentError (unstarted
  PropertyTable) + catches :exit instead of a whereis/subscribe TOCTOU.
- BluetoothLive: drop the duplicated with_last/1 helper; the radio rows
  use Tailwind last:border-b-0 instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a first-class Bluetooth UI to UniversalProxyWeb and reworks Bluetooth/audio hotplug detection to be event-driven (reducing CPU overhead), while also quieting device logging defaults on Nerves dev firmware builds.

Changes:

  • Introduces a new /bluetooth LiveView and nav tab, plus host-target LiveView tests and render smoke coverage.
  • Replaces periodic hotplug polling with event-driven signals: BlueZ adapter add/remove broadcasts for Bluetooth radio enumeration, and kernel uevents (with debounce + fallback timer) for audio output enumeration.
  • Sets Nerves target.exs Logger default to :info to avoid overly chatty ring buffer logging in dev-built firmware.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/universal_proxy/bluetooth/radio_monitor_test.exs Updates tests to drive RadioMonitor re-enumeration via the new adapter-change PubSub signal.
test/universal_proxy/audio/server_test.exs Adds coverage asserting sound uevents trigger a debounced refresh and unrelated uevents do not.
test/universal_proxy_web/live/render_smoke_test.exs Extends smoke coverage to ensure the Bluetooth tab renders on host.
test/universal_proxy_web/live/bluetooth_live_test.exs New host-target LiveView tests for disabled/unavailable Bluetooth behavior and UI error surfacing.
lib/universal_proxy/bluez/client.ex Tracks adapter paths, provides adapters_topic/0, and shifts adapter info reads to per-adapter GetAll plus broadcasts on adapter add/remove.
lib/universal_proxy/bluetooth/radio_monitor.ex Removes polling; subscribes to Bluez.Client adapter-change events and re-enumerates on demand/events.
lib/universal_proxy/audio/server.ex Switches hotplug detection to NervesUEvent subscription with debounce and timer fallback; adds robust subscribe behavior.
lib/universal_proxy_web/router.ex Wires the new /bluetooth LiveView route.
lib/universal_proxy_web/live/overview_live.ex Adds Bluetooth radios + USB peripheral rows to the Overview hardware table, routing to managing tabs.
lib/universal_proxy_web/live/bluetooth_live.ex New Bluetooth LiveView UI with status, toggles, stats, radio selection, and busy-guarded setters.
lib/universal_proxy_web/components/layouts/app.html.heex Adds the Bluetooth nav tab entry.
lib/universal_proxy_web/components/icons.ex Adds Bluetooth/chip/USB icons and a Bluetooth port-kind glyph.
config/target.exs Sets Logger level to :info for Nerves target builds by default.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/universal_proxy/bluez/client.ex
Comment thread lib/universal_proxy/bluez/client.ex
Comment thread lib/universal_proxy/audio/server.ex
- Commit the adapter-set MapSet update before broadcast_adapters_changed
  in the InterfacesAdded/Removed handlers. Behavior is unchanged
  (RadioMonitor's adapters_info call queues behind this handler), but
  committing first makes the ordering obvious without tracing the mailbox.
- Reword the discover_adapters comment: it runs at setup including each
  retry, not strictly once.
- Update the Audio.Server moduledoc: hotplug is uevent-driven now, with
  the 5s poll only as a host/dev fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment thread lib/universal_proxy/bluez/client.ex
The no-controllers-at-all branch logged adapter_path(), a persistent_term
that defaults to hci0 (or a previously-selected radio) — misrepresenting
the failure. Log the absence plus the desired radio for context instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bbangert
bbangert merged commit 9b254cd into main Jun 13, 2026
6 checks passed
@bbangert
bbangert deleted the feat/bluetooth-ui-tab branch June 13, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants