diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 54ffa51d..659340af 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -12,6 +12,7 @@ #include #include "audio_sink.hpp" +#include "bluetooth_types.hpp" #include "esp_log.h" #include "freertos/portmacro.h" #include "freertos/projdefs.h" @@ -51,14 +52,25 @@ std::shared_ptr AudioState::sOutput; std::optional AudioState::sCurrentTrack; bool AudioState::sIsPlaybackAllowed; -void AudioState::react(const system_fsm::KeyLockChanged& ev) { - if (ev.locking && sServices) { - sServices->nvs().AmpCurrentVolume(sOutput->GetVolume()); +void AudioState::react(const system_fsm::BluetoothEvent& ev) { + if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { + return; + } + auto dev = sServices->bluetooth().ConnectedDevice(); + if (!dev) { + return; } + auto vols = sServices->nvs().BluetoothVolumes(); + sBtOutput->SetVolume(vols.Get(dev->mac).value_or(10)); + events::Ui().Dispatch(VolumeChanged{ + .percent = sOutput->GetVolumePct(), + .db = sOutput->GetVolumeDb(), + }); } void AudioState::react(const StepUpVolume& ev) { if (sOutput->AdjustVolumeUp()) { + commitVolume(); events::Ui().Dispatch(VolumeChanged{ .percent = sOutput->GetVolumePct(), .db = sOutput->GetVolumeDb(), @@ -68,6 +80,7 @@ void AudioState::react(const StepUpVolume& ev) { void AudioState::react(const StepDownVolume& ev) { if (sOutput->AdjustVolumeDown()) { + commitVolume(); events::Ui().Dispatch(VolumeChanged{ .percent = sOutput->GetVolumePct(), .db = sOutput->GetVolumeDb(), @@ -126,6 +139,14 @@ void AudioState::react(const OutputModeChanged& ev) { } sOutput->SetMode(IAudioOutput::Modes::kOnPaused); sSampleConverter->SetOutput(sOutput); + + // Bluetooth volume isn't 'changed' until we've connected to a device. + if (new_mode == drivers::NvsStorage::Output::kHeadphones) { + events::Ui().Dispatch(VolumeChanged{ + .percent = sOutput->GetVolumePct(), + .db = sOutput->GetVolumeDb(), + }); + } } auto AudioState::playTrack(database::TrackId id) -> void { @@ -139,6 +160,22 @@ auto AudioState::playTrack(database::TrackId id) -> void { }); } +auto AudioState::commitVolume() -> void { + auto mode = sServices->nvs().OutputMode(); + auto vol = sOutput->GetVolume(); + if (mode == drivers::NvsStorage::Output::kHeadphones) { + sServices->nvs().AmpCurrentVolume(vol); + } else if (mode == drivers::NvsStorage::Output::kBluetooth) { + auto dev = sServices->bluetooth().ConnectedDevice(); + if (!dev) { + return; + } + auto vols = sServices->nvs().BluetoothVolumes(); + vols.Put(dev->mac, vol); + sServices->nvs().BluetoothVolumes(vols); + } +} + auto AudioState::readyToPlay() -> bool { return sCurrentTrack.has_value() && sIsPlaybackAllowed; } @@ -283,7 +320,7 @@ void Playback::exit() { // Stash the current volume now, in case it changed during playback, since we // might be powering off soon. - sServices->nvs().AmpCurrentVolume(sOutput->GetVolume()); + commitVolume(); events::System().Dispatch(PlaybackStopped{}); events::Ui().Dispatch(PlaybackStopped{}); diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index 884af8a8..29ec489a 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -52,8 +52,9 @@ class AudioState : public tinyfsm::Fsm { void react(const OutputModeChanged&); virtual void react(const system_fsm::BootComplete&) {} - virtual void react(const system_fsm::KeyLockChanged&); + virtual void react(const system_fsm::KeyLockChanged&) {}; virtual void react(const system_fsm::StorageMounted&) {} + virtual void react(const system_fsm::BluetoothEvent&); virtual void react(const PlayFile&) {} virtual void react(const QueueUpdate&) {} @@ -67,6 +68,7 @@ class AudioState : public tinyfsm::Fsm { protected: auto playTrack(database::TrackId id) -> void; + auto commitVolume() -> void; static std::shared_ptr sServices; @@ -88,6 +90,9 @@ namespace states { class Uninitialised : public AudioState { public: void react(const system_fsm::BootComplete&) override; + + void react(const system_fsm::BluetoothEvent&) override {}; + using AudioState::react; }; diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt index 4155ed66..0b7ead94 100644 --- a/src/drivers/CMakeLists.txt +++ b/src/drivers/CMakeLists.txt @@ -9,5 +9,5 @@ idf_component_register( "spiffs.cpp" INCLUDE_DIRS "include" REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks" "nvs_flash" "spiffs" - "bt" "tinyfsm") + "bt" "tinyfsm" "util") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index 1a303748..84c81de0 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -103,21 +103,12 @@ auto Bluetooth::IsConnected() -> bool { return bluetooth::BluetoothState::is_in_state(); } -auto Bluetooth::ConnectedDevice() -> std::optional { +auto Bluetooth::ConnectedDevice() -> std::optional { auto lock = bluetooth::BluetoothState::lock(); if (!bluetooth::BluetoothState::is_in_state()) { return {}; } - auto looking_for = bluetooth::BluetoothState::preferred_device(); - if (!looking_for) { - return {}; - } - for (const auto& dev : bluetooth::BluetoothState::devices()) { - if (dev.address == looking_for->mac) { - return dev; - } - } - return {}; + return bluetooth::BluetoothState::preferred_device(); } auto Bluetooth::KnownDevices() -> std::vector { diff --git a/src/drivers/include/bluetooth.hpp b/src/drivers/include/bluetooth.hpp index 5fdd527c..988c7e93 100644 --- a/src/drivers/include/bluetooth.hpp +++ b/src/drivers/include/bluetooth.hpp @@ -35,7 +35,7 @@ class Bluetooth { auto IsEnabled() -> bool; auto IsConnected() -> bool; - auto ConnectedDevice() -> std::optional; + auto ConnectedDevice() -> std::optional; auto KnownDevices() -> std::vector; diff --git a/src/drivers/include/nvs.hpp b/src/drivers/include/nvs.hpp index 1184b72c..264d9784 100644 --- a/src/drivers/include/nvs.hpp +++ b/src/drivers/include/nvs.hpp @@ -15,6 +15,7 @@ #include "bluetooth_types.hpp" #include "tasks.hpp" +#include "lru_cache.hpp" namespace drivers { @@ -28,6 +29,11 @@ class NvsStorage { auto PreferredBluetoothDevice() -> std::optional; auto PreferredBluetoothDevice(std::optional) -> bool; + using BtVolumes = util::LruCache<10, bluetooth::mac_addr_t, uint8_t> ; + + auto BluetoothVolumes() -> BtVolumes; + auto BluetoothVolumes(const BtVolumes&) -> bool; + enum class Output : uint8_t { kHeadphones = 0, kBluetooth = 1, diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index ab623d01..f7687dcb 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -13,6 +13,8 @@ #include "bluetooth.hpp" #include "bluetooth_types.hpp" +#include "cppbor.h" +#include "cppbor_parse.h" #include "esp_log.h" #include "nvs.h" #include "nvs_flash.h" @@ -27,6 +29,7 @@ static constexpr uint8_t kSchemaVersion = 1; static constexpr char kKeyVersion[] = "ver"; static constexpr char kKeyBluetoothMac[] = "bt_mac"; static constexpr char kKeyBluetoothName[] = "bt_name"; +static constexpr char kKeyBluetoothVolumes[] = "bt_vols"; static constexpr char kKeyOutput[] = "out"; static constexpr char kKeyBrightness[] = "bright"; static constexpr char kKeyAmpMaxVolume[] = "hp_vol_max"; @@ -135,6 +138,101 @@ auto NvsStorage::PreferredBluetoothDevice( return nvs_commit(handle_) == ESP_OK; } +class VolumesParseClient : public cppbor::ParseClient { + public: + VolumesParseClient(NvsStorage::BtVolumes& out) + : state_(State::kInit), mac_(), vol_(), out_(out) {} + + ParseClient* item(std::unique_ptr& item, + const uint8_t* hdrBegin, + const uint8_t* valueBegin, + const uint8_t* end) override { + if (item->type() == cppbor::ARRAY) { + if (state_ == State::kInit) { + ESP_LOGI(kTag, "enter root"); + state_ = State::kRoot; + } else if (state_ == State::kRoot) { + ESP_LOGI(kTag, "enter pair"); + state_ = State::kPair; + } + } else if (item->type() == cppbor::BSTR && state_ == State::kPair) { + ESP_LOGI(kTag, "get str"); + auto data = item->asBstr()->value(); + mac_.emplace(); + std::copy(data.begin(), data.end(), mac_->begin()); + } else if (item->type() == cppbor::UINT && state_ == State::kPair) { + vol_ = + std::clamp(item->asUint()->unsignedValue(), 0, UINT8_MAX); + } + return this; + } + + ParseClient* itemEnd(std::unique_ptr& item, + const uint8_t* hdrBegin, + const uint8_t* valueBegin, + const uint8_t* end) override { + if (item->type() == cppbor::ARRAY) { + if (state_ == State::kRoot) { + state_ = State::kFinished; + } else if (state_ == State::kPair) { + if (vol_ && mac_) { + out_.Put(*mac_, *vol_); + } + mac_.reset(); + vol_.reset(); + state_ = State::kRoot; + } + } + return this; + } + + void error(const uint8_t* position, + const std::string& errorMessage) override {} + + private: + enum class State { + kInit, + kRoot, + kPair, + kFinished, + }; + + State state_; + std::optional mac_; + std::optional vol_; + NvsStorage::BtVolumes& out_; +}; + +auto NvsStorage::BluetoothVolumes() -> BtVolumes { + BtVolumes out; + size_t encoded_len = 0; + if (nvs_get_str(handle_, kKeyBluetoothVolumes, NULL, &encoded_len) != + ESP_OK) { + return out; + } + auto encoded = std::unique_ptr{new char[encoded_len]}; + if (nvs_get_str(handle_, kKeyBluetoothVolumes, encoded.get(), &encoded_len) != + ESP_OK) { + return out; + } + VolumesParseClient client{out}; + auto data = reinterpret_cast(encoded.get()); + cppbor::parse(data, data + encoded_len, &client); + return out; +} + +auto NvsStorage::BluetoothVolumes(const BtVolumes& vols) -> bool { + cppbor::Array enc; + auto vols_list = vols.Get(); + for (auto vol = vols_list.rbegin(); vol < vols_list.rend(); vol++) { + enc.add(cppbor::Array{cppbor::Bstr{{vol->first.data(), vol->first.size()}}, + cppbor::Uint{vol->second}}); + } + std::string encoded = enc.toString(); + nvs_set_str(handle_, kKeyBluetoothVolumes, encoded.c_str()); + return nvs_commit(handle_) == ESP_OK; +} + auto NvsStorage::OutputMode() -> Output { uint8_t out = 0; nvs_get_u8(handle_, kKeyOutput, &out); diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index b166a02c..41f46df2 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -47,6 +47,7 @@ namespace states { static auto bt_event_cb(drivers::bluetooth::Event ev) -> void { events::Ui().Dispatch(BluetoothEvent{.event = ev}); + events::Audio().Dispatch(BluetoothEvent{.event = ev}); } static const TickType_t kInterruptCheckPeriod = pdMS_TO_TICKS(100); diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 12584ec7..630238e7 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -326,14 +326,20 @@ void UiState::react(const audio::VolumeLimitChanged& ev) { void UiState::react(const system_fsm::BluetoothEvent& ev) { auto bt = sServices->bluetooth(); + auto dev = bt.ConnectedDevice(); switch (ev.event) { case drivers::bluetooth::Event::kKnownDevicesChanged: sBluetoothDevices.Update(bt.KnownDevices()); break; case drivers::bluetooth::Event::kConnectionStateChanged: sBluetoothConnected.Update(bt.IsConnected()); - if (bt.ConnectedDevice()) { - sBluetoothPairedDevice.Update(bt.ConnectedDevice().value()); + if (dev) { + sBluetoothPairedDevice.Update(drivers::bluetooth::Device{ + .address = dev->mac, + .name = {dev->name.data(), dev->name.size()}, + .class_of_device = 0, + .signal_strength = 0, + }); } else { sBluetoothPairedDevice.Update(std::monostate{}); } @@ -457,9 +463,6 @@ void Lua::entry() { auto bt = sServices->bluetooth(); sBluetoothEnabled.Update(bt.IsEnabled()); sBluetoothConnected.Update(bt.IsConnected()); - if (bt.ConnectedDevice()) { - sBluetoothPairedDevice.Update(bt.ConnectedDevice().value()); - } sBluetoothDevices.Update(bt.KnownDevices()); sCurrentScreen.reset(); diff --git a/src/util/include/lru_cache.hpp b/src/util/include/lru_cache.hpp index 41293901..606a6387 100644 --- a/src/util/include/lru_cache.hpp +++ b/src/util/include/lru_cache.hpp @@ -10,9 +10,11 @@ #include #include #include +#include #include #include #include + #include "memory_resource.hpp" namespace util { @@ -64,9 +66,16 @@ class LruCache { key_to_it_.clear(); } + auto Get() const -> std::vector> { + std::vector> out; + out.resize(entries_.size()); + std::copy(entries_.begin(), entries_.end(), out.begin()); + return out; + } + private: std::pmr::list> entries_; - std::pmr::unordered_map key_to_it_; + std::pmr::map key_to_it_; }; } // namespace util