summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2023-11-14 13:20:04 +1100
committerjacqueline <me@jacqueline.id.au>2023-11-14 13:20:04 +1100
commit71ed09a6f70901c9097973a44b24d6a6ced2834f (patch)
tree3d02e4e180cd0a5caa1185eba89181607c4bccb9
parent8a0a167adbf3d9b6f8b6f16aaf20ca39ad5549de (diff)
downloadtangara-fw-71ed09a6f70901c9097973a44b24d6a6ced2834f.tar.gz
Add two-way databinding for lua, and flesh out the lua statusbar
-rw-r--r--lib/lvgl/lv_conf.h6
-rw-r--r--lua/assets/audio.png (renamed from tools/icons/raw/audio.png)bin623 -> 623 bytes
-rw-r--r--lua/assets/battery_20.png (renamed from tools/icons/raw/battery_20.png)bin617 -> 617 bytes
-rw-r--r--lua/assets/battery_40.png (renamed from tools/icons/raw/battery_40.png)bin617 -> 617 bytes
-rw-r--r--lua/assets/battery_60.png (renamed from tools/icons/raw/battery_60.png)bin618 -> 618 bytes
-rw-r--r--lua/assets/battery_80.png (renamed from tools/icons/raw/battery_80.png)bin622 -> 622 bytes
-rw-r--r--lua/assets/battery_empty.png (renamed from tools/icons/raw/battery_empty.png)bin614 -> 614 bytes
-rw-r--r--lua/assets/battery_full.binbin0 -> 292 bytes
-rw-r--r--lua/assets/battery_full.png (renamed from tools/icons/raw/battery_full.png)bin618 -> 618 bytes
-rw-r--r--lua/assets/bt.pngbin0 -> 8502 bytes
-rw-r--r--lua/assets/bt_conn.png (renamed from tools/icons/raw/bluetooth.png)bin654 -> 654 bytes
-rw-r--r--lua/assets/pause.png (renamed from tools/icons/raw/pause.png)bin608 -> 608 bytes
-rw-r--r--lua/assets/play.png (renamed from tools/icons/raw/play.png)bin616 -> 616 bytes
-rw-r--r--lua/backstack.lua37
-rw-r--r--lua/main.lua4
-rw-r--r--lua/main_menu.lua19
-rw-r--r--lua/widgets.lua95
-rw-r--r--src/lua/CMakeLists.txt2
-rw-r--r--src/lua/bridge.cpp45
-rw-r--r--src/lua/include/bridge.hpp9
-rw-r--r--src/lua/include/lua_thread.hpp2
-rw-r--r--src/lua/include/property.hpp47
-rw-r--r--src/lua/lua_thread.cpp2
-rw-r--r--src/lua/property.cpp196
-rw-r--r--src/ui/include/ui_fsm.hpp19
-rw-r--r--src/ui/ui_fsm.cpp43
26 files changed, 485 insertions, 41 deletions
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
index b8ad9071..b8ad9071 100644
--- a/tools/icons/raw/audio.png
+++ b/lua/assets/audio.png
Binary files differ
diff --git a/tools/icons/raw/battery_20.png b/lua/assets/battery_20.png
index 9012376f..9012376f 100644
--- a/tools/icons/raw/battery_20.png
+++ b/lua/assets/battery_20.png
Binary files differ
diff --git a/tools/icons/raw/battery_40.png b/lua/assets/battery_40.png
index 88a0b448..88a0b448 100644
--- a/tools/icons/raw/battery_40.png
+++ b/lua/assets/battery_40.png
Binary files differ
diff --git a/tools/icons/raw/battery_60.png b/lua/assets/battery_60.png
index d86c997a..d86c997a 100644
--- a/tools/icons/raw/battery_60.png
+++ b/lua/assets/battery_60.png
Binary files differ
diff --git a/tools/icons/raw/battery_80.png b/lua/assets/battery_80.png
index 344b3703..344b3703 100644
--- a/tools/icons/raw/battery_80.png
+++ b/lua/assets/battery_80.png
Binary files differ
diff --git a/tools/icons/raw/battery_empty.png b/lua/assets/battery_empty.png
index c9176e8c..c9176e8c 100644
--- a/tools/icons/raw/battery_empty.png
+++ b/lua/assets/battery_empty.png
Binary files differ
diff --git a/lua/assets/battery_full.bin b/lua/assets/battery_full.bin
new file mode 100644
index 00000000..b01ca61f
--- /dev/null
+++ b/lua/assets/battery_full.bin
Binary files differ
diff --git a/tools/icons/raw/battery_full.png b/lua/assets/battery_full.png
index 57122a23..57122a23 100644
--- a/tools/icons/raw/battery_full.png
+++ b/lua/assets/battery_full.png
Binary files differ
diff --git a/lua/assets/bt.png b/lua/assets/bt.png
new file mode 100644
index 00000000..180e6b3a
--- /dev/null
+++ b/lua/assets/bt.png
Binary files differ
diff --git a/tools/icons/raw/bluetooth.png b/lua/assets/bt_conn.png
index 7a5f2b27..7a5f2b27 100644
--- a/tools/icons/raw/bluetooth.png
+++ b/lua/assets/bt_conn.png
Binary files differ
diff --git a/tools/icons/raw/pause.png b/lua/assets/pause.png
index ec388cd5..ec388cd5 100644
--- a/tools/icons/raw/pause.png
+++ b/lua/assets/pause.png
Binary files differ
diff --git a/tools/icons/raw/play.png b/lua/assets/play.png
index 0d0bb34d..0d0bb34d 100644
--- a/tools/icons/raw/play.png
+++ b/lua/assets/play.png
Binary files differ
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 <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;