From 26df5c4a7f54d493a09724a5f4f8f4a3a5c90f31 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 12 Feb 2024 17:07:39 +1100 Subject: [PATCH 1/7] Remember per-device bluetooth volume --- src/audio/audio_fsm.cpp | 45 ++++++++++++-- src/audio/include/audio_fsm.hpp | 7 ++- src/drivers/CMakeLists.txt | 2 +- src/drivers/bluetooth.cpp | 13 +--- src/drivers/include/bluetooth.hpp | 2 +- src/drivers/include/nvs.hpp | 6 ++ src/drivers/nvs.cpp | 98 +++++++++++++++++++++++++++++++ src/system_fsm/booting.cpp | 1 + src/ui/ui_fsm.cpp | 13 ++-- src/util/include/lru_cache.hpp | 11 +++- 10 files changed, 174 insertions(+), 24 deletions(-) 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 From cb0c42060b83e6dddd6f71e303cd2d42170baa7a Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 12 Feb 2024 17:08:00 +1100 Subject: [PATCH 2/7] more i2s/bt drain buffer size tweaks --- src/audio/audio_fsm.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 659340af..bb0aef6d 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -199,11 +199,11 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { sServices = ev.services; constexpr size_t kDrainBufferSize = - drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 8; + drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 2 * 8; ESP_LOGI(kTag, "allocating drain buffer, size %u KiB", kDrainBufferSize / 1024); StreamBufferHandle_t stream = xStreamBufferCreateWithCaps( - kDrainBufferSize, sizeof(sample::Sample) * 2, MALLOC_CAP_DMA); + kDrainBufferSize, sizeof(sample::Sample), MALLOC_CAP_DMA); sFileSource.reset( new FatfsAudioInput(sServices->tag_parser(), sServices->bg_worker())); From a37b5173560f56567b27f33ea1df60c51c6e29ce Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 12 Feb 2024 17:08:19 +1100 Subject: [PATCH 3/7] Improve handing of tracks with missing titles --- src/audio/fatfs_audio_input.cpp | 3 +++ src/database/track.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index de64b14b..7726a94a 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -111,6 +111,9 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { ESP_LOGE(kTag, "failed to read tags"); return false; } + if (!tags->title()) { + tags->title(path); + } auto stream_type = ContainerToStreamType(tags->encoding()); if (!stream_type.has_value()) { diff --git a/src/database/track.cpp b/src/database/track.cpp index 943606ce..141a9b7e 100644 --- a/src/database/track.cpp +++ b/src/database/track.cpp @@ -302,6 +302,6 @@ auto Track::TitleOrFilename() const -> std::pmr::string { if (start == std::pmr::string::npos) { return data().filepath; } - return data().filepath.substr(start); + return data().filepath.substr(start + 1); } } // namespace database From 03c0968168090b1093bda7e05874c201ae58b57b Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 12 Feb 2024 17:09:17 +1100 Subject: [PATCH 4/7] Let's try avoiding allocs in the bt stacks --- sdkconfig.common | 1 - 1 file changed, 1 deletion(-) diff --git a/sdkconfig.common b/sdkconfig.common index 0fbe730c..7ae9aea2 100644 --- a/sdkconfig.common +++ b/sdkconfig.common @@ -13,7 +13,6 @@ CONFIG_BT_A2DP_ENABLE=y CONFIG_BT_STACK_NO_LOG=y # CONFIG_BT_MULTI_CONNECTION_ENBALE is not set CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST=y -CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY=y CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=y CONFIG_SPI_MASTER_IN_IRAM=y # CONFIG_SPI_SLAVE_ISR_IN_IRAM is not set From c5cef16c956a5c4df91d93b5aa167f0c9257ac43 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 12 Feb 2024 17:16:38 +1100 Subject: [PATCH 5/7] Remove some logging whoopsies --- src/drivers/nvs.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index f7687dcb..ed85487f 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -149,14 +149,11 @@ class VolumesParseClient : public cppbor::ParseClient { 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()); From 79879fbfa075007ba3db30086b0730016028daf5 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 12 Feb 2024 17:17:41 +1100 Subject: [PATCH 6/7] version bump --- tools/cmake/common.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/cmake/common.cmake b/tools/cmake/common.cmake index 65e9a3fb..2665c67b 100644 --- a/tools/cmake/common.cmake +++ b/tools/cmake/common.cmake @@ -5,7 +5,7 @@ # For more information about build system see # https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html -set(PROJECT_VER "0.4.1") +set(PROJECT_VER "0.5.0") # esp-idf sets the C++ standard weird. Set cmake vars to match. set(CMAKE_CXX_STANDARD 23) From cb379f4bc3c51eacf80b786566ab3c2675191164 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 13 Feb 2024 10:12:04 +1100 Subject: [PATCH 7/7] Cache pending nvs writes in memory Includes refactoring nvs settings to be a bit less duplicated --- src/audio/audio_fsm.cpp | 7 +- src/drivers/include/bluetooth_types.hpp | 2 + src/drivers/include/nvs.hpp | 88 ++++- src/drivers/nvs.cpp | 408 +++++++++++++++--------- src/system_fsm/idle.cpp | 3 + 5 files changed, 332 insertions(+), 176 deletions(-) diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 95abfa2a..ba6e5ffe 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -60,8 +60,7 @@ void AudioState::react(const system_fsm::BluetoothEvent& ev) { if (!dev) { return; } - auto vols = sServices->nvs().BluetoothVolumes(); - sBtOutput->SetVolume(vols.Get(dev->mac).value_or(10)); + sBtOutput->SetVolume(sServices->nvs().BluetoothVolume(dev->mac)); events::Ui().Dispatch(VolumeChanged{ .percent = sOutput->GetVolumePct(), .db = sOutput->GetVolumeDb(), @@ -170,9 +169,7 @@ auto AudioState::commitVolume() -> void { if (!dev) { return; } - auto vols = sServices->nvs().BluetoothVolumes(); - vols.Put(dev->mac, vol); - sServices->nvs().BluetoothVolumes(vols); + sServices->nvs().BluetoothVolume(dev->mac, vol); } } diff --git a/src/drivers/include/bluetooth_types.hpp b/src/drivers/include/bluetooth_types.hpp index 74434182..7cfb236f 100644 --- a/src/drivers/include/bluetooth_types.hpp +++ b/src/drivers/include/bluetooth_types.hpp @@ -14,6 +14,8 @@ typedef std::array mac_addr_t; struct MacAndName { mac_addr_t mac; std::string name; + + bool operator==(const MacAndName&) const = default; }; struct Device { diff --git a/src/drivers/include/nvs.hpp b/src/drivers/include/nvs.hpp index 264d9784..197591d5 100644 --- a/src/drivers/include/nvs.hpp +++ b/src/drivers/include/nvs.hpp @@ -14,47 +14,87 @@ #include "nvs.h" #include "bluetooth_types.hpp" -#include "tasks.hpp" #include "lru_cache.hpp" +#include "tasks.hpp" namespace drivers { +/* + * Wrapper for a single NVS setting, with its backing value cached in memory. + * NVS values that are just plain old data should generally use these for + * simpler implementation. + */ +template +class Setting { + public: + Setting(const char* name) : name_(name), val_(), dirty_(false) {} + + auto set(const std::optional&& v) -> void { + if (val_.has_value() != v.has_value() || *val_ != *v) { + val_ = v; + dirty_ = true; + } + } + auto get() -> std::optional& { return val_; } + + /* Reads the stored value from NVS and parses it into the correct type. */ + auto load(nvs_handle_t) -> std::optional; + /* Encodes the given value and writes it to NVS. */ + auto store(nvs_handle_t, T v) -> void; + + auto read(nvs_handle_t nvs) -> void { val_ = load(nvs); } + auto write(nvs_handle_t nvs) -> void { + if (!dirty_) { + return; + } + dirty_ = false; + if (val_) { + store(nvs, *val_); + } else { + nvs_erase_key(nvs, name_); + } + } + + private: + const char* name_; + std::optional val_; + bool dirty_; +}; + class NvsStorage { public: static auto OpenSync() -> NvsStorage*; + auto Read() -> void; + auto Write() -> bool; + auto LockPolarity() -> bool; - auto LockPolarity(bool) -> bool; + auto LockPolarity(bool) -> void; auto PreferredBluetoothDevice() -> std::optional; - auto PreferredBluetoothDevice(std::optional) -> bool; + auto PreferredBluetoothDevice(std::optional) -> void; - using BtVolumes = util::LruCache<10, bluetooth::mac_addr_t, uint8_t> ; - - auto BluetoothVolumes() -> BtVolumes; - auto BluetoothVolumes(const BtVolumes&) -> bool; + auto BluetoothVolume(const bluetooth::mac_addr_t&) -> uint8_t; + auto BluetoothVolume(const bluetooth::mac_addr_t&, uint8_t) -> void; enum class Output : uint8_t { kHeadphones = 0, kBluetooth = 1, }; auto OutputMode() -> Output; - auto OutputMode(Output) -> bool; + auto OutputMode(Output) -> void; auto ScreenBrightness() -> uint_fast8_t; - auto ScreenBrightness(uint_fast8_t) -> bool; + auto ScreenBrightness(uint_fast8_t) -> void; auto AmpMaxVolume() -> uint16_t; - auto AmpMaxVolume(uint16_t) -> bool; + auto AmpMaxVolume(uint16_t) -> void; auto AmpCurrentVolume() -> uint16_t; - auto AmpCurrentVolume(uint16_t) -> bool; + auto AmpCurrentVolume(uint16_t) -> void; auto AmpLeftBias() -> int_fast8_t; - auto AmpLeftBias(int_fast8_t) -> bool; - - auto HasShownOnboarding() -> bool; - auto HasShownOnboarding(bool) -> bool; + auto AmpLeftBias(int_fast8_t) -> void; enum class InputModes : uint8_t { kButtonsOnly = 0, @@ -64,7 +104,7 @@ class NvsStorage { }; auto PrimaryInput() -> InputModes; - auto PrimaryInput(InputModes) -> bool; + auto PrimaryInput(InputModes) -> void; explicit NvsStorage(nvs_handle_t); ~NvsStorage(); @@ -73,7 +113,23 @@ class NvsStorage { auto DowngradeSchemaSync() -> bool; auto SchemaVersionSync() -> uint8_t; + std::mutex mutex_; nvs_handle_t handle_; + + Setting lock_polarity_; + Setting brightness_; + Setting amp_max_vol_; + Setting amp_cur_vol_; + Setting amp_left_bias_; + Setting input_mode_; + Setting output_mode_; + Setting bt_preferred_; + + util::LruCache<10, bluetooth::mac_addr_t, uint8_t> bt_volumes_; + bool bt_volumes_dirty_; + + auto readBtVolumes() -> void; + auto writeBtVolumes() -> void; }; } // namespace drivers diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index ed85487f..a304c149 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -27,18 +27,105 @@ namespace drivers { 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 kKeyBluetoothPreferred[] = "bt_dev"; static constexpr char kKeyBluetoothVolumes[] = "bt_vols"; static constexpr char kKeyOutput[] = "out"; static constexpr char kKeyBrightness[] = "bright"; static constexpr char kKeyAmpMaxVolume[] = "hp_vol_max"; static constexpr char kKeyAmpCurrentVolume[] = "hp_vol"; static constexpr char kKeyAmpLeftBias[] = "hp_bias"; -static constexpr char kKeyOnboarded[] = "intro"; static constexpr char kKeyPrimaryInput[] = "in_pri"; static constexpr char kKeyLockPolarity[] = "lockpol"; +static auto nvs_get_string(nvs_handle_t nvs, const char* key) + -> std::optional { + size_t len = 0; + if (nvs_get_blob(nvs, key, NULL, &len) != ESP_OK) { + return {}; + } + auto raw = std::unique_ptr{new char[len]}; + if (nvs_get_blob(nvs, key, raw.get(), &len) != ESP_OK) { + return {}; + } + return {{raw.get(), len}}; +} + +template <> +auto Setting::load(nvs_handle_t nvs) -> std::optional { + uint16_t out; + if (nvs_get_u16(nvs, name_, &out) != ESP_OK) { + return {}; + } + return out; +} + +template <> +auto Setting::store(nvs_handle_t nvs, uint16_t v) -> void { + nvs_set_u16(nvs, name_, v); +} + +template <> +auto Setting::load(nvs_handle_t nvs) -> std::optional { + uint8_t out; + if (nvs_get_u8(nvs, name_, &out) != ESP_OK) { + return {}; + } + return out; +} + +template <> +auto Setting::store(nvs_handle_t nvs, uint8_t v) -> void { + nvs_set_u8(nvs, name_, v); +} + +template <> +auto Setting::load(nvs_handle_t nvs) -> std::optional { + int8_t out; + if (nvs_get_i8(nvs, name_, &out) != ESP_OK) { + return {}; + } + return out; +} + +template <> +auto Setting::store(nvs_handle_t nvs, int8_t v) -> void { + nvs_set_i8(nvs, name_, v); +} + +template <> +auto Setting::load(nvs_handle_t nvs) + -> std::optional { + auto raw = nvs_get_string(nvs, name_); + if (!raw) { + return {}; + } + auto [parsed, unused, err] = cppbor::parseWithViews( + reinterpret_cast(raw->data()), raw->size()); + if (parsed->type() != cppbor::ARRAY) { + return {}; + } + auto arr = parsed->asArray(); + auto mac = arr->get(1)->asViewBstr()->view(); + auto name = arr->get(0)->asViewTstr()->view(); + bluetooth::MacAndName res{ + .mac = {}, + .name = {name.begin(), name.end()}, + }; + std::copy(mac.begin(), mac.end(), res.mac.begin()); + return res; +} + +template <> +auto Setting::store(nvs_handle_t nvs, + bluetooth::MacAndName v) -> void { + cppbor::Array cbor{ + cppbor::Tstr{v.name}, + cppbor::Bstr{{v.mac.data(), v.mac.size()}}, + }; + auto encoded = cbor.encode(); + nvs_set_blob(nvs, name_, encoded.data(), encoded.size()); +} + auto NvsStorage::OpenSync() -> NvsStorage* { esp_err_t err = nvs_flash_init(); if (err == ESP_ERR_NVS_NO_FREE_PAGES) { @@ -64,17 +151,57 @@ auto NvsStorage::OpenSync() -> NvsStorage* { return nullptr; } + instance->Read(); + ESP_LOGI(kTag, "nvm storage initialised okay"); return instance.release(); } -NvsStorage::NvsStorage(nvs_handle_t handle) : handle_(handle) {} +NvsStorage::NvsStorage(nvs_handle_t handle) + : handle_(handle), + lock_polarity_(kKeyLockPolarity), + brightness_(kKeyBrightness), + amp_max_vol_(kKeyAmpMaxVolume), + amp_cur_vol_(kKeyAmpCurrentVolume), + amp_left_bias_(kKeyAmpLeftBias), + input_mode_(kKeyPrimaryInput), + output_mode_(kKeyOutput), + bt_preferred_(kKeyBluetoothPreferred), + bt_volumes_(), + bt_volumes_dirty_(false) {} NvsStorage::~NvsStorage() { nvs_close(handle_); nvs_flash_deinit(); } +auto NvsStorage::Read() -> void { + std::lock_guard lock{mutex_}; + lock_polarity_.read(handle_); + brightness_.read(handle_); + amp_max_vol_.read(handle_); + amp_cur_vol_.read(handle_); + amp_left_bias_.read(handle_); + input_mode_.read(handle_); + output_mode_.read(handle_); + bt_preferred_.read(handle_); + readBtVolumes(); +} + +auto NvsStorage::Write() -> bool { + std::lock_guard lock{mutex_}; + lock_polarity_.write(handle_); + brightness_.write(handle_); + amp_max_vol_.write(handle_); + amp_cur_vol_.write(handle_); + amp_left_bias_.write(handle_); + input_mode_.write(handle_); + output_mode_.write(handle_); + bt_preferred_.write(handle_); + writeBtVolumes(); + return nvs_commit(handle_) == ESP_OK; +} + auto NvsStorage::DowngradeSchemaSync() -> bool { ESP_LOGW(kTag, "namespace needs downgrading"); nvs_erase_all(handle_); @@ -91,56 +218,126 @@ auto NvsStorage::SchemaVersionSync() -> uint8_t { } auto NvsStorage::LockPolarity() -> bool { - uint8_t res; - if (nvs_get_u8(handle_, kKeyLockPolarity, &res) != ESP_OK) { - return false; - } - return res > 0; + std::lock_guard lock{mutex_}; + return lock_polarity_.get().value_or(0) > 0; } -auto NvsStorage::LockPolarity(bool p) -> bool { - nvs_set_u8(handle_, kKeyLockPolarity, p); - return nvs_commit(handle_) == ESP_OK; +auto NvsStorage::LockPolarity(bool p) -> void { + std::lock_guard lock{mutex_}; + lock_polarity_.set(p); } auto NvsStorage::PreferredBluetoothDevice() -> std::optional { - bluetooth::mac_addr_t mac{0}; - size_t size = mac.size(); - if (nvs_get_blob(handle_, kKeyBluetoothMac, mac.data(), &size) != ESP_OK) { - return {}; - } - size_t name_len = 0; - if (nvs_get_str(handle_, kKeyBluetoothName, NULL, &name_len) != ESP_OK) { - } - char* raw_name = new char[name_len]; - if (nvs_get_str(handle_, kKeyBluetoothName, raw_name, &name_len) != ESP_OK) { - delete[] raw_name; - return {}; - } - bluetooth::MacAndName out{ - .mac = mac, - .name = {raw_name, name_len}, - }; - delete[] raw_name; - return out; + std::lock_guard lock{mutex_}; + return bt_preferred_.get(); } auto NvsStorage::PreferredBluetoothDevice( - std::optional dev) -> bool { - if (!dev) { - nvs_erase_key(handle_, kKeyBluetoothMac); - nvs_erase_key(handle_, kKeyBluetoothName); - } else { - nvs_set_blob(handle_, kKeyBluetoothMac, dev->mac.data(), dev->mac.size()); - nvs_set_str(handle_, kKeyBluetoothName, dev->name.c_str()); + std::optional dev) -> void { + std::lock_guard lock{mutex_}; + bt_preferred_.set(std::move(dev)); +} + +auto NvsStorage::BluetoothVolume(const bluetooth::mac_addr_t& mac) -> uint8_t { + std::lock_guard lock{mutex_}; + // Note we don't set the dirty flag here, even though it's an LRU cache, so + // that we can avoid constantly re-writing this setting to flash when the + // user hasn't actually been changing their volume. + return bt_volumes_.Get(mac).value_or(10); +} + +auto NvsStorage::BluetoothVolume(const bluetooth::mac_addr_t& mac, uint8_t vol) + -> void { + std::lock_guard lock{mutex_}; + bt_volumes_dirty_ = true; + bt_volumes_.Put(mac, vol); +} + +auto NvsStorage::OutputMode() -> Output { + std::lock_guard lock{mutex_}; + switch (output_mode_.get().value_or(0xFF)) { + case static_cast(Output::kBluetooth): + return Output::kBluetooth; + case static_cast(Output::kHeadphones): + default: + return Output::kHeadphones; } - return nvs_commit(handle_) == ESP_OK; +} + +auto NvsStorage::OutputMode(Output out) -> void { + std::lock_guard lock{mutex_}; + output_mode_.set(static_cast(out)); + // Always write this immediately, to guard against any crashes caused by + // toggling the output mode. + output_mode_.write(handle_); + nvs_commit(handle_); +} + +auto NvsStorage::ScreenBrightness() -> uint_fast8_t { + std::lock_guard lock{mutex_}; + return std::clamp(brightness_.get().value_or(50), 0, 100); +} + +auto NvsStorage::ScreenBrightness(uint_fast8_t val) -> void { + std::lock_guard lock{mutex_}; + brightness_.set(val); +} + +auto NvsStorage::AmpMaxVolume() -> uint16_t { + std::lock_guard lock{mutex_}; + return amp_max_vol_.get().value_or(wm8523::kDefaultMaxVolume); +} + +auto NvsStorage::AmpMaxVolume(uint16_t val) -> void { + std::lock_guard lock{mutex_}; + amp_max_vol_.set(val); +} + +auto NvsStorage::AmpCurrentVolume() -> uint16_t { + std::lock_guard lock{mutex_}; + return amp_cur_vol_.get().value_or(wm8523::kDefaultVolume); +} + +auto NvsStorage::AmpCurrentVolume(uint16_t val) -> void { + std::lock_guard lock{mutex_}; + amp_cur_vol_.set(val); +} + +auto NvsStorage::AmpLeftBias() -> int_fast8_t { + std::lock_guard lock{mutex_}; + return amp_left_bias_.get().value_or(0); +} + +auto NvsStorage::AmpLeftBias(int_fast8_t val) -> void { + std::lock_guard lock{mutex_}; + amp_left_bias_.set(val); +} + +auto NvsStorage::PrimaryInput() -> InputModes { + std::lock_guard lock{mutex_}; + switch (input_mode_.get().value_or(3)) { + case static_cast(InputModes::kButtonsOnly): + return InputModes::kButtonsOnly; + case static_cast(InputModes::kButtonsWithWheel): + return InputModes::kButtonsWithWheel; + case static_cast(InputModes::kDirectionalWheel): + return InputModes::kDirectionalWheel; + case static_cast(InputModes::kRotatingWheel): + return InputModes::kRotatingWheel; + default: + return InputModes::kRotatingWheel; + } +} + +auto NvsStorage::PrimaryInput(InputModes mode) -> void { + std::lock_guard lock{mutex_}; + input_mode_.set(static_cast(mode)); } class VolumesParseClient : public cppbor::ParseClient { public: - VolumesParseClient(NvsStorage::BtVolumes& out) + VolumesParseClient(util::LruCache<10, bluetooth::mac_addr_t, uint8_t>& out) : state_(State::kInit), mac_(), vol_(), out_(out) {} ParseClient* item(std::unique_ptr& item, @@ -197,133 +394,34 @@ class VolumesParseClient : public cppbor::ParseClient { State state_; std::optional mac_; std::optional vol_; - NvsStorage::BtVolumes& out_; + util::LruCache<10, bluetooth::mac_addr_t, uint8_t>& 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; +auto NvsStorage::readBtVolumes() -> void { + bt_volumes_.Clear(); + auto raw = nvs_get_string(handle_, kKeyBluetoothVolumes); + if (!raw) { + return; } - VolumesParseClient client{out}; - auto data = reinterpret_cast(encoded.get()); - cppbor::parse(data, data + encoded_len, &client); - return out; + VolumesParseClient client{bt_volumes_}; + auto data = reinterpret_cast(raw->data()); + cppbor::parse(data, data + raw->size(), &client); } -auto NvsStorage::BluetoothVolumes(const BtVolumes& vols) -> bool { +auto NvsStorage::writeBtVolumes() -> void { + if (!bt_volumes_dirty_) { + return; + } + bt_volumes_dirty_ = false; + cppbor::Array enc; - auto vols_list = vols.Get(); + auto vols_list = bt_volumes_.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); - switch (out) { - case static_cast(Output::kBluetooth): - return Output::kBluetooth; - case static_cast(Output::kHeadphones): - default: - return Output::kHeadphones; - } -} - -auto NvsStorage::OutputMode(Output out) -> bool { - uint8_t as_int = static_cast(out); - nvs_set_u8(handle_, kKeyOutput, as_int); - return nvs_commit(handle_) == ESP_OK; -} - -auto NvsStorage::ScreenBrightness() -> uint_fast8_t { - uint8_t out = 50; - nvs_get_u8(handle_, kKeyBrightness, &out); - return out; -} - -auto NvsStorage::ScreenBrightness(uint_fast8_t val) -> bool { - nvs_set_u8(handle_, kKeyBrightness, val); - return nvs_commit(handle_) == ESP_OK; -} - -auto NvsStorage::AmpMaxVolume() -> uint16_t { - uint16_t out = wm8523::kDefaultMaxVolume; - nvs_get_u16(handle_, kKeyAmpMaxVolume, &out); - return out; -} - -auto NvsStorage::AmpMaxVolume(uint16_t val) -> bool { - nvs_set_u16(handle_, kKeyAmpMaxVolume, val); - return nvs_commit(handle_) == ESP_OK; -} - -auto NvsStorage::AmpCurrentVolume() -> uint16_t { - uint16_t out = wm8523::kDefaultVolume; - nvs_get_u16(handle_, kKeyAmpCurrentVolume, &out); - return out; -} - -auto NvsStorage::AmpCurrentVolume(uint16_t val) -> bool { - nvs_set_u16(handle_, kKeyAmpCurrentVolume, val); - return nvs_commit(handle_) == ESP_OK; -} - -auto NvsStorage::AmpLeftBias() -> int_fast8_t { - int8_t out = 0; - nvs_get_i8(handle_, kKeyAmpLeftBias, &out); - return out; -} - -auto NvsStorage::AmpLeftBias(int_fast8_t val) -> bool { - nvs_set_i8(handle_, kKeyAmpLeftBias, val); - return nvs_commit(handle_) == ESP_OK; -} - -auto NvsStorage::HasShownOnboarding() -> bool { - uint8_t out = false; - nvs_get_u8(handle_, kKeyOnboarded, &out); - return out; -} - -auto NvsStorage::HasShownOnboarding(bool val) -> bool { - nvs_set_u8(handle_, kKeyOnboarded, val); - return nvs_commit(handle_) == ESP_OK; -} - -auto NvsStorage::PrimaryInput() -> InputModes { - uint8_t out = 3; - nvs_get_u8(handle_, kKeyPrimaryInput, &out); - switch (out) { - case static_cast(InputModes::kButtonsOnly): - return InputModes::kButtonsOnly; - case static_cast(InputModes::kButtonsWithWheel): - return InputModes::kButtonsWithWheel; - case static_cast(InputModes::kDirectionalWheel): - return InputModes::kDirectionalWheel; - case static_cast(InputModes::kRotatingWheel): - return InputModes::kRotatingWheel; - default: - return InputModes::kRotatingWheel; - } -} - -auto NvsStorage::PrimaryInput(InputModes mode) -> bool { - uint8_t as_int = static_cast(mode); - nvs_set_u8(handle_, kKeyPrimaryInput, as_int); - return nvs_commit(handle_) == ESP_OK; } } // namespace drivers diff --git a/src/system_fsm/idle.cpp b/src/system_fsm/idle.cpp index 640f95cd..b6bb2572 100644 --- a/src/system_fsm/idle.cpp +++ b/src/system_fsm/idle.cpp @@ -34,6 +34,9 @@ static void timer_callback(TimerHandle_t timer) { */ void Idle::entry() { ESP_LOGI(kTag, "system became idle"); + + sServices->nvs().Write(); + events::Audio().Dispatch(OnIdle{}); events::Ui().Dispatch(OnIdle{});