diff --git a/lua/main.lua b/lua/main.lua index f2cbda3a..cd4bea3c 100644 --- a/lua/main.lua +++ b/lua/main.lua @@ -1,4 +1,36 @@ -require("font") +local font = require("font") +local vol = require("volume") + +-- Set up property bindings that are used across every screen. +GLOBAL_BINDINGS = { + vol.current_pct:bind(function(pct) + require("alerts").show(function() + local container = lvgl.Object(nil, { + w = lvgl.PCT(80), + h = lvgl.SIZE_CONTENT, + flex = { + flex_direction = "column", + justify_content = "center", + align_items = "center", + align_content = "center", + }, + bg_opa = lvgl.OPA(100), + bg_color = "#fafafa", + }) + container:Label { + text = string.format("Volume %i%%", pct), + text_font = font.fusion_10 + } + container:Bar { + w = lvgl.PCT(100), + h = 8, + range = { min = 0, max = 100 }, + value = pct, + } + container:center() + end) + end), +} local backstack = require("backstack") local main_menu = require("main_menu") diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index a5179156..a2f467cb 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -58,13 +58,19 @@ void AudioState::react(const system_fsm::KeyLockChanged& ev) { void AudioState::react(const StepUpVolume& ev) { if (sOutput->AdjustVolumeUp()) { - events::Ui().Dispatch(VolumeChanged{}); + events::Ui().Dispatch(VolumeChanged{ + .percent = sOutput->GetVolumePct(), + .db = sOutput->GetVolumeDb(), + }); } } void AudioState::react(const StepDownVolume& ev) { if (sOutput->AdjustVolumeDown()) { - events::Ui().Dispatch(VolumeChanged{}); + events::Ui().Dispatch(VolumeChanged{ + .percent = sOutput->GetVolumePct(), + .db = sOutput->GetVolumeDb(), + }); } } @@ -77,7 +83,7 @@ void AudioState::react(const system_fsm::HasPhonesChanged& ev) { } void AudioState::react(const ChangeMaxVolume& ev) { - ESP_LOGI(kTag, "new max volume %u db", + ESP_LOGI(kTag, "new max volume %i db", (ev.new_max - drivers::wm8523::kLineLevelReferenceVolume) / 4); sI2SOutput->SetMaxVolume(ev.new_max); sServices->nvs().AmpMaxVolume(ev.new_max); diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index 29d70fb5..4043574e 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -93,7 +93,6 @@ auto I2SAudioOutput::SetMaxVolume(uint16_t max) -> void { auto I2SAudioOutput::SetVolume(uint16_t vol) -> void { current_volume_ = std::clamp(vol, kMinVolume, max_volume_); - ESP_LOGI(kTag, "set volume to %u%% = %idB", GetVolumePct(), GetVolumeDb()); int32_t left_unclamped = current_volume_ + left_difference_; uint16_t left = std::clamp(left_unclamped, kMinVolume, max_volume_); diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index 68efcafb..3c5ab723 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -47,7 +47,10 @@ struct PlayFile : tinyfsm::Event { struct StepUpVolume : tinyfsm::Event {}; struct StepDownVolume : tinyfsm::Event {}; -struct VolumeChanged : tinyfsm::Event {}; +struct VolumeChanged : tinyfsm::Event { + uint_fast8_t percent; + int db; +}; struct ChangeMaxVolume : tinyfsm::Event { uint16_t new_max; }; diff --git a/src/lua/stubs/alerts.lua b/src/lua/stubs/alerts.lua new file mode 100644 index 00000000..9b541d84 --- /dev/null +++ b/src/lua/stubs/alerts.lua @@ -0,0 +1,13 @@ +--- Module for interacting with playback volume. The Bluetooth and wired outputs store their current volume separately; this API only allows interacting with the volume of the currently used output device. +-- @module alerts + +local alerts = {} + +--- Returns the current volume as a percentage of the current volume limit. +-- @tparam function constructor Called to create the UI for the alert. A new default root object and group will be set before calling this function.i Alerts are non-interactable; the group created for the constructor will not be granted focus. +function alerts.show(constructor) end + +--- Dismisses any visible alerts, removing them from the screen. +function alerts.hide() end + +return alerts diff --git a/src/lua/stubs/volume.lua b/src/lua/stubs/volume.lua new file mode 100644 index 00000000..15499630 --- /dev/null +++ b/src/lua/stubs/volume.lua @@ -0,0 +1,14 @@ +--- Module for interacting with playback volume. The Bluetooth and wired outputs store their current volume separately; this API only allows interacting with the volume of the currently used output device. +-- @module volume + +local volume = {} + +--- Returns the current volume as a percentage of the current volume limit. +-- @treturn types.Property an integer property +function volume.current_pct() end + +--- Returns the current volume in terms of dB from line level. +-- @treturn types.Property an integer property +function volume.current_db() end + +return volume diff --git a/src/ui/include/screen.hpp b/src/ui/include/screen.hpp index e9eaeeb0..4fe0a3b7 100644 --- a/src/ui/include/screen.hpp +++ b/src/ui/include/screen.hpp @@ -40,6 +40,7 @@ class Screen { auto root() -> lv_obj_t* { return root_; } auto content() -> lv_obj_t* { return content_; } + auto alert() -> lv_obj_t* { return alert_; } auto modal_content() -> lv_obj_t* { return modal_content_; } auto modal_group(lv_group_t* g) -> void { modal_group_ = g; } @@ -68,6 +69,7 @@ class Screen { lv_obj_t* const root_; lv_obj_t* content_; lv_obj_t* modal_content_; + lv_obj_t* alert_; lv_group_t* const group_; lv_group_t* modal_group_; diff --git a/src/ui/include/ui_events.hpp b/src/ui/include/ui_events.hpp index 111f37a8..59be7606 100644 --- a/src/ui/include/ui_events.hpp +++ b/src/ui/include/ui_events.hpp @@ -24,9 +24,9 @@ struct OnStorageChange : tinyfsm::Event { struct OnSystemError : tinyfsm::Event {}; - struct OnLuaError : tinyfsm::Event { - std::string message; - }; +struct OnLuaError : tinyfsm::Event { + std::string message; +}; namespace internal { @@ -54,6 +54,8 @@ struct OnboardingNavigate : tinyfsm::Event { struct ModalConfirmPressed : tinyfsm::Event {}; struct ModalCancelPressed : tinyfsm::Event {}; +struct DismissAlerts : tinyfsm::Event {}; + } // namespace internal } // namespace ui diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index ba3f5e3f..33f9eac4 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -60,6 +60,7 @@ class UiState : public tinyfsm::Fsm { virtual void react(const audio::PlaybackFinished&); virtual void react(const audio::PlaybackUpdate&); virtual void react(const audio::QueueUpdate&); + virtual void react(const audio::VolumeChanged&){}; virtual void react(const system_fsm::KeyLockChanged&); virtual void react(const OnLuaError&) {} @@ -76,6 +77,8 @@ class UiState : public tinyfsm::Fsm { void react(const internal::ControlSchemeChanged&); virtual void react(const internal::ReindexDatabase&){}; + void react(const internal::DismissAlerts&); + virtual void react(const database::event::UpdateStarted&){}; virtual void react(const database::event::UpdateProgress&){}; virtual void react(const database::event::UpdateFinished&){}; @@ -129,6 +132,7 @@ class Lua : public UiState { void react(const audio::PlaybackStarted&) override; void react(const audio::PlaybackUpdate&) override; void react(const audio::PlaybackFinished&) override; + void react(const audio::VolumeChanged&) override; void react(const internal::BackPressed&) override; using UiState::react; @@ -136,6 +140,10 @@ class Lua : public UiState { private: auto PushLuaScreen(lua_State*) -> int; auto PopLuaScreen(lua_State*) -> int; + + auto ShowAlert(lua_State*) -> int; + auto HideAlert(lua_State*) -> int; + auto SetPlaying(const lua::LuaValue&) -> bool; auto SetRandom(const lua::LuaValue&) -> bool; auto SetRepeat(const lua::LuaValue&) -> bool; @@ -154,6 +162,9 @@ class Lua : public UiState { std::shared_ptr queue_size_; std::shared_ptr queue_repeat_; std::shared_ptr queue_random_; + + std::shared_ptr volume_current_pct_; + std::shared_ptr volume_current_db_; }; class Browse : public UiState { diff --git a/src/ui/screen.cpp b/src/ui/screen.cpp index 0b0f7914..9ac5ec0e 100644 --- a/src/ui/screen.cpp +++ b/src/ui/screen.cpp @@ -10,6 +10,7 @@ #include "core/lv_obj_pos.h" #include "core/lv_obj_tree.h" +#include "hal/lv_hal_disp.h" #include "misc/lv_area.h" #include "misc/lv_color.h" #include "model_top_bar.hpp" @@ -21,14 +22,17 @@ Screen::Screen() : root_(lv_obj_create(NULL)), content_(lv_obj_create(root_)), modal_content_(lv_obj_create(root_)), + alert_(lv_obj_create(root_)), group_(lv_group_create()), modal_group_(nullptr) { lv_obj_set_size(root_, lv_pct(100), lv_pct(100)); lv_obj_set_size(content_, lv_pct(100), lv_pct(100)); lv_obj_set_size(modal_content_, lv_pct(100), lv_pct(100)); + lv_obj_set_size(alert_, LV_SIZE_CONTENT, LV_SIZE_CONTENT); lv_obj_center(root_); lv_obj_center(content_); lv_obj_center(modal_content_); + lv_obj_center(alert_); lv_obj_set_style_bg_opa(modal_content_, LV_OPA_TRANSP, 0); lv_obj_set_style_bg_color(modal_content_, lv_color_black(), 0); diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 8e355fd1..740383a4 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -9,6 +9,8 @@ #include #include +#include "freertos/portmacro.h" +#include "freertos/projdefs.h" #include "lua.h" #include "lua.hpp" @@ -44,6 +46,7 @@ #include "spiffs.hpp" #include "storage.hpp" #include "system_events.hpp" +#include "tinyfsm.hpp" #include "touchwheel.hpp" #include "track_queue.hpp" #include "ui_events.hpp" @@ -70,6 +73,13 @@ models::TopBar UiState::sTopBarModel{{}, UiState::sPlaybackModel.is_playing, UiState::sPlaybackModel.current_track}; +static TimerHandle_t sAlertTimer; +static lv_obj_t* sAlertContainer; + +static void alert_timer_callback(TimerHandle_t timer) { + events::Ui().Dispatch(internal::DismissAlerts{}); +} + auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { // Init LVGL first, since the display driver registers itself with LVGL. lv_init(); @@ -89,6 +99,7 @@ void UiState::PushScreen(std::shared_ptr screen) { sScreens.push(sCurrentScreen); } sCurrentScreen = screen; + lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert()); } int UiState::PopScreen() { @@ -96,6 +107,8 @@ int UiState::PopScreen() { return 0; } sCurrentScreen = sScreens.top(); + lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert()); + sScreens.pop(); return sScreens.size(); } @@ -127,6 +140,10 @@ void UiState::react(const internal::ControlSchemeChanged&) { sInput->mode(sServices->nvs().PrimaryInput()); } +void UiState::react(const internal::DismissAlerts&) { + lv_obj_clean(sAlertContainer); +} + namespace states { void Splash::exit() { @@ -164,6 +181,10 @@ void Splash::react(const system_fsm::StorageMounted&) { void Lua::entry() { if (!sLua) { + sAlertTimer = xTimerCreate("ui_alerts", pdMS_TO_TICKS(1000), false, NULL, + alert_timer_callback); + sAlertContainer = lv_obj_create(sCurrentScreen->alert()); + auto bat = sServices->battery().State().value_or(battery::Battery::BatteryState{}); battery_pct_ = @@ -184,6 +205,9 @@ void Lua::entry() { playback_track_ = std::make_shared(); playback_position_ = std::make_shared(); + volume_current_pct_ = std::make_shared(0); + volume_current_db_ = std::make_shared(0); + sLua.reset(lua::LuaThread::Start(*sServices, sCurrentScreen->content())); sLua->bridge().AddPropertyModule("power", { @@ -208,12 +232,23 @@ void Lua::entry() { {"replay", queue_repeat_}, {"random", queue_random_}, }); + sLua->bridge().AddPropertyModule("volume", + { + {"current_pct", volume_current_pct_}, + {"current_db", volume_current_db_}, + }); + sLua->bridge().AddPropertyModule( "backstack", { {"push", [&](lua_State* s) { return PushLuaScreen(s); }}, {"pop", [&](lua_State* s) { return PopLuaScreen(s); }}, }); + sLua->bridge().AddPropertyModule( + "alerts", { + {"show", [&](lua_State* s) { return ShowAlert(s); }}, + {"hide", [&](lua_State* s) { return HideAlert(s); }}, + }); sCurrentScreen.reset(); sLua->RunScript("/lua/main.lua"); @@ -265,6 +300,38 @@ auto Lua::SetPlaying(const lua::LuaValue& val) -> bool { return true; } +auto Lua::ShowAlert(lua_State* s) -> int { + if (!sCurrentScreen) { + return 0; + } + xTimerReset(sAlertTimer, portMAX_DELAY); + tinyfsm::FsmList::dispatch(internal::DismissAlerts{}); + + lv_group_t* prev_group = lv_group_get_default(); + + luavgl_set_root(s, sAlertContainer); + lv_group_t* catchall = lv_group_create(); + lv_group_set_default(catchall); + + // Call the constructor for the alert. + lua_settop(s, 1); // Make sure the function is actually at top of stack + lua::CallProtected(s, 0, 1); + + // Restore the previous group and default object. + luavgl_set_root(s, sCurrentScreen->content()); + lv_group_set_default(prev_group); + + lv_group_del(catchall); + + return 0; +} + +auto Lua::HideAlert(lua_State* s) -> int { + xTimerStop(sAlertTimer, portMAX_DELAY); + tinyfsm::FsmList::dispatch(internal::DismissAlerts{}); + return 0; +} + auto Lua::SetRandom(const lua::LuaValue& val) -> bool { if (!std::holds_alternative(val)) { return false; @@ -327,6 +394,11 @@ void Lua::react(const audio::PlaybackFinished&) { playback_playing_->Update(false); } +void Lua::react(const audio::VolumeChanged& ev) { + volume_current_pct_->Update(static_cast(ev.percent)); + volume_current_db_->Update(static_cast(ev.db)); +} + void Lua::react(const internal::BackPressed& ev) { PopLuaScreen(sLua->state()); }