From 71ed09a6f70901c9097973a44b24d6a6ced2834f Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 14 Nov 2023 13:20:04 +1100 Subject: [PATCH] Add two-way databinding for lua, and flesh out the lua statusbar --- lib/lvgl/lv_conf.h | 6 +- {tools/icons/raw => lua/assets}/audio.png | Bin .../icons/raw => lua/assets}/battery_20.png | Bin .../icons/raw => lua/assets}/battery_40.png | Bin .../icons/raw => lua/assets}/battery_60.png | Bin .../icons/raw => lua/assets}/battery_80.png | Bin .../raw => lua/assets}/battery_empty.png | Bin lua/assets/battery_full.bin | Bin 0 -> 292 bytes .../icons/raw => lua/assets}/battery_full.png | Bin lua/assets/bt.png | Bin 0 -> 8502 bytes .../bluetooth.png => lua/assets/bt_conn.png | Bin {tools/icons/raw => lua/assets}/pause.png | Bin {tools/icons/raw => lua/assets}/play.png | Bin lua/backstack.lua | 37 ++++ lua/main.lua | 4 +- lua/main_menu.lua | 19 +- lua/widgets.lua | 95 +++++++-- src/lua/CMakeLists.txt | 2 +- src/lua/bridge.cpp | 45 +++- src/lua/include/bridge.hpp | 9 + src/lua/include/lua_thread.hpp | 2 + src/lua/include/property.hpp | 47 +++++ src/lua/lua_thread.cpp | 2 +- src/lua/property.cpp | 196 ++++++++++++++++++ src/ui/include/ui_fsm.hpp | 19 +- src/ui/ui_fsm.cpp | 43 ++++ 26 files changed, 485 insertions(+), 41 deletions(-) rename {tools/icons/raw => lua/assets}/audio.png (100%) rename {tools/icons/raw => lua/assets}/battery_20.png (100%) rename {tools/icons/raw => lua/assets}/battery_40.png (100%) rename {tools/icons/raw => lua/assets}/battery_60.png (100%) rename {tools/icons/raw => lua/assets}/battery_80.png (100%) rename {tools/icons/raw => lua/assets}/battery_empty.png (100%) create mode 100644 lua/assets/battery_full.bin rename {tools/icons/raw => lua/assets}/battery_full.png (100%) create mode 100644 lua/assets/bt.png rename tools/icons/raw/bluetooth.png => lua/assets/bt_conn.png (100%) rename {tools/icons/raw => lua/assets}/pause.png (100%) rename {tools/icons/raw => lua/assets}/play.png (100%) create mode 100644 lua/backstack.lua create mode 100644 src/lua/include/property.hpp create mode 100644 src/lua/property.cpp diff --git a/lib/lvgl/lv_conf.h b/lib/lvgl/lv_conf.h index 06f19f40..07bfc9ad 100644 --- a/lib/lvgl/lv_conf.h +++ b/lib/lvgl/lv_conf.h @@ -597,9 +597,9 @@ /*File system interfaces for common APIs */ /*API for fopen, fread, etc*/ -#define LV_USE_FS_STDIO 0 +#define LV_USE_FS_STDIO 1 #if LV_USE_FS_STDIO - #define LV_FS_STDIO_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ + #define LV_FS_STDIO_LETTER '/' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ #define LV_FS_STDIO_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/ #define LV_FS_STDIO_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/ #endif @@ -628,7 +628,7 @@ #endif /*PNG decoder library*/ -#define LV_USE_PNG 0 +#define LV_USE_PNG 1 /*BMP decoder library*/ #define LV_USE_BMP 0 diff --git a/tools/icons/raw/audio.png b/lua/assets/audio.png similarity index 100% rename from tools/icons/raw/audio.png rename to lua/assets/audio.png diff --git a/tools/icons/raw/battery_20.png b/lua/assets/battery_20.png similarity index 100% rename from tools/icons/raw/battery_20.png rename to lua/assets/battery_20.png diff --git a/tools/icons/raw/battery_40.png b/lua/assets/battery_40.png similarity index 100% rename from tools/icons/raw/battery_40.png rename to lua/assets/battery_40.png diff --git a/tools/icons/raw/battery_60.png b/lua/assets/battery_60.png similarity index 100% rename from tools/icons/raw/battery_60.png rename to lua/assets/battery_60.png diff --git a/tools/icons/raw/battery_80.png b/lua/assets/battery_80.png similarity index 100% rename from tools/icons/raw/battery_80.png rename to lua/assets/battery_80.png diff --git a/tools/icons/raw/battery_empty.png b/lua/assets/battery_empty.png similarity index 100% rename from tools/icons/raw/battery_empty.png rename to lua/assets/battery_empty.png diff --git a/lua/assets/battery_full.bin b/lua/assets/battery_full.bin new file mode 100644 index 0000000000000000000000000000000000000000..b01ca61fb76af4126ff0022d78a9437ceb42731b GIT binary patch literal 292 tcmZQEXkh#g1q@IC<3nheD2PUuVPLRhg}^~C&R_?I81BR&j_@(WzX0_4on`<4 literal 0 HcmV?d00001 diff --git a/tools/icons/raw/battery_full.png b/lua/assets/battery_full.png similarity index 100% rename from tools/icons/raw/battery_full.png rename to lua/assets/battery_full.png diff --git a/lua/assets/bt.png b/lua/assets/bt.png new file mode 100644 index 0000000000000000000000000000000000000000..180e6b3a64257ee98e22fcb148beb174089ec3ae GIT binary patch literal 8502 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 literal 0 HcmV?d00001 diff --git a/tools/icons/raw/bluetooth.png b/lua/assets/bt_conn.png similarity index 100% rename from tools/icons/raw/bluetooth.png rename to lua/assets/bt_conn.png diff --git a/tools/icons/raw/pause.png b/lua/assets/pause.png similarity index 100% rename from tools/icons/raw/pause.png rename to lua/assets/pause.png diff --git a/tools/icons/raw/play.png b/lua/assets/play.png similarity index 100% rename from tools/icons/raw/play.png rename to lua/assets/play.png diff --git a/lua/backstack.lua b/lua/backstack.lua new file mode 100644 index 00000000..c54fbac4 --- /dev/null +++ b/lua/backstack.lua @@ -0,0 +1,37 @@ +local lvgl = require("lvgl") + +local backstack = { + root = lvgl.Object(nil, { + w = lvgl.HOR_RES(), + h = lvgl.VER_RES(), + }), + stack = {}, +} + +function backstack:Top() + return self.stack[#self.stack] +end + +function backstack:SetTopParent(parent) + local top = self:Top() + if top and top.root then + top.root:set_parent(parent) + end +end + +function backstack:Push(screen) + self:SetTopParent(nil) + table.insert(self.stack, screen) + self:SetTopParent(self.root) +end + +function backstack:Pop(num) + num = num or 1 + for _ = 1, num do + local removed = table.remove(self.stack) + removed.root:delete() + end + self:SetTopParent(self.root) +end + +return backstack diff --git a/lua/main.lua b/lua/main.lua index 2a80a571..ce9596af 100644 --- a/lua/main.lua +++ b/lua/main.lua @@ -1 +1,3 @@ -require("main_menu"):Create() +local backstack = require("backstack") +local main_menu = require("main_menu"):Create(backstack.root) +backstack:Push(main_menu) diff --git a/lua/main_menu.lua b/lua/main_menu.lua index 924b51cf..f0be33de 100644 --- a/lua/main_menu.lua +++ b/lua/main_menu.lua @@ -5,8 +5,9 @@ local database = require("database") local main_menu = {} -function main_menu:Create() - local root = lvgl.Object(nil, { +function main_menu:Create(parent) + local menu = {} + menu.root = lvgl.Object(parent, { flex = { flex_direction = "column", flex_wrap = "wrap", @@ -17,31 +18,33 @@ function main_menu:Create() w = lvgl.HOR_RES(), h = lvgl.VER_RES(), }) - root:center() + menu.root:center() - widgets.StatusBar(root, {}) + menu.status_bar = widgets.StatusBar(menu.root, {}) - local list = lvgl.List(root, { + menu.list = lvgl.List(menu.root, { w = lvgl.PCT(100), h = lvgl.PCT(100), flex_grow = 1, }) - list:add_btn(nil, "Now Playing"):onClicked(function() + menu.list:add_btn(nil, "Now Playing"):onClicked(function() legacy_ui.open_now_playing(); end) local indexes = database.get_indexes() for id, name in ipairs(indexes) do - local btn = list:add_btn(nil, name) + local btn = menu.list:add_btn(nil, name) btn:onClicked(function() legacy_ui.open_browse(id); end) end - list:add_btn(nil, "Settings"):onClicked(function() + menu.list:add_btn(nil, "Settings"):onClicked(function() legacy_ui.open_settings(); end) + + return menu end return main_menu diff --git a/lua/widgets.lua b/lua/widgets.lua index bcc3ca59..a281620e 100644 --- a/lua/widgets.lua +++ b/lua/widgets.lua @@ -1,37 +1,96 @@ local lvgl = require("lvgl") +local power = require("power") +local bluetooth = require("bluetooth") +local playback = require("playback") local widgets = {} -function widgets.StatusBar(parent) - local container = parent:Object { +function widgets.StatusBar(parent, opts) + local status_bar = {} + + status_bar.root = parent:Object { flex = { - flex_direction = "row", - justify_content = "flex-start", - align_items = "center", - align_content = "center", + flex_direction = "row", + justify_content = "flex-start", + align_items = "center", + align_content = "center", }, w = lvgl.HOR_RES(), - h = 16, + h = 18, } - container:Label { - w = lvgl.SIZE_CONTENT, - h = 12, - text = "<", - } + if opts.back_cb then + status_bar.back = status_bar.root:Label { + w = lvgl.SIZE_CONTENT, + h = 12, + text = "<", + } + status_bar.back:onClicked(opts.back_cb) + end - container:Label { + status_bar.title = status_bar.root:Label { w = lvgl.PCT(100), h = 16, - text = "cool title", + text = "", flex_grow = 1, } + if opts.title then + status_bar.title.set { text = opts.title } + end - container:Label { - w = lvgl.SIZE_CONTENT, - h = 16, - text = "69%", + status_bar.playing = status_bar.root:Image {} + status_bar.bluetooth = status_bar.root:Image {} + status_bar.battery = status_bar.root:Image {} + + status_bar.bindings = { + power.battery_pct:bind(function(percent) + local src + if percent >= 95 then + src = "battery_full.png" + elseif percent >= 75 then + src = "battery_80.png" + elseif percent >= 55 then + src = "battery_60.png" + elseif percent >= 35 then + src = "battery_40.png" + elseif percent >= 15 then + src = "battery_20.png" + else + src = "battery_empty.png" + end + status_bar.battery:set_src("//lua/assets/" .. src) + end), + playback.playing:bind(function(playing) + if playing then + status_bar.playing:set_src("//lua/assets/play.png") + else + status_bar.playing:set_src("//lua/assets/pause.png") + end + end), + playback.track:bind(function(track) + if track then + status_bar.playing:clear_flag(lvgl.FLAG.HIDDEN) + else + status_bar.playing:add_flag(lvgl.FLAG.HIDDEN) + end + end), + bluetooth.enabled:bind(function(en) + if en then + status_bar.bluetooth:clear_flag(lvgl.FLAG.HIDDEN) + else + status_bar.bluetooth:add_flag(lvgl.FLAG.HIDDEN) + end + end), + bluetooth.connected:bind(function(connected) + if connected then + status_bar.bluetooth:set_src("//lua/assets/bt_conn.png") + else + status_bar.bluetooth:set_src("//lua/assets/bt.png") + end + end), } + + return status_bar end return widgets diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt index a2dd8739..f179a881 100644 --- a/src/lua/CMakeLists.txt +++ b/src/lua/CMakeLists.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "lua_thread.cpp" "bridge.cpp" + SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" INCLUDE_DIRS "include" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "esp-idf-lua" "luavgl") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index acc64c31..ba6f50b4 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -10,10 +10,12 @@ #include #include "esp_log.h" -#include "event_queue.hpp" -#include "lua.h" +#include "lauxlib.h" #include "lua.hpp" #include "lvgl.h" + +#include "event_queue.hpp" +#include "property.hpp" #include "service_locator.hpp" #include "ui_events.hpp" @@ -53,9 +55,7 @@ static auto lua_legacy_ui(lua_State* state) -> int { } static auto get_indexes(lua_State* state) -> int { - lua_pushstring(state, kBridgeKey); - lua_gettable(state, LUA_REGISTRYINDEX); - Bridge* instance = reinterpret_cast(lua_touserdata(state, -1)); + Bridge* instance = Bridge::Get(state); lua_newtable(state); @@ -80,8 +80,14 @@ static auto lua_database(lua_State* state) -> int { return 1; } +auto Bridge::Get(lua_State* state) -> Bridge* { + lua_pushstring(state, kBridgeKey); + lua_gettable(state, LUA_REGISTRYINDEX); + return reinterpret_cast(lua_touserdata(state, -1)); +} + Bridge::Bridge(system_fsm::ServiceLocator& services, lua_State& s) - : services_(services), state_(s) { + : services_(services), state_(s), bindings_(s) { lua_pushstring(&s, kBridgeKey); lua_pushlightuserdata(&s, this); lua_settable(&s, LUA_REGISTRYINDEX); @@ -93,4 +99,31 @@ Bridge::Bridge(system_fsm::ServiceLocator& services, lua_State& s) lua_pop(&s, 1); } +static auto new_property_module(lua_State* state) -> int { + const char* name = luaL_checkstring(state, 1); + luaL_newmetatable(state, name); + + lua_pushstring(state, "__index"); + lua_pushvalue(state, -2); + lua_settable(state, -3); // metatable.__index = metatable + + return 1; +} + +auto Bridge::AddPropertyModule( + const std::string& name, + std::vector>> props) + -> void { + // Create the module (or retrieve it if one with this name already exists) + luaL_requiref(&state_, name.c_str(), new_property_module, true); + + for (const auto& prop : props) { + lua_pushstring(&state_, prop.first.c_str()); + bindings_.Register(&state_, prop.second.get()); + lua_settable(&state_, -3); // metatable.propname = property + } + + lua_pop(&state_, 1); // pop the module off the stack +} + } // namespace lua diff --git a/src/lua/include/bridge.hpp b/src/lua/include/bridge.hpp index 059d0604..26401d14 100644 --- a/src/lua/include/bridge.hpp +++ b/src/lua/include/bridge.hpp @@ -11,19 +11,28 @@ #include "lua.hpp" #include "lvgl.h" +#include "property.hpp" #include "service_locator.hpp" namespace lua { class Bridge { public: + static auto Get(lua_State* state) -> Bridge*; + Bridge(system_fsm::ServiceLocator&, lua_State& s); + auto AddPropertyModule( + const std::string&, + std::vector>>) -> void; + system_fsm::ServiceLocator& services() { return services_; } + PropertyBindings& bindings() { return bindings_; } private: system_fsm::ServiceLocator& services_; lua_State& state_; + PropertyBindings bindings_; }; } // namespace lua diff --git a/src/lua/include/lua_thread.hpp b/src/lua/include/lua_thread.hpp index 381b1bdb..939d0cda 100644 --- a/src/lua/include/lua_thread.hpp +++ b/src/lua/include/lua_thread.hpp @@ -27,6 +27,8 @@ class LuaThread { auto RunScript(const std::string& path) -> bool; + auto bridge() -> Bridge& { return *bridge_; } + private: LuaThread(std::unique_ptr&, std::unique_ptr&, lua_State*); diff --git a/src/lua/include/property.hpp b/src/lua/include/property.hpp new file mode 100644 index 00000000..b6b4718f --- /dev/null +++ b/src/lua/include/property.hpp @@ -0,0 +1,47 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include + +#include "lua.hpp" +#include "lvgl.h" +#include "service_locator.hpp" + +namespace lua { + +using LuaValue = std::variant; + +class Property { + public: + Property() : Property(std::monostate{}) {} + Property(const LuaValue&); + Property(const LuaValue&, std::function); + + auto IsTwoWay() -> bool { return cb_.has_value(); } + + auto PushValue(lua_State& s) -> int; + auto PopValue(lua_State& s) -> bool; + auto Update(const LuaValue& new_val) -> void; + + auto AddLuaBinding(lua_State*, int ref) -> void; + + private: + LuaValue value_; + std::optional> cb_; + std::vector> bindings_; +}; + +class PropertyBindings { + public: + PropertyBindings(lua_State&); + + auto Register(lua_State*, Property*) -> void; +}; + +} // namespace lua diff --git a/src/lua/lua_thread.cpp b/src/lua/lua_thread.cpp index cb7066a5..eb2f5107 100644 --- a/src/lua/lua_thread.cpp +++ b/src/lua/lua_thread.cpp @@ -5,11 +5,11 @@ */ #include "lua_thread.hpp" + #include #include "esp_heap_caps.h" #include "esp_log.h" -#include "lua.h" #include "lua.hpp" #include "luavgl.h" #include "service_locator.hpp" diff --git a/src/lua/property.cpp b/src/lua/property.cpp new file mode 100644 index 00000000..3130077b --- /dev/null +++ b/src/lua/property.cpp @@ -0,0 +1,196 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "property.hpp" + +#include +#include + +#include "lua.h" +#include "lua.hpp" +#include "lvgl.h" +#include "service_locator.hpp" + +namespace lua { + +static const char kMetatableName[] = "property"; +static const char kBindingsTable[] = "bindings"; + +static auto check_property(lua_State* state) -> Property* { + void* data = luaL_checkudata(state, 1, kMetatableName); + luaL_argcheck(state, data != NULL, 1, "`property` expected"); + return *reinterpret_cast(data); +} + +static auto property_get(lua_State* state) -> int { + Property* p = check_property(state); + p->PushValue(*state); + return 1; +} + +static auto property_set(lua_State* state) -> int { + Property* p = check_property(state); + luaL_argcheck(state, p->IsTwoWay(), 1, "property is read-only"); + bool valid = p->PopValue(*state); + lua_pushboolean(state, valid); + return 1; +} + +static auto property_bind(lua_State* state) -> int { + Property* p = check_property(state); + luaL_checktype(state, 2, LUA_TFUNCTION); + + // Copy the function, as we need to invoke it then store our reference. + lua_pushvalue(state, 2); + // ...and another copy, since we return the original closure. + lua_pushvalue(state, 2); + + // FIXME: This should ideally be lua_pcall, for safety. + p->PushValue(*state); + lua_call(state, 1, 0); // Invoke the initial binding. + + lua_pushstring(state, kBindingsTable); + lua_gettable(state, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] + lua_insert(state, -2); // Move bindings to the bottom, with fn above. + int ref = luaL_ref(state, -2); // bindings[ref] = fn + + p->AddLuaBinding(state, ref); + + // Pop the bindings table, leaving one of the copiesw of the callback fn at + // the top of the stack. + lua_pop(state, 1); + + return 1; +} + +static const struct luaL_Reg kPropertyBindingFuncs[] = {{"get", property_get}, + {"set", property_set}, + {"bind", property_bind}, + {NULL, NULL}}; + +PropertyBindings::PropertyBindings(lua_State& s) { + // Create the metatable responsible for the Property API. + luaL_newmetatable(&s, kMetatableName); + + lua_pushliteral(&s, "__index"); + lua_pushvalue(&s, -2); + lua_settable(&s, -3); // metatable.__index = metatable + + // Add our binding funcs (get, set, bind) to the metatable. + luaL_setfuncs(&s, kPropertyBindingFuncs, 0); + + // Create a weak table in the registry to hold live bindings. + lua_pushstring(&s, kBindingsTable); + lua_newtable(&s); // bindings = {} + + // Metatable for the weak table. Values are weak. + lua_newtable(&s); // meta = {} + lua_pushliteral(&s, "__mode"); + lua_pushliteral(&s, "v"); + lua_settable(&s, -3); // meta.__mode='v' + lua_setmetatable(&s, -2); // setmetatable(bindings, meta) + + lua_settable(&s, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] = bindings +} + +auto PropertyBindings::Register(lua_State* s, Property* prop) -> void { + Property** data = + reinterpret_cast(lua_newuserdata(s, sizeof(Property*))); + *data = prop; + + luaL_setmetatable(s, kMetatableName); +} + +template +inline constexpr bool always_false_v = false; + +Property::Property(const LuaValue& val) : value_(val), cb_() {} + +Property::Property(const LuaValue& val, + std::function cb) + : value_(val), cb_(cb) {} + +auto Property::PushValue(lua_State& s) -> int { + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + lua_pushnil(&s); + } else if constexpr (std::is_same_v) { + lua_pushinteger(&s, arg); + } else if constexpr (std::is_same_v) { + lua_pushnumber(&s, arg); + } else if constexpr (std::is_same_v) { + lua_pushboolean(&s, arg); + } else if constexpr (std::is_same_v) { + lua_pushstring(&s, arg.c_str()); + } else { + static_assert(always_false_v, "PushValue missing type"); + } + }, + value_); + return 1; +} + +auto Property::PopValue(lua_State& s) -> bool { + LuaValue new_val; + switch (lua_type(&s, 2)) { + case LUA_TNIL: + new_val = std::monostate{}; + break; + case LUA_TNUMBER: + if (lua_isinteger(&s, 2)) { + new_val = lua_tointeger(&s, 2); + } else { + new_val = lua_tonumber(&s, 2); + } + break; + case LUA_TBOOLEAN: + new_val = lua_toboolean(&s, 2); + break; + case LUA_TSTRING: + new_val = lua_tostring(&s, 2); + break; + default: + return false; + } + + if (cb_ && std::invoke(*cb_, new_val)) { + Update(new_val); + return true; + } + return false; +} + +auto Property::Update(const LuaValue& v) -> void { + value_ = v; + + for (int i = bindings_.size() - 1; i >= 0; i--) { + auto& b = bindings_[i]; + + lua_pushstring(b.first, kBindingsTable); + lua_gettable(b.first, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] + int type = lua_rawgeti(b.first, -1, b.second); // push bindings[i] + + // Has closure has been GCed? + if (type == LUA_TNIL) { + // Clean up after ourselves. + lua_pop(b.first, 1); + // Remove the binding. + bindings_.erase(bindings_.begin() + i); + continue; + } + + PushValue(*b.first); // push the argument + lua_call(b.first, 1, 0); // invoke the closure + } +} + +auto Property::AddLuaBinding(lua_State* state, int ref) -> void { + bindings_.push_back({state, ref}); +} + +} // namespace lua diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 7d1d62d6..39fae4b0 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -21,6 +21,7 @@ #include "model_playback.hpp" #include "model_top_bar.hpp" #include "nvs.hpp" +#include "property.hpp" #include "relative_wheel.hpp" #include "screen_playing.hpp" #include "screen_settings.hpp" @@ -56,9 +57,9 @@ class UiState : public tinyfsm::Fsm { /* Fallback event handler. Does nothing. */ void react(const tinyfsm::Event& ev) {} - void react(const system_fsm::BatteryStateChanged&); - void react(const audio::PlaybackStarted&); - void react(const audio::PlaybackFinished&); + virtual void react(const system_fsm::BatteryStateChanged&); + virtual void react(const audio::PlaybackStarted&); + virtual void react(const audio::PlaybackFinished&); void react(const audio::PlaybackUpdate&); void react(const audio::QueueUpdate&); @@ -127,7 +128,19 @@ class Lua : public UiState { void react(const internal::ShowNowPlaying&) override; void react(const internal::ShowSettingsPage&) override; + void react(const system_fsm::BatteryStateChanged&) override; + void react(const audio::PlaybackStarted&) override; + void react(const audio::PlaybackFinished&) override; + using UiState::react; + + private: + std::shared_ptr battery_pct_; + std::shared_ptr battery_mv_; + std::shared_ptr battery_charging_; + std::shared_ptr bluetooth_en_; + std::shared_ptr playback_playing_; + std::shared_ptr playback_track_; }; class Onboarding : public UiState { diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 748e08f9..9ecc9b7c 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -33,6 +33,7 @@ #include "modal_progress.hpp" #include "model_playback.hpp" #include "nvs.hpp" +#include "property.hpp" #include "relative_wheel.hpp" #include "screen.hpp" #include "screen_lua.hpp" @@ -183,7 +184,36 @@ void Lua::entry() { sCurrentScreen.reset(new Screen()); lv_group_set_default(sCurrentScreen->group()); + auto bat = + sServices->battery().State().value_or(battery::Battery::BatteryState{}); + battery_pct_ = + std::make_shared(static_cast(bat.percent)); + battery_mv_ = + std::make_shared(static_cast(bat.millivolts)); + battery_charging_ = std::make_shared(bat.is_charging); + + bluetooth_en_ = std::make_shared(false); + playback_playing_ = std::make_shared(false); + playback_track_ = std::make_shared(); + sLua.reset(lua::LuaThread::Start(*sServices, sCurrentScreen->content())); + sLua->bridge().AddPropertyModule("power", + { + {"battery_pct", battery_pct_}, + {"battery_millivolts", battery_mv_}, + {"plugged_in", battery_charging_}, + }); + sLua->bridge().AddPropertyModule("bluetooth", + { + {"enabled", bluetooth_en_}, + {"connected", bluetooth_en_}, + }); + sLua->bridge().AddPropertyModule("playback", + { + {"playing", playback_playing_}, + {"track", playback_track_}, + }); + sLua->RunScript("/lua/main.lua"); lv_group_set_default(NULL); @@ -216,6 +246,19 @@ void Lua::react(const internal::ShowSettingsPage& ev) { transit(); } +void Lua::react(const system_fsm::BatteryStateChanged& ev) { + battery_pct_->Update(static_cast(ev.new_state.percent)); + battery_mv_->Update(static_cast(ev.new_state.millivolts)); +} + +void Lua::react(const audio::PlaybackStarted&) { + playback_playing_->Update(true); +} + +void Lua::react(const audio::PlaybackFinished&) { + playback_playing_->Update(false); +} + void Onboarding::entry() { progress_ = 0; has_formatted_ = false;