From a3eb2dd9dc2399ce9c22cd3b07f482f080976440 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 11 Jul 2024 15:11:28 +1000 Subject: [PATCH] WIP improve bluetooth api and settings screen --- lua/settings.lua | 105 ++++-- src/drivers/bluetooth.cpp | 338 ++++++++++-------- src/drivers/include/drivers/bluetooth.hpp | 116 ++++-- .../include/drivers/bluetooth_types.hpp | 7 +- src/drivers/include/drivers/nvs.hpp | 7 + src/drivers/nvs.cpp | 83 +++++ src/tangara/app_console/app_console.cpp | 17 +- src/tangara/audio/audio_fsm.cpp | 11 +- src/tangara/audio/bt_audio_output.cpp | 13 +- src/tangara/lua/property.cpp | 30 +- src/tangara/lua/property.hpp | 4 +- src/tangara/system_fsm/booting.cpp | 3 +- src/tangara/ui/ui_fsm.cpp | 114 ++++-- src/tangara/ui/ui_fsm.hpp | 5 +- 14 files changed, 569 insertions(+), 284 deletions(-) diff --git a/lua/settings.lua b/lua/settings.lua index cb7f65e0..0691f2d1 100644 --- a/lua/settings.lua +++ b/lua/settings.lua @@ -30,10 +30,37 @@ local SettingsScreen = widgets.MenuScreen:new { end } +local BluetoothPairing = SettingsScreen:new { + title = "Nearby Devices", + createUi = function(self) + SettingsScreen.createUi(self) + + local devices = self.content:List { + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + } + + self.bindings = self.bindings + { + bluetooth.discovered_devices:bind(function(devs) + devices:clean() + for _, dev in pairs(devs) do + devices:add_btn(nil, dev.name):onClicked(function() + bluetooth.paired_device:set(dev) + end) + end + end) + } + end, + onShown = function() bluetooth.discovering:set(true) end, + onHidden = function() bluetooth.discovering:set(false) end, +} + local BluetoothSettings = SettingsScreen:new { title = "Bluetooth", createUi = function(self) SettingsScreen.createUi(self) + + -- Enable/Disable switch local enable_container = self.content:Object { flex = { flex_direction = "row", @@ -52,11 +79,43 @@ local BluetoothSettings = SettingsScreen:new { bluetooth.enabled:set(enabled) end) - theme.set_style(self.content:Label { - text = "Paired Device", - pad_bottom = 1, - }, "settings_title") + self.bindings = self.bindings + { + bluetooth.enabled:bind(function(en) + if en then + enable_sw:add_state(lvgl.STATE.CHECKED) + else + enable_sw:clear_state(lvgl.STATE.CHECKED) + end + end), + } + + -- Connection status + -- This is presented as a label on the field showing the currently paired + -- device. + + local paired_label = + self.content:Label { + text = "", + pad_bottom = 1, + } + theme.set_style(paired_label, "settings_title") + + self.bindings = self.bindings + { + bluetooth.connected:bind(function(conn) + if conn then + paired_label:set { text = "Connected to:" } + else + paired_label:set { text = "Paired with:" } + end + end), + bluetooth.connecting:bind(function(conn) + if conn then + paired_label:set { text = "Connecting to:" } + end + end), + } + -- The name of the currently paired device. local paired_container = self.content:Object { flex = { flex_direction = "row", @@ -78,24 +137,7 @@ local BluetoothSettings = SettingsScreen:new { bluetooth.paired_device:set() end) - theme.set_style(self.content:Label { - text = "Nearby Devices", - pad_bottom = 1, - }, "settings_title") - - local devices = self.content:List { - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - } - self.bindings = self.bindings + { - bluetooth.enabled:bind(function(en) - if en then - enable_sw:add_state(lvgl.STATE.CHECKED) - else - enable_sw:clear_state(lvgl.STATE.CHECKED) - end - end), bluetooth.paired_device:bind(function(device) if device then paired_device:set { text = device.name } @@ -105,7 +147,20 @@ local BluetoothSettings = SettingsScreen:new { clear_paired:add_flag(lvgl.FLAG.HIDDEN) end end), - bluetooth.devices:bind(function(devs) + } + + theme.set_style(self.content:Label { + text = "Known Devices", + pad_bottom = 1, + }, "settings_title") + + local devices = self.content:List { + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + } + + self.bindings = self.bindings + { + bluetooth.known_devices:bind(function(devs) devices:clean() for _, dev in pairs(devs) do devices:add_btn(nil, dev.name):onClicked(function() @@ -114,6 +169,12 @@ local BluetoothSettings = SettingsScreen:new { end end) } + + local pair_new = self.content:Button {} + pair_new:Label { text = "Pair new device" } + pair_new:onClicked(function() + backstack.push(BluetoothPairing:new()) + end) end } diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index 412cba1f..2edf5ad9 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -113,92 +114,111 @@ IRAM_ATTR auto a2dp_data_cb(uint8_t* buf, int32_t buf_size) -> int32_t { return buf_size; } -Bluetooth::Bluetooth(NvsStorage& storage, tasks::WorkerPool& bg_worker) { +Bluetooth::Bluetooth(NvsStorage& storage, + tasks::WorkerPool& bg_worker, + EventHandler cb) + : nvs_(storage) { sBgWorker = &bg_worker; - bluetooth::BluetoothState::Init(storage); + bluetooth::BluetoothState::Init(storage, cb); } -auto Bluetooth::Enable() -> bool { - auto lock = bluetooth::BluetoothState::lock(); - tinyfsm::FsmList::dispatch( - bluetooth::events::Enable{}); +auto Bluetooth::enable(bool en) -> void { + if (en) { + auto lock = bluetooth::BluetoothState::lock(); + tinyfsm::FsmList::dispatch( + bluetooth::events::Enable{}); + } else { + // FIXME: the BT tasks unfortunately call back into us while holding an + // internal lock, which then deadlocks with our fsm lock. + // auto lock = bluetooth::BluetoothState::lock(); + tinyfsm::FsmList::dispatch( + bluetooth::events::Disable{}); + } +} +auto Bluetooth::enabled() -> bool { + auto lock = bluetooth::BluetoothState::lock(); return !bluetooth::BluetoothState::is_in_state(); } -auto Bluetooth::Disable() -> void { - // FIXME: the BT tasks unfortunately call back into us while holding an - // internal lock, which then deadlocks with our fsm lock. - // auto lock = bluetooth::BluetoothState::lock(); +auto Bluetooth::source(PcmBuffer* src) -> void { + if (src == sStream) { + return; + } + auto lock = bluetooth::BluetoothState::lock(); + sStream = src; tinyfsm::FsmList::dispatch( - bluetooth::events::Disable{}); + bluetooth::events::SourceChanged{}); } -auto Bluetooth::IsEnabled() -> bool { - auto lock = bluetooth::BluetoothState::lock(); - return !bluetooth::BluetoothState::is_in_state(); +auto Bluetooth::softVolume(float f) -> void { + sVolumeFactor = f; } -auto Bluetooth::IsConnected() -> bool { +auto Bluetooth::connectionState() -> ConnectionState { auto lock = bluetooth::BluetoothState::lock(); - return bluetooth::BluetoothState::is_in_state(); + if (bluetooth::BluetoothState::is_in_state()) { + return ConnectionState::kConnected; + } else if (bluetooth::BluetoothState::is_in_state()) { + return ConnectionState::kConnecting; + } + return ConnectionState::kDisconnected; } -auto Bluetooth::ConnectedDevice() -> std::optional { +auto Bluetooth::pairedDevice() -> std::optional { auto lock = bluetooth::BluetoothState::lock(); - if (!bluetooth::BluetoothState::is_in_state()) { - return {}; - } - return bluetooth::BluetoothState::preferred_device(); + return bluetooth::BluetoothState::pairedDevice(); } -auto Bluetooth::KnownDevices() -> std::vector { +auto Bluetooth::pairedDevice(std::optional dev) -> void { auto lock = bluetooth::BluetoothState::lock(); - std::vector out = bluetooth::BluetoothState::devices(); - std::sort(out.begin(), out.end(), [](const auto& a, const auto& b) -> bool { - return a.signal_strength < b.signal_strength; - }); - return out; + bluetooth::BluetoothState::pairedDevice(dev); } -auto Bluetooth::SetPreferredDevice(std::optional dev) - -> void { - auto lock = bluetooth::BluetoothState::lock(); - auto cur = bluetooth::BluetoothState::preferred_device(); - if (dev && cur && dev->mac == cur->mac) { - return; - } - ESP_LOGI(kTag, "preferred is '%s' (%u%u%u%u%u%u)", dev->name.c_str(), - dev->mac[0], dev->mac[1], dev->mac[2], dev->mac[3], dev->mac[4], - dev->mac[5]); - bluetooth::BluetoothState::preferred_device(dev); - tinyfsm::FsmList::dispatch( - bluetooth::events::PreferredDeviceChanged{}); +auto Bluetooth::knownDevices() -> std::vector { + return nvs_.BluetoothNames(); } -auto Bluetooth::PreferredDevice() -> std::optional { - auto lock = bluetooth::BluetoothState::lock(); - return bluetooth::BluetoothState::preferred_device(); +auto Bluetooth::forgetKnownDevice(const bluetooth::mac_addr_t& mac) -> void { + nvs_.BluetoothName(mac, {}); } -auto Bluetooth::SetSource(PcmBuffer* src) -> void { +auto Bluetooth::discoveryEnabled(bool en) -> void { auto lock = bluetooth::BluetoothState::lock(); - if (src == bluetooth::BluetoothState::source()) { - return; - } - bluetooth::BluetoothState::source(src); - tinyfsm::FsmList::dispatch( - bluetooth::events::SourceChanged{}); + bluetooth::BluetoothState::discovery(en); } -auto Bluetooth::SetVolumeFactor(float f) -> void { - sVolumeFactor = f; +auto Bluetooth::discoveryEnabled() -> bool { + auto lock = bluetooth::BluetoothState::lock(); + return bluetooth::BluetoothState::discovery(); } -auto Bluetooth::SetEventHandler(std::function cb) - -> void { - auto lock = bluetooth::BluetoothState::lock(); - bluetooth::BluetoothState::event_handler(cb); +auto Bluetooth::discoveredDevices() -> std::vector { + std::vector discovered; + { + auto lock = bluetooth::BluetoothState::lock(); + discovered = bluetooth::BluetoothState::discoveredDevices(); + } + + // Show devices with stronger signals first, since they're more likely to be + // physically close (and therefore more likely to be what the user wants). + std::sort(discovered.begin(), discovered.end(), + [](const auto& a, const auto& b) -> bool { + return a.signal_strength < b.signal_strength; + }); + + // Convert to the right format. + std::vector out; + out.reserve(discovered.size()); + std::transform(discovered.begin(), discovered.end(), std::back_inserter(out), + [&](const bluetooth::Device& dev) { + return bluetooth::MacAndName{ + .mac = dev.address, + .name = {dev.name.data(), dev.name.size()}, + }; + }); + + return out; } static auto DeviceName() -> std::pmr::string { @@ -251,6 +271,10 @@ auto Scanner::StopScanningNow() -> void { } } +auto Scanner::enabled() -> bool { + return enabled_; +} + auto Scanner::HandleGapEvent(const events::internal::Gap& ev) -> void { switch (ev.type) { case ESP_BT_GAP_DISC_RES_EVT: @@ -343,17 +367,18 @@ NvsStorage* BluetoothState::sStorage_; Scanner* BluetoothState::sScanner_; std::mutex BluetoothState::sFsmMutex{}; -std::map BluetoothState::sDevices_{}; -std::optional BluetoothState::sPreferredDevice_{}; -std::optional BluetoothState::sConnectingDevice_{}; +std::map BluetoothState::sDiscoveredDevices_{}; +std::optional BluetoothState::sPairedWith_{}; +std::optional BluetoothState::sConnectingTo_{}; int BluetoothState::sConnectAttemptsRemaining_{0}; -std::atomic BluetoothState::sSource_; std::function BluetoothState::sEventHandler_; -auto BluetoothState::Init(NvsStorage& storage) -> void { +auto BluetoothState::Init(NvsStorage& storage, Bluetooth::EventHandler cb) + -> void { sStorage_ = &storage; - sPreferredDevice_ = storage.PreferredBluetoothDevice(); + sEventHandler_ = cb; + sPairedWith_ = storage.PreferredBluetoothDevice(); tinyfsm::FsmList::start(); } @@ -361,68 +386,85 @@ auto BluetoothState::lock() -> std::lock_guard { return std::lock_guard{sFsmMutex}; } -auto BluetoothState::devices() -> std::vector { - std::vector out; - for (const auto& device : sDevices_) { - out.push_back(device.second); - } - return out; +auto BluetoothState::pairedDevice() -> std::optional { + return sPairedWith_; } -auto BluetoothState::preferred_device() -> std::optional { - return sPreferredDevice_; -} +auto BluetoothState::pairedDevice(std::optional dev) -> void { + auto cur = sPairedWith_; + if (dev && cur && dev->mac == cur->mac) { + return; + } + if (dev) { + ESP_LOGI(kTag, "pairing with '%s' (%u%u%u%u%u%u)", dev->name.c_str(), + dev->mac[0], dev->mac[1], dev->mac[2], dev->mac[3], dev->mac[4], + dev->mac[5]); + } + sPairedWith_ = dev; + std::invoke(sEventHandler_, SimpleEvent::kDeviceDiscovered); -auto BluetoothState::preferred_device(std::optional addr) -> void { - sPreferredDevice_ = addr; + tinyfsm::FsmList::dispatch( + bluetooth::events::PairedDeviceChanged{}); } -auto BluetoothState::source() -> PcmBuffer* { - return sSource_.load(); +auto BluetoothState::discovery() -> bool { + return sScanner_->enabled(); } -auto BluetoothState::source(PcmBuffer* src) -> void { - sSource_.store(src); +auto BluetoothState::discovery(bool en) -> void { + if (en) { + if (!sScanner_->enabled()) { + sDiscoveredDevices_.clear(); + } + sScanner_->ScanContinuously(); + } else { + sScanner_->StopScanning(); + } } -auto BluetoothState::event_handler(std::function cb) -> void { - sEventHandler_ = cb; +auto BluetoothState::discoveredDevices() -> std::vector { + std::vector out; + for (const auto& device : sDiscoveredDevices_) { + out.push_back(device.second); + } + return out; } auto BluetoothState::react(const events::DeviceDiscovered& ev) -> void { - bool is_preferred = false; - bool already_known = sDevices_.contains(ev.device.address); - sDevices_[ev.device.address] = ev.device; + bool is_paired = false; + bool already_known = sDiscoveredDevices_.contains(ev.device.address); + sDiscoveredDevices_[ev.device.address] = ev.device; - if (sPreferredDevice_ && ev.device.address == sPreferredDevice_->mac) { - is_preferred = true; + if (sPairedWith_ && ev.device.address == sPairedWith_->mac) { + is_paired = true; } - if (sEventHandler_ && !already_known) { - std::invoke(sEventHandler_, SimpleEvent::kKnownDevicesChanged); + if (!already_known) { + std::invoke(sEventHandler_, SimpleEvent::kDeviceDiscovered); } - if (is_preferred && sPreferredDevice_) { - connect(*sPreferredDevice_); + if (is_paired && sPairedWith_) { + connect(*sPairedWith_); } } auto BluetoothState::connect(const MacAndName& dev) -> bool { - if (sConnectingDevice_ && sConnectingDevice_->mac == dev.mac) { + if (sConnectingTo_ && sConnectingTo_->mac == dev.mac) { sConnectAttemptsRemaining_--; } else { sConnectAttemptsRemaining_ = 3; } if (sConnectAttemptsRemaining_ == 0) { + sConnectingTo_ = {}; return false; } - sConnectingDevice_ = dev; + sConnectingTo_ = dev; ESP_LOGI(kTag, "connecting to '%s' (%u%u%u%u%u%u)", dev.name.c_str(), dev.mac[0], dev.mac[1], dev.mac[2], dev.mac[3], dev.mac[4], dev.mac[5]); - if (esp_a2d_source_connect(sConnectingDevice_->mac.data()) != ESP_OK) { + if (esp_a2d_source_connect(sConnectingTo_->mac.data()) != ESP_OK) { ESP_LOGI(kTag, "Connecting failed..."); if (sConnectAttemptsRemaining_ > 1) { ESP_LOGI(kTag, "Will retry."); @@ -508,11 +550,13 @@ void Disabled::react(const events::Enable&) { // AVRCP Target err = esp_avrc_tg_init(); if (err != ESP_OK) { - ESP_LOGE(kTag, "Error during target init: %s %d", esp_err_to_name(err), err); + ESP_LOGE(kTag, "Error during target init: %s %d", esp_err_to_name(err), + err); } err = esp_avrc_tg_register_callback(avrcp_tg_cb); if (err != ESP_OK) { - ESP_LOGE(kTag, "Error registering AVRC tg callback: %s %d", esp_err_to_name(err), err); + ESP_LOGE(kTag, "Error registering AVRC tg callback: %s %d", + esp_err_to_name(err), err); } // Set the supported passthrough commands on the tg @@ -522,19 +566,20 @@ void Disabled::react(const events::Enable&) { do { // Sleep for a bit vTaskDelay(pdMS_TO_TICKS(10)); - err = esp_avrc_tg_get_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_ALLOWED_CMD, &psth); + err = esp_avrc_tg_get_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_ALLOWED_CMD, + &psth); } while (err != ESP_OK); - err = esp_avrc_tg_set_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_SUPPORTED_CMD, &psth); + err = esp_avrc_tg_set_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_SUPPORTED_CMD, + &psth); if (err != ESP_OK) { ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); } esp_avrc_rn_evt_cap_mask_t evt_set = {0}; esp_avrc_rn_evt_bit_mask_operation(ESP_AVRC_BIT_MASK_OP_SET, &evt_set, - ESP_AVRC_RN_VOLUME_CHANGE); + ESP_AVRC_RN_VOLUME_CHANGE); assert(esp_avrc_tg_set_rn_evt_cap(&evt_set) == ESP_OK); - // Initialise A2DP. This handles streaming audio. Currently ESP-IDF's SBC // encoder only supports 2 channels of interleaved 16 bit samples, at // 44.1kHz, so there is no additional configuration to be done for the @@ -547,37 +592,27 @@ void Disabled::react(const events::Enable&) { esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE); ESP_LOGI(kTag, "bt enabled"); - if (sPreferredDevice_) { - ESP_LOGI(kTag, "connecting to preferred device '%s'", - sPreferredDevice_->name.c_str()); - connect(*sPreferredDevice_); + if (sPairedWith_) { + ESP_LOGI(kTag, "connecting to paired device '%s'", + sPairedWith_->name.c_str()); + connect(*sPairedWith_); } else { - ESP_LOGI(kTag, "scanning for devices"); transit(); } } void Idle::entry() { ESP_LOGI(kTag, "bt is idle"); - sScanner_->ScanContinuously(); } -void Idle::exit() { - sScanner_->StopScanning(); -} +void Idle::exit() {} void Idle::react(const events::Disable& ev) { transit(); } -void Idle::react(const events::PreferredDeviceChanged& ev) { - bool is_discovered = false; - if (sPreferredDevice_ && sDevices_.contains(sPreferredDevice_->mac)) { - is_discovered = true; - } - if (is_discovered) { - connect(*sPreferredDevice_); - } +void Idle::react(const events::PairedDeviceChanged& ev) { + connect(*sPairedWith_); } void Idle::react(events::internal::Gap ev) { @@ -599,7 +634,6 @@ void Connecting::entry() { timeoutCallback); xTimerStart(sTimeoutTimer, portMAX_DELAY); - sScanner_->StopScanning(); if (sEventHandler_) { std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged); } @@ -615,19 +649,24 @@ void Connecting::exit() { void Connecting::react(const events::ConnectTimedOut& ev) { ESP_LOGI(kTag, "timed out awaiting connection"); - esp_a2d_source_disconnect(sConnectingDevice_->mac.data()); - if (!connect(*sConnectingDevice_)) { + esp_a2d_source_disconnect(sConnectingTo_->mac.data()); + if (!connect(*sConnectingTo_)) { transit(); } } void Connecting::react(const events::Disable& ev) { - // TODO: disconnect gracefully + esp_a2d_source_disconnect(sConnectingTo_->mac.data()); transit(); } -void Connecting::react(const events::PreferredDeviceChanged& ev) { - // TODO. Cancel out and start again. +void Connecting::react(const events::PairedDeviceChanged& ev) { + esp_a2d_source_disconnect(sConnectingTo_->mac.data()); + if (sPairedWith_) { + connect(*sPairedWith_); + } else { + transit(); + } } void Connecting::react(events::internal::Gap ev) { @@ -698,15 +737,20 @@ void Connected::entry() { ESP_LOGI(kTag, "entering connected state"); transaction_num_ = 0; - connected_to_ = sConnectingDevice_->mac; - sPreferredDevice_ = sConnectingDevice_; - sConnectingDevice_ = {}; + connected_to_ = sConnectingTo_->mac; + sPairedWith_ = sConnectingTo_; + + sStorage_->BluetoothName(sConnectingTo_->mac, sConnectingTo_->name); + std::invoke(sEventHandler_, SimpleEvent::kKnownDevicesChanged); + + sConnectingTo_ = {}; auto stored_pref = sStorage_->PreferredBluetoothDevice(); - if (!stored_pref || (sPreferredDevice_->name != stored_pref->name || - sPreferredDevice_->mac != stored_pref->mac)) { - sStorage_->PreferredBluetoothDevice(sPreferredDevice_); + if (!stored_pref || (sPairedWith_->name != stored_pref->name || + sPairedWith_->mac != stored_pref->mac)) { + sStorage_->PreferredBluetoothDevice(sPairedWith_); } + // TODO: if we already have a source, immediately start playing } @@ -719,13 +763,11 @@ void Connected::react(const events::Disable& ev) { transit(); } -void Connected::react(const events::PreferredDeviceChanged& ev) { - sConnectingDevice_ = sPreferredDevice_; - transit(); +void Connected::react(const events::PairedDeviceChanged& ev) { + transit(); } void Connected::react(const events::SourceChanged& ev) { - sStream = sSource_; if (sStream != nullptr) { ESP_LOGI(kTag, "checking source is ready"); esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_CHECK_SRC_RDY); @@ -775,7 +817,8 @@ void Connected::react(events::internal::Avrc ev) { switch (ev.type) { case ESP_AVRC_CT_CONNECTION_STATE_EVT: if (ev.param.conn_stat.connected) { - auto err = esp_avrc_ct_send_register_notification_cmd(4, ESP_AVRC_RN_VOLUME_CHANGE, 0); + auto err = esp_avrc_ct_send_register_notification_cmd( + 4, ESP_AVRC_RN_VOLUME_CHANGE, 0); if (err != ESP_OK) { ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); } @@ -787,15 +830,20 @@ void Connected::react(events::internal::Avrc ev) { case ESP_AVRC_CT_REMOTE_FEATURES_EVT: // The remote device is telling us about its capabilities! We don't // currently care about any of them. - ESP_LOGI(kTag, "Recieved capabilitites: %lu", ev.param.rmt_feats.feat_mask); + ESP_LOGI(kTag, "Recieved capabilitites: %lu", + ev.param.rmt_feats.feat_mask); break; case ESP_AVRC_CT_CHANGE_NOTIFY_EVT: if (ev.param.change_ntf.event_id == ESP_AVRC_RN_VOLUME_CHANGE) { if (sEventHandler_) { - std::invoke(sEventHandler_, bluetooth::RemoteVolumeChanged{.new_vol = ev.param.change_ntf.event_parameter.volume}); + std::invoke( + sEventHandler_, + bluetooth::RemoteVolumeChanged{ + .new_vol = ev.param.change_ntf.event_parameter.volume}); } // Resubscribe to volume facts - auto err = esp_avrc_ct_send_register_notification_cmd(4, ESP_AVRC_RN_VOLUME_CHANGE, 0); + auto err = esp_avrc_ct_send_register_notification_cmd( + 4, ESP_AVRC_RN_VOLUME_CHANGE, 0); if (err != ESP_OK) { ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); } @@ -809,16 +857,20 @@ void Connected::react(events::internal::Avrc ev) { void Connected::react(const events::internal::Avrctg ev) { switch (ev.type) { case ESP_AVRC_TG_CONNECTION_STATE_EVT: - ESP_LOGI(kTag, "Got connection event. Connected: %s", ev.param.conn_stat.connected ? "true" : "false"); + ESP_LOGI(kTag, "Got connection event. Connected: %s", + ev.param.conn_stat.connected ? "true" : "false"); if (ev.param.conn_stat.connected) { } break; case ESP_AVRC_TG_REMOTE_FEATURES_EVT: - ESP_LOGI(kTag, "Got remote features feat flag %d", ev.param.rmt_feats.ct_feat_flag); - ESP_LOGI(kTag, "Got remote features feat mask %lu", ev.param.rmt_feats.feat_mask); + ESP_LOGI(kTag, "Got remote features feat flag %d", + ev.param.rmt_feats.ct_feat_flag); + ESP_LOGI(kTag, "Got remote features feat mask %lu", + ev.param.rmt_feats.feat_mask); break; case ESP_AVRC_TG_PASSTHROUGH_CMD_EVT: - ESP_LOGI(kTag, "Got passthrough event keycode: %x, %d", ev.param.psth_cmd.key_code, ev.param.psth_cmd.key_state); + ESP_LOGI(kTag, "Got passthrough event keycode: %x, %d", + ev.param.psth_cmd.key_code, ev.param.psth_cmd.key_state); if (ev.param.psth_cmd.key_state == 1 && sEventHandler_) { switch (ev.param.psth_cmd.key_code) { case ESP_AVRC_PT_CMD_PLAY: @@ -840,7 +892,8 @@ void Connected::react(const events::internal::Avrctg ev) { std::invoke(sEventHandler_, bluetooth::SimpleEvent::kBackward); break; default: - ESP_LOGI(kTag, "Unhandled passthrough cmd. Key code: %d", ev.param.psth_cmd.key_code); + ESP_LOGI(kTag, "Unhandled passthrough cmd. Key code: %d", + ev.param.psth_cmd.key_code); } } break; @@ -848,14 +901,15 @@ void Connected::react(const events::internal::Avrctg ev) { if (ev.param.reg_ntf.event_id == ESP_AVRC_RN_VOLUME_CHANGE) { // TODO: actually do this lol esp_avrc_rn_param_t rn_param; - rn_param.volume = 64; + rn_param.volume = 64; auto err = esp_avrc_tg_send_rn_rsp(ESP_AVRC_RN_VOLUME_CHANGE, - ESP_AVRC_RN_RSP_INTERIM, &rn_param); + ESP_AVRC_RN_RSP_INTERIM, &rn_param); if (err != ESP_OK) { ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); } } else { - ESP_LOGW(kTag, "unhandled AVRC TG Register Notification event: %u", ev.param.reg_ntf.event_id); + ESP_LOGW(kTag, "unhandled AVRC TG Register Notification event: %u", + ev.param.reg_ntf.event_id); } break; } diff --git a/src/drivers/include/drivers/bluetooth.hpp b/src/drivers/include/drivers/bluetooth.hpp index 94a85263..449812d6 100644 --- a/src/drivers/include/drivers/bluetooth.hpp +++ b/src/drivers/include/drivers/bluetooth.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -25,28 +26,68 @@ namespace drivers { /* - * A handle used to interact with the bluetooth state machine. + * A handle used to interact with the bluetooth state machine. This is the main + * API that the rest of the system should use to interact with Bluetooth. */ class Bluetooth { public: - Bluetooth(NvsStorage& storage, tasks::WorkerPool&); + /* + * Callback invoked when an event is generated by the Bluetooth stack. This + * callback is invoked synchronously from a Bluetooth task context, so + * implementations should immediately hop to a different task to process the + * event. + */ + using EventHandler = std::function; + + Bluetooth(NvsStorage&, tasks::WorkerPool&, EventHandler); + + /* Enables or disables the entire Bluetooth stack. */ + auto enable(bool en) -> void; + auto enabled() -> bool; + + auto source(PcmBuffer*) -> void; + auto softVolume(float) -> void; + + enum class ConnectionState { + kConnected, + kConnecting, + kDisconnected, + }; + + auto connectionState() -> ConnectionState; + + /* + * The 'paired' device is a device that will be preferred for connections. + * When Bluetooth is first enabled, we immediately try to connect to the + * paired device. If the paired device is seen during a scan, then we will + * also automatically connect to it. + */ + auto pairedDevice() -> std::optional; + + /* + * Sets the preferred device. If a device is provided, a connection will be + * attempted immediately, even if the device has not been detected in a + * previous scan. + */ + auto pairedDevice(std::optional dev) -> void; + + /* A list of devices that have previously been the paired device. */ + auto knownDevices() -> std::vector; + auto forgetKnownDevice(const bluetooth::mac_addr_t&) -> void; + + /* Enables or disables scanning for nearby Bluetooth devices. */ + auto discoveryEnabled(bool) -> void; + auto discoveryEnabled() -> bool; + + /* + * A list of nearby devices that have been discovered since discovery was + * last enabled. This list may include the paired device, as well as devices + * that are also present in the known devices list. + */ + auto discoveredDevices() -> std::vector; - auto Enable() -> bool; - auto Disable() -> void; - auto IsEnabled() -> bool; - - auto IsConnected() -> bool; - auto ConnectedDevice() -> std::optional; - - auto KnownDevices() -> std::vector; - - auto SetPreferredDevice(std::optional dev) -> void; - auto PreferredDevice() -> std::optional; - - auto SetSource(PcmBuffer*) -> void; - auto SetVolumeFactor(float) -> void; - - auto SetEventHandler(std::function cb) -> void; + private: + NvsStorage& nvs_; }; namespace bluetooth { @@ -56,7 +97,7 @@ struct Enable : public tinyfsm::Event {}; struct Disable : public tinyfsm::Event {}; struct ConnectTimedOut : public tinyfsm::Event {}; -struct PreferredDeviceChanged : public tinyfsm::Event {}; +struct PairedDeviceChanged : public tinyfsm::Event {}; struct SourceChanged : public tinyfsm::Event {}; struct DeviceDiscovered : public tinyfsm::Event { const Device& device; @@ -94,6 +135,8 @@ class Scanner { auto StopScanning() -> void; auto StopScanningNow() -> void; + auto enabled() -> bool; + auto HandleGapEvent(const events::internal::Gap&) -> void; private: @@ -103,25 +146,22 @@ class Scanner { auto HandleDeviceDiscovery(const esp_bt_gap_cb_param_t& param) -> void; }; +/* + * The main state machine for managing the state of the Bluetooth stack, and + * the current (if any) Bluetooth connection. + */ class BluetoothState : public tinyfsm::Fsm { public: - static auto Init(NvsStorage& storage) -> void; + static auto Init(NvsStorage& storage, Bluetooth::EventHandler) -> void; static auto lock() -> std::lock_guard; - static auto devices() -> std::vector; - - static auto preferred_device() -> std::optional; - static auto preferred_device(std::optional) -> void; + static auto pairedDevice() -> std::optional; + static auto pairedDevice(std::optional) -> void; - static auto scanning() -> bool; static auto discovery() -> bool; static auto discovery(bool) -> void; - - static auto source() -> PcmBuffer*; - static auto source(PcmBuffer*) -> void; - - static auto event_handler(std::function) -> void; + static auto discoveredDevices() -> std::vector; virtual ~BluetoothState(){}; @@ -131,7 +171,7 @@ class BluetoothState : public tinyfsm::Fsm { virtual void react(const events::Enable& ev){}; virtual void react(const events::Disable& ev) = 0; virtual void react(const events::ConnectTimedOut& ev){}; - virtual void react(const events::PreferredDeviceChanged& ev){}; + virtual void react(const events::PairedDeviceChanged& ev){}; virtual void react(const events::SourceChanged& ev){}; virtual void react(const events::DeviceDiscovered&); @@ -146,13 +186,11 @@ class BluetoothState : public tinyfsm::Fsm { static Scanner* sScanner_; static std::mutex sFsmMutex; - static std::map sDevices_; - static std::optional sPreferredDevice_; - - static std::optional sConnectingDevice_; + static std::map sDiscoveredDevices_; + static std::optional sPairedWith_; + static std::optional sConnectingTo_; static int sConnectAttemptsRemaining_; - static std::atomic sSource_; static std::function sEventHandler_; auto connect(const bluetooth::MacAndName&) -> bool; @@ -177,7 +215,7 @@ class Idle : public BluetoothState { void exit() override; void react(const events::Disable& ev) override; - void react(const events::PreferredDeviceChanged& ev) override; + void react(const events::PairedDeviceChanged& ev) override; void react(events::internal::Gap ev) override; @@ -189,7 +227,7 @@ class Connecting : public BluetoothState { void entry() override; void exit() override; - void react(const events::PreferredDeviceChanged& ev) override; + void react(const events::PairedDeviceChanged& ev) override; void react(const events::ConnectTimedOut& ev) override; void react(const events::Disable& ev) override; @@ -204,7 +242,7 @@ class Connected : public BluetoothState { void entry() override; void exit() override; - void react(const events::PreferredDeviceChanged& ev) override; + void react(const events::PairedDeviceChanged& ev) override; void react(const events::SourceChanged& ev) override; void react(const events::Disable& ev) override; diff --git a/src/drivers/include/drivers/bluetooth_types.hpp b/src/drivers/include/drivers/bluetooth_types.hpp index d2e55ee5..05caee47 100644 --- a/src/drivers/include/drivers/bluetooth_types.hpp +++ b/src/drivers/include/drivers/bluetooth_types.hpp @@ -27,9 +27,12 @@ struct Device { }; enum class SimpleEvent { - kKnownDevicesChanged, kConnectionStateChanged, - kPreferredDeviceChanged, + kPairedDeviceChanged, + kKnownDevicesChanged, + kDiscoveryChanged, + kDeviceDiscovered, + // Passthrough events kPlayPause, kStop, diff --git a/src/drivers/include/drivers/nvs.hpp b/src/drivers/include/drivers/nvs.hpp index 88dd5ae0..2bc77a31 100644 --- a/src/drivers/include/drivers/nvs.hpp +++ b/src/drivers/include/drivers/nvs.hpp @@ -96,6 +96,10 @@ class NvsStorage { auto BluetoothVolume(const bluetooth::mac_addr_t&) -> uint8_t; auto BluetoothVolume(const bluetooth::mac_addr_t&, uint8_t) -> void; + auto BluetoothNames() -> std::vector; + auto BluetoothName(const bluetooth::mac_addr_t&, std::optional) + -> void; + enum class Output : uint8_t { kHeadphones = 0, kBluetooth = 1, @@ -154,7 +158,10 @@ class NvsStorage { Setting amp_left_bias_; Setting input_mode_; Setting output_mode_; + Setting bt_preferred_; + Setting> bt_names_; + Setting db_auto_index_; util::LruCache<10, bluetooth::mac_addr_t, uint8_t> bt_volumes_; diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index 5c7d2218..c4d8dedc 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -26,6 +26,7 @@ static constexpr uint8_t kSchemaVersion = 1; static constexpr char kKeyVersion[] = "ver"; static constexpr char kKeyBluetoothPreferred[] = "bt_dev"; static constexpr char kKeyBluetoothVolumes[] = "bt_vols"; +static constexpr char kKeyBluetoothNames[] = "bt_names"; static constexpr char kKeyOutput[] = "out"; static constexpr char kKeyBrightness[] = "bright"; static constexpr char kKeyAmpMaxVolume[] = "hp_vol_max"; @@ -129,6 +130,44 @@ auto Setting::store(nvs_handle_t nvs, nvs_set_blob(nvs, name_, encoded.data(), encoded.size()); } +template <> +auto Setting>::load(nvs_handle_t nvs) + -> std::optional> { + auto raw = nvs_get_string(nvs, name_); + if (!raw) { + return {}; + } + auto [parsed, unused, err] = cppbor::parseWithViews( + reinterpret_cast(raw->data()), raw->size()); + if (parsed->type() != cppbor::MAP) { + return {}; + } + std::vector res; + for (const auto& i : *parsed->asMap()) { + auto mac = i.first->asViewBstr()->view(); + auto name = i.second->asViewTstr()->view(); + bluetooth::MacAndName entry{ + .mac = {}, + .name = {name.begin(), name.end()}, + }; + std::copy(mac.begin(), mac.end(), entry.mac.begin()); + res.push_back(entry); + } + return res; +} + +template <> +auto Setting>::store( + nvs_handle_t nvs, + std::vector v) -> void { + cppbor::Map cbor{}; + for (const auto& i : v) { + cbor.add(cppbor::Bstr{{i.mac.data(), i.mac.size()}}, cppbor::Tstr{i.name}); + } + auto encoded = cbor.encode(); + nvs_set_blob(nvs, name_, encoded.data(), encoded.size()); +} + template <> auto Setting::load(nvs_handle_t nvs) -> std::optional { @@ -208,6 +247,7 @@ NvsStorage::NvsStorage(nvs_handle_t handle) input_mode_(kKeyPrimaryInput), output_mode_(kKeyOutput), bt_preferred_(kKeyBluetoothPreferred), + bt_names_(kKeyBluetoothNames), db_auto_index_(kKeyDbAutoIndex), bt_volumes_(), bt_volumes_dirty_(false) {} @@ -232,6 +272,7 @@ auto NvsStorage::Read() -> void { input_mode_.read(handle_); output_mode_.read(handle_); bt_preferred_.read(handle_); + bt_names_.read(handle_); db_auto_index_.read(handle_); readBtVolumes(); } @@ -251,6 +292,7 @@ auto NvsStorage::Write() -> bool { input_mode_.write(handle_); output_mode_.write(handle_); bt_preferred_.write(handle_); + bt_names_.write(handle_); db_auto_index_.write(handle_); writeBtVolumes(); return nvs_commit(handle_) == ESP_OK; @@ -341,6 +383,47 @@ auto NvsStorage::BluetoothVolume(const bluetooth::mac_addr_t& mac, uint8_t vol) bt_volumes_.Put(mac, vol); } +auto NvsStorage::BluetoothNames() -> std::vector { + std::lock_guard lock{mutex_}; + return bt_names_.get().value_or(std::vector{}); +} + +auto NvsStorage::BluetoothName(const bluetooth::mac_addr_t& mac, + std::optional name) -> void { + std::lock_guard lock{mutex_}; + auto& val = bt_names_.get(); + if (!val) { + val.emplace(); + } + + bool mut = false; + bool found = false; + for (auto it = val->begin(); it != val->end(); it++) { + if (it->mac == mac) { + if (name) { + it->name = *name; + } else { + val->erase(it); + } + found = true; + mut = true; + break; + } + } + + if (!found && name) { + val->push_back(bluetooth::MacAndName{ + .mac = mac, + .name = *name, + }); + mut = true; + } + + if (mut) { + bt_names_.set(*val); + } +} + auto NvsStorage::OutputMode() -> Output { std::lock_guard lock{mutex_}; switch (output_mode_.get().value_or(0xFF)) { diff --git a/src/tangara/app_console/app_console.cpp b/src/tangara/app_console/app_console.cpp index f3593e1b..af9061fe 100644 --- a/src/tangara/app_console/app_console.cpp +++ b/src/tangara/app_console/app_console.cpp @@ -418,28 +418,21 @@ int CmdBtList(int argc, char** argv) { return 1; } - auto devices = AppConsole::sServices->bluetooth().KnownDevices(); + auto devices = AppConsole::sServices->bluetooth().knownDevices(); if (argc == 2) { int index = std::atoi(argv[1]); if (index < 0 || index >= devices.size()) { std::cout << "index out of range" << std::endl; return -1; } - drivers::bluetooth::MacAndName dev{ - .mac = devices[index].address, - .name = {devices[index].name.data(), devices[index].name.size()}, - }; - AppConsole::sServices->bluetooth().SetPreferredDevice(dev); + AppConsole::sServices->bluetooth().pairedDevice(devices[index]); } else { - std::cout << "mac\t\trssi\tname" << std::endl; + std::cout << "mac\t\tname" << std::endl; for (const auto& device : devices) { - for (size_t i = 0; i < device.address.size(); i++) { + for (size_t i = 0; i < device.mac.size(); i++) { std::cout << std::hex << std::setfill('0') << std::setw(2) - << static_cast(device.address[i]); + << static_cast(device.mac[i]); } - float perc = - (static_cast(device.signal_strength) + 127.0) / 256.0 * 100; - std::cout << "\t" << std::fixed << std::setprecision(0) << perc << "%"; std::cout << "\t" << device.name << std::endl; } } diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index 24f287ac..fbc38f97 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -222,7 +222,12 @@ void AudioState::react(const system_fsm::BluetoothEvent& ev) { auto simpleEvent = std::get(ev.event); switch (simpleEvent) { case SimpleEvent::kConnectionStateChanged: { - auto dev = sServices->bluetooth().ConnectedDevice(); + auto bt = sServices->bluetooth(); + if (bt.connectionState() != + drivers::Bluetooth::ConnectionState::kConnected) { + return; + } + auto dev = sServices->bluetooth().pairedDevice(); if (!dev) { return; } @@ -341,7 +346,7 @@ auto AudioState::commitVolume() -> void { if (mode == drivers::NvsStorage::Output::kHeadphones) { sServices->nvs().AmpCurrentVolume(vol); } else if (mode == drivers::NvsStorage::Output::kBluetooth) { - auto dev = sServices->bluetooth().ConnectedDevice(); + auto dev = sServices->bluetooth().pairedDevice(); if (!dev) { return; } @@ -372,7 +377,7 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { sOutput = sI2SOutput; } else { // Ensure Bluetooth gets enabled if it's the default sink. - sServices->bluetooth().Enable(); + sServices->bluetooth().enable(true); sOutput = sBtOutput; } sOutput->mode(IAudioOutput::Modes::kOnPaused); diff --git a/src/tangara/audio/bt_audio_output.cpp b/src/tangara/audio/bt_audio_output.cpp index 616a385f..336fc758 100644 --- a/src/tangara/audio/bt_audio_output.cpp +++ b/src/tangara/audio/bt_audio_output.cpp @@ -13,6 +13,7 @@ #include #include +#include "drivers/bluetooth.hpp" #include "esp_err.h" #include "esp_heap_caps.h" #include "freertos/portmacro.h" @@ -32,6 +33,8 @@ namespace audio { static constexpr uint16_t kVolumeRange = 60; +using ConnectionState = drivers::Bluetooth::ConnectionState; + BluetoothAudioOutput::BluetoothAudioOutput(drivers::Bluetooth& bt, drivers::PcmBuffer& buffer, tasks::WorkerPool& p) @@ -45,9 +48,9 @@ BluetoothAudioOutput::~BluetoothAudioOutput() {} auto BluetoothAudioOutput::changeMode(Modes mode) -> void { if (mode == Modes::kOnPlaying) { - bluetooth_.SetSource(&buffer_); + bluetooth_.source(&buffer_); } else { - bluetooth_.SetSource(nullptr); + bluetooth_.source(nullptr); } } @@ -60,7 +63,7 @@ auto BluetoothAudioOutput::SetVolume(uint16_t v) -> void { bg_worker_.Dispatch([&]() { float factor = pow(10, static_cast(kVolumeRange) * (volume_ - 100) / 100 / 20); - bluetooth_.SetVolumeFactor(factor); + bluetooth_.softVolume(factor); }); } @@ -95,7 +98,7 @@ auto BluetoothAudioOutput::SetVolumeDb(int_fast16_t val) -> bool { } auto BluetoothAudioOutput::AdjustVolumeUp() -> bool { - if (volume_ == 100 || !bluetooth_.IsConnected()) { + if (volume_ == 100) { return false; } volume_++; @@ -104,7 +107,7 @@ auto BluetoothAudioOutput::AdjustVolumeUp() -> bool { } auto BluetoothAudioOutput::AdjustVolumeDown() -> bool { - if (volume_ == 0 || !bluetooth_.IsConnected()) { + if (volume_ == 0) { return false; } volume_--; diff --git a/src/tangara/lua/property.cpp b/src/tangara/lua/property.cpp index 2b93809d..7b4f0b97 100644 --- a/src/tangara/lua/property.cpp +++ b/src/tangara/lua/property.cpp @@ -289,13 +289,14 @@ static void pushTrack(lua_State* L, const audio::TrackInfo& track) { lua_settable(L, -3); } -static void pushDevice(lua_State* L, const drivers::bluetooth::Device& dev) { +static void pushDevice(lua_State* L, + const drivers::bluetooth::MacAndName& dev) { lua_createtable(L, 0, 4); lua_pushliteral(L, "address"); auto* mac = reinterpret_cast( lua_newuserdata(L, sizeof(drivers::bluetooth::mac_addr_t))); - *mac = dev.address; + *mac = dev.mac; lua_rawset(L, -3); // What I just did there was perfectly safe. Look, I can prove it: @@ -308,14 +309,8 @@ static void pushDevice(lua_State* L, const drivers::bluetooth::Device& dev) { lua_pushlstring(L, dev.name.data(), dev.name.size()); lua_rawset(L, -3); - // FIXME: This field deserves a little more structure. - lua_pushliteral(L, "class"); - lua_pushinteger(L, dev.class_of_device); - lua_rawset(L, -3); - - lua_pushliteral(L, "signal_strength"); - lua_pushinteger(L, dev.signal_strength); - lua_rawset(L, -3); + // FIXME: Plumbing through device classes to here could be useful if we ever + // want to show cute little icons. } auto Property::pushValue(lua_State& s) -> int { @@ -332,10 +327,12 @@ auto Property::pushValue(lua_State& s) -> int { lua_pushstring(&s, arg.c_str()); } else if constexpr (std::is_same_v) { pushTrack(&s, arg); - } else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { pushDevice(&s, arg); } else if constexpr (std::is_same_v< - T, std::vector>) { + T, + std::vector>) { lua_createtable(&s, arg.size(), 0); size_t i = 1; for (const auto& dev : arg) { @@ -364,15 +361,10 @@ auto popRichType(lua_State* L) -> LuaValue { lua_pushliteral(L, "name"); lua_gettable(L, -2); - std::pmr::string name = lua_tostring(L, -1); + std::string name = lua_tostring(L, -1); lua_pop(L, 1); - return drivers::bluetooth::Device{ - .address = mac, - .name = name, - .class_of_device = 0, - .signal_strength = 0, - }; + return drivers::bluetooth::MacAndName{.mac = mac, .name = name}; } return std::monostate{}; diff --git a/src/tangara/lua/property.hpp b/src/tangara/lua/property.hpp index 9f925766..d45821bd 100644 --- a/src/tangara/lua/property.hpp +++ b/src/tangara/lua/property.hpp @@ -24,8 +24,8 @@ using LuaValue = std::variant>; + drivers::bluetooth::MacAndName, + std::vector>; using LuaFunction = std::function; diff --git a/src/tangara/system_fsm/booting.cpp b/src/tangara/system_fsm/booting.cpp index 9d505f81..86993767 100644 --- a/src/tangara/system_fsm/booting.cpp +++ b/src/tangara/system_fsm/booting.cpp @@ -104,8 +104,7 @@ auto Booting::entry() -> void { ESP_LOGI(kTag, "init bluetooth"); sServices->bluetooth(std::make_unique( - sServices->nvs(), sServices->bg_worker())); - sServices->bluetooth().SetEventHandler(bt_event_cb); + sServices->nvs(), sServices->bg_worker(), bt_event_cb)); BootComplete ev{.services = sServices}; events::Audio().Dispatch(ev); diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index 7c4147a3..c5ede83c 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -7,11 +7,13 @@ #include "ui/ui_fsm.hpp" #include +#include #include #include #include #include "FreeRTOSConfig.h" +#include "drivers/bluetooth.hpp" #include "lvgl.h" #include "core/lv_group.h" @@ -100,32 +102,57 @@ lua::Property UiState::sBluetoothEnabled{ if (!std::holds_alternative(val)) { return false; } + // Note we always write the OutputMode NVS change before actually + // modifying the peripheral. We do this because ESP-IDF's Bluetooth stack + // breaks in surprising ways when repeatedly initialised/uninitialised. if (std::get(val)) { sServices->nvs().OutputMode(drivers::NvsStorage::Output::kBluetooth); - sServices->bluetooth().Enable(); + sServices->bluetooth().enable(true); } else { sServices->nvs().OutputMode(drivers::NvsStorage::Output::kHeadphones); - sServices->bluetooth().Disable(); + sServices->bluetooth().enable(false); } events::Audio().Dispatch(audio::OutputModeChanged{}); return true; }}; +lua::Property UiState::sBluetoothConnecting{false}; lua::Property UiState::sBluetoothConnected{false}; + +lua::Property UiState::sBluetoothDiscovering{ + false, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } + // Note we always write the OutputMode NVS change before actually + // modifying the peripheral. We do this because ESP-IDF's Bluetooth stack + // breaks in surprising ways when repeatedly initialised/uninitialised. + if (std::get(val)) { + sServices->bluetooth().discoveryEnabled(true); + } else { + sServices->bluetooth().discoveryEnabled(false); + } + return true; + }}; + lua::Property UiState::sBluetoothPairedDevice{ std::monostate{}, [](const lua::LuaValue& val) { - if (std::holds_alternative(val)) { - auto dev = std::get(val); - sServices->bluetooth().SetPreferredDevice( - drivers::bluetooth::MacAndName{ - .mac = dev.address, - .name = {dev.name.data(), dev.name.size()}, - }); + if (std::holds_alternative(val)) { + auto dev = std::get(val); + sServices->bluetooth().pairedDevice(dev); + } else if (std::holds_alternative(val)) { + sServices->bluetooth().pairedDevice({}); + } else { + // Don't accept any other types. + return false; } - return false; + return true; }}; -lua::Property UiState::sBluetoothDevices{ - std::vector{}}; + +lua::Property UiState::sBluetoothKnownDevices{ + std::vector{}}; +lua::Property UiState::sBluetoothDiscoveredDevices{ + std::vector{}}; lua::Property UiState::sPlaybackPlaying{ false, [](const lua::LuaValue& val) { @@ -412,8 +439,13 @@ void UiState::react(const audio::VolumeLimitChanged& ev) { void UiState::react(const system_fsm::BluetoothEvent& ev) { using drivers::bluetooth::SimpleEvent; + using ConnectionState = drivers::Bluetooth::ConnectionState; + ConnectionState state; auto bt = sServices->bluetooth(); - auto dev = bt.ConnectedDevice(); + + std::optional dev; + std::vector devs; + if (std::holds_alternative(ev.event)) { switch (std::get(ev.event)) { case SimpleEvent::kPlayPause: @@ -438,30 +470,36 @@ void UiState::react(const system_fsm::BluetoothEvent& ev) { break; case SimpleEvent::kFastForward: break; - case SimpleEvent::kKnownDevicesChanged: - sBluetoothDevices.setDirect(bt.KnownDevices()); - break; case SimpleEvent::kConnectionStateChanged: - sBluetoothConnected.setDirect(bt.IsConnected()); + state = bt.connectionState(); + sBluetoothConnected.setDirect(state == ConnectionState::kConnected); + sBluetoothConnecting.setDirect(state == ConnectionState::kConnecting); + break; + case SimpleEvent::kPairedDeviceChanged: + dev = bt.pairedDevice(); if (dev) { - sBluetoothPairedDevice.setDirect(drivers::bluetooth::Device{ - .address = dev->mac, - .name = {dev->name.data(), dev->name.size()}, - .class_of_device = 0, - .signal_strength = 0, - }); + sBluetoothPairedDevice.setDirect(*dev); } else { sBluetoothPairedDevice.setDirect(std::monostate{}); } break; - case SimpleEvent::kPreferredDeviceChanged: + case SimpleEvent::kKnownDevicesChanged: + sBluetoothKnownDevices.setDirect(bt.knownDevices()); + break; + case SimpleEvent::kDiscoveryChanged: + sBluetoothDiscovering.setDirect(bt.discoveryEnabled()); + // Dump the old list of discovered devices when discovery is toggled. + sBluetoothDiscoveredDevices.setDirect(bt.discoveredDevices()); + break; + case SimpleEvent::kDeviceDiscovered: + sBluetoothDiscoveredDevices.setDirect(bt.discoveredDevices()); break; default: break; } } else if (std::holds_alternative( ev.event)) { - // Todo: Do something with this (ie, bt volume alert) + // TODO: Do something with this (ie, bt volume alert) ESP_LOGI( kTag, "Recieved volume changed event with new volume: %d", std::get(ev.event).new_vol); @@ -517,13 +555,16 @@ void Lua::entry() { {"battery_millivolts", &sBatteryMv}, {"plugged_in", &sBatteryCharging}, }); - registry.AddPropertyModule("bluetooth", - { - {"enabled", &sBluetoothEnabled}, - {"connected", &sBluetoothConnected}, - {"paired_device", &sBluetoothPairedDevice}, - {"devices", &sBluetoothDevices}, - }); + registry.AddPropertyModule( + "bluetooth", { + {"enabled", &sBluetoothEnabled}, + {"connected", &sBluetoothConnected}, + {"connecting", &sBluetoothConnecting}, + {"discovering", &sBluetoothDiscovering}, + {"paired_device", &sBluetoothPairedDevice}, + {"discovered_devices", &sBluetoothDiscoveredDevices}, + {"known_devices", &sBluetoothKnownDevices}, + }); registry.AddPropertyModule("playback", { {"playing", &sPlaybackPlaying}, {"track", &sPlaybackTrack}, @@ -601,9 +642,12 @@ void Lua::entry() { sDatabaseAutoUpdate.setDirect(sServices->nvs().DbAutoIndex()); auto bt = sServices->bluetooth(); - sBluetoothEnabled.setDirect(bt.IsEnabled()); - sBluetoothConnected.setDirect(bt.IsConnected()); - sBluetoothDevices.setDirect(bt.KnownDevices()); + sBluetoothEnabled.setDirect(bt.enabled()); + auto paired = bt.pairedDevice(); + if (paired) { + sBluetoothPairedDevice.setDirect(*paired); + } + sBluetoothKnownDevices.setDirect(bt.knownDevices()); if (sServices->sd() == drivers::SdState::kMounted) { sLua->RunScript("/sdcard/config.lua"); diff --git a/src/tangara/ui/ui_fsm.hpp b/src/tangara/ui/ui_fsm.hpp index 7e34db34..72688fa0 100644 --- a/src/tangara/ui/ui_fsm.hpp +++ b/src/tangara/ui/ui_fsm.hpp @@ -102,9 +102,12 @@ class UiState : public tinyfsm::Fsm { static lua::Property sBatteryCharging; static lua::Property sBluetoothEnabled; + static lua::Property sBluetoothConnecting; static lua::Property sBluetoothConnected; + static lua::Property sBluetoothDiscovering; static lua::Property sBluetoothPairedDevice; - static lua::Property sBluetoothDevices; + static lua::Property sBluetoothKnownDevices; + static lua::Property sBluetoothDiscoveredDevices; static lua::Property sPlaybackPlaying;