diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index 5bb4a5b4..a962a280 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -97,6 +97,15 @@ auto Bluetooth::SetPreferredDevice(const bluetooth::mac_addr_t& mac) -> void { bluetooth::events::PreferredDeviceChanged{}); } +auto Bluetooth::SetDeviceDiscovery(bool allowed) -> void { + if (allowed == bluetooth::BluetoothState::discovery()) { + return; + } + bluetooth::BluetoothState::discovery(allowed); + tinyfsm::FsmList::dispatch( + bluetooth::events::DiscoveryChanged{}); +} + auto Bluetooth::SetSource(StreamBufferHandle_t src) -> void { if (src == bluetooth::BluetoothState::source()) { return; @@ -122,12 +131,144 @@ auto DeviceName() -> std::pmr::string { namespace bluetooth { +static constexpr uint8_t kDiscoveryTimeSeconds = 5; +static constexpr uint8_t kDiscoveryMaxResults = 0; + +Scanner::Scanner() : enabled_(false), is_discovering_(false) {} + +auto Scanner::ScanContinuously() -> void { + if (enabled_) { + return; + } + ESP_LOGI(kTag, "beginning continuous scan"); + enabled_ = true; + if (enabled_ && !is_discovering_) { + ScanOnce(); + } +} + +auto Scanner::ScanOnce() -> void { + if (is_discovering_) { + return; + } + is_discovering_ = true; + ESP_LOGI(kTag, "scanning..."); + esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, + kDiscoveryTimeSeconds, kDiscoveryMaxResults); +} + +auto Scanner::StopScanning() -> void { + ESP_LOGI(kTag, "stopping scan"); + enabled_ = false; +} + +auto Scanner::StopScanningNow() -> void { + StopScanning(); + if (is_discovering_) { + ESP_LOGI(kTag, "cancelling scan"); + is_discovering_ = false; + esp_bt_gap_cancel_discovery(); + } +} + +auto Scanner::HandleGapEvent(const events::internal::Gap& ev) -> void { + switch (ev.type) { + case ESP_BT_GAP_DISC_RES_EVT: + if (ev.param != nullptr) { + // Handle device discovery even if we've been told to stop discovering. + HandleDeviceDiscovery(*ev.param); + } + break; + case ESP_BT_GAP_DISC_STATE_CHANGED_EVT: + if (ev.param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) { + ESP_LOGI(kTag, "discovery finished"); + if (enabled_) { + ESP_LOGI(kTag, "restarting discovery"); + esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, + kDiscoveryTimeSeconds, + kDiscoveryMaxResults); + } else { + is_discovering_ = false; + } + } + break; + case ESP_BT_GAP_MODE_CHG_EVT: + // todo: mode change. is this important? + ESP_LOGI(kTag, "GAP mode changed"); + break; + default: + ESP_LOGW(kTag, "unhandled GAP event: %u", ev.type); + } +} + +auto Scanner::HandleDeviceDiscovery(const esp_bt_gap_cb_param_t& param) + -> void { + Device device{}; + std::copy(std::begin(param.disc_res.bda), std::end(param.disc_res.bda), + device.address.begin()); + + // Discovery results come back to us as a grab-bag of different key/value + // pairs. Parse these into a more structured format first so that they're + // easier to work with. + uint8_t* eir = nullptr; + for (size_t i = 0; i < param.disc_res.num_prop; i++) { + esp_bt_gap_dev_prop_t& property = param.disc_res.prop[i]; + switch (property.type) { + case ESP_BT_GAP_DEV_PROP_BDNAME: + // Ignored -- we get the device name from the EIR field instead. + break; + case ESP_BT_GAP_DEV_PROP_COD: + device.class_of_device = *reinterpret_cast(property.val); + break; + case ESP_BT_GAP_DEV_PROP_RSSI: + device.signal_strength = *reinterpret_cast(property.val); + break; + case ESP_BT_GAP_DEV_PROP_EIR: + eir = reinterpret_cast(property.val); + break; + default: + ESP_LOGW(kTag, "unknown GAP param %u", property.type); + } + } + + // Ignore devices with missing or malformed data. + if (!esp_bt_gap_is_valid_cod(device.class_of_device) || eir == nullptr) { + return; + } + + // Note: ESP-IDF example code does additional filterering by class of device + // at this point. We don't! Per the Bluetooth spec; "No assumptions should be + // made about specific functionality or characteristics of any application + // based solely on the assignment of the Major or Minor device class." + + // Resolve the name of the device. + uint8_t* name; + uint8_t length; + name = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_CMPL_LOCAL_NAME, + &length); + if (!name) { + name = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_SHORT_LOCAL_NAME, + &length); + } + + if (!name) { + return; + } + + device.name = std::pmr::string{reinterpret_cast(name), + static_cast(length)}; + events::DeviceDiscovered ev{.device = device}; + tinyfsm::FsmList::dispatch(ev); +} + NvsStorage* BluetoothState::sStorage_; +Scanner* BluetoothState::sScanner_; std::mutex BluetoothState::sDevicesMutex_{}; std::map BluetoothState::sDevices_{}; std::optional BluetoothState::sPreferredDevice_{}; -mac_addr_t BluetoothState::sCurrentDevice_; +mac_addr_t BluetoothState::sCurrentDevice_{0}; +bool BluetoothState::sIsDiscoveryAllowed_{false}; std::atomic BluetoothState::sSource_; std::function BluetoothState::sEventHandler_; @@ -152,11 +293,21 @@ auto BluetoothState::preferred_device() -> std::optional { return sPreferredDevice_; } -auto BluetoothState::preferred_device(const mac_addr_t& addr) -> void { +auto BluetoothState::preferred_device(std::optional addr) -> void { std::lock_guard lock{sDevicesMutex_}; sPreferredDevice_ = addr; } +auto BluetoothState::discovery() -> bool { + std::lock_guard lock{sDevicesMutex_}; + return sIsDiscoveryAllowed_; +} + +auto BluetoothState::discovery(bool en) -> void { + std::lock_guard lock{sDevicesMutex_}; + sIsDiscoveryAllowed_ = en; +} + auto BluetoothState::source() -> StreamBufferHandle_t { std::lock_guard lock{sDevicesMutex_}; return sSource_.load(); @@ -172,21 +323,67 @@ auto BluetoothState::event_handler(std::function cb) -> void { sEventHandler_ = cb; } +auto BluetoothState::react(const events::DeviceDiscovered& ev) -> void { + ESP_LOGI(kTag, "discovered device %s", ev.device.name.c_str()); + bool is_preferred = false; + { + std::lock_guard lock{sDevicesMutex_}; + sDevices_[ev.device.address] = ev.device; + + if (ev.device.address == sPreferredDevice_) { + sCurrentDevice_ = ev.device.address; + is_preferred = true; + } + + if (sEventHandler_) { + std::invoke(sEventHandler_, Event::kKnownDevicesChanged); + } + } + + if (is_preferred && is_in_state()) { + ESP_LOGI(kTag, "new device is preferred. connecting."); + transit(); + } +} + +auto BluetoothState::react(const events::DiscoveryChanged& ev) -> void { + if (sIsDiscoveryAllowed_) { + sScanner_->ScanContinuously(); + } else { + sScanner_->StopScanning(); + } +} + static bool sIsFirstEntry = true; void Disabled::entry() { if (sIsFirstEntry) { // We only use BT Classic, to claw back ~60KiB from the BLE firmware. esp_bt_controller_mem_release(ESP_BT_MODE_BLE); + sScanner_ = new Scanner(); sIsFirstEntry = false; return; } + sScanner_->StopScanningNow(); + esp_bluedroid_disable(); esp_bluedroid_deinit(); esp_bt_controller_disable(); } +void Disabled::exit() { + if (sIsDiscoveryAllowed_) { + ESP_LOGI(kTag, "bt enabled, beginning discovery"); + sScanner_->ScanContinuously(); + } else if (sPreferredDevice_) { + ESP_LOGI(kTag, "bt enabled, checking for preferred device"); + sScanner_->ScanOnce(); + } else { + ESP_LOGI(kTag, "bt enabled, but not scanning"); + } +} + void Disabled::react(const events::Enable&) { esp_bt_controller_config_t config = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); if (esp_bt_controller_init(&config) != ESP_OK) { @@ -237,103 +434,18 @@ void Disabled::react(const events::Enable&) { // Don't let anyone interact with us before we're ready. esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE); - transit(); + transit(); } -static constexpr uint8_t kDiscoveryTimeSeconds = 10; -static constexpr uint8_t kDiscoveryMaxResults = 0; - -void Scanning::entry() { - ESP_LOGI(kTag, "scanning for devices"); - esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, - kDiscoveryTimeSeconds, kDiscoveryMaxResults); -} - -void Scanning::exit() { - esp_bt_gap_cancel_discovery(); +void Idle::entry() { + ESP_LOGI(kTag, "bt is idle"); } -void Scanning::react(const events::Disable& ev) { +void Idle::react(const events::Disable& ev) { transit(); } -auto Scanning::OnDeviceDiscovered(esp_bt_gap_cb_param_t* param) -> void { - Device device{}; - std::copy(std::begin(param->disc_res.bda), std::end(param->disc_res.bda), - device.address.begin()); - - // Discovery results come back to us as a grab-bag of different key/value - // pairs. Parse these into a more structured format first so that they're - // easier to work with. - uint8_t* eir = nullptr; - for (size_t i = 0; i < param->disc_res.num_prop; i++) { - esp_bt_gap_dev_prop_t& property = param->disc_res.prop[i]; - switch (property.type) { - case ESP_BT_GAP_DEV_PROP_BDNAME: - // Ignored -- we get the device name from the EIR field instead. - break; - case ESP_BT_GAP_DEV_PROP_COD: - device.class_of_device = *reinterpret_cast(property.val); - break; - case ESP_BT_GAP_DEV_PROP_RSSI: - device.signal_strength = *reinterpret_cast(property.val); - break; - case ESP_BT_GAP_DEV_PROP_EIR: - eir = reinterpret_cast(property.val); - break; - default: - ESP_LOGW(kTag, "unknown GAP param %u", property.type); - } - } - - // Ignore devices with missing or malformed data. - if (!esp_bt_gap_is_valid_cod(device.class_of_device) || eir == nullptr) { - return; - } - - // Note: ESP-IDF example code does additional filterering by class of device - // at this point. We don't! Per the Bluetooth spec; "No assumptions should be - // made about specific functionality or characteristics of any application - // based solely on the assignment of the Major or Minor device class." - - // Resolve the name of the device. - uint8_t* name; - uint8_t length; - name = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_CMPL_LOCAL_NAME, - &length); - if (!name) { - name = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_SHORT_LOCAL_NAME, - &length); - } - - if (!name) { - return; - } - - device.name = std::pmr::string{reinterpret_cast(name), - static_cast(length)}; - - bool is_preferred = false; - { - std::lock_guard lock{sDevicesMutex_}; - sDevices_[device.address] = device; - - if (device.address == sPreferredDevice_) { - sCurrentDevice_ = device.address; - is_preferred = true; - } - - if (sEventHandler_) { - std::invoke(sEventHandler_, Event::kKnownDevicesChanged); - } - } - - if (is_preferred) { - transit(); - } -} - -void Scanning::react(const events::PreferredDeviceChanged& ev) { +void Idle::react(const events::PreferredDeviceChanged& ev) { bool is_discovered = false; { std::lock_guard lock{sDevicesMutex_}; @@ -342,28 +454,13 @@ void Scanning::react(const events::PreferredDeviceChanged& ev) { } } if (is_discovered) { + ESP_LOGI(kTag, "selected known device"); transit(); } } -void Scanning::react(const events::internal::Gap& ev) { - switch (ev.type) { - case ESP_BT_GAP_DISC_RES_EVT: - OnDeviceDiscovered(ev.param); - break; - case ESP_BT_GAP_DISC_STATE_CHANGED_EVT: - if (ev.param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) { - esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, - kDiscoveryTimeSeconds, kDiscoveryMaxResults); - } - break; - case ESP_BT_GAP_MODE_CHG_EVT: - // todo: mode change. is this important? - ESP_LOGI(kTag, "GAP mode changed"); - break; - default: - ESP_LOGW(kTag, "unhandled GAP event: %u", ev.type); - } +void Idle::react(const events::internal::Gap& ev) { + sScanner_->HandleGapEvent(ev); } void Connecting::entry() { @@ -375,6 +472,7 @@ void Connecting::exit() {} void Connecting::react(const events::Disable& ev) { // TODO: disconnect gracefully + transit(); } void Connecting::react(const events::PreferredDeviceChanged& ev) { @@ -382,12 +480,13 @@ void Connecting::react(const events::PreferredDeviceChanged& ev) { } void Connecting::react(const events::internal::Gap& ev) { + sScanner_->HandleGapEvent(ev); switch (ev.type) { case ESP_BT_GAP_AUTH_CMPL_EVT: if (ev.param->auth_cmpl.stat != ESP_BT_STATUS_SUCCESS) { ESP_LOGE(kTag, "auth failed"); sPreferredDevice_ = {}; - transit(); + transit(); } break; case ESP_BT_GAP_ACL_CONN_CMPL_STAT_EVT: @@ -397,22 +496,22 @@ void Connecting::react(const events::internal::Gap& ev) { case ESP_BT_GAP_PIN_REQ_EVT: ESP_LOGW(kTag, "device needs a pin to connect"); sPreferredDevice_ = {}; - transit(); + transit(); break; case ESP_BT_GAP_CFM_REQ_EVT: ESP_LOGW(kTag, "user needs to do cfm. idk man."); sPreferredDevice_ = {}; - transit(); + transit(); break; case ESP_BT_GAP_KEY_NOTIF_EVT: ESP_LOGW(kTag, "the device is telling us a password??"); sPreferredDevice_ = {}; - transit(); + transit(); break; case ESP_BT_GAP_KEY_REQ_EVT: ESP_LOGW(kTag, "the device wants a password!"); sPreferredDevice_ = {}; - transit(); + transit(); break; case ESP_BT_GAP_MODE_CHG_EVT: ESP_LOGI(kTag, "GAP mode changed"); @@ -464,6 +563,7 @@ void Connected::exit() { void Connected::react(const events::Disable& ev) { // TODO: disconnect gracefully + transit(); } void Connected::react(const events::PreferredDeviceChanged& ev) { @@ -481,6 +581,7 @@ void Connected::react(const events::SourceChanged& ev) { } void Connected::react(const events::internal::Gap& ev) { + sScanner_->HandleGapEvent(ev); switch (ev.type) { case ESP_BT_GAP_MODE_CHG_EVT: // todo: is this important? diff --git a/src/drivers/include/bluetooth.hpp b/src/drivers/include/bluetooth.hpp index 1489b790..f3623fb8 100644 --- a/src/drivers/include/bluetooth.hpp +++ b/src/drivers/include/bluetooth.hpp @@ -32,6 +32,12 @@ class Bluetooth { auto Disable() -> void; auto IsEnabled() -> bool; + /* + * Sets whether or not the bluetooth stack is allowed to actively scan for + * new devices. + */ + auto SetDeviceDiscovery(bool) -> void; + auto KnownDevices() -> std::vector; auto SetPreferredDevice(const bluetooth::mac_addr_t& mac) -> void; @@ -47,6 +53,10 @@ struct Disable : public tinyfsm::Event {}; struct PreferredDeviceChanged : public tinyfsm::Event {}; struct SourceChanged : public tinyfsm::Event {}; +struct DiscoveryChanged : public tinyfsm::Event {}; +struct DeviceDiscovered : public tinyfsm::Event { + const Device& device; +}; namespace internal { struct Gap : public tinyfsm::Event { @@ -64,13 +74,37 @@ struct Avrc : public tinyfsm::Event { } // namespace internal } // namespace events +/* + * Utility for managing scanning, independent of the current connection state. + */ +class Scanner { + public: + Scanner(); + + auto ScanContinuously() -> void; + auto ScanOnce() -> void; + auto StopScanning() -> void; + auto StopScanningNow() -> void; + + auto HandleGapEvent(const events::internal::Gap&) -> void; + + private: + bool enabled_; + bool is_discovering_; + + auto HandleDeviceDiscovery(const esp_bt_gap_cb_param_t& param) -> void; +}; + class BluetoothState : public tinyfsm::Fsm { public: static auto Init(NvsStorage& storage) -> void; static auto devices() -> std::vector; static auto preferred_device() -> std::optional; - static auto preferred_device(const mac_addr_t&) -> void; + static auto preferred_device(std::optional) -> void; + + static auto discovery() -> bool; + static auto discovery(bool) -> void; static auto source() -> StreamBufferHandle_t; static auto source(StreamBufferHandle_t) -> void; @@ -86,6 +120,9 @@ class BluetoothState : public tinyfsm::Fsm { virtual void react(const events::Disable& ev) = 0; virtual void react(const events::PreferredDeviceChanged& ev){}; virtual void react(const events::SourceChanged& ev){}; + virtual void react(const events::DiscoveryChanged&); + + virtual void react(const events::DeviceDiscovered&); virtual void react(const events::internal::Gap& ev) = 0; virtual void react(const events::internal::A2dp& ev){}; @@ -93,11 +130,13 @@ class BluetoothState : public tinyfsm::Fsm { protected: static NvsStorage* sStorage_; + static Scanner* sScanner_; static std::mutex sDevicesMutex_; static std::map sDevices_; static std::optional sPreferredDevice_; static mac_addr_t sCurrentDevice_; + static bool sIsDiscoveryAllowed_; static std::atomic sSource_; static std::function sEventHandler_; @@ -106,19 +145,21 @@ class BluetoothState : public tinyfsm::Fsm { class Disabled : public BluetoothState { public: void entry() override; + void exit() override; void react(const events::Enable& ev) override; void react(const events::Disable& ev) override{}; void react(const events::internal::Gap& ev) override {} void react(const events::internal::A2dp& ev) override {} + void react(const events::DiscoveryChanged& ev) override{}; + using BluetoothState::react; }; -class Scanning : public BluetoothState { +class Idle : public BluetoothState { public: void entry() override; - void exit() override; void react(const events::Disable& ev) override; void react(const events::PreferredDeviceChanged& ev) override; @@ -126,9 +167,6 @@ class Scanning : public BluetoothState { void react(const events::internal::Gap& ev) override; using BluetoothState::react; - - private: - auto OnDeviceDiscovered(esp_bt_gap_cb_param_t*) -> void; }; class Connecting : public BluetoothState { diff --git a/src/ui/include/screen_settings.hpp b/src/ui/include/screen_settings.hpp index ae0b6aed..033cb7fa 100644 --- a/src/ui/include/screen_settings.hpp +++ b/src/ui/include/screen_settings.hpp @@ -33,10 +33,11 @@ class Settings : public MenuScreen { class Bluetooth : public MenuScreen { public: Bluetooth(models::TopBar&, drivers::Bluetooth& bt, drivers::NvsStorage& nvs); + ~Bluetooth(); auto ChangeEnabledState(bool enabled) -> void; auto RefreshDevicesList() -> void; - auto OnDeviceSelected(size_t index) -> void; + auto OnDeviceSelected(ssize_t index) -> void; private: auto RemoveAllDevices() -> void; diff --git a/src/ui/screen_settings.cpp b/src/ui/screen_settings.cpp index 021b5bfc..a3a24eeb 100644 --- a/src/ui/screen_settings.cpp +++ b/src/ui/screen_settings.cpp @@ -119,6 +119,11 @@ static auto select_device_cb(lv_event_t* ev) { instance->OnDeviceSelected(lv_obj_get_index(ev->target)); } +static auto remove_preferred_cb(lv_event_t* ev) { + Bluetooth* instance = reinterpret_cast(ev->user_data); + instance->OnDeviceSelected(-1); +} + Bluetooth::Bluetooth(models::TopBar& bar, drivers::Bluetooth& bt, drivers::NvsStorage& nvs) @@ -141,6 +146,11 @@ Bluetooth::Bluetooth(models::TopBar& bar, devices_list_ = lv_list_create(content_); RefreshDevicesList(); + bt_.SetDeviceDiscovery(true); +} + +Bluetooth::~Bluetooth() { + bt_.SetDeviceDiscovery(false); } auto Bluetooth::ChangeEnabledState(bool enabled) -> void { @@ -228,6 +238,11 @@ auto Bluetooth::RemoveAllDevices() -> void { auto Bluetooth::AddPreferredDevice(const drivers::bluetooth::Device& dev) -> void { preferred_device_ = lv_list_add_btn(devices_list_, NULL, dev.name.c_str()); + lv_obj_t* remove = lv_btn_create(preferred_device_); + lv_obj_t* remove_icon = lv_label_create(remove); + lv_label_set_text(remove_icon, "x"); + lv_group_add_obj(group_, remove); + macs_in_list_.push_back(dev.address); } @@ -238,7 +253,16 @@ auto Bluetooth::AddDevice(const drivers::bluetooth::Device& dev) -> void { macs_in_list_.push_back(dev.address); } -auto Bluetooth::OnDeviceSelected(size_t index) -> void { +auto Bluetooth::OnDeviceSelected(ssize_t index) -> void { + if (index == -1) { + events::System().RunOnTask([=]() { + nvs_.PreferredBluetoothDevice({}); + bt_.SetPreferredDevice({}); + }); + RefreshDevicesList(); + return; + } + // Tell the bluetooth driver that our preference changed. auto it = macs_in_list_.begin(); std::advance(it, index);