WIP improve bluetooth api and settings screen

custom
jacqueline 9 months ago
parent a9d2335e1d
commit a3eb2dd9dc
  1. 103
      lua/settings.lua
  2. 316
      src/drivers/bluetooth.cpp
  3. 106
      src/drivers/include/drivers/bluetooth.hpp
  4. 7
      src/drivers/include/drivers/bluetooth_types.hpp
  5. 7
      src/drivers/include/drivers/nvs.hpp
  6. 83
      src/drivers/nvs.cpp
  7. 17
      src/tangara/app_console/app_console.cpp
  8. 11
      src/tangara/audio/audio_fsm.cpp
  9. 13
      src/tangara/audio/bt_audio_output.cpp
  10. 30
      src/tangara/lua/property.cpp
  11. 4
      src/tangara/lua/property.hpp
  12. 3
      src/tangara/system_fsm/booting.cpp
  13. 106
      src/tangara/ui/ui_fsm.cpp
  14. 5
      src/tangara/ui/ui_fsm.hpp

@ -30,10 +30,37 @@ local SettingsScreen = widgets.MenuScreen:new {
end end
} }
local BluetoothPairing = SettingsScreen:new {
title = "Nearby Devices",
createUi = function(self)
SettingsScreen.createUi(self)
local devices = self.content:List {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
}
self.bindings = self.bindings + {
bluetooth.discovered_devices:bind(function(devs)
devices:clean()
for _, dev in pairs(devs) do
devices:add_btn(nil, dev.name):onClicked(function()
bluetooth.paired_device:set(dev)
end)
end
end)
}
end,
onShown = function() bluetooth.discovering:set(true) end,
onHidden = function() bluetooth.discovering:set(false) end,
}
local BluetoothSettings = SettingsScreen:new { local BluetoothSettings = SettingsScreen:new {
title = "Bluetooth", title = "Bluetooth",
createUi = function(self) createUi = function(self)
SettingsScreen.createUi(self) SettingsScreen.createUi(self)
-- Enable/Disable switch
local enable_container = self.content:Object { local enable_container = self.content:Object {
flex = { flex = {
flex_direction = "row", flex_direction = "row",
@ -52,11 +79,43 @@ local BluetoothSettings = SettingsScreen:new {
bluetooth.enabled:set(enabled) bluetooth.enabled:set(enabled)
end) end)
theme.set_style(self.content:Label { self.bindings = self.bindings + {
text = "Paired Device", bluetooth.enabled:bind(function(en)
if en then
enable_sw:add_state(lvgl.STATE.CHECKED)
else
enable_sw:clear_state(lvgl.STATE.CHECKED)
end
end),
}
-- Connection status
-- This is presented as a label on the field showing the currently paired
-- device.
local paired_label =
self.content:Label {
text = "",
pad_bottom = 1, pad_bottom = 1,
}, "settings_title") }
theme.set_style(paired_label, "settings_title")
self.bindings = self.bindings + {
bluetooth.connected:bind(function(conn)
if conn then
paired_label:set { text = "Connected to:" }
else
paired_label:set { text = "Paired with:" }
end
end),
bluetooth.connecting:bind(function(conn)
if conn then
paired_label:set { text = "Connecting to:" }
end
end),
}
-- The name of the currently paired device.
local paired_container = self.content:Object { local paired_container = self.content:Object {
flex = { flex = {
flex_direction = "row", flex_direction = "row",
@ -78,24 +137,7 @@ local BluetoothSettings = SettingsScreen:new {
bluetooth.paired_device:set() bluetooth.paired_device:set()
end) end)
theme.set_style(self.content:Label {
text = "Nearby Devices",
pad_bottom = 1,
}, "settings_title")
local devices = self.content:List {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
}
self.bindings = self.bindings + { self.bindings = self.bindings + {
bluetooth.enabled:bind(function(en)
if en then
enable_sw:add_state(lvgl.STATE.CHECKED)
else
enable_sw:clear_state(lvgl.STATE.CHECKED)
end
end),
bluetooth.paired_device:bind(function(device) bluetooth.paired_device:bind(function(device)
if device then if device then
paired_device:set { text = device.name } paired_device:set { text = device.name }
@ -105,7 +147,20 @@ local BluetoothSettings = SettingsScreen:new {
clear_paired:add_flag(lvgl.FLAG.HIDDEN) clear_paired:add_flag(lvgl.FLAG.HIDDEN)
end end
end), end),
bluetooth.devices:bind(function(devs) }
theme.set_style(self.content:Label {
text = "Known Devices",
pad_bottom = 1,
}, "settings_title")
local devices = self.content:List {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
}
self.bindings = self.bindings + {
bluetooth.known_devices:bind(function(devs)
devices:clean() devices:clean()
for _, dev in pairs(devs) do for _, dev in pairs(devs) do
devices:add_btn(nil, dev.name):onClicked(function() devices:add_btn(nil, dev.name):onClicked(function()
@ -114,6 +169,12 @@ local BluetoothSettings = SettingsScreen:new {
end end
end) end)
} }
local pair_new = self.content:Button {}
pair_new:Label { text = "Pair new device" }
pair_new:onClicked(function()
backstack.push(BluetoothPairing:new())
end)
end end
} }

@ -4,6 +4,7 @@
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <iterator>
#include <mutex> #include <mutex>
#include <ostream> #include <ostream>
#include <sstream> #include <sstream>
@ -113,92 +114,111 @@ IRAM_ATTR auto a2dp_data_cb(uint8_t* buf, int32_t buf_size) -> int32_t {
return buf_size; return buf_size;
} }
Bluetooth::Bluetooth(NvsStorage& storage, tasks::WorkerPool& bg_worker) { Bluetooth::Bluetooth(NvsStorage& storage,
tasks::WorkerPool& bg_worker,
EventHandler cb)
: nvs_(storage) {
sBgWorker = &bg_worker; sBgWorker = &bg_worker;
bluetooth::BluetoothState::Init(storage); bluetooth::BluetoothState::Init(storage, cb);
} }
auto Bluetooth::Enable() -> bool { auto Bluetooth::enable(bool en) -> void {
if (en) {
auto lock = bluetooth::BluetoothState::lock(); auto lock = bluetooth::BluetoothState::lock();
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch( tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::Enable{}); bluetooth::events::Enable{});
} else {
return !bluetooth::BluetoothState::is_in_state<bluetooth::Disabled>();
}
auto Bluetooth::Disable() -> void {
// FIXME: the BT tasks unfortunately call back into us while holding an // FIXME: the BT tasks unfortunately call back into us while holding an
// internal lock, which then deadlocks with our fsm lock. // internal lock, which then deadlocks with our fsm lock.
// auto lock = bluetooth::BluetoothState::lock(); // auto lock = bluetooth::BluetoothState::lock();
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch( tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::Disable{}); bluetooth::events::Disable{});
}
} }
auto Bluetooth::IsEnabled() -> bool { auto Bluetooth::enabled() -> bool {
auto lock = bluetooth::BluetoothState::lock(); auto lock = bluetooth::BluetoothState::lock();
return !bluetooth::BluetoothState::is_in_state<bluetooth::Disabled>(); return !bluetooth::BluetoothState::is_in_state<bluetooth::Disabled>();
} }
auto Bluetooth::IsConnected() -> bool { auto Bluetooth::source(PcmBuffer* src) -> void {
if (src == sStream) {
return;
}
auto lock = bluetooth::BluetoothState::lock(); auto lock = bluetooth::BluetoothState::lock();
return bluetooth::BluetoothState::is_in_state<bluetooth::Connected>(); sStream = src;
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::SourceChanged{});
} }
auto Bluetooth::ConnectedDevice() -> std::optional<bluetooth::MacAndName> { auto Bluetooth::softVolume(float f) -> void {
sVolumeFactor = f;
}
auto Bluetooth::connectionState() -> ConnectionState {
auto lock = bluetooth::BluetoothState::lock(); auto lock = bluetooth::BluetoothState::lock();
if (!bluetooth::BluetoothState::is_in_state<bluetooth::Connected>()) { if (bluetooth::BluetoothState::is_in_state<bluetooth::Connected>()) {
return {}; return ConnectionState::kConnected;
} else if (bluetooth::BluetoothState::is_in_state<bluetooth::Connecting>()) {
return ConnectionState::kConnecting;
} }
return bluetooth::BluetoothState::preferred_device(); return ConnectionState::kDisconnected;
} }
auto Bluetooth::KnownDevices() -> std::vector<bluetooth::Device> { auto Bluetooth::pairedDevice() -> std::optional<bluetooth::MacAndName> {
auto lock = bluetooth::BluetoothState::lock(); auto lock = bluetooth::BluetoothState::lock();
std::vector<bluetooth::Device> out = bluetooth::BluetoothState::devices(); return bluetooth::BluetoothState::pairedDevice();
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(std::optional<bluetooth::MacAndName> dev) auto Bluetooth::pairedDevice(std::optional<bluetooth::MacAndName> dev) -> void {
-> void {
auto lock = bluetooth::BluetoothState::lock(); auto lock = bluetooth::BluetoothState::lock();
auto cur = bluetooth::BluetoothState::preferred_device(); bluetooth::BluetoothState::pairedDevice(dev);
if (dev && cur && dev->mac == cur->mac) {
return;
}
ESP_LOGI(kTag, "preferred is '%s' (%u%u%u%u%u%u)", dev->name.c_str(),
dev->mac[0], dev->mac[1], dev->mac[2], dev->mac[3], dev->mac[4],
dev->mac[5]);
bluetooth::BluetoothState::preferred_device(dev);
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::PreferredDeviceChanged{});
} }
auto Bluetooth::PreferredDevice() -> std::optional<bluetooth::MacAndName> { auto Bluetooth::knownDevices() -> std::vector<bluetooth::MacAndName> {
auto lock = bluetooth::BluetoothState::lock(); return nvs_.BluetoothNames();
return bluetooth::BluetoothState::preferred_device();
} }
auto Bluetooth::SetSource(PcmBuffer* src) -> void { auto Bluetooth::forgetKnownDevice(const bluetooth::mac_addr_t& mac) -> void {
nvs_.BluetoothName(mac, {});
}
auto Bluetooth::discoveryEnabled(bool en) -> void {
auto lock = bluetooth::BluetoothState::lock(); auto lock = bluetooth::BluetoothState::lock();
if (src == bluetooth::BluetoothState::source()) { bluetooth::BluetoothState::discovery(en);
return;
}
bluetooth::BluetoothState::source(src);
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::SourceChanged{});
} }
auto Bluetooth::SetVolumeFactor(float f) -> void { auto Bluetooth::discoveryEnabled() -> bool {
sVolumeFactor = f; auto lock = bluetooth::BluetoothState::lock();
return bluetooth::BluetoothState::discovery();
} }
auto Bluetooth::SetEventHandler(std::function<void(bluetooth::Event)> cb) auto Bluetooth::discoveredDevices() -> std::vector<bluetooth::MacAndName> {
-> void { std::vector<bluetooth::Device> discovered;
{
auto lock = bluetooth::BluetoothState::lock(); auto lock = bluetooth::BluetoothState::lock();
bluetooth::BluetoothState::event_handler(cb); discovered = bluetooth::BluetoothState::discoveredDevices();
}
// Show devices with stronger signals first, since they're more likely to be
// physically close (and therefore more likely to be what the user wants).
std::sort(discovered.begin(), discovered.end(),
[](const auto& a, const auto& b) -> bool {
return a.signal_strength < b.signal_strength;
});
// Convert to the right format.
std::vector<bluetooth::MacAndName> out;
out.reserve(discovered.size());
std::transform(discovered.begin(), discovered.end(), std::back_inserter(out),
[&](const bluetooth::Device& dev) {
return bluetooth::MacAndName{
.mac = dev.address,
.name = {dev.name.data(), dev.name.size()},
};
});
return out;
} }
static auto DeviceName() -> std::pmr::string { static auto DeviceName() -> std::pmr::string {
@ -251,6 +271,10 @@ auto Scanner::StopScanningNow() -> void {
} }
} }
auto Scanner::enabled() -> bool {
return enabled_;
}
auto Scanner::HandleGapEvent(const events::internal::Gap& ev) -> void { auto Scanner::HandleGapEvent(const events::internal::Gap& ev) -> void {
switch (ev.type) { switch (ev.type) {
case ESP_BT_GAP_DISC_RES_EVT: case ESP_BT_GAP_DISC_RES_EVT:
@ -343,17 +367,18 @@ NvsStorage* BluetoothState::sStorage_;
Scanner* BluetoothState::sScanner_; Scanner* BluetoothState::sScanner_;
std::mutex BluetoothState::sFsmMutex{}; std::mutex BluetoothState::sFsmMutex{};
std::map<mac_addr_t, Device> BluetoothState::sDevices_{}; std::map<mac_addr_t, Device> BluetoothState::sDiscoveredDevices_{};
std::optional<MacAndName> BluetoothState::sPreferredDevice_{}; std::optional<MacAndName> BluetoothState::sPairedWith_{};
std::optional<MacAndName> BluetoothState::sConnectingDevice_{}; std::optional<MacAndName> BluetoothState::sConnectingTo_{};
int BluetoothState::sConnectAttemptsRemaining_{0}; int BluetoothState::sConnectAttemptsRemaining_{0};
std::atomic<PcmBuffer*> BluetoothState::sSource_;
std::function<void(Event)> BluetoothState::sEventHandler_; std::function<void(Event)> BluetoothState::sEventHandler_;
auto BluetoothState::Init(NvsStorage& storage) -> void { auto BluetoothState::Init(NvsStorage& storage, Bluetooth::EventHandler cb)
-> void {
sStorage_ = &storage; sStorage_ = &storage;
sPreferredDevice_ = storage.PreferredBluetoothDevice(); sEventHandler_ = cb;
sPairedWith_ = storage.PreferredBluetoothDevice();
tinyfsm::FsmList<bluetooth::BluetoothState>::start(); tinyfsm::FsmList<bluetooth::BluetoothState>::start();
} }
@ -361,68 +386,85 @@ auto BluetoothState::lock() -> std::lock_guard<std::mutex> {
return std::lock_guard<std::mutex>{sFsmMutex}; return std::lock_guard<std::mutex>{sFsmMutex};
} }
auto BluetoothState::devices() -> std::vector<Device> { auto BluetoothState::pairedDevice() -> std::optional<MacAndName> {
std::vector<Device> out; return sPairedWith_;
for (const auto& device : sDevices_) {
out.push_back(device.second);
}
return out;
} }
auto BluetoothState::preferred_device() -> std::optional<MacAndName> { auto BluetoothState::pairedDevice(std::optional<MacAndName> dev) -> void {
return sPreferredDevice_; auto cur = sPairedWith_;
} if (dev && cur && dev->mac == cur->mac) {
return;
}
if (dev) {
ESP_LOGI(kTag, "pairing with '%s' (%u%u%u%u%u%u)", dev->name.c_str(),
dev->mac[0], dev->mac[1], dev->mac[2], dev->mac[3], dev->mac[4],
dev->mac[5]);
}
sPairedWith_ = dev;
std::invoke(sEventHandler_, SimpleEvent::kDeviceDiscovered);
auto BluetoothState::preferred_device(std::optional<MacAndName> addr) -> void { tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
sPreferredDevice_ = addr; bluetooth::events::PairedDeviceChanged{});
} }
auto BluetoothState::source() -> PcmBuffer* { auto BluetoothState::discovery() -> bool {
return sSource_.load(); return sScanner_->enabled();
} }
auto BluetoothState::source(PcmBuffer* src) -> void { auto BluetoothState::discovery(bool en) -> void {
sSource_.store(src); if (en) {
if (!sScanner_->enabled()) {
sDiscoveredDevices_.clear();
}
sScanner_->ScanContinuously();
} else {
sScanner_->StopScanning();
}
} }
auto BluetoothState::event_handler(std::function<void(Event)> cb) -> void { auto BluetoothState::discoveredDevices() -> std::vector<Device> {
sEventHandler_ = cb; std::vector<Device> out;
for (const auto& device : sDiscoveredDevices_) {
out.push_back(device.second);
}
return out;
} }
auto BluetoothState::react(const events::DeviceDiscovered& ev) -> void { auto BluetoothState::react(const events::DeviceDiscovered& ev) -> void {
bool is_preferred = false; bool is_paired = false;
bool already_known = sDevices_.contains(ev.device.address); bool already_known = sDiscoveredDevices_.contains(ev.device.address);
sDevices_[ev.device.address] = ev.device; sDiscoveredDevices_[ev.device.address] = ev.device;
if (sPreferredDevice_ && ev.device.address == sPreferredDevice_->mac) { if (sPairedWith_ && ev.device.address == sPairedWith_->mac) {
is_preferred = true; is_paired = true;
} }
if (sEventHandler_ && !already_known) { if (!already_known) {
std::invoke(sEventHandler_, SimpleEvent::kKnownDevicesChanged); std::invoke(sEventHandler_, SimpleEvent::kDeviceDiscovered);
} }
if (is_preferred && sPreferredDevice_) { if (is_paired && sPairedWith_) {
connect(*sPreferredDevice_); connect(*sPairedWith_);
} }
} }
auto BluetoothState::connect(const MacAndName& dev) -> bool { auto BluetoothState::connect(const MacAndName& dev) -> bool {
if (sConnectingDevice_ && sConnectingDevice_->mac == dev.mac) { if (sConnectingTo_ && sConnectingTo_->mac == dev.mac) {
sConnectAttemptsRemaining_--; sConnectAttemptsRemaining_--;
} else { } else {
sConnectAttemptsRemaining_ = 3; sConnectAttemptsRemaining_ = 3;
} }
if (sConnectAttemptsRemaining_ == 0) { if (sConnectAttemptsRemaining_ == 0) {
sConnectingTo_ = {};
return false; return false;
} }
sConnectingDevice_ = dev; sConnectingTo_ = dev;
ESP_LOGI(kTag, "connecting to '%s' (%u%u%u%u%u%u)", dev.name.c_str(), ESP_LOGI(kTag, "connecting to '%s' (%u%u%u%u%u%u)", dev.name.c_str(),
dev.mac[0], dev.mac[1], dev.mac[2], dev.mac[3], dev.mac[4], dev.mac[0], dev.mac[1], dev.mac[2], dev.mac[3], dev.mac[4],
dev.mac[5]); dev.mac[5]);
if (esp_a2d_source_connect(sConnectingDevice_->mac.data()) != ESP_OK) { if (esp_a2d_source_connect(sConnectingTo_->mac.data()) != ESP_OK) {
ESP_LOGI(kTag, "Connecting failed..."); ESP_LOGI(kTag, "Connecting failed...");
if (sConnectAttemptsRemaining_ > 1) { if (sConnectAttemptsRemaining_ > 1) {
ESP_LOGI(kTag, "Will retry."); ESP_LOGI(kTag, "Will retry.");
@ -508,11 +550,13 @@ void Disabled::react(const events::Enable&) {
// AVRCP Target // AVRCP Target
err = esp_avrc_tg_init(); err = esp_avrc_tg_init();
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(kTag, "Error during target init: %s %d", esp_err_to_name(err), err); ESP_LOGE(kTag, "Error during target init: %s %d", esp_err_to_name(err),
err);
} }
err = esp_avrc_tg_register_callback(avrcp_tg_cb); err = esp_avrc_tg_register_callback(avrcp_tg_cb);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(kTag, "Error registering AVRC tg callback: %s %d", esp_err_to_name(err), err); ESP_LOGE(kTag, "Error registering AVRC tg callback: %s %d",
esp_err_to_name(err), err);
} }
// Set the supported passthrough commands on the tg // Set the supported passthrough commands on the tg
@ -522,10 +566,12 @@ void Disabled::react(const events::Enable&) {
do { do {
// Sleep for a bit // Sleep for a bit
vTaskDelay(pdMS_TO_TICKS(10)); vTaskDelay(pdMS_TO_TICKS(10));
err = esp_avrc_tg_get_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_ALLOWED_CMD, &psth); err = esp_avrc_tg_get_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_ALLOWED_CMD,
&psth);
} while (err != ESP_OK); } while (err != ESP_OK);
err = esp_avrc_tg_set_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_SUPPORTED_CMD, &psth); err = esp_avrc_tg_set_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_SUPPORTED_CMD,
&psth);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err);
} }
@ -534,7 +580,6 @@ void Disabled::react(const events::Enable&) {
ESP_AVRC_RN_VOLUME_CHANGE); ESP_AVRC_RN_VOLUME_CHANGE);
assert(esp_avrc_tg_set_rn_evt_cap(&evt_set) == ESP_OK); assert(esp_avrc_tg_set_rn_evt_cap(&evt_set) == ESP_OK);
// Initialise A2DP. This handles streaming audio. Currently ESP-IDF's SBC // Initialise A2DP. This handles streaming audio. Currently ESP-IDF's SBC
// encoder only supports 2 channels of interleaved 16 bit samples, at // encoder only supports 2 channels of interleaved 16 bit samples, at
// 44.1kHz, so there is no additional configuration to be done for the // 44.1kHz, so there is no additional configuration to be done for the
@ -547,37 +592,27 @@ void Disabled::react(const events::Enable&) {
esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE); esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
ESP_LOGI(kTag, "bt enabled"); ESP_LOGI(kTag, "bt enabled");
if (sPreferredDevice_) { if (sPairedWith_) {
ESP_LOGI(kTag, "connecting to preferred device '%s'", ESP_LOGI(kTag, "connecting to paired device '%s'",
sPreferredDevice_->name.c_str()); sPairedWith_->name.c_str());
connect(*sPreferredDevice_); connect(*sPairedWith_);
} else { } else {
ESP_LOGI(kTag, "scanning for devices");
transit<Idle>(); transit<Idle>();
} }
} }
void Idle::entry() { void Idle::entry() {
ESP_LOGI(kTag, "bt is idle"); ESP_LOGI(kTag, "bt is idle");
sScanner_->ScanContinuously();
} }
void Idle::exit() { void Idle::exit() {}
sScanner_->StopScanning();
}
void Idle::react(const events::Disable& ev) { void Idle::react(const events::Disable& ev) {
transit<Disabled>(); transit<Disabled>();
} }
void Idle::react(const events::PreferredDeviceChanged& ev) { void Idle::react(const events::PairedDeviceChanged& ev) {
bool is_discovered = false; connect(*sPairedWith_);
if (sPreferredDevice_ && sDevices_.contains(sPreferredDevice_->mac)) {
is_discovered = true;
}
if (is_discovered) {
connect(*sPreferredDevice_);
}
} }
void Idle::react(events::internal::Gap ev) { void Idle::react(events::internal::Gap ev) {
@ -599,7 +634,6 @@ void Connecting::entry() {
timeoutCallback); timeoutCallback);
xTimerStart(sTimeoutTimer, portMAX_DELAY); xTimerStart(sTimeoutTimer, portMAX_DELAY);
sScanner_->StopScanning();
if (sEventHandler_) { if (sEventHandler_) {
std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged); std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged);
} }
@ -615,19 +649,24 @@ void Connecting::exit() {
void Connecting::react(const events::ConnectTimedOut& ev) { void Connecting::react(const events::ConnectTimedOut& ev) {
ESP_LOGI(kTag, "timed out awaiting connection"); ESP_LOGI(kTag, "timed out awaiting connection");
esp_a2d_source_disconnect(sConnectingDevice_->mac.data()); esp_a2d_source_disconnect(sConnectingTo_->mac.data());
if (!connect(*sConnectingDevice_)) { if (!connect(*sConnectingTo_)) {
transit<Idle>(); transit<Idle>();
} }
} }
void Connecting::react(const events::Disable& ev) { void Connecting::react(const events::Disable& ev) {
// TODO: disconnect gracefully esp_a2d_source_disconnect(sConnectingTo_->mac.data());
transit<Disabled>(); transit<Disabled>();
} }
void Connecting::react(const events::PreferredDeviceChanged& ev) { void Connecting::react(const events::PairedDeviceChanged& ev) {
// TODO. Cancel out and start again. esp_a2d_source_disconnect(sConnectingTo_->mac.data());
if (sPairedWith_) {
connect(*sPairedWith_);
} else {
transit<Idle>();
}
} }
void Connecting::react(events::internal::Gap ev) { void Connecting::react(events::internal::Gap ev) {
@ -698,15 +737,20 @@ void Connected::entry() {
ESP_LOGI(kTag, "entering connected state"); ESP_LOGI(kTag, "entering connected state");
transaction_num_ = 0; transaction_num_ = 0;
connected_to_ = sConnectingDevice_->mac; connected_to_ = sConnectingTo_->mac;
sPreferredDevice_ = sConnectingDevice_; sPairedWith_ = sConnectingTo_;
sConnectingDevice_ = {};
sStorage_->BluetoothName(sConnectingTo_->mac, sConnectingTo_->name);
std::invoke(sEventHandler_, SimpleEvent::kKnownDevicesChanged);
sConnectingTo_ = {};
auto stored_pref = sStorage_->PreferredBluetoothDevice(); auto stored_pref = sStorage_->PreferredBluetoothDevice();
if (!stored_pref || (sPreferredDevice_->name != stored_pref->name || if (!stored_pref || (sPairedWith_->name != stored_pref->name ||
sPreferredDevice_->mac != stored_pref->mac)) { sPairedWith_->mac != stored_pref->mac)) {
sStorage_->PreferredBluetoothDevice(sPreferredDevice_); sStorage_->PreferredBluetoothDevice(sPairedWith_);
} }
// TODO: if we already have a source, immediately start playing // TODO: if we already have a source, immediately start playing
} }
@ -719,13 +763,11 @@ void Connected::react(const events::Disable& ev) {
transit<Disabled>(); transit<Disabled>();
} }
void Connected::react(const events::PreferredDeviceChanged& ev) { void Connected::react(const events::PairedDeviceChanged& ev) {
sConnectingDevice_ = sPreferredDevice_; transit<Idle>();
transit<Connecting>();
} }
void Connected::react(const events::SourceChanged& ev) { void Connected::react(const events::SourceChanged& ev) {
sStream = sSource_;
if (sStream != nullptr) { if (sStream != nullptr) {
ESP_LOGI(kTag, "checking source is ready"); ESP_LOGI(kTag, "checking source is ready");
esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_CHECK_SRC_RDY); esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_CHECK_SRC_RDY);
@ -775,7 +817,8 @@ void Connected::react(events::internal::Avrc ev) {
switch (ev.type) { switch (ev.type) {
case ESP_AVRC_CT_CONNECTION_STATE_EVT: case ESP_AVRC_CT_CONNECTION_STATE_EVT:
if (ev.param.conn_stat.connected) { if (ev.param.conn_stat.connected) {
auto err = esp_avrc_ct_send_register_notification_cmd(4, ESP_AVRC_RN_VOLUME_CHANGE, 0); auto err = esp_avrc_ct_send_register_notification_cmd(
4, ESP_AVRC_RN_VOLUME_CHANGE, 0);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err);
} }
@ -787,15 +830,20 @@ void Connected::react(events::internal::Avrc ev) {
case ESP_AVRC_CT_REMOTE_FEATURES_EVT: case ESP_AVRC_CT_REMOTE_FEATURES_EVT:
// The remote device is telling us about its capabilities! We don't // The remote device is telling us about its capabilities! We don't
// currently care about any of them. // currently care about any of them.
ESP_LOGI(kTag, "Recieved capabilitites: %lu", ev.param.rmt_feats.feat_mask); ESP_LOGI(kTag, "Recieved capabilitites: %lu",
ev.param.rmt_feats.feat_mask);
break; break;
case ESP_AVRC_CT_CHANGE_NOTIFY_EVT: case ESP_AVRC_CT_CHANGE_NOTIFY_EVT:
if (ev.param.change_ntf.event_id == ESP_AVRC_RN_VOLUME_CHANGE) { if (ev.param.change_ntf.event_id == ESP_AVRC_RN_VOLUME_CHANGE) {
if (sEventHandler_) { if (sEventHandler_) {
std::invoke(sEventHandler_, bluetooth::RemoteVolumeChanged{.new_vol = ev.param.change_ntf.event_parameter.volume}); std::invoke(
sEventHandler_,
bluetooth::RemoteVolumeChanged{
.new_vol = ev.param.change_ntf.event_parameter.volume});
} }
// Resubscribe to volume facts // Resubscribe to volume facts
auto err = esp_avrc_ct_send_register_notification_cmd(4, ESP_AVRC_RN_VOLUME_CHANGE, 0); auto err = esp_avrc_ct_send_register_notification_cmd(
4, ESP_AVRC_RN_VOLUME_CHANGE, 0);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err);
} }
@ -809,16 +857,20 @@ void Connected::react(events::internal::Avrc ev) {
void Connected::react(const events::internal::Avrctg ev) { void Connected::react(const events::internal::Avrctg ev) {
switch (ev.type) { switch (ev.type) {
case ESP_AVRC_TG_CONNECTION_STATE_EVT: case ESP_AVRC_TG_CONNECTION_STATE_EVT:
ESP_LOGI(kTag, "Got connection event. Connected: %s", ev.param.conn_stat.connected ? "true" : "false"); ESP_LOGI(kTag, "Got connection event. Connected: %s",
ev.param.conn_stat.connected ? "true" : "false");
if (ev.param.conn_stat.connected) { if (ev.param.conn_stat.connected) {
} }
break; break;
case ESP_AVRC_TG_REMOTE_FEATURES_EVT: case ESP_AVRC_TG_REMOTE_FEATURES_EVT:
ESP_LOGI(kTag, "Got remote features feat flag %d", ev.param.rmt_feats.ct_feat_flag); ESP_LOGI(kTag, "Got remote features feat flag %d",
ESP_LOGI(kTag, "Got remote features feat mask %lu", ev.param.rmt_feats.feat_mask); ev.param.rmt_feats.ct_feat_flag);
ESP_LOGI(kTag, "Got remote features feat mask %lu",
ev.param.rmt_feats.feat_mask);
break; break;
case ESP_AVRC_TG_PASSTHROUGH_CMD_EVT: case ESP_AVRC_TG_PASSTHROUGH_CMD_EVT:
ESP_LOGI(kTag, "Got passthrough event keycode: %x, %d", ev.param.psth_cmd.key_code, ev.param.psth_cmd.key_state); ESP_LOGI(kTag, "Got passthrough event keycode: %x, %d",
ev.param.psth_cmd.key_code, ev.param.psth_cmd.key_state);
if (ev.param.psth_cmd.key_state == 1 && sEventHandler_) { if (ev.param.psth_cmd.key_state == 1 && sEventHandler_) {
switch (ev.param.psth_cmd.key_code) { switch (ev.param.psth_cmd.key_code) {
case ESP_AVRC_PT_CMD_PLAY: case ESP_AVRC_PT_CMD_PLAY:
@ -840,7 +892,8 @@ void Connected::react(const events::internal::Avrctg ev) {
std::invoke(sEventHandler_, bluetooth::SimpleEvent::kBackward); std::invoke(sEventHandler_, bluetooth::SimpleEvent::kBackward);
break; break;
default: default:
ESP_LOGI(kTag, "Unhandled passthrough cmd. Key code: %d", ev.param.psth_cmd.key_code); ESP_LOGI(kTag, "Unhandled passthrough cmd. Key code: %d",
ev.param.psth_cmd.key_code);
} }
} }
break; break;
@ -855,7 +908,8 @@ void Connected::react(const events::internal::Avrctg ev) {
ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err);
} }
} else { } else {
ESP_LOGW(kTag, "unhandled AVRC TG Register Notification event: %u", ev.param.reg_ntf.event_id); ESP_LOGW(kTag, "unhandled AVRC TG Register Notification event: %u",
ev.param.reg_ntf.event_id);
} }
break; break;
} }

@ -3,6 +3,7 @@
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <functional>
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
@ -25,28 +26,68 @@
namespace drivers { namespace drivers {
/* /*
* A handle used to interact with the bluetooth state machine. * A handle used to interact with the bluetooth state machine. This is the main
* API that the rest of the system should use to interact with Bluetooth.
*/ */
class Bluetooth { class Bluetooth {
public: public:
Bluetooth(NvsStorage& storage, tasks::WorkerPool&); /*
* Callback invoked when an event is generated by the Bluetooth stack. This
* callback is invoked synchronously from a Bluetooth task context, so
* implementations should immediately hop to a different task to process the
* event.
*/
using EventHandler = std::function<void(bluetooth::Event)>;
Bluetooth(NvsStorage&, tasks::WorkerPool&, EventHandler);
auto Enable() -> bool; /* Enables or disables the entire Bluetooth stack. */
auto Disable() -> void; auto enable(bool en) -> void;
auto IsEnabled() -> bool; auto enabled() -> bool;
auto IsConnected() -> bool; auto source(PcmBuffer*) -> void;
auto ConnectedDevice() -> std::optional<bluetooth::MacAndName>; auto softVolume(float) -> void;
auto KnownDevices() -> std::vector<bluetooth::Device>; enum class ConnectionState {
kConnected,
kConnecting,
kDisconnected,
};
auto SetPreferredDevice(std::optional<bluetooth::MacAndName> dev) -> void; auto connectionState() -> ConnectionState;
auto PreferredDevice() -> std::optional<bluetooth::MacAndName>;
auto SetSource(PcmBuffer*) -> void; /*
auto SetVolumeFactor(float) -> void; * The 'paired' device is a device that will be preferred for connections.
* When Bluetooth is first enabled, we immediately try to connect to the
* paired device. If the paired device is seen during a scan, then we will
* also automatically connect to it.
*/
auto pairedDevice() -> std::optional<bluetooth::MacAndName>;
/*
* Sets the preferred device. If a device is provided, a connection will be
* attempted immediately, even if the device has not been detected in a
* previous scan.
*/
auto pairedDevice(std::optional<bluetooth::MacAndName> dev) -> void;
/* A list of devices that have previously been the paired device. */
auto knownDevices() -> std::vector<bluetooth::MacAndName>;
auto forgetKnownDevice(const bluetooth::mac_addr_t&) -> void;
/* Enables or disables scanning for nearby Bluetooth devices. */
auto discoveryEnabled(bool) -> void;
auto discoveryEnabled() -> bool;
/*
* A list of nearby devices that have been discovered since discovery was
* last enabled. This list may include the paired device, as well as devices
* that are also present in the known devices list.
*/
auto discoveredDevices() -> std::vector<bluetooth::MacAndName>;
auto SetEventHandler(std::function<void(bluetooth::Event)> cb) -> void; private:
NvsStorage& nvs_;
}; };
namespace bluetooth { namespace bluetooth {
@ -56,7 +97,7 @@ struct Enable : public tinyfsm::Event {};
struct Disable : public tinyfsm::Event {}; struct Disable : public tinyfsm::Event {};
struct ConnectTimedOut : public tinyfsm::Event {}; struct ConnectTimedOut : public tinyfsm::Event {};
struct PreferredDeviceChanged : public tinyfsm::Event {}; struct PairedDeviceChanged : public tinyfsm::Event {};
struct SourceChanged : public tinyfsm::Event {}; struct SourceChanged : public tinyfsm::Event {};
struct DeviceDiscovered : public tinyfsm::Event { struct DeviceDiscovered : public tinyfsm::Event {
const Device& device; const Device& device;
@ -94,6 +135,8 @@ class Scanner {
auto StopScanning() -> void; auto StopScanning() -> void;
auto StopScanningNow() -> void; auto StopScanningNow() -> void;
auto enabled() -> bool;
auto HandleGapEvent(const events::internal::Gap&) -> void; auto HandleGapEvent(const events::internal::Gap&) -> void;
private: private:
@ -103,25 +146,22 @@ class Scanner {
auto HandleDeviceDiscovery(const esp_bt_gap_cb_param_t& param) -> void; auto HandleDeviceDiscovery(const esp_bt_gap_cb_param_t& param) -> void;
}; };
/*
* The main state machine for managing the state of the Bluetooth stack, and
* the current (if any) Bluetooth connection.
*/
class BluetoothState : public tinyfsm::Fsm<BluetoothState> { class BluetoothState : public tinyfsm::Fsm<BluetoothState> {
public: public:
static auto Init(NvsStorage& storage) -> void; static auto Init(NvsStorage& storage, Bluetooth::EventHandler) -> void;
static auto lock() -> std::lock_guard<std::mutex>; static auto lock() -> std::lock_guard<std::mutex>;
static auto devices() -> std::vector<Device>; static auto pairedDevice() -> std::optional<bluetooth::MacAndName>;
static auto pairedDevice(std::optional<bluetooth::MacAndName>) -> void;
static auto preferred_device() -> std::optional<bluetooth::MacAndName>;
static auto preferred_device(std::optional<bluetooth::MacAndName>) -> void;
static auto scanning() -> bool;
static auto discovery() -> bool; static auto discovery() -> bool;
static auto discovery(bool) -> void; static auto discovery(bool) -> void;
static auto discoveredDevices() -> std::vector<Device>;
static auto source() -> PcmBuffer*;
static auto source(PcmBuffer*) -> void;
static auto event_handler(std::function<void(Event)>) -> void;
virtual ~BluetoothState(){}; virtual ~BluetoothState(){};
@ -131,7 +171,7 @@ class BluetoothState : public tinyfsm::Fsm<BluetoothState> {
virtual void react(const events::Enable& ev){}; virtual void react(const events::Enable& ev){};
virtual void react(const events::Disable& ev) = 0; virtual void react(const events::Disable& ev) = 0;
virtual void react(const events::ConnectTimedOut& ev){}; virtual void react(const events::ConnectTimedOut& ev){};
virtual void react(const events::PreferredDeviceChanged& ev){}; virtual void react(const events::PairedDeviceChanged& ev){};
virtual void react(const events::SourceChanged& ev){}; virtual void react(const events::SourceChanged& ev){};
virtual void react(const events::DeviceDiscovered&); virtual void react(const events::DeviceDiscovered&);
@ -146,13 +186,11 @@ class BluetoothState : public tinyfsm::Fsm<BluetoothState> {
static Scanner* sScanner_; static Scanner* sScanner_;
static std::mutex sFsmMutex; static std::mutex sFsmMutex;
static std::map<mac_addr_t, Device> sDevices_; static std::map<mac_addr_t, Device> sDiscoveredDevices_;
static std::optional<bluetooth::MacAndName> sPreferredDevice_; static std::optional<bluetooth::MacAndName> sPairedWith_;
static std::optional<bluetooth::MacAndName> sConnectingTo_;
static std::optional<bluetooth::MacAndName> sConnectingDevice_;
static int sConnectAttemptsRemaining_; static int sConnectAttemptsRemaining_;
static std::atomic<PcmBuffer*> sSource_;
static std::function<void(Event)> sEventHandler_; static std::function<void(Event)> sEventHandler_;
auto connect(const bluetooth::MacAndName&) -> bool; auto connect(const bluetooth::MacAndName&) -> bool;
@ -177,7 +215,7 @@ class Idle : public BluetoothState {
void exit() override; void exit() override;
void react(const events::Disable& ev) override; void react(const events::Disable& ev) override;
void react(const events::PreferredDeviceChanged& ev) override; void react(const events::PairedDeviceChanged& ev) override;
void react(events::internal::Gap ev) override; void react(events::internal::Gap ev) override;
@ -189,7 +227,7 @@ class Connecting : public BluetoothState {
void entry() override; void entry() override;
void exit() override; void exit() override;
void react(const events::PreferredDeviceChanged& ev) override; void react(const events::PairedDeviceChanged& ev) override;
void react(const events::ConnectTimedOut& ev) override; void react(const events::ConnectTimedOut& ev) override;
void react(const events::Disable& ev) override; void react(const events::Disable& ev) override;
@ -204,7 +242,7 @@ class Connected : public BluetoothState {
void entry() override; void entry() override;
void exit() override; void exit() override;
void react(const events::PreferredDeviceChanged& ev) override; void react(const events::PairedDeviceChanged& ev) override;
void react(const events::SourceChanged& ev) override; void react(const events::SourceChanged& ev) override;
void react(const events::Disable& ev) override; void react(const events::Disable& ev) override;

@ -27,9 +27,12 @@ struct Device {
}; };
enum class SimpleEvent { enum class SimpleEvent {
kKnownDevicesChanged,
kConnectionStateChanged, kConnectionStateChanged,
kPreferredDeviceChanged, kPairedDeviceChanged,
kKnownDevicesChanged,
kDiscoveryChanged,
kDeviceDiscovered,
// Passthrough events // Passthrough events
kPlayPause, kPlayPause,
kStop, kStop,

@ -96,6 +96,10 @@ class NvsStorage {
auto BluetoothVolume(const bluetooth::mac_addr_t&) -> uint8_t; auto BluetoothVolume(const bluetooth::mac_addr_t&) -> uint8_t;
auto BluetoothVolume(const bluetooth::mac_addr_t&, uint8_t) -> void; auto BluetoothVolume(const bluetooth::mac_addr_t&, uint8_t) -> void;
auto BluetoothNames() -> std::vector<bluetooth::MacAndName>;
auto BluetoothName(const bluetooth::mac_addr_t&, std::optional<std::string>)
-> void;
enum class Output : uint8_t { enum class Output : uint8_t {
kHeadphones = 0, kHeadphones = 0,
kBluetooth = 1, kBluetooth = 1,
@ -154,7 +158,10 @@ class NvsStorage {
Setting<int8_t> amp_left_bias_; Setting<int8_t> amp_left_bias_;
Setting<uint8_t> input_mode_; Setting<uint8_t> input_mode_;
Setting<uint8_t> output_mode_; Setting<uint8_t> output_mode_;
Setting<bluetooth::MacAndName> bt_preferred_; Setting<bluetooth::MacAndName> bt_preferred_;
Setting<std::vector<bluetooth::MacAndName>> bt_names_;
Setting<uint8_t> db_auto_index_; Setting<uint8_t> db_auto_index_;
util::LruCache<10, bluetooth::mac_addr_t, uint8_t> bt_volumes_; util::LruCache<10, bluetooth::mac_addr_t, uint8_t> bt_volumes_;

@ -26,6 +26,7 @@ static constexpr uint8_t kSchemaVersion = 1;
static constexpr char kKeyVersion[] = "ver"; static constexpr char kKeyVersion[] = "ver";
static constexpr char kKeyBluetoothPreferred[] = "bt_dev"; static constexpr char kKeyBluetoothPreferred[] = "bt_dev";
static constexpr char kKeyBluetoothVolumes[] = "bt_vols"; static constexpr char kKeyBluetoothVolumes[] = "bt_vols";
static constexpr char kKeyBluetoothNames[] = "bt_names";
static constexpr char kKeyOutput[] = "out"; static constexpr char kKeyOutput[] = "out";
static constexpr char kKeyBrightness[] = "bright"; static constexpr char kKeyBrightness[] = "bright";
static constexpr char kKeyAmpMaxVolume[] = "hp_vol_max"; static constexpr char kKeyAmpMaxVolume[] = "hp_vol_max";
@ -129,6 +130,44 @@ auto Setting<bluetooth::MacAndName>::store(nvs_handle_t nvs,
nvs_set_blob(nvs, name_, encoded.data(), encoded.size()); nvs_set_blob(nvs, name_, encoded.data(), encoded.size());
} }
template <>
auto Setting<std::vector<bluetooth::MacAndName>>::load(nvs_handle_t nvs)
-> std::optional<std::vector<bluetooth::MacAndName>> {
auto raw = nvs_get_string(nvs, name_);
if (!raw) {
return {};
}
auto [parsed, unused, err] = cppbor::parseWithViews(
reinterpret_cast<const uint8_t*>(raw->data()), raw->size());
if (parsed->type() != cppbor::MAP) {
return {};
}
std::vector<bluetooth::MacAndName> res;
for (const auto& i : *parsed->asMap()) {
auto mac = i.first->asViewBstr()->view();
auto name = i.second->asViewTstr()->view();
bluetooth::MacAndName entry{
.mac = {},
.name = {name.begin(), name.end()},
};
std::copy(mac.begin(), mac.end(), entry.mac.begin());
res.push_back(entry);
}
return res;
}
template <>
auto Setting<std::vector<bluetooth::MacAndName>>::store(
nvs_handle_t nvs,
std::vector<bluetooth::MacAndName> v) -> void {
cppbor::Map cbor{};
for (const auto& i : v) {
cbor.add(cppbor::Bstr{{i.mac.data(), i.mac.size()}}, cppbor::Tstr{i.name});
}
auto encoded = cbor.encode();
nvs_set_blob(nvs, name_, encoded.data(), encoded.size());
}
template <> template <>
auto Setting<NvsStorage::LraData>::load(nvs_handle_t nvs) auto Setting<NvsStorage::LraData>::load(nvs_handle_t nvs)
-> std::optional<NvsStorage::LraData> { -> std::optional<NvsStorage::LraData> {
@ -208,6 +247,7 @@ NvsStorage::NvsStorage(nvs_handle_t handle)
input_mode_(kKeyPrimaryInput), input_mode_(kKeyPrimaryInput),
output_mode_(kKeyOutput), output_mode_(kKeyOutput),
bt_preferred_(kKeyBluetoothPreferred), bt_preferred_(kKeyBluetoothPreferred),
bt_names_(kKeyBluetoothNames),
db_auto_index_(kKeyDbAutoIndex), db_auto_index_(kKeyDbAutoIndex),
bt_volumes_(), bt_volumes_(),
bt_volumes_dirty_(false) {} bt_volumes_dirty_(false) {}
@ -232,6 +272,7 @@ auto NvsStorage::Read() -> void {
input_mode_.read(handle_); input_mode_.read(handle_);
output_mode_.read(handle_); output_mode_.read(handle_);
bt_preferred_.read(handle_); bt_preferred_.read(handle_);
bt_names_.read(handle_);
db_auto_index_.read(handle_); db_auto_index_.read(handle_);
readBtVolumes(); readBtVolumes();
} }
@ -251,6 +292,7 @@ auto NvsStorage::Write() -> bool {
input_mode_.write(handle_); input_mode_.write(handle_);
output_mode_.write(handle_); output_mode_.write(handle_);
bt_preferred_.write(handle_); bt_preferred_.write(handle_);
bt_names_.write(handle_);
db_auto_index_.write(handle_); db_auto_index_.write(handle_);
writeBtVolumes(); writeBtVolumes();
return nvs_commit(handle_) == ESP_OK; return nvs_commit(handle_) == ESP_OK;
@ -341,6 +383,47 @@ auto NvsStorage::BluetoothVolume(const bluetooth::mac_addr_t& mac, uint8_t vol)
bt_volumes_.Put(mac, vol); bt_volumes_.Put(mac, vol);
} }
auto NvsStorage::BluetoothNames() -> std::vector<bluetooth::MacAndName> {
std::lock_guard<std::mutex> lock{mutex_};
return bt_names_.get().value_or(std::vector<bluetooth::MacAndName>{});
}
auto NvsStorage::BluetoothName(const bluetooth::mac_addr_t& mac,
std::optional<std::string> name) -> void {
std::lock_guard<std::mutex> lock{mutex_};
auto& val = bt_names_.get();
if (!val) {
val.emplace();
}
bool mut = false;
bool found = false;
for (auto it = val->begin(); it != val->end(); it++) {
if (it->mac == mac) {
if (name) {
it->name = *name;
} else {
val->erase(it);
}
found = true;
mut = true;
break;
}
}
if (!found && name) {
val->push_back(bluetooth::MacAndName{
.mac = mac,
.name = *name,
});
mut = true;
}
if (mut) {
bt_names_.set(*val);
}
}
auto NvsStorage::OutputMode() -> Output { auto NvsStorage::OutputMode() -> Output {
std::lock_guard<std::mutex> lock{mutex_}; std::lock_guard<std::mutex> lock{mutex_};
switch (output_mode_.get().value_or(0xFF)) { switch (output_mode_.get().value_or(0xFF)) {

@ -418,28 +418,21 @@ int CmdBtList(int argc, char** argv) {
return 1; return 1;
} }
auto devices = AppConsole::sServices->bluetooth().KnownDevices(); auto devices = AppConsole::sServices->bluetooth().knownDevices();
if (argc == 2) { if (argc == 2) {
int index = std::atoi(argv[1]); int index = std::atoi(argv[1]);
if (index < 0 || index >= devices.size()) { if (index < 0 || index >= devices.size()) {
std::cout << "index out of range" << std::endl; std::cout << "index out of range" << std::endl;
return -1; return -1;
} }
drivers::bluetooth::MacAndName dev{ AppConsole::sServices->bluetooth().pairedDevice(devices[index]);
.mac = devices[index].address,
.name = {devices[index].name.data(), devices[index].name.size()},
};
AppConsole::sServices->bluetooth().SetPreferredDevice(dev);
} else { } else {
std::cout << "mac\t\trssi\tname" << std::endl; std::cout << "mac\t\tname" << std::endl;
for (const auto& device : devices) { for (const auto& device : devices) {
for (size_t i = 0; i < device.address.size(); i++) { for (size_t i = 0; i < device.mac.size(); i++) {
std::cout << std::hex << std::setfill('0') << std::setw(2) std::cout << std::hex << std::setfill('0') << std::setw(2)
<< static_cast<int>(device.address[i]); << static_cast<int>(device.mac[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; std::cout << "\t" << device.name << std::endl;
} }
} }

@ -222,7 +222,12 @@ void AudioState::react(const system_fsm::BluetoothEvent& ev) {
auto simpleEvent = std::get<SimpleEvent>(ev.event); auto simpleEvent = std::get<SimpleEvent>(ev.event);
switch (simpleEvent) { switch (simpleEvent) {
case SimpleEvent::kConnectionStateChanged: { case SimpleEvent::kConnectionStateChanged: {
auto dev = sServices->bluetooth().ConnectedDevice(); auto bt = sServices->bluetooth();
if (bt.connectionState() !=
drivers::Bluetooth::ConnectionState::kConnected) {
return;
}
auto dev = sServices->bluetooth().pairedDevice();
if (!dev) { if (!dev) {
return; return;
} }
@ -341,7 +346,7 @@ auto AudioState::commitVolume() -> void {
if (mode == drivers::NvsStorage::Output::kHeadphones) { if (mode == drivers::NvsStorage::Output::kHeadphones) {
sServices->nvs().AmpCurrentVolume(vol); sServices->nvs().AmpCurrentVolume(vol);
} else if (mode == drivers::NvsStorage::Output::kBluetooth) { } else if (mode == drivers::NvsStorage::Output::kBluetooth) {
auto dev = sServices->bluetooth().ConnectedDevice(); auto dev = sServices->bluetooth().pairedDevice();
if (!dev) { if (!dev) {
return; return;
} }
@ -372,7 +377,7 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) {
sOutput = sI2SOutput; sOutput = sI2SOutput;
} else { } else {
// Ensure Bluetooth gets enabled if it's the default sink. // Ensure Bluetooth gets enabled if it's the default sink.
sServices->bluetooth().Enable(); sServices->bluetooth().enable(true);
sOutput = sBtOutput; sOutput = sBtOutput;
} }
sOutput->mode(IAudioOutput::Modes::kOnPaused); sOutput->mode(IAudioOutput::Modes::kOnPaused);

@ -13,6 +13,7 @@
#include <memory> #include <memory>
#include <variant> #include <variant>
#include "drivers/bluetooth.hpp"
#include "esp_err.h" #include "esp_err.h"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
@ -32,6 +33,8 @@ namespace audio {
static constexpr uint16_t kVolumeRange = 60; static constexpr uint16_t kVolumeRange = 60;
using ConnectionState = drivers::Bluetooth::ConnectionState;
BluetoothAudioOutput::BluetoothAudioOutput(drivers::Bluetooth& bt, BluetoothAudioOutput::BluetoothAudioOutput(drivers::Bluetooth& bt,
drivers::PcmBuffer& buffer, drivers::PcmBuffer& buffer,
tasks::WorkerPool& p) tasks::WorkerPool& p)
@ -45,9 +48,9 @@ BluetoothAudioOutput::~BluetoothAudioOutput() {}
auto BluetoothAudioOutput::changeMode(Modes mode) -> void { auto BluetoothAudioOutput::changeMode(Modes mode) -> void {
if (mode == Modes::kOnPlaying) { if (mode == Modes::kOnPlaying) {
bluetooth_.SetSource(&buffer_); bluetooth_.source(&buffer_);
} else { } else {
bluetooth_.SetSource(nullptr); bluetooth_.source(nullptr);
} }
} }
@ -60,7 +63,7 @@ auto BluetoothAudioOutput::SetVolume(uint16_t v) -> void {
bg_worker_.Dispatch<void>([&]() { bg_worker_.Dispatch<void>([&]() {
float factor = float factor =
pow(10, static_cast<double>(kVolumeRange) * (volume_ - 100) / 100 / 20); pow(10, static_cast<double>(kVolumeRange) * (volume_ - 100) / 100 / 20);
bluetooth_.SetVolumeFactor(factor); bluetooth_.softVolume(factor);
}); });
} }
@ -95,7 +98,7 @@ auto BluetoothAudioOutput::SetVolumeDb(int_fast16_t val) -> bool {
} }
auto BluetoothAudioOutput::AdjustVolumeUp() -> bool { auto BluetoothAudioOutput::AdjustVolumeUp() -> bool {
if (volume_ == 100 || !bluetooth_.IsConnected()) { if (volume_ == 100) {
return false; return false;
} }
volume_++; volume_++;
@ -104,7 +107,7 @@ auto BluetoothAudioOutput::AdjustVolumeUp() -> bool {
} }
auto BluetoothAudioOutput::AdjustVolumeDown() -> bool { auto BluetoothAudioOutput::AdjustVolumeDown() -> bool {
if (volume_ == 0 || !bluetooth_.IsConnected()) { if (volume_ == 0) {
return false; return false;
} }
volume_--; volume_--;

@ -289,13 +289,14 @@ static void pushTrack(lua_State* L, const audio::TrackInfo& track) {
lua_settable(L, -3); lua_settable(L, -3);
} }
static void pushDevice(lua_State* L, const drivers::bluetooth::Device& dev) { static void pushDevice(lua_State* L,
const drivers::bluetooth::MacAndName& dev) {
lua_createtable(L, 0, 4); lua_createtable(L, 0, 4);
lua_pushliteral(L, "address"); lua_pushliteral(L, "address");
auto* mac = reinterpret_cast<drivers::bluetooth::mac_addr_t*>( auto* mac = reinterpret_cast<drivers::bluetooth::mac_addr_t*>(
lua_newuserdata(L, sizeof(drivers::bluetooth::mac_addr_t))); lua_newuserdata(L, sizeof(drivers::bluetooth::mac_addr_t)));
*mac = dev.address; *mac = dev.mac;
lua_rawset(L, -3); lua_rawset(L, -3);
// What I just did there was perfectly safe. Look, I can prove it: // What I just did there was perfectly safe. Look, I can prove it:
@ -308,14 +309,8 @@ static void pushDevice(lua_State* L, const drivers::bluetooth::Device& dev) {
lua_pushlstring(L, dev.name.data(), dev.name.size()); lua_pushlstring(L, dev.name.data(), dev.name.size());
lua_rawset(L, -3); lua_rawset(L, -3);
// FIXME: This field deserves a little more structure. // FIXME: Plumbing through device classes to here could be useful if we ever
lua_pushliteral(L, "class"); // want to show cute little icons.
lua_pushinteger(L, dev.class_of_device);
lua_rawset(L, -3);
lua_pushliteral(L, "signal_strength");
lua_pushinteger(L, dev.signal_strength);
lua_rawset(L, -3);
} }
auto Property::pushValue(lua_State& s) -> int { auto Property::pushValue(lua_State& s) -> int {
@ -332,10 +327,12 @@ auto Property::pushValue(lua_State& s) -> int {
lua_pushstring(&s, arg.c_str()); lua_pushstring(&s, arg.c_str());
} else if constexpr (std::is_same_v<T, audio::TrackInfo>) { } else if constexpr (std::is_same_v<T, audio::TrackInfo>) {
pushTrack(&s, arg); pushTrack(&s, arg);
} else if constexpr (std::is_same_v<T, drivers::bluetooth::Device>) { } else if constexpr (std::is_same_v<T,
drivers::bluetooth::MacAndName>) {
pushDevice(&s, arg); pushDevice(&s, arg);
} else if constexpr (std::is_same_v< } else if constexpr (std::is_same_v<
T, std::vector<drivers::bluetooth::Device>>) { T,
std::vector<drivers::bluetooth::MacAndName>>) {
lua_createtable(&s, arg.size(), 0); lua_createtable(&s, arg.size(), 0);
size_t i = 1; size_t i = 1;
for (const auto& dev : arg) { for (const auto& dev : arg) {
@ -364,15 +361,10 @@ auto popRichType(lua_State* L) -> LuaValue {
lua_pushliteral(L, "name"); lua_pushliteral(L, "name");
lua_gettable(L, -2); lua_gettable(L, -2);
std::pmr::string name = lua_tostring(L, -1); std::string name = lua_tostring(L, -1);
lua_pop(L, 1); lua_pop(L, 1);
return drivers::bluetooth::Device{ return drivers::bluetooth::MacAndName{.mac = mac, .name = name};
.address = mac,
.name = name,
.class_of_device = 0,
.signal_strength = 0,
};
} }
return std::monostate{}; return std::monostate{};

@ -24,8 +24,8 @@ using LuaValue = std::variant<std::monostate,
bool, bool,
std::string, std::string,
audio::TrackInfo, audio::TrackInfo,
drivers::bluetooth::Device, drivers::bluetooth::MacAndName,
std::vector<drivers::bluetooth::Device>>; std::vector<drivers::bluetooth::MacAndName>>;
using LuaFunction = std::function<int(lua_State*)>; using LuaFunction = std::function<int(lua_State*)>;

@ -104,8 +104,7 @@ auto Booting::entry() -> void {
ESP_LOGI(kTag, "init bluetooth"); ESP_LOGI(kTag, "init bluetooth");
sServices->bluetooth(std::make_unique<drivers::Bluetooth>( sServices->bluetooth(std::make_unique<drivers::Bluetooth>(
sServices->nvs(), sServices->bg_worker())); sServices->nvs(), sServices->bg_worker(), bt_event_cb));
sServices->bluetooth().SetEventHandler(bt_event_cb);
BootComplete ev{.services = sServices}; BootComplete ev{.services = sServices};
events::Audio().Dispatch(ev); events::Audio().Dispatch(ev);

@ -7,11 +7,13 @@
#include "ui/ui_fsm.hpp" #include "ui/ui_fsm.hpp"
#include <stdint.h> #include <stdint.h>
#include <algorithm>
#include <memory> #include <memory>
#include <memory_resource> #include <memory_resource>
#include <variant> #include <variant>
#include "FreeRTOSConfig.h" #include "FreeRTOSConfig.h"
#include "drivers/bluetooth.hpp"
#include "lvgl.h" #include "lvgl.h"
#include "core/lv_group.h" #include "core/lv_group.h"
@ -100,32 +102,57 @@ lua::Property UiState::sBluetoothEnabled{
if (!std::holds_alternative<bool>(val)) { if (!std::holds_alternative<bool>(val)) {
return false; return false;
} }
// Note we always write the OutputMode NVS change before actually
// modifying the peripheral. We do this because ESP-IDF's Bluetooth stack
// breaks in surprising ways when repeatedly initialised/uninitialised.
if (std::get<bool>(val)) { if (std::get<bool>(val)) {
sServices->nvs().OutputMode(drivers::NvsStorage::Output::kBluetooth); sServices->nvs().OutputMode(drivers::NvsStorage::Output::kBluetooth);
sServices->bluetooth().Enable(); sServices->bluetooth().enable(true);
} else { } else {
sServices->nvs().OutputMode(drivers::NvsStorage::Output::kHeadphones); sServices->nvs().OutputMode(drivers::NvsStorage::Output::kHeadphones);
sServices->bluetooth().Disable(); sServices->bluetooth().enable(false);
} }
events::Audio().Dispatch(audio::OutputModeChanged{}); events::Audio().Dispatch(audio::OutputModeChanged{});
return true; return true;
}}; }};
lua::Property UiState::sBluetoothConnecting{false};
lua::Property UiState::sBluetoothConnected{false}; lua::Property UiState::sBluetoothConnected{false};
lua::Property UiState::sBluetoothDiscovering{
false, [](const lua::LuaValue& val) {
if (!std::holds_alternative<bool>(val)) {
return false;
}
// Note we always write the OutputMode NVS change before actually
// modifying the peripheral. We do this because ESP-IDF's Bluetooth stack
// breaks in surprising ways when repeatedly initialised/uninitialised.
if (std::get<bool>(val)) {
sServices->bluetooth().discoveryEnabled(true);
} else {
sServices->bluetooth().discoveryEnabled(false);
}
return true;
}};
lua::Property UiState::sBluetoothPairedDevice{ lua::Property UiState::sBluetoothPairedDevice{
std::monostate{}, [](const lua::LuaValue& val) { std::monostate{}, [](const lua::LuaValue& val) {
if (std::holds_alternative<drivers::bluetooth::Device>(val)) { if (std::holds_alternative<drivers::bluetooth::MacAndName>(val)) {
auto dev = std::get<drivers::bluetooth::Device>(val); auto dev = std::get<drivers::bluetooth::MacAndName>(val);
sServices->bluetooth().SetPreferredDevice( sServices->bluetooth().pairedDevice(dev);
drivers::bluetooth::MacAndName{ } else if (std::holds_alternative<std::monostate>(val)) {
.mac = dev.address, sServices->bluetooth().pairedDevice({});
.name = {dev.name.data(), dev.name.size()}, } else {
}); // Don't accept any other types.
}
return false; return false;
}
return true;
}}; }};
lua::Property UiState::sBluetoothDevices{
std::vector<drivers::bluetooth::Device>{}}; lua::Property UiState::sBluetoothKnownDevices{
std::vector<drivers::bluetooth::MacAndName>{}};
lua::Property UiState::sBluetoothDiscoveredDevices{
std::vector<drivers::bluetooth::MacAndName>{}};
lua::Property UiState::sPlaybackPlaying{ lua::Property UiState::sPlaybackPlaying{
false, [](const lua::LuaValue& val) { false, [](const lua::LuaValue& val) {
@ -412,8 +439,13 @@ void UiState::react(const audio::VolumeLimitChanged& ev) {
void UiState::react(const system_fsm::BluetoothEvent& ev) { void UiState::react(const system_fsm::BluetoothEvent& ev) {
using drivers::bluetooth::SimpleEvent; using drivers::bluetooth::SimpleEvent;
using ConnectionState = drivers::Bluetooth::ConnectionState;
ConnectionState state;
auto bt = sServices->bluetooth(); auto bt = sServices->bluetooth();
auto dev = bt.ConnectedDevice();
std::optional<drivers::bluetooth::MacAndName> dev;
std::vector<drivers::bluetooth::MacAndName> devs;
if (std::holds_alternative<SimpleEvent>(ev.event)) { if (std::holds_alternative<SimpleEvent>(ev.event)) {
switch (std::get<SimpleEvent>(ev.event)) { switch (std::get<SimpleEvent>(ev.event)) {
case SimpleEvent::kPlayPause: case SimpleEvent::kPlayPause:
@ -438,30 +470,36 @@ void UiState::react(const system_fsm::BluetoothEvent& ev) {
break; break;
case SimpleEvent::kFastForward: case SimpleEvent::kFastForward:
break; break;
case SimpleEvent::kKnownDevicesChanged:
sBluetoothDevices.setDirect(bt.KnownDevices());
break;
case SimpleEvent::kConnectionStateChanged: case SimpleEvent::kConnectionStateChanged:
sBluetoothConnected.setDirect(bt.IsConnected()); state = bt.connectionState();
sBluetoothConnected.setDirect(state == ConnectionState::kConnected);
sBluetoothConnecting.setDirect(state == ConnectionState::kConnecting);
break;
case SimpleEvent::kPairedDeviceChanged:
dev = bt.pairedDevice();
if (dev) { if (dev) {
sBluetoothPairedDevice.setDirect(drivers::bluetooth::Device{ sBluetoothPairedDevice.setDirect(*dev);
.address = dev->mac,
.name = {dev->name.data(), dev->name.size()},
.class_of_device = 0,
.signal_strength = 0,
});
} else { } else {
sBluetoothPairedDevice.setDirect(std::monostate{}); sBluetoothPairedDevice.setDirect(std::monostate{});
} }
break; break;
case SimpleEvent::kPreferredDeviceChanged: case SimpleEvent::kKnownDevicesChanged:
sBluetoothKnownDevices.setDirect(bt.knownDevices());
break;
case SimpleEvent::kDiscoveryChanged:
sBluetoothDiscovering.setDirect(bt.discoveryEnabled());
// Dump the old list of discovered devices when discovery is toggled.
sBluetoothDiscoveredDevices.setDirect(bt.discoveredDevices());
break;
case SimpleEvent::kDeviceDiscovered:
sBluetoothDiscoveredDevices.setDirect(bt.discoveredDevices());
break; break;
default: default:
break; break;
} }
} else if (std::holds_alternative<drivers::bluetooth::RemoteVolumeChanged>( } else if (std::holds_alternative<drivers::bluetooth::RemoteVolumeChanged>(
ev.event)) { ev.event)) {
// Todo: Do something with this (ie, bt volume alert) // TODO: Do something with this (ie, bt volume alert)
ESP_LOGI( ESP_LOGI(
kTag, "Recieved volume changed event with new volume: %d", kTag, "Recieved volume changed event with new volume: %d",
std::get<drivers::bluetooth::RemoteVolumeChanged>(ev.event).new_vol); std::get<drivers::bluetooth::RemoteVolumeChanged>(ev.event).new_vol);
@ -517,12 +555,15 @@ void Lua::entry() {
{"battery_millivolts", &sBatteryMv}, {"battery_millivolts", &sBatteryMv},
{"plugged_in", &sBatteryCharging}, {"plugged_in", &sBatteryCharging},
}); });
registry.AddPropertyModule("bluetooth", registry.AddPropertyModule(
{ "bluetooth", {
{"enabled", &sBluetoothEnabled}, {"enabled", &sBluetoothEnabled},
{"connected", &sBluetoothConnected}, {"connected", &sBluetoothConnected},
{"connecting", &sBluetoothConnecting},
{"discovering", &sBluetoothDiscovering},
{"paired_device", &sBluetoothPairedDevice}, {"paired_device", &sBluetoothPairedDevice},
{"devices", &sBluetoothDevices}, {"discovered_devices", &sBluetoothDiscoveredDevices},
{"known_devices", &sBluetoothKnownDevices},
}); });
registry.AddPropertyModule("playback", { registry.AddPropertyModule("playback", {
{"playing", &sPlaybackPlaying}, {"playing", &sPlaybackPlaying},
@ -601,9 +642,12 @@ void Lua::entry() {
sDatabaseAutoUpdate.setDirect(sServices->nvs().DbAutoIndex()); sDatabaseAutoUpdate.setDirect(sServices->nvs().DbAutoIndex());
auto bt = sServices->bluetooth(); auto bt = sServices->bluetooth();
sBluetoothEnabled.setDirect(bt.IsEnabled()); sBluetoothEnabled.setDirect(bt.enabled());
sBluetoothConnected.setDirect(bt.IsConnected()); auto paired = bt.pairedDevice();
sBluetoothDevices.setDirect(bt.KnownDevices()); if (paired) {
sBluetoothPairedDevice.setDirect(*paired);
}
sBluetoothKnownDevices.setDirect(bt.knownDevices());
if (sServices->sd() == drivers::SdState::kMounted) { if (sServices->sd() == drivers::SdState::kMounted) {
sLua->RunScript("/sdcard/config.lua"); sLua->RunScript("/sdcard/config.lua");

@ -102,9 +102,12 @@ class UiState : public tinyfsm::Fsm<UiState> {
static lua::Property sBatteryCharging; static lua::Property sBatteryCharging;
static lua::Property sBluetoothEnabled; static lua::Property sBluetoothEnabled;
static lua::Property sBluetoothConnecting;
static lua::Property sBluetoothConnected; static lua::Property sBluetoothConnected;
static lua::Property sBluetoothDiscovering;
static lua::Property sBluetoothPairedDevice; static lua::Property sBluetoothPairedDevice;
static lua::Property sBluetoothDevices; static lua::Property sBluetoothKnownDevices;
static lua::Property sBluetoothDiscoveredDevices;
static lua::Property sPlaybackPlaying; static lua::Property sPlaybackPlaying;

Loading…
Cancel
Save