summaryrefslogtreecommitdiff
path: root/src/drivers/bluetooth.cpp
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2023-08-15 13:53:30 +1000
committerjacqueline <me@jacqueline.id.au>2023-08-15 13:53:30 +1000
commitd6b83fcf4a1a3039c06e0b1d1a1f7e2af2351efb (patch)
tree03c6a534931736a2755aacef86e271ecc5b8e87c /src/drivers/bluetooth.cpp
parent205e3053506191fab69d01e7523e733dccc09d77 (diff)
downloadtangara-fw-d6b83fcf4a1a3039c06e0b1d1a1f7e2af2351efb.tar.gz
Flesh out basic bluetooth support
No ui yet, and performance isn't great. It kinda works though!!
Diffstat (limited to 'src/drivers/bluetooth.cpp')
-rw-r--r--src/drivers/bluetooth.cpp286
1 files changed, 269 insertions, 17 deletions
diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp
index f9ab4e95..79999b2c 100644
--- a/src/drivers/bluetooth.cpp
+++ b/src/drivers/bluetooth.cpp
@@ -2,25 +2,31 @@
#include <stdint.h>
+#include <algorithm>
#include <atomic>
+#include <mutex>
#include <ostream>
#include <sstream>
#include "esp_a2dp_api.h"
#include "esp_avrc_api.h"
#include "esp_bt.h"
+#include "esp_bt_defs.h"
#include "esp_bt_device.h"
#include "esp_bt_main.h"
#include "esp_gap_bt_api.h"
#include "esp_log.h"
#include "esp_mac.h"
+#include "esp_wifi.h"
+#include "esp_wifi_types.h"
+#include "freertos/portmacro.h"
#include "tinyfsm/include/tinyfsm.hpp"
namespace drivers {
static constexpr char kTag[] = "bluetooth";
-static std::atomic<StreamBufferHandle_t> sStream;
+static StreamBufferHandle_t sStream = nullptr;
auto gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t* param) -> void {
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
@@ -42,7 +48,7 @@ auto a2dp_data_cb(uint8_t* buf, int32_t buf_size) -> int32_t {
if (buf == nullptr || buf_size <= 0) {
return 0;
}
- StreamBufferHandle_t stream = sStream.load();
+ StreamBufferHandle_t stream = sStream;
if (stream == nullptr) {
return 0;
}
@@ -65,6 +71,32 @@ auto Bluetooth::Disable() -> void {
bluetooth::events::Disable{});
}
+auto Bluetooth::KnownDevices() -> std::vector<bluetooth::Device> {
+ std::vector<bluetooth::Device> 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;
+}
+
+auto Bluetooth::SetPreferredDevice(const bluetooth::mac_addr_t& mac) -> void {
+ if (mac == bluetooth::BluetoothState::preferred_device()) {
+ return;
+ }
+ bluetooth::BluetoothState::preferred_device(mac);
+ tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
+ bluetooth::events::PreferredDeviceChanged{});
+}
+
+auto Bluetooth::SetSource(StreamBufferHandle_t src) -> void {
+ if (src == bluetooth::BluetoothState::source()) {
+ return;
+ }
+ bluetooth::BluetoothState::source(src);
+ tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
+ bluetooth::events::SourceChanged{});
+}
+
auto DeviceName() -> std::string {
uint8_t mac[8]{0};
esp_efuse_mac_get_default(mac);
@@ -75,6 +107,42 @@ auto DeviceName() -> std::string {
namespace bluetooth {
+std::mutex BluetoothState::sDevicesMutex_;
+std::map<mac_addr_t, Device> BluetoothState::sDevices_;
+std::optional<mac_addr_t> BluetoothState::sPreferredDevice_;
+mac_addr_t BluetoothState::sCurrentDevice_;
+
+std::atomic<StreamBufferHandle_t> BluetoothState::sSource_;
+
+auto BluetoothState::devices() -> std::vector<Device> {
+ std::lock_guard lock{sDevicesMutex_};
+ std::vector<Device> out;
+ for (const auto& device : sDevices_) {
+ out.push_back(device.second);
+ }
+ return out;
+}
+
+auto BluetoothState::preferred_device() -> std::optional<mac_addr_t> {
+ std::lock_guard lock{sDevicesMutex_};
+ return sPreferredDevice_;
+}
+
+auto BluetoothState::preferred_device(const mac_addr_t& addr) -> void {
+ std::lock_guard lock{sDevicesMutex_};
+ sPreferredDevice_ = addr;
+}
+
+auto BluetoothState::source() -> StreamBufferHandle_t {
+ std::lock_guard lock{sDevicesMutex_};
+ return sSource_.load();
+}
+
+auto BluetoothState::source(StreamBufferHandle_t src) -> void {
+ std::lock_guard lock{sDevicesMutex_};
+ sSource_.store(src);
+}
+
static bool sIsFirstEntry = true;
void Disabled::entry() {
@@ -126,8 +194,8 @@ void Disabled::react(const events::Enable&) {
esp_bt_gap_register_callback(gap_cb);
// Initialise AVRCP. This handles playback controls; play/pause/volume/etc.
- // esp_avrc_ct_init();
- // esp_avrc_ct_register_callback(avrcp_cb);
+ esp_avrc_ct_init();
+ esp_avrc_ct_register_callback(avrcp_cb);
// Initialise A2DP. This handles streaming audio. Currently ESP-IDF's SBC
// encoder only supports 2 channels of interleaved 16 bit samples, at
@@ -156,8 +224,93 @@ void Scanning::exit() {
esp_bt_gap_cancel_discovery();
}
-auto OnDeviceDiscovered(esp_bt_gap_cb_param_t* param) -> void {
- ESP_LOGI(kTag, "device discovered");
+void Scanning::react(const events::Disable& ev) {
+ transit<Disabled>();
+}
+
+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<uint32_t*>(property.val);
+ break;
+ case ESP_BT_GAP_DEV_PROP_RSSI:
+ device.signal_strength = *reinterpret_cast<int8_t*>(property.val);
+ break;
+ case ESP_BT_GAP_DEV_PROP_EIR:
+ eir = reinterpret_cast<uint8_t*>(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::string{reinterpret_cast<char*>(name), static_cast<size_t>(length)};
+
+ bool is_preferred = false;
+ {
+ std::lock_guard<std::mutex> lock{sDevicesMutex_};
+ sDevices_[device.address] = device;
+
+ if (device.address == sPreferredDevice_) {
+ sCurrentDevice_ = device.address;
+ is_preferred = true;
+ }
+ }
+
+ if (is_preferred) {
+ transit<Connecting>();
+ }
+}
+
+void Scanning::react(const events::PreferredDeviceChanged& ev) {
+ bool is_discovered = false;
+ {
+ std::lock_guard<std::mutex> lock{sDevicesMutex_};
+ if (sPreferredDevice_ && sDevices_.contains(sPreferredDevice_.value())) {
+ is_discovered = true;
+ }
+ }
+ if (is_discovered) {
+ transit<Connecting>();
+ }
}
void Scanning::react(const events::internal::Gap& ev) {
@@ -167,7 +320,6 @@ void Scanning::react(const events::internal::Gap& ev) {
break;
case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
if (ev.param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) {
- ESP_LOGI(kTag, "still scanning");
esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY,
kDiscoveryTimeSeconds, kDiscoveryMaxResults);
}
@@ -183,30 +335,63 @@ void Scanning::react(const events::internal::Gap& ev) {
void Connecting::entry() {
ESP_LOGI(kTag, "connecting to device");
- esp_a2d_source_connect(nullptr);
+ esp_a2d_source_connect(sPreferredDevice_.value().data());
}
void Connecting::exit() {}
+void Connecting::react(const events::Disable& ev) {
+ // TODO: disconnect gracefully
+}
+
+void Connecting::react(const events::PreferredDeviceChanged& ev) {
+ // TODO. Cancel out and start again.
+}
+
void Connecting::react(const events::internal::Gap& ev) {
switch (ev.type) {
case ESP_BT_GAP_AUTH_CMPL_EVT:
- // todo: auth completed. check if we succeeded.
+ if (ev.param->auth_cmpl.stat != ESP_BT_STATUS_SUCCESS) {
+ ESP_LOGE(kTag, "auth failed");
+ sPreferredDevice_ = {};
+ transit<Scanning>();
+ }
+ break;
+ case ESP_BT_GAP_ACL_CONN_CMPL_STAT_EVT:
+ // ACL connection complete. We're now ready to send data to this
+ // device(?)
break;
case ESP_BT_GAP_PIN_REQ_EVT:
- // todo: device needs a pin to connect.
+ ESP_LOGW(kTag, "device needs a pin to connect");
+ sPreferredDevice_ = {};
+ transit<Scanning>();
break;
case ESP_BT_GAP_CFM_REQ_EVT:
- // todo: device needs user to click okay.
+ ESP_LOGW(kTag, "user needs to do cfm. idk man.");
+ sPreferredDevice_ = {};
+ transit<Scanning>();
break;
case ESP_BT_GAP_KEY_NOTIF_EVT:
- // todo: device is telling us a password?
+ ESP_LOGW(kTag, "the device is telling us a password??");
+ sPreferredDevice_ = {};
+ transit<Scanning>();
break;
case ESP_BT_GAP_KEY_REQ_EVT:
- // todo: device needs a password
+ ESP_LOGW(kTag, "the device wants a password!");
+ sPreferredDevice_ = {};
+ transit<Scanning>();
break;
case ESP_BT_GAP_MODE_CHG_EVT:
- // todo: mode change. is this important?
+ ESP_LOGI(kTag, "GAP mode changed");
+ break;
+ case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
+ // Discovery state changed. Probably because we stopped scanning, but
+ // either way this isn't actionable or useful.
+ break;
+ case ESP_BT_GAP_DISC_RES_EVT:
+ // New device discovered. We could actually process this so that the
+ // device list remains fresh whilst we're connecting, but for now just
+ // ignore it.
break;
default:
ESP_LOGW(kTag, "unhandled GAP event: %u", ev.type);
@@ -216,21 +401,78 @@ void Connecting::react(const events::internal::Gap& ev) {
void Connecting::react(const events::internal::A2dp& ev) {
switch (ev.type) {
case ESP_A2D_CONNECTION_STATE_EVT:
- // todo: connection state changed. we might be connected!
+ if (ev.param->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTED) {
+ ESP_LOGI(kTag, "connected okay!");
+ transit<Connected>();
+ }
+ break;
+ case ESP_A2D_REPORT_SNK_DELAY_VALUE_EVT:
+ // The sink is telling us how much of a delay to expect with playback.
+ // We don't care about this yet.
break;
default:
ESP_LOGW(kTag, "unhandled A2DP event: %u", ev.type);
}
}
+void Connected::entry() {
+ ESP_LOGI(kTag, "entering connected state");
+ // TODO: if we already have a source, immediately start playing
+}
+
+void Connected::exit() {
+ ESP_LOGI(kTag, "exiting connected state");
+}
+
+void Connected::react(const events::Disable& ev) {
+ // TODO: disconnect gracefully
+}
+
+void Connected::react(const events::PreferredDeviceChanged& ev) {
+ // TODO: disconnect, move to connecting? or scanning?
+}
+
+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);
+ } else {
+ esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_STOP);
+ }
+}
+
+void Connected::react(const events::internal::Gap& ev) {
+ switch (ev.type) {
+ case ESP_BT_GAP_MODE_CHG_EVT:
+ // todo: is this important?
+ ESP_LOGI(kTag, "GAP mode changed");
+ break;
+ default:
+ ESP_LOGW(kTag, "unhandled GAP event: %u", ev.type);
+ }
+}
+
void Connected::react(const events::internal::A2dp& ev) {
switch (ev.type) {
case ESP_A2D_CONNECTION_STATE_EVT:
- // todo: connection state changed. we might have dropped
+ if (ev.param->conn_stat.state != ESP_A2D_CONNECTION_STATE_CONNECTED &&
+ ev.param->conn_stat.state != ESP_A2D_CONNECTION_STATE_DISCONNECTING) {
+ ESP_LOGE(kTag, "a2dp connection dropped :(");
+ transit<Connecting>();
+ }
break;
case ESP_A2D_AUDIO_STATE_EVT:
// todo: audio state changed. who knows, dude.
break;
+ case ESP_A2D_MEDIA_CTRL_ACK_EVT:
+ // Sink is responding to our media control request.
+ if (ev.param->media_ctrl_stat.cmd == ESP_A2D_MEDIA_CTRL_CHECK_SRC_RDY) {
+ // TODO: check if success
+ ESP_LOGI(kTag, "starting playback");
+ esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_START);
+ }
+ break;
default:
ESP_LOGW(kTag, "unhandled A2DP event: %u", ev.type);
}
@@ -239,7 +481,17 @@ void Connected::react(const events::internal::A2dp& ev) {
void Connected::react(const events::internal::Avrc& ev) {
switch (ev.type) {
case ESP_AVRC_CT_CONNECTION_STATE_EVT:
- // todo: avrc connected. send our capabilities.
+ if (ev.param->conn_stat.connected) {
+ // TODO: tell the target about our capabilities
+ }
+ // Don't worry about disconnect events; if there's a serious problem then
+ // the entire bluetooth connection will drop out, which is handled
+ // elsewhere.
+ break;
+ 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.
+ break;
default:
ESP_LOGW(kTag, "unhandled AVRC event: %u", ev.type);
}