Bluetooth web tab, event-driven hotplug, and quieter device logging - #58
Merged
Conversation
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>
There was a problem hiding this comment.
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
/bluetoothLiveView 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.exsLogger default to:infoto 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.
- 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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)/bluetoothLiveView (nav tab after Audio): proxy status card, master + active-connections toggles, live stats, and radio selection — wired to theUniversalProxy.BluetoothAPI and itsbluetooth:state/:stats/:radiosPubSub topics. Controls disable while a setter is in flight.2. Event-driven hotplug — CPU regression fix (
8ae9361)RadioMonitorpolledadapters_infoevery 5s, which ran a full org.bluezGetManagedObjects(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_infonow does a cheap per-adapterAdapter1.GetAllover paths the Client tracks fromInterfacesAdded/Removed.RadioMonitordrops 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 toNervesUEvent(sound subsystem, debounced), keeping the timer only as a host/dev fallback.adapters_info49ms → 3ms and no longer polled; 15-min loadavg 0.14 → 0.04.3. Device Logger defaults to
:info(5b4b348)MIX_ENV=dev, wheretarget.exsleft the level at Elixir's:debugdefault — capturing espex's per-advertisement debug logging into the ring buffer.prod.exsalready set:info, so production was unaffected; this makes dev-build firmware match.4. Review polish (
d633d2f)busyguards on the mutating LiveView handlers (a scripted client can't stack espex restarts); robustsubscribe_uevents(rescue ArgumentError + catch :exit); drop a duplicatedwith_last/1helper for Tailwindlast: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)
🤖 Generated with Claude Code