From c6f2b523312320f1ab2f6b2396c507d1e76d4a5b Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 6 Nov 2023 08:19:28 +1100 Subject: Improve bt scanning responsiveness --- src/drivers/bluetooth.cpp | 331 ++++++++++++++++++++++++++++++---------------- 1 file changed, 216 insertions(+), 115 deletions(-) (limited to 'src/drivers/bluetooth.cpp') 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? -- cgit v1.2.3