From a3eb2dd9dc2399ce9c22cd3b07f482f080976440 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 11 Jul 2024 15:11:28 +1000 Subject: WIP improve bluetooth api and settings screen --- src/drivers/bluetooth.cpp | 338 ++++++++++++++---------- src/drivers/include/drivers/bluetooth.hpp | 116 +++++--- src/drivers/include/drivers/bluetooth_types.hpp | 7 +- src/drivers/include/drivers/nvs.hpp | 7 + src/drivers/nvs.cpp | 83 ++++++ 5 files changed, 368 insertions(+), 183 deletions(-) (limited to 'src/drivers') 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)) { -- cgit v1.2.3