diff options
| author | jacqueline <me@jacqueline.id.au> | 2023-11-14 13:20:04 +1100 |
|---|---|---|
| committer | jacqueline <me@jacqueline.id.au> | 2023-11-14 13:20:04 +1100 |
| commit | 71ed09a6f70901c9097973a44b24d6a6ced2834f (patch) | |
| tree | 3d02e4e180cd0a5caa1185eba89181607c4bccb9 /src | |
| parent | 8a0a167adbf3d9b6f8b6f16aaf20ca39ad5549de (diff) | |
| download | tangara-fw-71ed09a6f70901c9097973a44b24d6a6ced2834f.tar.gz | |
Add two-way databinding for lua, and flesh out the lua statusbar
Diffstat (limited to 'src')
| -rw-r--r-- | src/lua/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/lua/bridge.cpp | 45 | ||||
| -rw-r--r-- | src/lua/include/bridge.hpp | 9 | ||||
| -rw-r--r-- | src/lua/include/lua_thread.hpp | 2 | ||||
| -rw-r--r-- | src/lua/include/property.hpp | 47 | ||||
| -rw-r--r-- | src/lua/lua_thread.cpp | 2 | ||||
| -rw-r--r-- | src/lua/property.cpp | 196 | ||||
| -rw-r--r-- | src/ui/include/ui_fsm.hpp | 19 | ||||
| -rw-r--r-- | src/ui/ui_fsm.cpp | 43 |
9 files changed, 354 insertions, 11 deletions
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 <string> #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<Bridge*>(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<Bridge*>(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<std::pair<std::string, std::shared_ptr<Property>>> 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<std::pair<std::string, std::shared_ptr<Property>>>) -> 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<Allocator>&, std::unique_ptr<Bridge>&, 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 <me@jacqueline.id.au> + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include <memory> +#include <string> + +#include "lua.hpp" +#include "lvgl.h" +#include "service_locator.hpp" + +namespace lua { + +using LuaValue = std::variant<std::monostate, int, float, bool, std::string>; + +class Property { + public: + Property() : Property(std::monostate{}) {} + Property(const LuaValue&); + Property(const LuaValue&, std::function<bool(const LuaValue&)>); + + 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<std::function<bool(const LuaValue&)>> cb_; + std::vector<std::pair<lua_State*, int>> 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 <memory> #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 <me@jacqueline.id.au> + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "property.hpp" + +#include <memory> +#include <string> + +#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<Property**>(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<Property**>(lua_newuserdata(s, sizeof(Property*))); + *data = prop; + + luaL_setmetatable(s, kMetatableName); +} + +template <class... Ts> +inline constexpr bool always_false_v = false; + +Property::Property(const LuaValue& val) : value_(val), cb_() {} + +Property::Property(const LuaValue& val, + std::function<bool(const LuaValue& val)> cb) + : value_(val), cb_(cb) {} + +auto Property::PushValue(lua_State& s) -> int { + std::visit( + [&](auto&& arg) { + using T = std::decay_t<decltype(arg)>; + if constexpr (std::is_same_v<T, std::monostate>) { + lua_pushnil(&s); + } else if constexpr (std::is_same_v<T, int>) { + lua_pushinteger(&s, arg); + } else if constexpr (std::is_same_v<T, float>) { + lua_pushnumber(&s, arg); + } else if constexpr (std::is_same_v<T, bool>) { + lua_pushboolean(&s, arg); + } else if constexpr (std::is_same_v<T, std::string>) { + lua_pushstring(&s, arg.c_str()); + } else { + static_assert(always_false_v<T>, "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<UiState> { /* 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<lua::Property> battery_pct_; + std::shared_ptr<lua::Property> battery_mv_; + std::shared_ptr<lua::Property> battery_charging_; + std::shared_ptr<lua::Property> bluetooth_en_; + std::shared_ptr<lua::Property> playback_playing_; + std::shared_ptr<lua::Property> 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<lua::Property>(static_cast<int>(bat.percent)); + battery_mv_ = + std::make_shared<lua::Property>(static_cast<int>(bat.millivolts)); + battery_charging_ = std::make_shared<lua::Property>(bat.is_charging); + + bluetooth_en_ = std::make_shared<lua::Property>(false); + playback_playing_ = std::make_shared<lua::Property>(false); + playback_track_ = std::make_shared<lua::Property>(); + 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<Browse>(); } +void Lua::react(const system_fsm::BatteryStateChanged& ev) { + battery_pct_->Update(static_cast<int>(ev.new_state.percent)); + battery_mv_->Update(static_cast<int>(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; |
