From d6b83fcf4a1a3039c06e0b1d1a1f7e2af2351efb Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 15 Aug 2023 13:53:30 +1000 Subject: [PATCH] Flesh out basic bluetooth support No ui yet, and performance isn't great. It kinda works though!! --- src/app_console/app_console.cpp | 43 ++++ src/app_console/include/app_console.hpp | 2 + src/audio/CMakeLists.txt | 2 +- src/audio/audio_fsm.cpp | 8 + src/audio/bt_audio_output.cpp | 79 +++++++ src/audio/i2s_audio_output.cpp | 7 +- src/audio/include/audio_fsm.hpp | 3 + src/audio/include/audio_sink.hpp | 8 +- src/audio/include/bt_audio_output.hpp | 49 ++++ src/drivers/bluetooth.cpp | 286 ++++++++++++++++++++++-- src/drivers/include/bluetooth.hpp | 63 +++++- src/system_fsm/booting.cpp | 7 +- src/system_fsm/include/system_fsm.hpp | 2 + src/system_fsm/system_fsm.cpp | 1 + src/tasks/tasks.cpp | 5 +- 15 files changed, 534 insertions(+), 31 deletions(-) create mode 100644 src/audio/bt_audio_output.cpp create mode 100644 src/audio/include/bt_audio_output.hpp diff --git a/src/app_console/app_console.cpp b/src/app_console/app_console.cpp index 30b7d2dc..7804fd34 100644 --- a/src/app_console/app_console.cpp +++ b/src/app_console/app_console.cpp @@ -37,6 +37,7 @@ namespace console { std::weak_ptr AppConsole::sDatabase; audio::TrackQueue* AppConsole::sTrackQueue; +drivers::Bluetooth* AppConsole::sBluetooth; int CmdListDir(int argc, char** argv) { auto lock = AppConsole::sDatabase.lock(); @@ -439,6 +440,47 @@ void RegisterTaskStates() { esp_console_cmd_register(&cmd); } +int CmdBtList(int argc, char** argv) { + static const std::string usage = "usage: bt_list "; + if (argc > 2) { + std::cout << usage << std::endl; + return 1; + } + + auto devices = AppConsole::sBluetooth->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; + } + AppConsole::sBluetooth->SetPreferredDevice(devices[index].address); + } else { + std::cout << "mac\t\trssi\tname" << std::endl; + for (const auto& device : devices) { + for (size_t i = 0; i < device.address.size(); i++) { + std::cout << std::hex << std::setfill('0') << std::setw(2) + << static_cast(device.address[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; + } + } + + return 0; +} + +void RegisterBtList() { + esp_console_cmd_t cmd{.command = "bt_list", + .help = "lists and connects to bluetooth devices", + .hint = NULL, + .func = &CmdBtList, + .argtable = NULL}; + esp_console_cmd_register(&cmd); +} + auto AppConsole::RegisterExtraComponents() -> void { RegisterListDir(); RegisterPlayFile(); @@ -452,6 +494,7 @@ auto AppConsole::RegisterExtraComponents() -> void { RegisterDbIndex(); RegisterDbDump(); RegisterTaskStates(); + RegisterBtList(); } } // namespace console diff --git a/src/app_console/include/app_console.hpp b/src/app_console/include/app_console.hpp index 3cb62b21..667e452f 100644 --- a/src/app_console/include/app_console.hpp +++ b/src/app_console/include/app_console.hpp @@ -8,6 +8,7 @@ #include +#include "bluetooth.hpp" #include "console.hpp" #include "database.hpp" #include "track_queue.hpp" @@ -18,6 +19,7 @@ class AppConsole : public Console { public: static std::weak_ptr sDatabase; static audio::TrackQueue* sTrackQueue; + static drivers::Bluetooth* sBluetooth; protected: virtual auto RegisterExtraComponents() -> void; diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 2d332a1e..a7dda8fd 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -6,7 +6,7 @@ idf_component_register( SRCS "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp" "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" "track_queue.cpp" "stream_event.cpp" "stream_info.cpp" "audio_fsm.cpp" "sink_mixer.cpp" "resample.cpp" - "fatfs_source.cpp" + "fatfs_source.cpp" "bt_audio_output.cpp" INCLUDE_DIRS "include" REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm" "playlist" "speexdsp") diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 617272b3..8791b9c4 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -11,6 +11,8 @@ #include "audio_decoder.hpp" #include "audio_events.hpp" #include "audio_task.hpp" +#include "bluetooth.hpp" +#include "bt_audio_output.hpp" #include "esp_log.h" #include "event_queue.hpp" #include "fatfs_audio_input.hpp" @@ -35,6 +37,7 @@ std::weak_ptr AudioState::sDatabase; std::unique_ptr AudioState::sTask; std::unique_ptr AudioState::sFileSource; std::unique_ptr AudioState::sI2SOutput; +std::unique_ptr AudioState::sBtOutput; TrackQueue* AudioState::sTrackQueue; std::optional AudioState::sCurrentTrack; @@ -42,6 +45,7 @@ std::optional AudioState::sCurrentTrack; auto AudioState::Init(drivers::IGpios* gpio_expander, std::weak_ptr database, std::shared_ptr tag_parser, + drivers::Bluetooth* bluetooth, TrackQueue* queue) -> bool { sIGpios = gpio_expander; sTrackQueue = queue; @@ -55,8 +59,10 @@ auto AudioState::Init(drivers::IGpios* gpio_expander, sFileSource.reset(new FatfsAudioInput(tag_parser)); sI2SOutput.reset(new I2SAudioOutput(sIGpios, sDac)); + // sBtOutput.reset(new BluetoothAudioOutput(bluetooth)); AudioTask::Start(sFileSource.get(), sI2SOutput.get()); + // AudioTask::Start(sFileSource.get(), sBtOutput.get()); return true; } @@ -125,6 +131,7 @@ void Standby::react(const QueueUpdate& ev) { void Playback::entry() { ESP_LOGI(kTag, "beginning playback"); sI2SOutput->SetInUse(true); + // sBtOutput->SetInUse(true); } void Playback::exit() { @@ -133,6 +140,7 @@ void Playback::exit() { // to drain. vTaskDelay(pdMS_TO_TICKS(250)); sI2SOutput->SetInUse(false); + // sBtOutput->SetInUse(false); } void Playback::react(const QueueUpdate& ev) { diff --git a/src/audio/bt_audio_output.cpp b/src/audio/bt_audio_output.cpp new file mode 100644 index 00000000..71e40d02 --- /dev/null +++ b/src/audio/bt_audio_output.cpp @@ -0,0 +1,79 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "bt_audio_output.hpp" +#include +#include + +#include +#include +#include +#include +#include + +#include "esp_err.h" +#include "esp_heap_caps.h" +#include "freertos/portmacro.h" + +#include "audio_element.hpp" +#include "freertos/projdefs.h" +#include "gpios.hpp" +#include "i2c.hpp" +#include "i2s_dac.hpp" +#include "result.hpp" +#include "stream_info.hpp" +#include "wm8523.hpp" + +static const char* kTag = "BTOUT"; + +namespace audio { + +static constexpr size_t kDrainBufferSize = 48 * 1024; + +BluetoothAudioOutput::BluetoothAudioOutput(drivers::Bluetooth* bt) + : IAudioSink(kDrainBufferSize, MALLOC_CAP_SPIRAM), bluetooth_(bt) {} + +BluetoothAudioOutput::~BluetoothAudioOutput() {} + +auto BluetoothAudioOutput::SetInUse(bool in_use) -> void { + if (in_use) { + bluetooth_->SetSource(stream()); + } else { + bluetooth_->SetSource(nullptr); + } +} + +auto BluetoothAudioOutput::SetVolumeImbalance(int_fast8_t balance) -> void {} + +auto BluetoothAudioOutput::SetVolume(uint_fast8_t percent) -> void {} + +auto BluetoothAudioOutput::GetVolume() -> uint_fast8_t { + return 50; +} + +auto BluetoothAudioOutput::AdjustVolumeUp() -> bool { + return false; +} + +auto BluetoothAudioOutput::AdjustVolumeDown() -> bool { + return false; +} + +auto BluetoothAudioOutput::PrepareFormat(const Format& orig) -> Format { + // ESP-IDF's current Bluetooth implementation currently handles SBC encoding, + // but requires a fixed input format. + return Format{ + .sample_rate = 44100, + .num_channels = 2, + .bits_per_sample = 16, + }; +} + +auto BluetoothAudioOutput::Configure(const Format& fmt) -> void { + // No configuration necessary; the output format is fixed. +} + +} // namespace audio diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index b7fcf104..8b7d130f 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -14,7 +14,9 @@ #include #include +#include "audio_sink.hpp" #include "esp_err.h" +#include "esp_heap_caps.h" #include "freertos/portmacro.h" #include "audio_element.hpp" @@ -41,9 +43,12 @@ static constexpr uint16_t kMaxVolumeBeforeClipping = 0x185; static constexpr uint16_t kLineLevelVolume = 0x13d; static constexpr uint16_t kDefaultVolume = 0x128; +static constexpr size_t kDrainBufferSize = 8 * 1024; + I2SAudioOutput::I2SAudioOutput(drivers::IGpios* expander, std::weak_ptr dac) - : expander_(expander), + : IAudioSink(kDrainBufferSize, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT), + expander_(expander), dac_(dac.lock()), current_config_(), left_difference_(0), diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index d10f31e1..5d44fcda 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -12,6 +12,7 @@ #include "audio_events.hpp" #include "audio_task.hpp" +#include "bt_audio_output.hpp" #include "database.hpp" #include "display.hpp" #include "fatfs_audio_input.hpp" @@ -33,6 +34,7 @@ class AudioState : public tinyfsm::Fsm { static auto Init(drivers::IGpios* gpio_expander, std::weak_ptr, std::shared_ptr, + drivers::Bluetooth* bluetooth, TrackQueue* queue) -> bool; virtual ~AudioState() {} @@ -68,6 +70,7 @@ class AudioState : public tinyfsm::Fsm { static std::unique_ptr sTask; static std::unique_ptr sFileSource; static std::unique_ptr sI2SOutput; + static std::unique_ptr sBtOutput; static TrackQueue* sTrackQueue; static std::optional sCurrentTrack; diff --git a/src/audio/include/audio_sink.hpp b/src/audio/include/audio_sink.hpp index 2fb4bf63..b5d6ef57 100644 --- a/src/audio/include/audio_sink.hpp +++ b/src/audio/include/audio_sink.hpp @@ -18,15 +18,11 @@ namespace audio { class IAudioSink { private: - static const std::size_t kDrainBufferSize = 24 * 1024; StreamBufferHandle_t stream_; public: - IAudioSink() - : stream_(xStreamBufferCreateWithCaps( - kDrainBufferSize, - 1, - MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)) {} + IAudioSink(size_t buffer_size, uint32_t caps) + : stream_(xStreamBufferCreateWithCaps(buffer_size, 1, caps)) {} virtual ~IAudioSink() { vStreamBufferDeleteWithCaps(stream_); } diff --git a/src/audio/include/bt_audio_output.hpp b/src/audio/include/bt_audio_output.hpp new file mode 100644 index 00000000..e11a5d44 --- /dev/null +++ b/src/audio/include/bt_audio_output.hpp @@ -0,0 +1,49 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include +#include + +#include "audio_element.hpp" +#include "audio_sink.hpp" +#include "bluetooth.hpp" +#include "chunk.hpp" +#include "result.hpp" + +#include "gpios.hpp" +#include "i2s_dac.hpp" +#include "stream_info.hpp" + +namespace audio { + +class BluetoothAudioOutput : public IAudioSink { + public: + BluetoothAudioOutput(drivers::Bluetooth* bt); + ~BluetoothAudioOutput(); + + auto SetInUse(bool) -> void override; + + auto SetVolumeImbalance(int_fast8_t balance) -> void override; + auto SetVolume(uint_fast8_t percent) -> void override; + auto GetVolume() -> uint_fast8_t override; + auto AdjustVolumeUp() -> bool override; + auto AdjustVolumeDown() -> bool override; + + auto PrepareFormat(const Format&) -> Format override; + auto Configure(const Format& format) -> void override; + + BluetoothAudioOutput(const BluetoothAudioOutput&) = delete; + BluetoothAudioOutput& operator=(const BluetoothAudioOutput&) = delete; + + private: + drivers::Bluetooth* bluetooth_; +}; + +} // namespace audio 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 +#include #include +#include #include #include #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 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::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 { + 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; +} + +auto Bluetooth::SetPreferredDevice(const bluetooth::mac_addr_t& mac) -> void { + if (mac == bluetooth::BluetoothState::preferred_device()) { + return; + } + bluetooth::BluetoothState::preferred_device(mac); + tinyfsm::FsmList::dispatch( + bluetooth::events::PreferredDeviceChanged{}); +} + +auto Bluetooth::SetSource(StreamBufferHandle_t src) -> void { + if (src == bluetooth::BluetoothState::source()) { + return; + } + bluetooth::BluetoothState::source(src); + tinyfsm::FsmList::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 BluetoothState::sDevices_; +std::optional BluetoothState::sPreferredDevice_; +mac_addr_t BluetoothState::sCurrentDevice_; + +std::atomic BluetoothState::sSource_; + +auto BluetoothState::devices() -> std::vector { + std::lock_guard lock{sDevicesMutex_}; + std::vector out; + for (const auto& device : sDevices_) { + out.push_back(device.second); + } + return out; +} + +auto BluetoothState::preferred_device() -> std::optional { + 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(); +} + +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::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 (is_preferred) { + transit(); + } +} + +void Scanning::react(const events::PreferredDeviceChanged& ev) { + bool is_discovered = false; + { + std::lock_guard lock{sDevicesMutex_}; + if (sPreferredDevice_ && sDevices_.contains(sPreferredDevice_.value())) { + is_discovered = true; + } + } + if (is_discovered) { + transit(); + } } 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(); + } + 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(); 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(); 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(); break; case ESP_BT_GAP_KEY_REQ_EVT: - // todo: device needs a password + ESP_LOGW(kTag, "the device wants a password!"); + sPreferredDevice_ = {}; + transit(); 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(); + } + 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(); + } 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); } diff --git a/src/drivers/include/bluetooth.hpp b/src/drivers/include/bluetooth.hpp index 2b5e6a8d..bdc45910 100644 --- a/src/drivers/include/bluetooth.hpp +++ b/src/drivers/include/bluetooth.hpp @@ -1,6 +1,11 @@ #pragma once +#include +#include +#include +#include +#include #include #include @@ -14,6 +19,18 @@ namespace drivers { +namespace bluetooth { + +typedef std::array mac_addr_t; + +struct Device { + mac_addr_t address; + std::string name; + uint32_t class_of_device; + int8_t signal_strength; +}; +} // namespace bluetooth + /* * A handle used to interact with the bluetooth state machine. */ @@ -24,6 +41,9 @@ class Bluetooth { auto Enable() -> bool; auto Disable() -> void; + auto KnownDevices() -> std::vector; + auto SetPreferredDevice(const bluetooth::mac_addr_t& mac) -> void; + auto SetSource(StreamBufferHandle_t) -> void; }; @@ -33,6 +53,9 @@ namespace events { struct Enable : public tinyfsm::Event {}; struct Disable : public tinyfsm::Event {}; +struct PreferredDeviceChanged : public tinyfsm::Event {}; +struct SourceChanged : public tinyfsm::Event {}; + namespace internal { struct Gap : public tinyfsm::Event { esp_bt_gap_cb_event_t type; @@ -51,6 +74,13 @@ struct Avrc : public tinyfsm::Event { class BluetoothState : public tinyfsm::Fsm { public: + static auto devices() -> std::vector; + static auto preferred_device() -> std::optional; + static auto preferred_device(const mac_addr_t&) -> void; + + static auto source() -> StreamBufferHandle_t; + static auto source(StreamBufferHandle_t) -> void; + virtual ~BluetoothState(){}; virtual void entry() {} @@ -58,13 +88,24 @@ 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::PreferredDeviceChanged& ev){}; + virtual void react(const events::SourceChanged& ev){}; virtual void react(const events::internal::Gap& ev) = 0; - virtual void react(const events::internal::A2dp& ev) = 0; + virtual void react(const events::internal::A2dp& ev){}; virtual void react(const events::internal::Avrc& ev){}; + + protected: + static std::mutex sDevicesMutex_; + static std::map sDevices_; + static std::optional sPreferredDevice_; + static mac_addr_t sCurrentDevice_; + + static std::atomic sSource_; }; class Disabled : public BluetoothState { + public: void entry() override; void react(const events::Enable& ev) override; @@ -72,35 +113,53 @@ class Disabled : public BluetoothState { void react(const events::internal::Gap& ev) override {} void react(const events::internal::A2dp& ev) override {} + using BluetoothState::react; }; class Scanning : public BluetoothState { + public: void entry() override; void exit() override; void react(const events::Disable& ev) override; + void react(const events::PreferredDeviceChanged& ev) override; void react(const events::internal::Gap& ev) override; - void react(const events::internal::A2dp& ev) override; + + using BluetoothState::react; + + private: + auto OnDeviceDiscovered(esp_bt_gap_cb_param_t*) -> void; }; class Connecting : public BluetoothState { + public: void entry() override; void exit() override; + void react(const events::PreferredDeviceChanged& 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; + + using BluetoothState::react; }; class Connected : public BluetoothState { + public: void entry() override; void exit() override; + void react(const events::PreferredDeviceChanged& ev) override; + void react(const events::SourceChanged& 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::internal::Avrc& ev) override; + + using BluetoothState::react; }; } // namespace bluetooth diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index 3d6c6a46..7bd5f890 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -6,6 +6,7 @@ #include "assert.h" #include "audio_fsm.hpp" +#include "bluetooth.hpp" #include "core/lv_obj.h" #include "display_init.hpp" #include "esp_err.h" @@ -60,12 +61,15 @@ auto Booting::entry() -> void { return; } + ESP_LOGI(kTag, "starting bluetooth"); + sBluetooth.reset(new drivers::Bluetooth()); + // At this point we've done all of the essential boot tasks. Start remaining // state machines and inform them that the system is ready. ESP_LOGI(kTag, "starting audio"); if (!audio::AudioState::Init(sGpios.get(), sDatabase, sTagParser, - sTrackQueue.get())) { + sBluetooth.get(), sTrackQueue.get())) { events::System().Dispatch(FatalError{}); events::Ui().Dispatch(FatalError{}); return; @@ -80,6 +84,7 @@ auto Booting::exit() -> void { // TODO(jacqueline): Gate this on something. Debug flag? Flashing mode? sAppConsole = new console::AppConsole(); sAppConsole->sTrackQueue = sTrackQueue.get(); + sAppConsole->sBluetooth = sBluetooth.get(); sAppConsole->Launch(); } diff --git a/src/system_fsm/include/system_fsm.hpp b/src/system_fsm/include/system_fsm.hpp index d30a712c..3d513666 100644 --- a/src/system_fsm/include/system_fsm.hpp +++ b/src/system_fsm/include/system_fsm.hpp @@ -10,6 +10,7 @@ #include "app_console.hpp" #include "battery.hpp" +#include "bluetooth.hpp" #include "database.hpp" #include "display.hpp" #include "gpios.hpp" @@ -62,6 +63,7 @@ class SystemState : public tinyfsm::Fsm { static std::shared_ptr sBattery; static std::shared_ptr sStorage; static std::shared_ptr sDisplay; + static std::shared_ptr sBluetooth; static std::shared_ptr sDatabase; static std::shared_ptr sTagParser; diff --git a/src/system_fsm/system_fsm.cpp b/src/system_fsm/system_fsm.cpp index 527a8770..96e806f4 100644 --- a/src/system_fsm/system_fsm.cpp +++ b/src/system_fsm/system_fsm.cpp @@ -24,6 +24,7 @@ std::shared_ptr SystemState::sRelativeTouch; std::shared_ptr SystemState::sBattery; std::shared_ptr SystemState::sStorage; std::shared_ptr SystemState::sDisplay; +std::shared_ptr SystemState::sBluetooth; std::shared_ptr SystemState::sDatabase; std::shared_ptr SystemState::sTagParser; diff --git a/src/tasks/tasks.cpp b/src/tasks/tasks.cpp index ce5f17dc..493d6ba9 100644 --- a/src/tasks/tasks.cpp +++ b/src/tasks/tasks.cpp @@ -62,7 +62,7 @@ auto AllocateStack() -> cpp::span { template <> auto AllocateStack() -> cpp::span { std::size_t size = 32 * 1024; - return {static_cast(heap_caps_malloc(size, MALLOC_CAP_DEFAULT)), + return {static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)), size}; } // UI flushes *must* be done from internal RAM. Thankfully, there is very little @@ -84,8 +84,7 @@ auto AllocateStack() -> cpp::span { template <> auto AllocateStack() -> cpp::span { std::size_t size = 4 * 1024; - return {static_cast( - heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)), + return {static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)), size}; }