Flesh out basic bluetooth support
No ui yet, and performance isn't great. It kinda works though!!
This commit is contained in:
@@ -37,6 +37,7 @@ namespace console {
|
||||
|
||||
std::weak_ptr<database::Database> 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 <index>";
|
||||
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<int>(device.address[i]);
|
||||
}
|
||||
float perc =
|
||||
(static_cast<double>(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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#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<database::Database> sDatabase;
|
||||
static audio::TrackQueue* sTrackQueue;
|
||||
static drivers::Bluetooth* sBluetooth;
|
||||
|
||||
protected:
|
||||
virtual auto RegisterExtraComponents() -> void;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<database::Database> AudioState::sDatabase;
|
||||
std::unique_ptr<AudioTask> AudioState::sTask;
|
||||
std::unique_ptr<FatfsAudioInput> AudioState::sFileSource;
|
||||
std::unique_ptr<I2SAudioOutput> AudioState::sI2SOutput;
|
||||
std::unique_ptr<BluetoothAudioOutput> AudioState::sBtOutput;
|
||||
|
||||
TrackQueue* AudioState::sTrackQueue;
|
||||
std::optional<database::TrackId> AudioState::sCurrentTrack;
|
||||
@@ -42,6 +45,7 @@ std::optional<database::TrackId> AudioState::sCurrentTrack;
|
||||
auto AudioState::Init(drivers::IGpios* gpio_expander,
|
||||
std::weak_ptr<database::Database> database,
|
||||
std::shared_ptr<database::ITagParser> 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) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#include "bt_audio_output.hpp"
|
||||
#include <stdint.h>
|
||||
#include <sys/_stdint.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
|
||||
#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
|
||||
@@ -14,7 +14,9 @@
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
|
||||
#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<drivers::I2SDac> dac)
|
||||
: expander_(expander),
|
||||
: IAudioSink(kDrainBufferSize, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT),
|
||||
expander_(expander),
|
||||
dac_(dac.lock()),
|
||||
current_config_(),
|
||||
left_difference_(0),
|
||||
|
||||
@@ -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<AudioState> {
|
||||
static auto Init(drivers::IGpios* gpio_expander,
|
||||
std::weak_ptr<database::Database>,
|
||||
std::shared_ptr<database::ITagParser>,
|
||||
drivers::Bluetooth* bluetooth,
|
||||
TrackQueue* queue) -> bool;
|
||||
|
||||
virtual ~AudioState() {}
|
||||
@@ -68,6 +70,7 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
|
||||
static std::unique_ptr<AudioTask> sTask;
|
||||
static std::unique_ptr<FatfsAudioInput> sFileSource;
|
||||
static std::unique_ptr<I2SAudioOutput> sI2SOutput;
|
||||
static std::unique_ptr<BluetoothAudioOutput> sBtOutput;
|
||||
|
||||
static TrackQueue* sTrackQueue;
|
||||
static std::optional<database::TrackId> sCurrentTrack;
|
||||
|
||||
@@ -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_); }
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sys/_stdint.h>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#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
|
||||
+269
-17
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -14,6 +19,18 @@
|
||||
|
||||
namespace drivers {
|
||||
|
||||
namespace bluetooth {
|
||||
|
||||
typedef std::array<uint8_t, 6> 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<bluetooth::Device>;
|
||||
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<BluetoothState> {
|
||||
public:
|
||||
static auto devices() -> std::vector<Device>;
|
||||
static auto preferred_device() -> std::optional<mac_addr_t>;
|
||||
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<BluetoothState> {
|
||||
|
||||
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<mac_addr_t, Device> sDevices_;
|
||||
static std::optional<mac_addr_t> sPreferredDevice_;
|
||||
static mac_addr_t sCurrentDevice_;
|
||||
|
||||
static std::atomic<StreamBufferHandle_t> 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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<SystemState> {
|
||||
static std::shared_ptr<drivers::Battery> sBattery;
|
||||
static std::shared_ptr<drivers::SdStorage> sStorage;
|
||||
static std::shared_ptr<drivers::Display> sDisplay;
|
||||
static std::shared_ptr<drivers::Bluetooth> sBluetooth;
|
||||
|
||||
static std::shared_ptr<database::Database> sDatabase;
|
||||
static std::shared_ptr<database::TagParserImpl> sTagParser;
|
||||
|
||||
@@ -24,6 +24,7 @@ std::shared_ptr<drivers::RelativeWheel> SystemState::sRelativeTouch;
|
||||
std::shared_ptr<drivers::Battery> SystemState::sBattery;
|
||||
std::shared_ptr<drivers::SdStorage> SystemState::sStorage;
|
||||
std::shared_ptr<drivers::Display> SystemState::sDisplay;
|
||||
std::shared_ptr<drivers::Bluetooth> SystemState::sBluetooth;
|
||||
|
||||
std::shared_ptr<database::Database> SystemState::sDatabase;
|
||||
std::shared_ptr<database::TagParserImpl> SystemState::sTagParser;
|
||||
|
||||
+2
-3
@@ -62,7 +62,7 @@ auto AllocateStack<Type::kAudio>() -> cpp::span<StackType_t> {
|
||||
template <>
|
||||
auto AllocateStack<Type::kUi>() -> cpp::span<StackType_t> {
|
||||
std::size_t size = 32 * 1024;
|
||||
return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_DEFAULT)),
|
||||
return {static_cast<StackType_t*>(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<Type::kFileStreamer>() -> cpp::span<StackType_t> {
|
||||
template <>
|
||||
auto AllocateStack<Type::kMixer>() -> cpp::span<StackType_t> {
|
||||
std::size_t size = 4 * 1024;
|
||||
return {static_cast<StackType_t*>(
|
||||
heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)),
|
||||
return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)),
|
||||
size};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user