diff options
| author | jacqueline <me@jacqueline.id.au> | 2024-05-02 19:12:26 +1000 |
|---|---|---|
| committer | jacqueline <me@jacqueline.id.au> | 2024-05-02 19:12:26 +1000 |
| commit | 1573a8c4cde1cd9528b422b2dcc598e37ffe94a7 (patch) | |
| tree | d162822b8fd7054f81bace0c7a65ab4d5e6f93ef /src/tangara/ui/ui_fsm.cpp | |
| parent | a231fd1c8afedbeb14b0bc77d76bad61db986059 (diff) | |
| download | tangara-fw-1573a8c4cde1cd9528b422b2dcc598e37ffe94a7.tar.gz | |
WIP merge cyclically dependent components into one big component
Diffstat (limited to 'src/tangara/ui/ui_fsm.cpp')
| -rw-r--r-- | src/tangara/ui/ui_fsm.cpp | 693 |
1 files changed, 693 insertions, 0 deletions
diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp new file mode 100644 index 00000000..1cbf1be4 --- /dev/null +++ b/src/tangara/ui/ui_fsm.cpp @@ -0,0 +1,693 @@ +/* + * Copyright 2023 jacqueline <me@jacqueline.id.au> + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "ui_fsm.hpp" + +#include <memory> +#include <memory_resource> +#include <variant> + +#include "bluetooth_types.hpp" +#include "db_events.hpp" +#include "device_factory.hpp" +#include "display_init.hpp" +#include "esp_spp_api.h" +#include "feedback_haptics.hpp" +#include "freertos/portmacro.h" +#include "freertos/projdefs.h" +#include "input_device.hpp" +#include "input_touch_wheel.hpp" +#include "input_volume_buttons.hpp" +#include "lua.h" +#include "lua.hpp" + +#include "audio_fsm.hpp" +#include "battery.hpp" +#include "core/lv_group.h" +#include "core/lv_obj.h" +#include "core/lv_obj_tree.h" +#include "database.hpp" +#include "esp_heap_caps.h" +#include "esp_timer.h" +#include "haptics.hpp" +#include "lauxlib.h" +#include "lua_thread.hpp" +#include "luavgl.h" +#include "lvgl_input_driver.hpp" +#include "memory_resource.hpp" +#include "misc/lv_gc.h" + +#include "audio_events.hpp" +#include "display.hpp" +#include "event_queue.hpp" +#include "gpios.hpp" +#include "lua_registry.hpp" +#include "lvgl_task.hpp" +#include "nvs.hpp" +#include "property.hpp" +#include "samd.hpp" +#include "screen.hpp" +#include "screen_lua.hpp" +#include "screen_splash.hpp" +#include "spiffs.hpp" +#include "storage.hpp" +#include "system_events.hpp" +#include "tinyfsm.hpp" +#include "touchwheel.hpp" +#include "track_queue.hpp" +#include "ui_events.hpp" +#include "widgets/lv_label.h" + +namespace ui { + +[[maybe_unused]] static constexpr char kTag[] = "ui_fsm"; + +std::unique_ptr<UiTask> UiState::sTask; +std::shared_ptr<system_fsm::ServiceLocator> UiState::sServices; +std::unique_ptr<drivers::Display> UiState::sDisplay; + +std::shared_ptr<input::LvglInputDriver> UiState::sInput; +std::unique_ptr<input::DeviceFactory> UiState::sDeviceFactory; + +std::stack<std::shared_ptr<Screen>> UiState::sScreens; +std::shared_ptr<Screen> UiState::sCurrentScreen; +std::shared_ptr<Modal> UiState::sCurrentModal; +std::shared_ptr<lua::LuaThread> UiState::sLua; + +static TimerHandle_t sAlertTimer; +static lv_obj_t* sAlertContainer; + +static void alert_timer_callback(TimerHandle_t timer) { + events::Ui().Dispatch(internal::DismissAlerts{}); +} + +lua::Property UiState::sBatteryPct{0}; +lua::Property UiState::sBatteryMv{0}; +lua::Property UiState::sBatteryCharging{false}; + +lua::Property UiState::sBluetoothEnabled{ + false, [](const lua::LuaValue& val) { + if (!std::holds_alternative<bool>(val)) { + return false; + } + if (std::get<bool>(val)) { + sServices->nvs().OutputMode(drivers::NvsStorage::Output::kBluetooth); + sServices->bluetooth().Enable(); + } else { + sServices->nvs().OutputMode(drivers::NvsStorage::Output::kHeadphones); + sServices->bluetooth().Disable(); + } + events::Audio().Dispatch(audio::OutputModeChanged{}); + return true; + }}; + +lua::Property UiState::sBluetoothConnected{false}; +lua::Property UiState::sBluetoothPairedDevice{ + std::monostate{}, [](const lua::LuaValue& val) { + if (std::holds_alternative<drivers::bluetooth::Device>(val)) { + auto dev = std::get<drivers::bluetooth::Device>(val); + sServices->bluetooth().SetPreferredDevice( + drivers::bluetooth::MacAndName{ + .mac = dev.address, + .name = {dev.name.data(), dev.name.size()}, + }); + } + return false; + }}; +lua::Property UiState::sBluetoothDevices{ + std::vector<drivers::bluetooth::Device>{}}; + +lua::Property UiState::sPlaybackPlaying{ + false, [](const lua::LuaValue& val) { + if (!std::holds_alternative<bool>(val)) { + return false; + } + bool new_val = std::get<bool>(val); + events::Audio().Dispatch(audio::TogglePlayPause{.set_to = new_val}); + return true; + }}; + +lua::Property UiState::sPlaybackTrack{}; +lua::Property UiState::sPlaybackPosition{ + 0, [](const lua::LuaValue& val) { + int current_val = std::get<int>(sPlaybackPosition.get()); + if (!std::holds_alternative<int>(val)) { + return false; + } + int new_val = std::get<int>(val); + if (current_val != new_val) { + auto track = sPlaybackTrack.get(); + if (!std::holds_alternative<audio::TrackInfo>(track)) { + return false; + } + events::Audio().Dispatch(audio::SetTrack{ + .new_track = std::get<audio::TrackInfo>(track).uri, + .seek_to_second = (uint32_t)new_val, + }); + } + return true; + }}; + +lua::Property UiState::sQueuePosition{0}; +lua::Property UiState::sQueueSize{0}; +lua::Property UiState::sQueueRepeat{false, [](const lua::LuaValue& val) { + if (!std::holds_alternative<bool>(val)) { + return false; + } + bool new_val = std::get<bool>(val); + sServices->track_queue().repeat(new_val); + return true; + }}; +lua::Property UiState::sQueueReplay{false, [](const lua::LuaValue& val) { + if (!std::holds_alternative<bool>(val)) { + return false; + } + bool new_val = std::get<bool>(val); + sServices->track_queue().replay(new_val); + return true; + }}; +lua::Property UiState::sQueueRandom{false, [](const lua::LuaValue& val) { + if (!std::holds_alternative<bool>(val)) { + return false; + } + bool new_val = std::get<bool>(val); + sServices->track_queue().random(new_val); + return true; + }}; + +lua::Property UiState::sVolumeCurrentPct{ + 0, [](const lua::LuaValue& val) { + if (!std::holds_alternative<int>(val)) { + return false; + } + events::Audio().Dispatch(audio::SetVolume{ + .percent = std::get<int>(val), + .db = {}, + }); + return true; + }}; +lua::Property UiState::sVolumeCurrentDb{ + 0, [](const lua::LuaValue& val) { + if (!std::holds_alternative<int>(val)) { + return false; + } + events::Audio().Dispatch(audio::SetVolume{ + .percent = {}, + .db = std::get<int>(val), + }); + return true; + }}; +lua::Property UiState::sVolumeLeftBias{ + 0, [](const lua::LuaValue& val) { + if (!std::holds_alternative<int>(val)) { + return false; + } + events::Audio().Dispatch(audio::SetVolumeBalance{ + .left_bias = std::get<int>(val), + }); + return true; + }}; +lua::Property UiState::sVolumeLimit{ + 0, [](const lua::LuaValue& val) { + if (!std::holds_alternative<int>(val)) { + return false; + } + int limit = std::get<int>(val); + events::Audio().Dispatch(audio::SetVolumeLimit{ + .limit_db = limit, + }); + return true; + }}; + +lua::Property UiState::sDisplayBrightness{ + 0, [](const lua::LuaValue& val) { + std::optional<int> brightness = 0; + std::visit( + [&](auto&& v) { + using T = std::decay_t<decltype(v)>; + if constexpr (std::is_same_v<T, int>) { + brightness = v; + } + }, + val); + if (!brightness) { + return false; + } + sDisplay->SetBrightness(*brightness); + sServices->nvs().ScreenBrightness(*brightness); + return true; + }}; + +lua::Property UiState::sLockSwitch{false}; + +lua::Property UiState::sDatabaseUpdating{false}; +lua::Property UiState::sDatabaseAutoUpdate{ + false, [](const lua::LuaValue& val) { + if (!std::holds_alternative<bool>(val)) { + return false; + } + sServices->nvs().DbAutoIndex(std::get<bool>(val)); + return true; + }}; + +lua::Property UiState::sUsbMassStorageEnabled{ + false, [](const lua::LuaValue& val) { + if (!std::holds_alternative<bool>(val)) { + return false; + } + bool enable = std::get<bool>(val); + // FIXME: Check for system busy. + events::System().Dispatch(system_fsm::SamdUsbMscChanged{.en = enable}); + return true; + }}; + +lua::Property UiState::sUsbMassStorageBusy{false}; + +auto UiState::InitBootSplash(drivers::IGpios& gpios, drivers::NvsStorage& nvs) + -> bool { + events::Ui().Dispatch(internal::InitDisplay{ + .gpios = gpios, + .nvs = nvs, + }); + sTask.reset(UiTask::Start()); + return true; +} + +void UiState::react(const internal::InitDisplay& ev) { + // Init LVGL first, since the display driver registers itself with LVGL. + lv_init(); + + drivers::displays::InitialisationData init_data = drivers::displays::kST7735R; + + // HACK: correct the display size for our prototypes. + // nvs.DisplaySize({161, 130}); + + auto actual_size = ev.nvs.DisplaySize(); + init_data.width = actual_size.first.value_or(init_data.width); + init_data.height = actual_size.second.value_or(init_data.height); + sDisplay.reset(drivers::Display::Create(ev.gpios, init_data)); + + sCurrentScreen.reset(new screens::Splash()); + + // Display will only actually come on after LVGL finishes its first flush. + sDisplay->SetDisplayOn(!ev.gpios.IsLocked()); +} + +void UiState::PushScreen(std::shared_ptr<Screen> screen) { + lv_obj_set_parent(sAlertContainer, screen->alert()); + + if (sCurrentScreen) { + sCurrentScreen->onHidden(); + sScreens.push(sCurrentScreen); + } + sCurrentScreen = screen; + sCurrentScreen->onShown(); +} + +int UiState::PopScreen() { + if (sScreens.empty()) { + return 0; + } + lv_obj_set_parent(sAlertContainer, sScreens.top()->alert()); + + sCurrentScreen->onHidden(); + + sCurrentScreen = sScreens.top(); + sScreens.pop(); + + sCurrentScreen->onShown(); + + return sScreens.size(); +} + +void UiState::react(const system_fsm::KeyLockChanged& ev) { + sDisplay->SetDisplayOn(!ev.locking); + sInput->lock(ev.locking); + sLockSwitch.setDirect(ev.locking); +} + +void UiState::react(const system_fsm::SamdUsbStatusChanged& ev) { + sUsbMassStorageBusy.setDirect(ev.new_status == + drivers::Samd::UsbStatus::kAttachedBusy); +} + +void UiState::react(const database::event::UpdateStarted&) { + sDatabaseUpdating.setDirect(true); +} + +void UiState::react(const database::event::UpdateFinished&) { + sDatabaseUpdating.setDirect(false); +} + +void UiState::react(const internal::DismissAlerts&) { + lv_obj_clean(sAlertContainer); +} + +void UiState::react(const system_fsm::BatteryStateChanged& ev) { + sBatteryPct.setDirect(static_cast<int>(ev.new_state.percent)); + sBatteryMv.setDirect(static_cast<int>(ev.new_state.millivolts)); + sBatteryCharging.setDirect(ev.new_state.is_charging); +} + +void UiState::react(const audio::QueueUpdate&) { + auto& queue = sServices->track_queue(); + sQueueSize.setDirect(static_cast<int>(queue.totalSize())); + + int current_pos = queue.currentPosition(); + if (queue.current()) { + current_pos++; + } + sQueuePosition.setDirect(current_pos); + sQueueRandom.setDirect(queue.random()); + sQueueRepeat.setDirect(queue.repeat()); + sQueueReplay.setDirect(queue.replay()); +} + +void UiState::react(const audio::PlaybackUpdate& ev) { + if (ev.current_track) { + sPlaybackTrack.setDirect(*ev.current_track); + } else { + sPlaybackTrack.setDirect(std::monostate{}); + } + sPlaybackPlaying.setDirect(!ev.paused); + sPlaybackPosition.setDirect(static_cast<int>(ev.track_position.value_or(0))); +} + +void UiState::react(const audio::VolumeChanged& ev) { + sVolumeCurrentPct.setDirect(static_cast<int>(ev.percent)); + sVolumeCurrentDb.setDirect(static_cast<int>(ev.db)); +} + +void UiState::react(const audio::VolumeBalanceChanged& ev) { + sVolumeLeftBias.setDirect(ev.left_bias); +} + +void UiState::react(const audio::VolumeLimitChanged& ev) { + sVolumeLimit.setDirect(ev.new_limit_db); +} + +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.setDirect(bt.KnownDevices()); + break; + case drivers::bluetooth::Event::kConnectionStateChanged: + sBluetoothConnected.setDirect(bt.IsConnected()); + if (dev) { + sBluetoothPairedDevice.setDirect(drivers::bluetooth::Device{ + .address = dev->mac, + .name = {dev->name.data(), dev->name.size()}, + .class_of_device = 0, + .signal_strength = 0, + }); + } else { + sBluetoothPairedDevice.setDirect(std::monostate{}); + } + break; + case drivers::bluetooth::Event::kPreferredDeviceChanged: + break; + } +} + +namespace states { + +void Splash::exit() { + // buzz a bit to tell the user we're done booting + events::System().Dispatch(system_fsm::HapticTrigger{ + .effect = drivers::Haptics::Effect::kLongDoubleSharpTick1_100Pct, + }); +} + +void Splash::react(const system_fsm::BootComplete& ev) { + sServices = ev.services; + + // The system has finished booting! We now need to prepare to show real UI. + // This basically just involves reading and applying the user's preferences. + + lv_theme_t* base_theme = lv_theme_basic_init(NULL); + lv_disp_set_theme(NULL, base_theme); + themes::Theme::instance()->Apply(); + + int brightness = sServices->nvs().ScreenBrightness(); + sDisplayBrightness.setDirect(brightness); + sDisplay->SetBrightness(brightness); + + sDeviceFactory = std::make_unique<input::DeviceFactory>(sServices); + sInput = std::make_shared<input::LvglInputDriver>(sServices->nvs(), + *sDeviceFactory); + + sTask->input(sInput); +} + +void Splash::react(const system_fsm::StorageMounted&) { + transit<Lua>(); +} + +void Lua::entry() { + if (!sLua) { + sAlertTimer = xTimerCreate("ui_alerts", pdMS_TO_TICKS(1000), false, NULL, + alert_timer_callback); + sAlertContainer = lv_obj_create(sCurrentScreen->alert()); + lv_obj_set_style_bg_opa(sAlertContainer, LV_OPA_TRANSP, 0); + + auto& registry = lua::Registry::instance(*sServices); + sLua = registry.uiThread(); + registry.AddPropertyModule("power", { + {"battery_pct", &sBatteryPct}, + {"battery_millivolts", &sBatteryMv}, + {"plugged_in", &sBatteryCharging}, + }); + registry.AddPropertyModule("bluetooth", + { + {"enabled", &sBluetoothEnabled}, + {"connected", &sBluetoothConnected}, + {"paired_device", &sBluetoothPairedDevice}, + {"devices", &sBluetoothDevices}, + }); + registry.AddPropertyModule("playback", { + {"playing", &sPlaybackPlaying}, + {"track", &sPlaybackTrack}, + {"position", &sPlaybackPosition}, + }); + registry.AddPropertyModule( + "queue", + { + {"next", [&](lua_State* s) { return QueueNext(s); }}, + {"previous", [&](lua_State* s) { return QueuePrevious(s); }}, + {"position", &sQueuePosition}, + {"size", &sQueueSize}, + {"replay", &sQueueReplay}, + {"repeat_track", &sQueueRepeat}, + {"random", &sQueueRandom}, + }); + registry.AddPropertyModule("volume", + { + {"current_pct", &sVolumeCurrentPct}, + {"current_db", &sVolumeCurrentDb}, + {"left_bias", &sVolumeLeftBias}, + {"limit_db", &sVolumeLimit}, + }); + + registry.AddPropertyModule("display", + { + {"brightness", &sDisplayBrightness}, + }); + + registry.AddPropertyModule( + "controls", + { + {"scheme", &sInput->mode()}, + {"lock_switch", &sLockSwitch}, + {"hooks", [&](lua_State* L) { return sInput->pushHooks(L); }}, + }); + + if (sDeviceFactory->touch_wheel()) { + registry.AddPropertyModule( + "controls", {{"scroll_sensitivity", + &sDeviceFactory->touch_wheel()->sensitivity()}}); + } + + registry.AddPropertyModule( + "backstack", + { + {"push", [&](lua_State* s) { return PushLuaScreen(s); }}, + {"pop", [&](lua_State* s) { return PopLuaScreen(s); }}, + }); + registry.AddPropertyModule( + "alerts", { + {"show", [&](lua_State* s) { return ShowAlert(s); }}, + {"hide", [&](lua_State* s) { return HideAlert(s); }}, + }); + + registry.AddPropertyModule( + "time", { + {"ticks", [&](lua_State* s) { return Ticks(s); }}, + }); + registry.AddPropertyModule("database", + { + {"updating", &sDatabaseUpdating}, + {"auto_update", &sDatabaseAutoUpdate}, + }); + registry.AddPropertyModule("usb", + { + {"msc_enabled", &sUsbMassStorageEnabled}, + {"msc_busy", &sUsbMassStorageBusy}, + }); + + sDatabaseAutoUpdate.setDirect(sServices->nvs().DbAutoIndex()); + + auto bt = sServices->bluetooth(); + sBluetoothEnabled.setDirect(bt.IsEnabled()); + sBluetoothConnected.setDirect(bt.IsConnected()); + sBluetoothDevices.setDirect(bt.KnownDevices()); + + sCurrentScreen.reset(); + sLua->RunScript("/sdcard/config.lua"); + sLua->RunScript("/lua/main.lua"); + } +} + +auto Lua::PushLuaScreen(lua_State* s) -> int { + // Ensure the arg looks right before continuing. + luaL_checktype(s, 1, LUA_TTABLE); + + // First, create a new plain old Screen object. We will use its root and + // group for the Lua screen. Allocate it in external ram so that arbitrarily + // deep screen stacks don't cause too much memory pressure. + auto new_screen = + std::allocate_shared<screens::Lua, + std::pmr::polymorphic_allocator<screens::Lua>>( + &memory::kSpiRamResource); + + // Tell lvgl about the new roots. + luavgl_set_root(s, new_screen->content()); + lv_group_set_default(new_screen->group()); + + // Call the constructor for this screen. + // lua_settop(s, 1); // Make sure the screen is actually at top of stack + lua_pushliteral(s, "createUi"); + if (lua_gettable(s, 1) == LUA_TFUNCTION) { + lua_pushvalue(s, 1); + lua::CallProtected(s, 1, 0); + } + + // Store the reference for this screen's table. + lua_settop(s, 1); + new_screen->SetObjRef(s); + + // Finally, push the now-initialised screen as if it were a regular C++ + // screen. + PushScreen(new_screen); + + return 0; +} + +auto Lua::QueueNext(lua_State*) -> int { + sServices->track_queue().next(); + return 0; +} + +auto Lua::QueuePrevious(lua_State*) -> int { + sServices->track_queue().previous(); + return 0; +} + +auto Lua::PopLuaScreen(lua_State* s) -> int { + if (!sCurrentScreen->canPop()) { + return 0; + } + PopScreen(); + luavgl_set_root(s, sCurrentScreen->content()); + lv_group_set_default(sCurrentScreen->group()); + return 0; +} + +auto Lua::Ticks(lua_State* s) -> int { + lua_pushinteger(s, esp_timer_get_time() / 1000); + return 1; +} + +auto Lua::ShowAlert(lua_State* s) -> int { + if (!sCurrentScreen) { + return 0; + } + xTimerReset(sAlertTimer, portMAX_DELAY); + tinyfsm::FsmList<UiState>::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<UiState>::dispatch(internal::DismissAlerts{}); + return 0; +} + +auto Lua::SetRandom(const lua::LuaValue& val) -> bool { + if (!std::holds_alternative<bool>(val)) { + return false; + } + bool b = std::get<bool>(val); + sServices->track_queue().random(b); + return true; +} + +auto Lua::SetRepeat(const lua::LuaValue& val) -> bool { + if (!std::holds_alternative<bool>(val)) { + return false; + } + bool b = std::get<bool>(val); + sServices->track_queue().repeat(b); + return true; +} + +auto Lua::SetReplay(const lua::LuaValue& val) -> bool { + if (!std::holds_alternative<bool>(val)) { + return false; + } + bool b = std::get<bool>(val); + sServices->track_queue().replay(b); + return true; +} + +void Lua::exit() { + lv_group_set_default(NULL); +} + +void Lua::react(const OnLuaError& err) { + ESP_LOGE("lua", "%s", err.message.c_str()); +} + +void Lua::react(const DumpLuaStack& ev) { + sLua->DumpStack(); +} + +void Lua::react(const internal::BackPressed& ev) { + PopLuaScreen(sLua->state()); +} + +} // namespace states +} // namespace ui + +FSM_INITIAL_STATE(ui::UiState, ui::states::Splash) |
