From 1573a8c4cde1cd9528b422b2dcc598e37ffe94a7 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 2 May 2024 19:12:26 +1000 Subject: WIP merge cyclically dependent components into one big component --- src/tangara/lua/bridge.cpp | 143 ++++++++++++ src/tangara/lua/bridge.hpp | 54 +++++ src/tangara/lua/lua_controls.cpp | 58 +++++ src/tangara/lua/lua_controls.hpp | 15 ++ src/tangara/lua/lua_database.cpp | 306 ++++++++++++++++++++++++++ src/tangara/lua/lua_database.hpp | 19 ++ src/tangara/lua/lua_queue.cpp | 74 +++++++ src/tangara/lua/lua_queue.hpp | 15 ++ src/tangara/lua/lua_registry.hpp | 51 +++++ src/tangara/lua/lua_screen.cpp | 79 +++++++ src/tangara/lua/lua_screen.hpp | 15 ++ src/tangara/lua/lua_theme.cpp | 89 ++++++++ src/tangara/lua/lua_theme.hpp | 15 ++ src/tangara/lua/lua_thread.cpp | 187 ++++++++++++++++ src/tangara/lua/lua_thread.hpp | 44 ++++ src/tangara/lua/lua_version.cpp | 68 ++++++ src/tangara/lua/lua_version.hpp | 15 ++ src/tangara/lua/property.cpp | 460 +++++++++++++++++++++++++++++++++++++++ src/tangara/lua/property.hpp | 107 +++++++++ src/tangara/lua/registry.cpp | 73 +++++++ 20 files changed, 1887 insertions(+) create mode 100644 src/tangara/lua/bridge.cpp create mode 100644 src/tangara/lua/bridge.hpp create mode 100644 src/tangara/lua/lua_controls.cpp create mode 100644 src/tangara/lua/lua_controls.hpp create mode 100644 src/tangara/lua/lua_database.cpp create mode 100644 src/tangara/lua/lua_database.hpp create mode 100644 src/tangara/lua/lua_queue.cpp create mode 100644 src/tangara/lua/lua_queue.hpp create mode 100644 src/tangara/lua/lua_registry.hpp create mode 100644 src/tangara/lua/lua_screen.cpp create mode 100644 src/tangara/lua/lua_screen.hpp create mode 100644 src/tangara/lua/lua_theme.cpp create mode 100644 src/tangara/lua/lua_theme.hpp create mode 100644 src/tangara/lua/lua_thread.cpp create mode 100644 src/tangara/lua/lua_thread.hpp create mode 100644 src/tangara/lua/lua_version.cpp create mode 100644 src/tangara/lua/lua_version.hpp create mode 100644 src/tangara/lua/property.cpp create mode 100644 src/tangara/lua/property.hpp create mode 100644 src/tangara/lua/registry.cpp (limited to 'src/tangara/lua') diff --git a/src/tangara/lua/bridge.cpp b/src/tangara/lua/bridge.cpp new file mode 100644 index 00000000..cfa9d5f7 --- /dev/null +++ b/src/tangara/lua/bridge.cpp @@ -0,0 +1,143 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "bridge.hpp" +#include + +#include +#include + +#include "database.hpp" +#include "esp_log.h" +#include "index.hpp" +#include "lauxlib.h" +#include "lua.h" +#include "lua.hpp" +#include "lua_controls.hpp" +#include "lua_database.hpp" +#include "lua_queue.hpp" +#include "lua_screen.hpp" +#include "lua_version.hpp" +#include "lua_theme.hpp" +#include "lvgl.h" + +#include "font/lv_font_loader.h" +#include "luavgl.h" + +#include "event_queue.hpp" +#include "property.hpp" +#include "service_locator.hpp" +#include "ui_events.hpp" + +extern "C" { +int luaopen_linenoise(lua_State* L); +int luaopen_term_core(lua_State* L); +} + +LV_FONT_DECLARE(font_fusion_12); +LV_FONT_DECLARE(font_fusion_10); + +namespace lua { + +[[maybe_unused]] static constexpr char kTag[] = "lua_bridge"; + +static constexpr char kBridgeKey[] = "bridge"; + +static auto make_font_cb(const char* name, int size, int weight) + -> const lv_font_t* { + if (std::string{"fusion"} == name) { + if (size == 12) { + return &font_fusion_12; + } + if (size == 10) { + return &font_fusion_10; + } + } + return NULL; +} + +static auto delete_font_cb(lv_font_t* font) -> void {} + +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) : services_(services) {} + +auto Bridge::installBaseModules(lua_State* L) -> void { + lua_pushstring(L, kBridgeKey); + lua_pushlightuserdata(L, this); + lua_settable(L, LUA_REGISTRYINDEX); + + bindings_.install(L); + + luaL_requiref(L, "linenoise", luaopen_linenoise, true); + lua_pop(L, 1); + + luaL_requiref(L, "term.core", luaopen_term_core, true); + lua_pop(L, 1); + + RegisterControlsModule(L); + RegisterDatabaseModule(L); + RegisterQueueModule(L); + RegisterVersionModule(L); + RegisterThemeModule(L); + RegisterScreenModule(L); +} + +auto Bridge::installLvgl(lua_State* L) -> void { + luavgl_set_pcall(L, CallProtected); + luavgl_set_font_extension(L, make_font_cb, delete_font_cb); + luaL_requiref(L, "lvgl", luaopen_lvgl, true); + lua_pop(L, 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; +} + +template +inline constexpr bool always_false_v = false; + +auto Bridge::installPropertyModule( + lua_State* L, + const std::string& name, + std::vector>>& + props) -> void { + // Create the module (or retrieve it if one with this name already exists) + luaL_requiref(L, name.c_str(), new_property_module, true); + + for (const auto& prop : props) { + lua_pushstring(L, prop.first.c_str()); + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + bindings_.Register(L, arg); + } else if constexpr (std::is_same_v) { + bindings_.Register(L, arg); + } else { + static_assert(always_false_v, "missing case"); + } + }, + prop.second); + + lua_settable(L, -3); // metatable.propname = property + } + + lua_pop(L, 1); // pop the module off the stack +} + +} // namespace lua diff --git a/src/tangara/lua/bridge.hpp b/src/tangara/lua/bridge.hpp new file mode 100644 index 00000000..64f14e0e --- /dev/null +++ b/src/tangara/lua/bridge.hpp @@ -0,0 +1,54 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include + +#include "lua.hpp" +#include "lvgl.h" +#include "property.hpp" +#include "service_locator.hpp" + +namespace lua { + +/* + * Responsible for adding C/C++ module bindings to Lua threads. This class + * keeps no thread-specific internal state, and instead uses the LUA_REGISTRY + * table of its host threads to store data. + */ +class Bridge { + public: + /* + * Utility for retrieving the Bridge from a Lua thread in which the Bridge's + * bindings have been installed. Use by Lua's C callbacks to access the rest + * of the system. + */ + static auto Get(lua_State* state) -> Bridge*; + + Bridge(system_fsm::ServiceLocator& s); + + system_fsm::ServiceLocator& services() { return services_; } + + auto installBaseModules(lua_State* L) -> void; + auto installLvgl(lua_State* L) -> void; + auto installPropertyModule( + lua_State* L, + const std::string&, + std::vector< + std::pair>>&) + -> void; + + Bridge(const Bridge&) = delete; + Bridge& operator=(const Bridge&) = delete; + + private: + system_fsm::ServiceLocator& services_; + PropertyBindings bindings_; +}; + +} // namespace lua diff --git a/src/tangara/lua/lua_controls.cpp b/src/tangara/lua/lua_controls.cpp new file mode 100644 index 00000000..2da0ed11 --- /dev/null +++ b/src/tangara/lua/lua_controls.cpp @@ -0,0 +1,58 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_controls.hpp" + +#include +#include + +#include "lua.hpp" + +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lvgl.h" + +#include "nvs.hpp" +#include "ui_events.hpp" + +namespace lua { + +[[maybe_unused]] static constexpr char kTag[] = "lua_controls"; + +static auto controls_schemes(lua_State* L) -> int { + lua_newtable(L); + + lua_pushliteral(L, "Buttons Only"); + lua_rawseti(L, -2, + static_cast(drivers::NvsStorage::InputModes::kButtonsOnly)); + + lua_pushliteral(L, "D-Pad"); + lua_rawseti( + L, -2, + static_cast(drivers::NvsStorage::InputModes::kDirectionalWheel)); + + lua_pushliteral(L, "Touchwheel"); + lua_rawseti( + L, -2, static_cast(drivers::NvsStorage::InputModes::kRotatingWheel)); + + return 1; +} + +static const struct luaL_Reg kControlsFuncs[] = {{"schemes", controls_schemes}, + {NULL, NULL}}; + +static auto lua_controls(lua_State* state) -> int { + luaL_newlib(state, kControlsFuncs); + return 1; +} + +auto RegisterControlsModule(lua_State* s) -> void { + luaL_requiref(s, "controls", lua_controls, true); + lua_pop(s, 1); +} + +} // namespace lua diff --git a/src/tangara/lua/lua_controls.hpp b/src/tangara/lua/lua_controls.hpp new file mode 100644 index 00000000..18ad261d --- /dev/null +++ b/src/tangara/lua/lua_controls.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterControlsModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/tangara/lua/lua_database.cpp b/src/tangara/lua/lua_database.cpp new file mode 100644 index 00000000..d0612fdd --- /dev/null +++ b/src/tangara/lua/lua_database.cpp @@ -0,0 +1,306 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_database.hpp" + +#include +#include +#include +#include + +#include "bridge.hpp" +#include "lua.hpp" + +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lua_thread.hpp" +#include "lvgl.h" + +#include "database.hpp" +#include "event_queue.hpp" +#include "index.hpp" +#include "property.hpp" +#include "records.hpp" +#include "service_locator.hpp" +#include "track.hpp" +#include "ui_events.hpp" + +namespace lua { + +[[maybe_unused]] static constexpr char kTag[] = "lua_db"; + +static constexpr char kDbIndexMetatable[] = "db_index"; +static constexpr char kDbRecordMetatable[] = "db_record"; +static constexpr char kDbIteratorMetatable[] = "db_iterator"; + +struct LuaIndexInfo { + database::IndexId id; + size_t name_size; + char name_data[]; + + auto name() -> std::string_view { return {name_data, name_size}; } +}; + +static_assert(std::is_trivially_destructible()); +static_assert(std::is_trivially_copy_assignable()); + +static auto indexes(lua_State* state) -> int { + Bridge* instance = Bridge::Get(state); + + lua_newtable(state); + + auto db = instance->services().database().lock(); + if (!db) { + return 1; + } + + for (const auto& i : db->getIndexes()) { + LuaIndexInfo* data = reinterpret_cast( + lua_newuserdata(state, sizeof(LuaIndexInfo) + i.name.size())); + luaL_setmetatable(state, kDbIndexMetatable); + *data = LuaIndexInfo{ + .id = i.id, + .name_size = i.name.size(), + }; + std::memcpy(data->name_data, i.name.data(), i.name.size()); + lua_rawseti(state, -2, i.id); + } + + return 1; +} + +static auto version(lua_State* L) -> int { + Bridge* instance = Bridge::Get(L); + auto db = instance->services().database().lock(); + if (!db) { + return 0; + } + auto res = db->schemaVersion(); + lua_pushlstring(L, res.data(), res.size()); + return 1; +} + +static auto size(lua_State* L) -> int { + Bridge* instance = Bridge::Get(L); + auto db = instance->services().database().lock(); + if (!db) { + return 0; + } + lua_pushinteger(L, db->sizeOnDiskBytes()); + return 1; +} + +static auto recreate(lua_State* L) -> int { + ESP_LOGI(kTag, "recreate"); + return 0; +} + +static auto update(lua_State* L) -> int { + Bridge* instance = Bridge::Get(L); + auto db = instance->services().database().lock(); + if (!db) { + return 0; + } + + instance->services().bg_worker().Dispatch( + [=]() { db->updateIndexes(); }); + return 0; +} + +static const struct luaL_Reg kDatabaseFuncs[] = { + {"indexes", indexes}, {"version", version}, {"size", size}, + {"recreate", recreate}, {"update", update}, {NULL, NULL}}; + +/* + * Struct to be used as userdata for the Lua representation of database records. + * In order to push these large values into PSRAM as much as possible, memory + * for these is allocated and managed by Lua. This struct must therefore be + * trivially copyable. + */ +struct LuaRecord { + std::variant contents; + size_t text_size; + char text[]; +}; + +static_assert(std::is_trivially_destructible()); +static_assert(std::is_trivially_copy_assignable()); + +static auto push_lua_record(lua_State* L, const database::Record& r) -> void { + // Create and init the userdata. + LuaRecord* record = reinterpret_cast( + lua_newuserdata(L, sizeof(LuaRecord) + r.text().size())); + luaL_setmetatable(L, kDbRecordMetatable); + + // Init all the fields + *record = { + .contents = r.contents(), + .text_size = r.text().size(), + }; + + // Copy the string data across. + std::memcpy(record->text, r.text().data(), r.text().size()); +} + +auto db_check_iterator(lua_State* L, int stack_pos) -> database::Iterator* { + database::Iterator* it = *reinterpret_cast( + luaL_checkudata(L, stack_pos, kDbIteratorMetatable)); + return it; +} + +static auto push_iterator(lua_State* state, const database::Iterator& it) + -> void { + database::Iterator** data = reinterpret_cast( + lua_newuserdata(state, sizeof(uintptr_t))); + *data = new database::Iterator(it); + luaL_setmetatable(state, kDbIteratorMetatable); +} + +static auto db_iterate_prev(lua_State* state) -> int { + database::Iterator* it = db_check_iterator(state, 1); + std::optional res = (*it)--; + + if (res) { + push_lua_record(state, *res); + } else { + lua_pushnil(state); + } + + return 1; +} + +static auto db_iterate(lua_State* state) -> int { + database::Iterator* it = db_check_iterator(state, 1); + std::optional res = (*it)++; + + if (res) { + push_lua_record(state, *res); + } else { + lua_pushnil(state); + } + + return 1; +} + +static auto db_iterator_clone(lua_State* state) -> int { + database::Iterator* it = db_check_iterator(state, 1); + push_iterator(state, *it); + return 1; +} + +static auto db_iterator_gc(lua_State* state) -> int { + database::Iterator* it = db_check_iterator(state, 1); + delete it; + return 0; +} + +static const struct luaL_Reg kDbIteratorFuncs[] = {{"next", db_iterate}, + {"prev", db_iterate_prev}, + {"clone", db_iterator_clone}, + {"__call", db_iterate}, + {"__gc", db_iterator_gc}, + {NULL, NULL}}; + +static auto record_text(lua_State* state) -> int { + LuaRecord* data = reinterpret_cast( + luaL_checkudata(state, 1, kDbRecordMetatable)); + lua_pushlstring(state, data->text, data->text_size); + return 1; +} + +static auto record_contents(lua_State* state) -> int { + LuaRecord* data = reinterpret_cast( + luaL_checkudata(state, 1, kDbRecordMetatable)); + + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + lua_pushinteger(state, arg); + } else if constexpr (std::is_same_v) { + Bridge* bridge = Bridge::Get(state); + auto db = bridge->services().database().lock(); + if (!db) { + lua_pushnil(state); + } else { + push_iterator(state, database::Iterator{db, arg}); + } + } + }, + data->contents); + + return 1; +} + +static const struct luaL_Reg kDbRecordFuncs[] = {{"title", record_text}, + {"contents", record_contents}, + {"__tostring", record_text}, + {NULL, NULL}}; + +static auto index_name(lua_State* state) -> int { + LuaIndexInfo* data = reinterpret_cast( + luaL_checkudata(state, 1, kDbIndexMetatable)); + if (data == NULL) { + return 0; + } + lua_pushlstring(state, data->name_data, data->name_size); + return 1; +} + +static auto index_iter(lua_State* state) -> int { + LuaIndexInfo* data = reinterpret_cast( + luaL_checkudata(state, 1, kDbIndexMetatable)); + if (data == NULL) { + return 0; + } + Bridge* bridge = Bridge::Get(state); + auto db = bridge->services().database().lock(); + if (!db) { + lua_pushnil(state); + } + push_iterator(state, database::Iterator{db, data->id}); + return 1; +} + +static const struct luaL_Reg kDbIndexFuncs[] = {{"name", index_name}, + {"iter", index_iter}, + {"__tostring", index_name}, + {NULL, NULL}}; + +static auto lua_database(lua_State* state) -> int { + // Metatable for indexes + luaL_newmetatable(state, kDbIndexMetatable); + + lua_pushliteral(state, "__index"); + lua_pushvalue(state, -2); + lua_settable(state, -3); // metatable.__index = metatable + + // Add member funcs to the metatable. + luaL_setfuncs(state, kDbIndexFuncs, 0); + + luaL_newmetatable(state, kDbIteratorMetatable); + lua_pushliteral(state, "__index"); + lua_pushvalue(state, -2); + lua_settable(state, -3); // metatable.__index = metatable + luaL_setfuncs(state, kDbIteratorFuncs, 0); + + luaL_newmetatable(state, kDbRecordMetatable); + lua_pushliteral(state, "__index"); + lua_pushvalue(state, -2); + lua_settable(state, -3); // metatable.__index = metatable + luaL_setfuncs(state, kDbRecordFuncs, 0); + + luaL_newlib(state, kDatabaseFuncs); + return 1; +} + +auto RegisterDatabaseModule(lua_State* s) -> void { + luaL_requiref(s, "database", lua_database, true); + lua_pop(s, 1); +} + +} // namespace lua diff --git a/src/tangara/lua/lua_database.hpp b/src/tangara/lua/lua_database.hpp new file mode 100644 index 00000000..b0d2acbd --- /dev/null +++ b/src/tangara/lua/lua_database.hpp @@ -0,0 +1,19 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +#include "database.hpp" + +namespace lua { + +auto db_check_iterator(lua_State*, int stack_pos) -> database::Iterator*; + +auto RegisterDatabaseModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/tangara/lua/lua_queue.cpp b/src/tangara/lua/lua_queue.cpp new file mode 100644 index 00000000..dfb820c2 --- /dev/null +++ b/src/tangara/lua/lua_queue.cpp @@ -0,0 +1,74 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_database.hpp" + +#include +#include + +#include "lua.hpp" + +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lvgl.h" + +#include "bridge.hpp" +#include "database.hpp" +#include "event_queue.hpp" +#include "index.hpp" +#include "property.hpp" +#include "service_locator.hpp" +#include "track.hpp" +#include "track_queue.hpp" +#include "ui_events.hpp" + +namespace lua { + +[[maybe_unused]] static constexpr char kTag[] = "lua_queue"; + +static auto queue_add(lua_State* state) -> int { + Bridge* instance = Bridge::Get(state); + + if (lua_isinteger(state, 1)) { + database::TrackId id = luaL_checkinteger(state, 1); + instance->services().bg_worker().Dispatch([=]() { + audio::TrackQueue& queue = instance->services().track_queue(); + queue.append(id); + }); + } else { + database::Iterator* it = db_check_iterator(state, 1); + instance->services().bg_worker().Dispatch([=]() { + audio::TrackQueue& queue = instance->services().track_queue(); + queue.append(database::TrackIterator{*it}); + }); + } + + return 0; +} + +static auto queue_clear(lua_State* state) -> int { + Bridge* instance = Bridge::Get(state); + audio::TrackQueue& queue = instance->services().track_queue(); + queue.clear(); + return 0; +} + +static const struct luaL_Reg kQueueFuncs[] = {{"add", queue_add}, + {"clear", queue_clear}, + {NULL, NULL}}; + +static auto lua_queue(lua_State* state) -> int { + luaL_newlib(state, kQueueFuncs); + return 1; +} + +auto RegisterQueueModule(lua_State* s) -> void { + luaL_requiref(s, "queue", lua_queue, true); + lua_pop(s, 1); +} + +} // namespace lua diff --git a/src/tangara/lua/lua_queue.hpp b/src/tangara/lua/lua_queue.hpp new file mode 100644 index 00000000..c5cfe04d --- /dev/null +++ b/src/tangara/lua/lua_queue.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterQueueModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/tangara/lua/lua_registry.hpp b/src/tangara/lua/lua_registry.hpp new file mode 100644 index 00000000..abc5063e --- /dev/null +++ b/src/tangara/lua/lua_registry.hpp @@ -0,0 +1,51 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include + +#include "lua.hpp" + +#include "bridge.hpp" +#include "lua_thread.hpp" +#include "service_locator.hpp" + +namespace lua { + +class Registry { + public: + static auto instance(system_fsm::ServiceLocator&) -> Registry&; + + auto uiThread() -> std::shared_ptr; + auto newThread() -> std::shared_ptr; + + auto AddPropertyModule( + const std::string&, + std::vector>>) + -> void; + + Registry(const Registry&) = delete; + Registry& operator=(const Registry&) = delete; + + private: + Registry(system_fsm::ServiceLocator&); + + system_fsm::ServiceLocator& services_; + std::unique_ptr bridge_; + + std::shared_ptr ui_thread_; + std::list> threads_; + + std::vector< + std::pair>>>> + modules_; +}; + +} // namespace lua diff --git a/src/tangara/lua/lua_screen.cpp b/src/tangara/lua/lua_screen.cpp new file mode 100644 index 00000000..f17f6b1a --- /dev/null +++ b/src/tangara/lua/lua_screen.cpp @@ -0,0 +1,79 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_screen.hpp" + +#include +#include + +#include "lua.hpp" + +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lvgl.h" + +#include "bridge.hpp" +#include "database.hpp" +#include "event_queue.hpp" +#include "index.hpp" +#include "property.hpp" +#include "service_locator.hpp" +#include "track.hpp" +#include "track_queue.hpp" +#include "ui_events.hpp" + +namespace lua { + +static auto screen_new(lua_State* L) -> int { + // o = o or {} + if (lua_gettop(L) != 2) { + lua_settop(L, 1); + lua_newtable(L); + } + // Swap o and self on the stack. + lua_insert(L, 1); + + lua_pushliteral(L, "__index"); + lua_pushvalue(L, 1); + lua_settable(L, 1); // self.__index = self + + lua_setmetatable(L, 1); // setmetatable(o, self) + + return 1; // return o +} + +static auto screen_noop(lua_State* state) -> int { + return 0; +} + +static auto screen_true(lua_State* state) -> int { + lua_pushboolean(state, true); + return 1; +} + +static const struct luaL_Reg kScreenFuncs[] = { + {"new", screen_new}, {"createUi", screen_noop}, + {"onShown", screen_noop}, {"onHidden", screen_noop}, + {"canPop", screen_true}, {NULL, NULL}}; + +static auto lua_screen(lua_State* state) -> int { + luaL_newlib(state, kScreenFuncs); + + lua_pushliteral(state, "__index"); + lua_pushvalue(state, -2); + lua_rawset(state, -3); + + return 1; +} + +auto RegisterScreenModule(lua_State* s) -> void { + luaL_requiref(s, "screen", lua_screen, true); + + lua_pop(s, 1); +} + +} // namespace lua diff --git a/src/tangara/lua/lua_screen.hpp b/src/tangara/lua/lua_screen.hpp new file mode 100644 index 00000000..1c3bed1a --- /dev/null +++ b/src/tangara/lua/lua_screen.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterScreenModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/tangara/lua/lua_theme.cpp b/src/tangara/lua/lua_theme.cpp new file mode 100644 index 00000000..72434d97 --- /dev/null +++ b/src/tangara/lua/lua_theme.cpp @@ -0,0 +1,89 @@ + +/* + * Copyright 2023 ailurux + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_version.hpp" + +#include + +#include "bridge.hpp" +#include "lua.hpp" + +#include "esp_app_desc.h" +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lua_thread.hpp" +#include "luavgl.h" +#include "themes.hpp" + +namespace lua { + +static auto set_style(lua_State* L) -> int { + // Get the object and class name from the stack + std::string class_name = luaL_checkstring(L, -1); + lv_obj_t* obj = luavgl_to_obj(L, -2); + if (obj != NULL) { + ui::themes::Theme::instance()->ApplyStyle(obj, class_name); + } + return 0; +} + +static auto set_theme(lua_State* L) -> int { + std::string class_name; + luaL_checktype(L, -1, LUA_TTABLE); + lua_pushnil(L); /* first key */ + while (lua_next(L, -2) != 0) { + /* uses 'key' (at index -2) and 'value' (at index -1) */ + if (lua_type(L, -2) == LUA_TSTRING) { + class_name = lua_tostring(L, -2); + } + if (lua_type(L, -1) == LUA_TTABLE) { + // Nesting + lua_pushnil(L); // First key + while (lua_next(L, -2) != 0) { + // Nesting the second + int selector = -1; + lua_pushnil(L); // First key + while (lua_next(L, -2) != 0) { + int idx = lua_tointeger(L, -2); + if (idx == 1) { + // Selector + selector = lua_tointeger(L, -1); + } else if (idx == 2) { + // Style + lv_style_t* style = luavgl_to_style(L, -1); + if (style == NULL) { + ESP_LOGI("lua_theme", "Style was null or malformed"); + return 0; + } else { + ui::themes::Theme::instance()->AddStyle(class_name, selector, style); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + } + /* removes 'value'; keeps 'key' for next iteration */ + lua_pop(L, 1); + } + return 0; +} + +static const struct luaL_Reg kThemeFuncs[] = {{"set", set_theme}, {"set_style", set_style}, {NULL, NULL}}; + +static auto lua_theme(lua_State* L) -> int { + luaL_newlib(L, kThemeFuncs); + return 1; +} + +auto RegisterThemeModule(lua_State* L) -> void { + luaL_requiref(L, "theme", lua_theme, true); + lua_pop(L, 1); +} + +} // namespace lua diff --git a/src/tangara/lua/lua_theme.hpp b/src/tangara/lua/lua_theme.hpp new file mode 100644 index 00000000..fed710e0 --- /dev/null +++ b/src/tangara/lua/lua_theme.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2024 ailurux + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterThemeModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/tangara/lua/lua_thread.cpp b/src/tangara/lua/lua_thread.cpp new file mode 100644 index 00000000..dd72e41d --- /dev/null +++ b/src/tangara/lua/lua_thread.cpp @@ -0,0 +1,187 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_thread.hpp" + +#include +#include + +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "lua.hpp" + +#include "bridge.hpp" +#include "event_queue.hpp" +#include "memory_resource.hpp" +#include "service_locator.hpp" +#include "ui_events.hpp" + +namespace lua { + +[[maybe_unused]] static constexpr char kTag[] = "lua"; + +class Allocator { + public: + Allocator() : total_allocated_(0) {} + + auto alloc(void* ptr, size_t osize, size_t nsize) -> void* { + total_allocated_ = total_allocated_ - osize + nsize; + // ESP_LOGI(kTag, "lua realloc -> %u KiB", total_allocated_ / 1024); + if (nsize == 0) { + heap_caps_free(ptr); + return NULL; + } else { + return heap_caps_realloc(ptr, nsize, MALLOC_CAP_SPIRAM); + } + } + + private: + size_t total_allocated_; +}; + +static auto lua_alloc(void* ud, void* ptr, size_t osize, size_t nsize) + -> void* { + Allocator* instance = reinterpret_cast(ud); + return instance->alloc(ptr, osize, nsize); +} + +static int lua_panic(lua_State* L) { + ESP_LOGE(kTag, "!! PANIC !! %s", lua_tostring(L, -1)); + return 0; +} + +auto LuaThread::Start(system_fsm::ServiceLocator& services) -> LuaThread* { + auto alloc = std::make_unique(); + lua_State* state = lua_newstate(lua_alloc, alloc.get()); + if (!state) { + return nullptr; + } + + luaL_openlibs(state); + lua_atpanic(state, lua_panic); + + return new LuaThread(alloc, state); +} + +LuaThread::LuaThread(std::unique_ptr& alloc, lua_State* state) + : alloc_(std::move(alloc)), state_(state) {} + +LuaThread::~LuaThread() { + lua_close(state_); +} + +auto LuaThread::RunScript(const std::string& path) -> bool { + int res = luaL_loadfilex(state_, path.c_str(), NULL); + if (res != LUA_OK) { + return false; + } + CallProtected(state_, 0, 0); + return true; +} + +auto LuaThread::RunString(const std::string& script) -> bool { + int res = luaL_loadstring(state_, script.c_str()); + if (res != LUA_OK) { + return false; + } + CallProtected(state_, 0, 0); + return true; +} + +auto LuaThread::DumpStack() -> void { + int top = lua_gettop(state_); + std::cout << "stack size: " << top << std::endl; + for (size_t i = 1; i <= top; i++) { + std::cout << "[" << i << "]\t" << luaL_typename(state_, i); + switch (lua_type(state_, i)) { + case LUA_TNUMBER: + std::cout << "\t("; + if (lua_isinteger(state_, i)) { + std::cout << lua_tointeger(state_, i); + } else { + std::cout << lua_tonumber(state_, i); + } + std::cout << ")"; + break; + case LUA_TSTRING: + std::cout << "\t('" << lua_tostring(state_, i) << "')"; + break; + case LUA_TBOOLEAN: + std::cout << "\t(" << lua_toboolean(state_, i) << ")"; + break; + case LUA_TNIL: + // Value is implied. + break; + case LUA_TTABLE: + lua_pushnil(state_); + while (lua_next(state_, i) != 0) { + // Keys + std::cout << std::endl << "\t\t" << luaL_typename(state_, -2); + if (lua_type(state_, -2) == LUA_TSTRING) { + std::cout << "\t(" << lua_tostring(state_, -2) << ")"; + } else if (lua_type(state_, -2) == LUA_TNUMBER) { + std::cout << "\t(" << lua_tonumber(state_, -2) << ")"; + } + + // Values + std::cout << "\t\t" << luaL_typename(state_, -1); + if (lua_type(state_, -1) == LUA_TSTRING) { + std::cout << "\t(" << lua_tostring(state_, -1) << ")"; + } else if (lua_type(state_, -1) == LUA_TNUMBER) { + std::cout << "\t(" << lua_tonumber(state_, -1) << ")"; + } + // Pop the value; we don't care about it. Leave the key on the stack + // for the next call to lua_next. + lua_pop(state_, 1); + } + break; + default: + std::cout << "\t(" << lua_topointer(state_, i) << ")"; + break; + } + std::cout << std::endl; + } +} + +static int msg_handler(lua_State* L) { + if (!lua_isstring(L, 1)) { + return 1; + } + + const char* msg = lua_tostring(L, 1); + if (msg == NULL) { /* is error object not a string? */ + if (luaL_callmeta(L, 1, "__tostring") && /* does it have a metamethod */ + lua_type(L, -1) == LUA_TSTRING) /* that produces a string? */ + return 1; /* that is the message */ + else + msg = lua_pushfstring(L, "(error object is a %s value)", + luaL_typename(L, 1)); + } + + /* append a standard traceback */ + luaL_traceback(L, L, msg, 1); + return 1; +} + +auto CallProtected(lua_State* s, int nargs, int nresults) -> int { + int base = lua_gettop(s) - nargs; + // Place our message handler under the function to be called. + lua_pushcfunction(s, msg_handler); + lua_insert(s, base); + + // Invoke the function. + int ret = lua_pcall(s, nargs, nresults, base); + if (ret != LUA_OK) { + events::Ui().Dispatch(ui::OnLuaError{.message = lua_tostring(s, -1)}); + } + + // Clean up our message handler + lua_remove(s, base); + + return ret; +} + +} // namespace lua diff --git a/src/tangara/lua/lua_thread.hpp b/src/tangara/lua/lua_thread.hpp new file mode 100644 index 00000000..384de61d --- /dev/null +++ b/src/tangara/lua/lua_thread.hpp @@ -0,0 +1,44 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include + +#include "lua.hpp" + +#include "service_locator.hpp" + +namespace lua { + +class Allocator; + +auto CallProtected(lua_State*, int nargs, int nresults) -> int; + +class LuaThread { + public: + static auto Start(system_fsm::ServiceLocator&) -> LuaThread*; + ~LuaThread(); + + auto RunScript(const std::string& path) -> bool; + auto RunString(const std::string& path) -> bool; + + auto DumpStack() -> void; + + auto state() -> lua_State* { return state_; } + + LuaThread(const LuaThread&) = delete; + LuaThread& operator=(const LuaThread&) = delete; + + private: + LuaThread(std::unique_ptr&, lua_State*); + + std::unique_ptr alloc_; + lua_State* state_; +}; + +} // namespace lua diff --git a/src/tangara/lua/lua_version.cpp b/src/tangara/lua/lua_version.cpp new file mode 100644 index 00000000..e5f06bb5 --- /dev/null +++ b/src/tangara/lua/lua_version.cpp @@ -0,0 +1,68 @@ + +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_version.hpp" + +#include + +#include "bridge.hpp" +#include "lua.hpp" + +#include "esp_app_desc.h" +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lua_thread.hpp" + +namespace lua { + +static auto esp(lua_State* L) -> int { + auto desc = esp_app_get_description(); + lua_pushstring(L, desc->version); + return 1; +} + +static auto samd(lua_State* L) -> int { + Bridge* instance = Bridge::Get(L); + auto& samd = instance->services().samd(); + auto version = samd.Version(); + lua_pushlstring(L, version.data(), version.size()); + return 1; +} + +static auto update_samd(lua_State* L) -> int { + Bridge* instance = Bridge::Get(L); + auto& samd = instance->services().samd(); + samd.ResetToFlashSamd(); + return 0; +} + +static auto collator(lua_State* L) -> int { + Bridge* instance = Bridge::Get(L); + auto& collator = instance->services().collator(); + auto version = collator.Describe().value_or("None"); + lua_pushlstring(L, version.data(), version.size()); + return 1; +} + +static const struct luaL_Reg kVersionFuncs[] = {{"esp", esp}, + {"samd", samd}, + {"collator", collator}, + {"update_samd", update_samd}, + {NULL, NULL}}; + +static auto lua_version(lua_State* L) -> int { + luaL_newlib(L, kVersionFuncs); + return 1; +} + +auto RegisterVersionModule(lua_State* L) -> void { + luaL_requiref(L, "version", lua_version, true); + lua_pop(L, 1); +} + +} // namespace lua diff --git a/src/tangara/lua/lua_version.hpp b/src/tangara/lua/lua_version.hpp new file mode 100644 index 00000000..4ba4be94 --- /dev/null +++ b/src/tangara/lua/lua_version.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterVersionModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/tangara/lua/property.cpp b/src/tangara/lua/property.cpp new file mode 100644 index 00000000..9f4a1908 --- /dev/null +++ b/src/tangara/lua/property.cpp @@ -0,0 +1,460 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "property.hpp" +#include + +#include +#include +#include +#include +#include +#include + +#include "bluetooth_types.hpp" +#include "lauxlib.h" +#include "lua.h" +#include "lua.hpp" +#include "lua_thread.hpp" +#include "lvgl.h" +#include "memory_resource.hpp" +#include "service_locator.hpp" +#include "track.hpp" +#include "types.hpp" + +namespace lua { + +static const char kPropertyMetatable[] = "property"; +static const char kFunctionMetatable[] = "c_func"; +static const char kBindingMetatable[] = "binding"; +static const char kBindingsTable[] = "bindings"; +static const char kBinderKey[] = "binder"; + +auto Binding::get(lua_State* L, int idx) -> Binding* { + return reinterpret_cast(luaL_testudata(L, idx, kBindingMetatable)); +} + +auto Binding::apply(lua_State* L, int idx) -> bool { + Binding* b = get(L, idx); + if (b->dirty && b->active) { + b->dirty = false; + // The binding needs to be reapplied. Push the Lua callback, then its arg. + lua_getiuservalue(L, idx, 1); + b->property->pushValue(*L); + + // Invoke the callback. + return CallProtected(L, 1, 0) == LUA_OK; + } + return true; +} + +static auto check_property(lua_State* state) -> Property* { + void* data = luaL_checkudata(state, 1, kPropertyMetatable); + 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); + + // Fetch the table of live bindings. + lua_pushstring(state, kBindingsTable); + lua_gettable(state, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] + + // Create the userdata holding the new binding's metadata. + Binding* binding = + reinterpret_cast(lua_newuserdatauv(state, sizeof(Binding), 1)); + *binding = Binding{.property = p, .active = true, .dirty = true}; + luaL_setmetatable(state, kBindingMetatable); + + // Associate the callback function with the new binding. + lua_pushvalue(state, 2); + lua_setiuservalue(state, -2, 1); + + // Put a reference to the binding into the bindings table, so that we can + // look it up later. + lua_pushvalue(state, -1); + int binding_ref = luaL_ref(state, 3); + + // Tell the property about the new binding. This was also perform the initial + // bind. + p->addLuaBinding(state, binding_ref); + + // Return the only remaining strong reference to the new Binding. + return 1; +} + +static auto property_tostring(lua_State* state) -> int { + Property* p = check_property(state); + p->pushValue(*state); + + std::stringstream str{}; + str << "property { " << luaL_tolstring(state, -1, NULL); + if (!p->isTwoWay()) { + str << ", read-only"; + } + str << " }"; + + lua_settop(state, 0); + + std::string res = str.str(); + lua_pushlstring(state, res.data(), res.size()); + return 1; +} + +static const struct luaL_Reg kPropertyBindingFuncs[] = { + {"get", property_get}, + {"set", property_set}, + {"bind", property_bind}, + {"__tostring", property_tostring}, + {NULL, NULL}}; + +static auto generic_function_cb(lua_State* state) -> int { + lua_pushstring(state, kBinderKey); + lua_gettable(state, LUA_REGISTRYINDEX); + PropertyBindings* binder = + reinterpret_cast(lua_touserdata(state, -1)); + + size_t* index = + reinterpret_cast(luaL_checkudata(state, 1, kFunctionMetatable)); + const LuaFunction& fn = binder->GetFunction(*index); + + // Ensure the C++ function is called with a clean stack; we don't want it to + // see the index we just used. + lua_remove(state, 1); + + return std::invoke(fn, state); +} + +PropertyBindings::PropertyBindings() : functions_(&memory::kSpiRamResource) {} + +auto PropertyBindings::install(lua_State* L) -> void { + lua_pushstring(L, kBinderKey); + lua_pushlightuserdata(L, this); + lua_settable(L, LUA_REGISTRYINDEX); + + // Create the metatable responsible for the Property API. + luaL_newmetatable(L, kPropertyMetatable); + + lua_pushliteral(L, "__index"); + lua_pushvalue(L, -2); + lua_settable(L, -3); // metatable.__index = metatable + + // Add our binding funcs (get, set, bind) to the metatable. + luaL_setfuncs(L, kPropertyBindingFuncs, 0); + + // We've finished setting up the metatable, so pop it. + lua_pop(L, 1); + + // Create the metatable responsible for each Binding. This metatable is empty + // as it's only used for identification. + luaL_newmetatable(L, kBindingMetatable); + lua_pop(L, 1); + + // Create a weak table in the registry to hold live bindings. + lua_pushstring(L, kBindingsTable); + lua_newtable(L); // bindings = {} + + // Metatable for the weak table. Values are weak. + lua_newtable(L); // meta = {} + lua_pushliteral(L, "__mode"); + lua_pushliteral(L, "v"); + lua_settable(L, -3); // meta.__mode='v' + lua_setmetatable(L, -2); // setmetatable(bindings, meta) + + lua_settable(L, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] = bindings + + // Create the metatable for C++ functions. + luaL_newmetatable(L, kFunctionMetatable); + + lua_pushliteral(L, "__call"); + lua_pushcfunction(L, generic_function_cb); + lua_settable(L, -3); // metatable.__call = metatable + + lua_pop(L, 1); // Clean up the function metatable +} + +auto PropertyBindings::Register(lua_State* s, Property* prop) -> void { + Property** data = + reinterpret_cast(lua_newuserdata(s, sizeof(uintptr_t))); + *data = prop; + + luaL_setmetatable(s, kPropertyMetatable); +} + +auto PropertyBindings::Register(lua_State* s, LuaFunction fn) -> void { + size_t* index = reinterpret_cast(lua_newuserdata(s, sizeof(size_t))); + *index = functions_.size(); + functions_.push_back(fn); + + luaL_setmetatable(s, kFunctionMetatable); +} + +auto PropertyBindings::GetFunction(size_t i) -> const LuaFunction& { + assert(i < functions_.size()); + return functions_[i]; +}; + +template +inline constexpr bool always_false_v = false; + +Property::Property(const LuaValue& val) + : value_(memory::SpiRamAllocator().new_object(val)), + cb_(), + bindings_(&memory::kSpiRamResource) {} + +Property::Property(const LuaValue& val, + std::function cb) + : value_(memory::SpiRamAllocator().new_object(val)), + cb_(cb), + bindings_(&memory::kSpiRamResource) {} + +auto Property::setDirect(const LuaValue& val) -> void { + *value_ = val; + reapplyAll(); +} + +auto Property::set(const LuaValue& val) -> bool { + if (cb_ && !std::invoke(*cb_, val)) { + return false; + } + setDirect(val); + return true; +} + +static auto pushTagValue(lua_State* L, const database::TagValue& val) -> void { + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + lua_pushlstring(L, arg.data(), arg.size()); + } else if constexpr (std::is_same_v< + T, std::span>) { + lua_createtable(L, 0, arg.size()); + for (const auto& i : arg) { + lua_pushlstring(L, i.data(), i.size()); + lua_pushboolean(L, true); + lua_rawset(L, -3); + } + } else if constexpr (std::is_same_v) { + lua_pushinteger(L, arg); + } else { + lua_pushnil(L); + } + }, + val); +} + +static void pushTrack(lua_State* L, const audio::TrackInfo& track) { + lua_newtable(L); + + for (const auto& tag : track.tags->allPresent()) { + lua_pushstring(L, database::tagName(tag).c_str()); + pushTagValue(L, track.tags->get(tag)); + lua_settable(L, -3); + } + + if (track.duration) { + lua_pushliteral(L, "duration"); + lua_pushinteger(L, track.duration.value()); + lua_settable(L, -3); + } + + if (track.bitrate_kbps) { + lua_pushliteral(L, "bitrate_kbps"); + lua_pushinteger(L, track.bitrate_kbps.value()); + lua_settable(L, -3); + } + + lua_pushliteral(L, "encoding"); + lua_pushstring(L, codecs::StreamTypeToString(track.encoding).c_str()); + lua_settable(L, -3); +} + +static void pushDevice(lua_State* L, const drivers::bluetooth::Device& dev) { + lua_createtable(L, 0, 4); + + lua_pushliteral(L, "address"); + auto* mac = reinterpret_cast( + lua_newuserdata(L, sizeof(drivers::bluetooth::mac_addr_t))); + *mac = dev.address; + lua_rawset(L, -3); + + // What I just did there was perfectly safe. Look, I can prove it: + static_assert( + std::is_trivially_copy_assignable()); + static_assert( + std::is_trivially_destructible()); + + lua_pushliteral(L, "name"); + lua_pushlstring(L, dev.name.data(), dev.name.size()); + lua_rawset(L, -3); + + // FIXME: This field deserves a little more structure. + lua_pushliteral(L, "class"); + lua_pushinteger(L, dev.class_of_device); + lua_rawset(L, -3); + + lua_pushliteral(L, "signal_strength"); + lua_pushinteger(L, dev.signal_strength); + lua_rawset(L, -3); +} + +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_pushboolean(&s, arg); + } else if constexpr (std::is_same_v) { + lua_pushstring(&s, arg.c_str()); + } else if constexpr (std::is_same_v) { + pushTrack(&s, arg); + } else if constexpr (std::is_same_v) { + pushDevice(&s, arg); + } else if constexpr (std::is_same_v< + T, std::vector>) { + lua_createtable(&s, arg.size(), 0); + size_t i = 1; + for (const auto& dev : arg) { + pushDevice(&s, dev); + lua_rawseti(&s, -2, i++); + } + } else { + static_assert(always_false_v, "PushValue missing type"); + } + }, + *value_); + return 1; +} + +auto popRichType(lua_State* L) -> LuaValue { + lua_pushliteral(L, "address"); + lua_gettable(L, -2); + + if (lua_isuserdata(L, -1)) { + // This must be a bt device! + drivers::bluetooth::mac_addr_t mac = + *reinterpret_cast( + lua_touserdata(L, -1)); + lua_pop(L, 1); + + lua_pushliteral(L, "name"); + lua_gettable(L, -2); + + std::pmr::string name = lua_tostring(L, -1); + lua_pop(L, 1); + + return drivers::bluetooth::Device{ + .address = mac, + .name = name, + .class_of_device = 0, + .signal_strength = 0, + }; + } + + return std::monostate{}; +} + +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 = static_cast(std::round(lua_tonumber(&s, 2))); + } + break; + case LUA_TBOOLEAN: + new_val = static_cast(lua_toboolean(&s, 2)); + break; + case LUA_TSTRING: + new_val = lua_tostring(&s, 2); + break; + default: + if (lua_istable(&s, 2)) { + new_val = popRichType(&s); + if (std::holds_alternative(new_val)) { + return false; + } + } else { + return false; + } + } + + return set(new_val); +} + +auto Property::reapplyAll() -> void { + for (int i = bindings_.size() - 1; i >= 0; i--) { + auto& b = bindings_[i]; + if (!applySingle(b.first, b.second, true)) { + // Remove the binding if we weren't able to apply it. This is usually due + // to the binding getting GC'd. + bindings_.erase(bindings_.begin() + i); + } + } +} + +auto Property::applySingle(lua_State* L, int ref, bool mark_dirty) -> bool { + int top = lua_gettop(L); + + // Push the table of bindings. + lua_pushstring(L, kBindingsTable); + lua_gettable(L, LUA_REGISTRYINDEX); + + // Resolve the reference. + int type = lua_rawgeti(L, -1, ref); + if (type == LUA_TNIL) { + lua_settop(L, top); + return false; + } + + // Defensively check that the ref was actually for a Binding. + Binding* b = Binding::get(L, -1); + if (!b) { + lua_settop(L, top); + return false; + } + + if (mark_dirty) { + b->dirty = true; + } + + bool ret = Binding::apply(L, -1); + lua_settop(L, top); + return ret; +} + +auto Property::addLuaBinding(lua_State* state, int ref) -> void { + bindings_.push_back({state, ref}); + applySingle(state, ref, true); +} + +} // namespace lua diff --git a/src/tangara/lua/property.hpp b/src/tangara/lua/property.hpp new file mode 100644 index 00000000..724261be --- /dev/null +++ b/src/tangara/lua/property.hpp @@ -0,0 +1,107 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include + +#include "audio_events.hpp" +#include "bluetooth_types.hpp" +#include "lua.hpp" +#include "lvgl.h" +#include "service_locator.hpp" + +namespace lua { + +// FIXME: We should use some kind of interface for this instead. +using LuaValue = std::variant>; + +using LuaFunction = std::function; + +class Property { + public: + Property() : Property(std::monostate{}) {} + Property(const LuaValue&); + Property(const LuaValue&, std::function filter); + + auto get() -> const LuaValue& { return *value_; } + + /* + * Assigns a new value to this property, bypassing the filter fn. All + * bindings will be marked as dirty, and if active, will be reapplied. + */ + auto setDirect(const LuaValue&) -> void; + /* + * Invokes the filter fn, and if successful, assigns the new value to this + * property. All bindings will be marked as dirty, and if active, will be + * reapplied. + */ + auto set(const LuaValue&) -> bool; + + /* Returns whether or not this Property can be written from Lua. */ + auto isTwoWay() -> bool { return cb_.has_value(); } + + auto pushValue(lua_State& s) -> int; + auto popValue(lua_State& s) -> bool; + + /* Reapplies all active, dirty bindings associated with this Property. */ + auto reapplyAll() -> void; + + auto addLuaBinding(lua_State*, int ref) -> void; + auto applySingle(lua_State*, int ref, bool mark_dirty) -> bool; + + private: + std::unique_ptr value_; + std::optional> cb_; + std::pmr::vector> bindings_; +}; + +/* + * Container for a Lua function that should be invoked whenever a Property's + * value changes, as well as some extra accounting metadata. + */ +struct Binding { + /* Checks the value at idx is a Binding, returning a pointer to it if so. */ + static auto get(lua_State*, int idx) -> Binding*; + /* + * If the value at idx is a dirty, active Binding, applies the current value + * from its Property. Returns false if the binding was active and dirty, but + * invoking the Lua callback failed. + */ + static auto apply(lua_State*, int idx) -> bool; + + Property* property; + bool active; + bool dirty; +}; + +static_assert(std::is_trivially_destructible()); +static_assert(std::is_trivially_copy_assignable()); + +class PropertyBindings { + public: + PropertyBindings(); + + auto install(lua_State*) -> void; + + auto Register(lua_State*, Property*) -> void; + auto Register(lua_State*, LuaFunction) -> void; + + auto GetFunction(size_t i) -> const LuaFunction&; + + private: + std::pmr::vector functions_; +}; + +} // namespace lua diff --git a/src/tangara/lua/registry.cpp b/src/tangara/lua/registry.cpp new file mode 100644 index 00000000..a6487858 --- /dev/null +++ b/src/tangara/lua/registry.cpp @@ -0,0 +1,73 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_registry.hpp" + +#include +#include + +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "lua.hpp" + +#include "bridge.hpp" +#include "event_queue.hpp" +#include "memory_resource.hpp" +#include "service_locator.hpp" +#include "ui_events.hpp" + +namespace lua { + +[[maybe_unused]] static constexpr char kTag[] = "lua"; + +auto Registry::instance(system_fsm::ServiceLocator& s) -> Registry& { + static Registry sRegistry{s}; + return sRegistry; +} + +Registry::Registry(system_fsm::ServiceLocator& services) + : services_(services), bridge_(new Bridge(services)) {} + +auto Registry::uiThread() -> std::shared_ptr { + if (!ui_thread_) { + ui_thread_ = newThread(); + bridge_->installLvgl(ui_thread_->state()); + } + return ui_thread_; +} + +auto Registry::newThread() -> std::shared_ptr { + std::shared_ptr thread{LuaThread::Start(services_)}; + bridge_->installBaseModules(thread->state()); + for (auto& module : modules_) { + bridge_->installPropertyModule(thread->state(), module.first, + module.second); + } + threads_.push_back(thread); + return thread; +} + +auto Registry::AddPropertyModule( + const std::string& name, + std::vector>> + properties) -> void { + modules_.push_back(std::make_pair(name, properties)); + + // Any live threads will need to be updated to include the new module. + auto it = threads_.begin(); + while (it != threads_.end()) { + auto thread = it->lock(); + if (!thread) { + // Thread has been destroyed; stop tracking it. + it = threads_.erase(it); + } else { + bridge_->installPropertyModule(thread->state(), name, properties); + it++; + } + } +} + +} // namespace lua -- cgit v1.2.3