diff --git a/ldoc-stubs/alerts.lua b/ldoc-stubs/alerts.lua index 9b541d84..6fecdd7c 100644 --- a/ldoc-stubs/alerts.lua +++ b/ldoc-stubs/alerts.lua @@ -1,9 +1,9 @@ ---- Module for interacting with playback volume. The Bluetooth and wired outputs store their current volume separately; this API only allows interacting with the volume of the currently used output device. +--- Module for showing transient popups over the current screen. -- @module alerts local alerts = {} ---- Returns the current volume as a percentage of the current volume limit. +--- Shows a new alert, replacing any already visible alerts. -- @tparam function constructor Called to create the UI for the alert. A new default root object and group will be set before calling this function.i Alerts are non-interactable; the group created for the constructor will not be granted focus. function alerts.show(constructor) end diff --git a/lib/libtags/examples/readtags.c b/lib/libtags/examples/readtags.c index fb5b5d21..81c5a80e 100644 --- a/lib/libtags/examples/readtags.c +++ b/lib/libtags/examples/readtags.c @@ -35,6 +35,8 @@ static const char *t2s[] = [Ttrackpeak] = "trackpeak", [Tgenre] = "genre", [Timage] = "image", + [Tcomposer] = "composer", + [Tcomment] = "comment", }; static void @@ -43,7 +45,9 @@ tag(Tagctx *ctx, int t, const char *k, const char *v, int offset, int size, Tagr USED(ctx); USED(k); USED(f); if(t == Timage) print("%-12s %s %d %d\n", t2s[t], v, offset, size); - else if(t != Tunknown) + else if(t == Tunknown) + print("%-12s %s\n", k, v); + else print("%-12s %s\n", t2s[t], v); } diff --git a/lib/libtags/id3v1.c b/lib/libtags/id3v1.c index afcf90e9..d375cfd7 100644 --- a/lib/libtags/id3v1.c +++ b/lib/libtags/id3v1.c @@ -36,6 +36,9 @@ tagid3v1(Tagctx *ctx) if((ctx->found & 1<found & 1<found & 1< 0){ snprint((char*)out, Outsz, "%d", in[126]); txtcb(ctx, Ttrack, "", out); diff --git a/lib/libtags/id3v2.c b/lib/libtags/id3v2.c index e1529d4a..78a0a5fe 100644 --- a/lib/libtags/id3v2.c +++ b/lib/libtags/id3v2.c @@ -31,6 +31,8 @@ v2cb(Tagctx *ctx, char *k, char *v) txtcb(ctx, Ttrack, k-1, v); else if(strcmp(k, "LEN") == 0) ctx->duration = atoi(v); + else if(strcmp(k, "CM") == 0 || strcmp(k, "COM") == 0) + txtcb(ctx, Tcomposer, k-1, v); else if(strcmp(k, "CO") == 0 || strcmp(k, "CON") == 0){ for(; v[0]; v++){ if(v[0] == '(' && v[1] <= '9' && v[1] >= '0'){ @@ -64,6 +66,8 @@ v2cb(Tagctx *ctx, char *k, char *v) txtcb(ctx, type, k-1, v+5); else return 0; + }else if(strcmp(k-1, "COM") == 0 || strcmp(k-1, "COMM") == 0){ + txtcb(ctx, Tcomment, k-1, v); }else{ txtcb(ctx, Tunknown, k-1, v); } diff --git a/lib/libtags/m4a.c b/lib/libtags/m4a.c index 924ba51a..ec254c3f 100644 --- a/lib/libtags/m4a.c +++ b/lib/libtags/m4a.c @@ -93,6 +93,10 @@ tagm4a(Tagctx *ctx) type = Timage; else if(memcmp(d, "trkn", 4) == 0) type = Ttrack; + else if(memcmp(d, "\251wrt", 4) == 0) + type = Tcomposer; + else if(memcmp(d, "\251cmt", 4) == 0) + type = Tcomment; else if(memcmp(d, "mdhd", 4) == 0){ if(ctx->read(ctx, d, 4) != 4) return -1; @@ -133,7 +137,7 @@ tagm4a(Tagctx *ctx) sz -= 4; snprint((char*)d, ctx->bufsz, "%d", beuint(d)); txtcb(ctx, type, "", d); - }else if(type == Tgenre){ + }else if(type == Tgenre && dtype != 1){ if(ctx->read(ctx, d, 2) != 2) return -1; sz -= 2; diff --git a/lib/libtags/opus.c b/lib/libtags/opus.c index fcea57d8..fa2c7d40 100644 --- a/lib/libtags/opus.c +++ b/lib/libtags/opus.c @@ -63,7 +63,7 @@ tagopus(Tagctx *ctx) ctx->buf[sz] = 0; if((v = strchr(ctx->buf, '=')) == nil) - continue; + return -1; *v++ = 0; cbvorbiscomment(ctx, ctx->buf, v); } diff --git a/lib/libtags/tags.c b/lib/libtags/tags.c index a027a9ab..750d9077 100644 --- a/lib/libtags/tags.c +++ b/lib/libtags/tags.c @@ -46,8 +46,8 @@ tagscallcb(Tagctx *ctx, int type, const char *k, char *s, int offset, int size, e = s + strlen(s); while(e != s && (uchar)e[-1] <= ' ') e--; - if (*e != 0) - *e = 0; + if(*e != 0) + *e = 0; } if(*s){ ctx->tag(ctx, type, k, s, offset, size, f); diff --git a/lib/libtags/tags.h b/lib/libtags/tags.h index 91045218..4b10349a 100644 --- a/lib/libtags/tags.h +++ b/lib/libtags/tags.h @@ -21,6 +21,8 @@ enum Ttrackpeak, Tgenre, Timage, + Tcomposer, + Tcomment, }; /* Format of the audio file. */ diff --git a/lib/libtags/vorbis.c b/lib/libtags/vorbis.c index c98a0e4e..e57f8989 100644 --- a/lib/libtags/vorbis.c +++ b/lib/libtags/vorbis.c @@ -4,33 +4,38 @@ */ #include "tagspriv.h" +static const struct { + char *s; + int type; +}t[] = { + {"album", Talbum}, + {"title", Ttitle}, + {"artist", Tartist}, + {"albumartist", Talbumartist}, + {"tracknumber", Ttrack}, + {"date", Tdate}, + {"replaygain_track_peak", Ttrackpeak}, + {"replaygain_track_gain", Ttrackgain}, + {"replaygain_album_peak", Talbumpeak}, + {"replaygain_album_gain", Talbumgain}, + {"genre", Tgenre}, + {"composer", Tcomposer}, + {"comment", Tcomment}, +}; + void cbvorbiscomment(Tagctx *ctx, char *k, char *v){ + int i; + if(*v == 0) return; - if(cistrcmp(k, "album") == 0) - txtcb(ctx, Talbum, k, v); - else if(cistrcmp(k, "title") == 0) - txtcb(ctx, Ttitle, k, v); - else if(cistrcmp(k, "artist") == 0) - txtcb(ctx, Tartist, k, v); - else if(cistrcmp(k, "albumartist") == 0) - txtcb(ctx, Talbumartist, k, v); - else if(cistrcmp(k, "tracknumber") == 0) - txtcb(ctx, Ttrack, k, v); - else if(cistrcmp(k, "date") == 0) - txtcb(ctx, Tdate, k, v); - else if(cistrcmp(k, "replaygain_track_peak") == 0) - txtcb(ctx, Ttrackpeak, k, v); - else if(cistrcmp(k, "replaygain_track_gain") == 0) - txtcb(ctx, Ttrackgain, k, v); - else if(cistrcmp(k, "replaygain_album_peak") == 0) - txtcb(ctx, Talbumpeak, k, v); - else if(cistrcmp(k, "replaygain_album_gain") == 0) - txtcb(ctx, Talbumgain, k, v); - else if(cistrcmp(k, "genre") == 0) - txtcb(ctx, Tgenre, k, v); - else + for(i = 0; i < nelem(t); i++){ + if(cistrcmp(k, t[i].s) == 0){ + txtcb(ctx, t[i].type, k, v); + break; + } + } + if(i == nelem(t)) txtcb(ctx, Tunknown, k, v); } diff --git a/lib/libtags/wav.c b/lib/libtags/wav.c index 55e1566b..69b1946a 100644 --- a/lib/libtags/wav.c +++ b/lib/libtags/wav.c @@ -2,7 +2,7 @@ #define le16u(d) (u16int)((d)[0] | (d)[1]<<8) -static struct { +static const struct { char *s; int type; }t[] = { @@ -12,6 +12,8 @@ static struct { {"INAM", Ttitle}, {"IPRD", Talbum}, {"ITRK", Ttrack}, + {"ICMT", Tcomment}, + {"????", Tunknown}, }; int @@ -26,7 +28,7 @@ tagwav(Tagctx *ctx) sz = 1; info = 0; - for(i = 0; i < 8 && sz > 0; i++){ + for(i = 0; sz > 0; i++){ if(ctx->read(ctx, d, 4+4+(i?0:4)) != 4+4+(i?0:4)) return -1; if(i == 0){ @@ -66,20 +68,20 @@ tagwav(Tagctx *ctx) }else if(memcmp(d, "LIST", 4) == 0){ sz = csz - 4; continue; - }else if(memcmp(d, "data", 4) == 0){ - break; - }else if(info){ - csz++; + }else if(info && csz < (u32int)ctx->bufsz){ for(n = 0; n < nelem(t); n++){ - if(memcmp(d, t[n].s, 4) == 0){ - if(ctx->read(ctx, d, csz) != (int)csz) + if(memcmp(d, t[n].s, 4) == 0 || t[n].type == Tunknown){ + if(ctx->read(ctx, d+5, csz) != (int)csz) return -1; - d[csz-1] = 0; - txtcb(ctx, t[n].type, "", d); + d[4] = 0; + d[5+csz] = 0; + txtcb(ctx, t[n].type, t[n].type == Tunknown ? (char*)d : "", d+5); csz = 0; break; } } + if(n < nelem(t)) + continue; } if(ctx->seek(ctx, csz, 1) < 0) diff --git a/sdkconfig.common b/sdkconfig.common index 7ae9aea2..45f21e9a 100644 --- a/sdkconfig.common +++ b/sdkconfig.common @@ -22,7 +22,6 @@ CONFIG_SPI_MASTER_IN_IRAM=y # CONFIG_TWAI_ERRATA_FIX_RX_FIFO_CORRUPT is not set # CONFIG_TWAI_ERRATA_FIX_LISTEN_ONLY_DOM is not set CONFIG_GPIO_CTRL_FUNC_IN_IRAM=y -CONFIG_I2S_ISR_IRAM_SAFE=y # CONFIG_ETH_USE_ESP32_EMAC is not set # CONFIG_ETH_USE_SPI_ETHERNET is not set # CONFIG_ESP_EVENT_POST_FROM_ISR is not set diff --git a/src/app_console/app_console.cpp b/src/app_console/app_console.cpp index 33f41306..94a48955 100644 --- a/src/app_console/app_console.cpp +++ b/src/app_console/app_console.cpp @@ -40,7 +40,7 @@ #include "freertos/projdefs.h" #include "haptics.hpp" #include "index.hpp" -#include "lua_thread.hpp" +#include "lua_registry.hpp" #include "memory_resource.hpp" #include "samd.hpp" #include "service_locator.hpp" @@ -628,8 +628,7 @@ static const char kReplMain[] = "repl:run()\n"; int CmdLua(int argc, char** argv) { - std::unique_ptr context{ - lua::LuaThread::Start(*AppConsole::sServices)}; + auto context = lua::Registry::instance(*AppConsole::sServices).newThread(); if (!context) { return 1; } @@ -652,8 +651,7 @@ int CmdLua(int argc, char** argv) { } int CmdLuaRun(int argc, char** argv) { - std::unique_ptr context{ - lua::LuaThread::Start(*AppConsole::sServices)}; + auto context = lua::Registry::instance(*AppConsole::sServices).newThread(); if (!context) { return 1; } diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index bb7d33dc..b5b6e04b 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -13,7 +13,9 @@ #include "audio_sink.hpp" #include "bluetooth_types.hpp" +#include "esp_heap_caps.h" #include "esp_log.h" +#include "freertos/FreeRTOS.h" #include "freertos/portmacro.h" #include "freertos/projdefs.h" @@ -192,20 +194,28 @@ void AudioState::react(const TogglePlayPause& ev) { namespace states { +// Two seconds of samples for two channels, at a representative sample rate. +constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4; +static StreamBufferHandle_t sDrainBuffer; + void Uninitialised::react(const system_fsm::BootComplete& ev) { sServices = ev.services; - constexpr size_t kDrainBufferSize = - drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 2 * 8; ESP_LOGI(kTag, "allocating drain buffer, size %u KiB", kDrainBufferSize / 1024); - StreamBufferHandle_t stream = xStreamBufferCreateWithCaps( - kDrainBufferSize, sizeof(sample::Sample), MALLOC_CAP_DMA); + + auto meta = reinterpret_cast( + heap_caps_malloc(sizeof(StaticStreamBuffer_t), MALLOC_CAP_DMA)); + auto storage = reinterpret_cast( + heap_caps_malloc(kDrainBufferSize, MALLOC_CAP_SPIRAM)); + + sDrainBuffer = xStreamBufferCreateStatic( + kDrainBufferSize, sizeof(sample::Sample), storage, meta); sFileSource.reset( new FatfsAudioInput(sServices->tag_parser(), sServices->bg_worker())); - sI2SOutput.reset(new I2SAudioOutput(stream, sServices->gpios())); - sBtOutput.reset(new BluetoothAudioOutput(stream, sServices->bluetooth(), + sI2SOutput.reset(new I2SAudioOutput(sDrainBuffer, sServices->gpios())); + sBtOutput.reset(new BluetoothAudioOutput(sDrainBuffer, sServices->bluetooth(), sServices->bg_worker())); auto& nvs = sServices->nvs(); @@ -366,6 +376,12 @@ void Playback::react(const internal::InputFileFinished& ev) { ESP_LOGI(kTag, "finished playing file"); sServices->track_queue().finish(); if (!sServices->track_queue().current()) { + for (int i = 0; i < 20; i++) { + if (xStreamBufferIsEmpty(sDrainBuffer)) { + break; + } + vTaskDelay(pdMS_TO_TICKS(200)); + } transit(); } } diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index 534da10c..b75230fc 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -200,39 +200,52 @@ auto TrackQueue::append(Item i) -> void { } auto TrackQueue::next() -> void { - const std::unique_lock lock(mutex_); - if (shuffle_) { - shuffle_->next(); - pos_ = shuffle_->current(); - } else { - if (pos_ + 1 >= tracks_.size()) { - if (replay_) { - pos_ = 0; - } + bool changed = true; + + { + const std::unique_lock lock(mutex_); + if (shuffle_) { + shuffle_->next(); + pos_ = shuffle_->current(); } else { - pos_++; + if (pos_ + 1 >= tracks_.size()) { + if (replay_) { + pos_ = 0; + } else { + pos_ = tracks_.size(); + changed = false; + } + } else { + pos_++; + } } } - notifyChanged(true); + notifyChanged(changed); } auto TrackQueue::previous() -> void { - const std::unique_lock lock(mutex_); - if (shuffle_) { - shuffle_->prev(); - pos_ = shuffle_->current(); - } else { - if (pos_ == 0) { - if (repeat_) { - pos_ = tracks_.size() - 1; - } + bool changed = true; + + { + const std::unique_lock lock(mutex_); + if (shuffle_) { + shuffle_->prev(); + pos_ = shuffle_->current(); } else { - pos_--; + if (pos_ == 0) { + if (repeat_) { + pos_ = tracks_.size() - 1; + } else { + changed = false; + } + } else { + pos_--; + } } } - notifyChanged(true); + notifyChanged(changed); } auto TrackQueue::finish() -> void { diff --git a/src/database/database.cpp b/src/database/database.cpp index b596063c..ec11455b 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -383,8 +383,7 @@ auto Database::updateIndexes() -> void { ESP_LOGI(kTag, "scanning for new tracks"); uint64_t num_processed = 0; std::pair newest_track = last_update; - file_gatherer_.FindFiles("", [&](const std::string& path, - const FILINFO& info) { + file_gatherer_.FindFiles("", [&](std::string_view path, const FILINFO& info) { num_processed++; events::Ui().Dispatch(event::UpdateProgress{ .stage = event::UpdateProgress::Stage::kScanningForNewTracks, @@ -456,9 +455,7 @@ auto Database::updateIndexes() -> void { dbCreateIndexesForTrack(*t); } else if (existing_data->filepath != std::pmr::string{path.data(), path.size()}) { - ESP_LOGW(kTag, "tag hash collision for %s and %s", - existing_data->filepath.c_str(), path.c_str()); - ESP_LOGI(kTag, "hash components: %s, %s, %s", + ESP_LOGW(kTag, "hash collision: %s, %s, %s", tags->title().value_or("no title").c_str(), tags->artist().value_or("no artist").c_str(), tags->album().value_or("no album").c_str()); diff --git a/src/database/file_gatherer.cpp b/src/database/file_gatherer.cpp index dde363bd..b7b7271e 100644 --- a/src/database/file_gatherer.cpp +++ b/src/database/file_gatherer.cpp @@ -22,12 +22,12 @@ static_assert(sizeof(TCHAR) == sizeof(char), "TCHAR must be CHAR"); auto FileGathererImpl::FindFiles( const std::string& root, - std::function cb) -> void { - std::deque to_explore; - to_explore.push_back(root); + std::function cb) -> void { + std::pmr::deque to_explore{&memory::kSpiRamResource}; + to_explore.push_back({root.data(), root.size()}); while (!to_explore.empty()) { - std::string next_path_str = to_explore.front(); + auto next_path_str = to_explore.front(); to_explore.pop_front(); const TCHAR* next_path = static_cast(next_path_str.c_str()); @@ -56,7 +56,7 @@ auto FileGathererImpl::FindFiles( // System or hidden file. Ignore it and move on. continue; } else { - std::string full_path; + std::pmr::string full_path{&memory::kSpiRamResource}; full_path += next_path_str; full_path += "/"; full_path += info.fname; diff --git a/src/database/include/file_gatherer.hpp b/src/database/include/file_gatherer.hpp index 66127bb7..685bdb2c 100644 --- a/src/database/include/file_gatherer.hpp +++ b/src/database/include/file_gatherer.hpp @@ -21,7 +21,7 @@ class IFileGatherer { virtual auto FindFiles( const std::string& root, - std::function cb) + std::function cb) -> void = 0; }; @@ -29,7 +29,7 @@ class FileGathererImpl : public IFileGatherer { public: virtual auto FindFiles( const std::string& root, - std::function cb) + std::function cb) -> void override; }; diff --git a/src/database/include/tag_parser.hpp b/src/database/include/tag_parser.hpp index f196c479..966258b5 100644 --- a/src/database/include/tag_parser.hpp +++ b/src/database/include/tag_parser.hpp @@ -16,18 +16,18 @@ namespace database { class ITagParser { public: virtual ~ITagParser() {} - virtual auto ReadAndParseTags(const std::string& path) + virtual auto ReadAndParseTags(std::string_view path) -> std::shared_ptr = 0; }; class TagParserImpl : public ITagParser { public: TagParserImpl(); - auto ReadAndParseTags(const std::string& path) + auto ReadAndParseTags(std::string_view path) -> std::shared_ptr override; private: - auto parseNew(const std::string& path) -> std::shared_ptr; + auto parseNew(std::string_view path) -> std::shared_ptr; /* * Cache of tags that have already been extracted from files. Ideally this diff --git a/src/database/tag_parser.cpp b/src/database/tag_parser.cpp index c247bb7d..cbcbdcb5 100644 --- a/src/database/tag_parser.cpp +++ b/src/database/tag_parser.cpp @@ -108,7 +108,7 @@ static const std::size_t kBufSize = 1024; TagParserImpl::TagParserImpl() {} -auto TagParserImpl::ReadAndParseTags(const std::string& path) +auto TagParserImpl::ReadAndParseTags(std::string_view path) -> std::shared_ptr { { std::lock_guard lock{cache_mutex_}; @@ -130,7 +130,7 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path) if (!tags->track()) { auto slash_pos = path.find_last_of("/"); if (slash_pos != std::string::npos && path.size() - slash_pos > 1) { - std::string trunc = path.substr(slash_pos + 1); + auto trunc = path.substr(slash_pos + 1); tags->track({trunc.data(), trunc.size()}); } } @@ -143,8 +143,8 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path) return tags; } -auto TagParserImpl::parseNew(const std::string& path) - -> std::shared_ptr { +auto TagParserImpl::parseNew(std::string_view p) -> std::shared_ptr { + std::string path{p}; libtags::Aux aux; auto out = TrackTags::create(); aux.tags = out.get(); diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt index d89b22e8..ff0831c9 100644 --- a/src/lua/CMakeLists.txt +++ b/src/lua/CMakeLists.txt @@ -4,7 +4,7 @@ idf_component_register( SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp" - "lua_queue.cpp" "lua_version.cpp" "lua_controls.cpp" + "lua_queue.cpp" "lua_version.cpp" "lua_controls.cpp" "registry.cpp" INCLUDE_DIRS "include" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term" diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index a680a521..a26f74bb 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -22,6 +22,9 @@ #include "lua_version.hpp" #include "lvgl.h" +#include "font/lv_font_loader.h" +#include "luavgl.h" + #include "event_queue.hpp" #include "property.hpp" #include "service_locator.hpp" @@ -32,34 +35,62 @@ 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, lua_State& s) - : services_(services), state_(s), bindings_(s) { - lua_pushstring(&s, kBridgeKey); - lua_pushlightuserdata(&s, this); - lua_settable(&s, LUA_REGISTRYINDEX); +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(&s, "linenoise", luaopen_linenoise, true); - lua_pop(&s, 1); + luaL_requiref(L, "linenoise", luaopen_linenoise, true); + lua_pop(L, 1); - luaL_requiref(&s, "term.core", luaopen_term_core, true); - lua_pop(&s, 1); + luaL_requiref(L, "term.core", luaopen_term_core, true); + lua_pop(L, 1); + + RegisterControlsModule(L); + RegisterDatabaseModule(L); + RegisterQueueModule(L); + RegisterVersionModule(L); +} - RegisterControlsModule(&s); - RegisterDatabaseModule(&s); - RegisterQueueModule(&s); - RegisterVersionModule(&s); +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 { @@ -76,32 +107,33 @@ static auto new_property_module(lua_State* state) -> int { template inline constexpr bool always_false_v = false; -auto Bridge::AddPropertyModule( +auto Bridge::installPropertyModule( + lua_State* L, const std::string& name, - std::vector>> + 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); + luaL_requiref(L, name.c_str(), new_property_module, true); for (const auto& prop : props) { - lua_pushstring(&state_, prop.first.c_str()); + lua_pushstring(L, prop.first.c_str()); std::visit( [&](auto&& arg) { using T = std::decay_t; if constexpr (std::is_same_v) { - bindings_.Register(&state_, arg); + bindings_.Register(L, arg); } else if constexpr (std::is_same_v) { - bindings_.Register(&state_, arg); + bindings_.Register(L, arg); } else { static_assert(always_false_v, "missing case"); } }, prop.second); - lua_settable(&state_, -3); // metatable.propname = property + lua_settable(L, -3); // metatable.propname = property } - lua_pop(&state_, 1); // pop the module off the stack + lua_pop(L, 1); // pop the module off the stack } } // namespace lua diff --git a/src/lua/include/bridge.hpp b/src/lua/include/bridge.hpp index 62fbc340..64f14e0e 100644 --- a/src/lua/include/bridge.hpp +++ b/src/lua/include/bridge.hpp @@ -16,25 +16,38 @@ 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&, lua_State& s); + Bridge(system_fsm::ServiceLocator& s); - auto AddPropertyModule( + 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>>) + std::pair>>&) -> void; - system_fsm::ServiceLocator& services() { return services_; } - PropertyBindings& bindings() { return bindings_; } + Bridge(const Bridge&) = delete; + Bridge& operator=(const Bridge&) = delete; private: system_fsm::ServiceLocator& services_; - lua_State& state_; PropertyBindings bindings_; }; diff --git a/src/lua/include/lua_registry.hpp b/src/lua/include/lua_registry.hpp new file mode 100644 index 00000000..abc5063e --- /dev/null +++ b/src/lua/include/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/lua/include/lua_thread.hpp b/src/lua/include/lua_thread.hpp index d10dba3a..384de61d 100644 --- a/src/lua/include/lua_thread.hpp +++ b/src/lua/include/lua_thread.hpp @@ -10,9 +10,7 @@ #include #include "lua.hpp" -#include "lvgl.h" -#include "bridge.hpp" #include "service_locator.hpp" namespace lua { @@ -23,8 +21,7 @@ auto CallProtected(lua_State*, int nargs, int nresults) -> int; class LuaThread { public: - static auto Start(system_fsm::ServiceLocator&, lv_obj_t* lvgl_root = nullptr) - -> LuaThread*; + static auto Start(system_fsm::ServiceLocator&) -> LuaThread*; ~LuaThread(); auto RunScript(const std::string& path) -> bool; @@ -32,14 +29,15 @@ class LuaThread { auto DumpStack() -> void; - auto bridge() -> Bridge& { return *bridge_; } auto state() -> lua_State* { return state_; } + LuaThread(const LuaThread&) = delete; + LuaThread& operator=(const LuaThread&) = delete; + private: - LuaThread(std::unique_ptr&, std::unique_ptr&, lua_State*); + LuaThread(std::unique_ptr&, lua_State*); std::unique_ptr alloc_; - std::unique_ptr bridge_; lua_State* state_; }; diff --git a/src/lua/include/property.hpp b/src/lua/include/property.hpp index 03229bc1..7d160fba 100644 --- a/src/lua/include/property.hpp +++ b/src/lua/include/property.hpp @@ -53,7 +53,9 @@ class Property { class PropertyBindings { public: - PropertyBindings(lua_State&); + PropertyBindings(); + + auto install(lua_State*) -> void; auto Register(lua_State*, Property*) -> void; auto Register(lua_State*, LuaFunction) -> void; diff --git a/src/lua/lua_queue.cpp b/src/lua/lua_queue.cpp index 69d3b03d..dfb820c2 100644 --- a/src/lua/lua_queue.cpp +++ b/src/lua/lua_queue.cpp @@ -16,6 +16,7 @@ #include "lua.h" #include "lvgl.h" +#include "bridge.hpp" #include "database.hpp" #include "event_queue.hpp" #include "index.hpp" @@ -70,4 +71,4 @@ auto RegisterQueueModule(lua_State* s) -> void { lua_pop(s, 1); } -} // namespace lua \ No newline at end of file +} // namespace lua diff --git a/src/lua/lua_thread.cpp b/src/lua/lua_thread.cpp index b94b70ab..dd72e41d 100644 --- a/src/lua/lua_thread.cpp +++ b/src/lua/lua_thread.cpp @@ -9,22 +9,16 @@ #include #include -#include "lauxlib.h" -#include "lua.h" -#include "lua.hpp" - -#include "font/lv_font_loader.h" -#include "luavgl.h" - #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" -LV_FONT_DECLARE(font_fusion_12); -LV_FONT_DECLARE(font_fusion_10); - namespace lua { [[maybe_unused]] static constexpr char kTag[] = "lua"; @@ -59,23 +53,7 @@ static int lua_panic(lua_State* L) { return 0; } -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 LuaThread::Start(system_fsm::ServiceLocator& services, lv_obj_t* lvgl_root) - -> LuaThread* { +auto LuaThread::Start(system_fsm::ServiceLocator& services) -> LuaThread* { auto alloc = std::make_unique(); lua_State* state = lua_newstate(lua_alloc, alloc.get()); if (!state) { @@ -85,24 +63,11 @@ auto LuaThread::Start(system_fsm::ServiceLocator& services, lv_obj_t* lvgl_root) luaL_openlibs(state); lua_atpanic(state, lua_panic); - auto bridge = std::make_unique(services, *state); - - // FIXME: luavgl init should probably be a part of the bridge. - if (lvgl_root) { - luavgl_set_pcall(state, CallProtected); - luavgl_set_font_extension(state, make_font_cb, delete_font_cb); - luavgl_set_root(state, lvgl_root); - luaL_requiref(state, "lvgl", luaopen_lvgl, true); - lua_pop(state, 1); - } - - return new LuaThread(alloc, bridge, state); + return new LuaThread(alloc, state); } -LuaThread::LuaThread(std::unique_ptr& alloc, - std::unique_ptr& bridge, - lua_State* state) - : alloc_(std::move(alloc)), bridge_(std::move(bridge)), state_(state) {} +LuaThread::LuaThread(std::unique_ptr& alloc, lua_State* state) + : alloc_(std::move(alloc)), state_(state) {} LuaThread::~LuaThread() { lua_close(state_); diff --git a/src/lua/property.cpp b/src/lua/property.cpp index 5357ccc5..f721f9ce 100644 --- a/src/lua/property.cpp +++ b/src/lua/property.cpp @@ -10,10 +10,12 @@ #include #include #include +#include #include #include #include "bluetooth_types.hpp" +#include "lauxlib.h" #include "lua.h" #include "lua.hpp" #include "lua_thread.hpp" @@ -76,10 +78,30 @@ static auto property_bind(lua_State* state) -> int { return 1; } -static const struct luaL_Reg kPropertyBindingFuncs[] = {{"get", property_get}, - {"set", property_set}, - {"bind", property_bind}, - {NULL, NULL}}; +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); @@ -98,45 +120,47 @@ static auto generic_function_cb(lua_State* state) -> int { return std::invoke(fn, state); } -PropertyBindings::PropertyBindings(lua_State& s) { - lua_pushstring(&s, kBinderKey); - lua_pushlightuserdata(&s, this); - lua_settable(&s, LUA_REGISTRYINDEX); +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(&s, kPropertyMetatable); + luaL_newmetatable(L, kPropertyMetatable); - lua_pushliteral(&s, "__index"); - lua_pushvalue(&s, -2); - lua_settable(&s, -3); // metatable.__index = metatable + 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(&s, kPropertyBindingFuncs, 0); + luaL_setfuncs(L, kPropertyBindingFuncs, 0); // We've finished setting up the metatable, so pop it. - lua_pop(&s, 1); + lua_pop(L, 1); // Create a weak table in the registry to hold live bindings. - lua_pushstring(&s, kBindingsTable); - lua_newtable(&s); // bindings = {} + lua_pushstring(L, kBindingsTable); + lua_newtable(L); // 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_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(&s, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] = bindings + lua_settable(L, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] = bindings // Create the metatable for C++ functions. - luaL_newmetatable(&s, kFunctionMetatable); + luaL_newmetatable(L, kFunctionMetatable); - lua_pushliteral(&s, "__call"); - lua_pushcfunction(&s, generic_function_cb); - lua_settable(&s, -3); // metatable.__call = metatable + lua_pushliteral(L, "__call"); + lua_pushcfunction(L, generic_function_cb); + lua_settable(L, -3); // metatable.__call = metatable - lua_pop(&s, 1); // Clean up the function metatable + lua_pop(L, 1); // Clean up the function metatable } auto PropertyBindings::Register(lua_State* s, Property* prop) -> void { diff --git a/src/lua/registry.cpp b/src/lua/registry.cpp new file mode 100644 index 00000000..a6487858 --- /dev/null +++ b/src/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 diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index ffaff0bb..6cf2ba4c 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -163,6 +163,8 @@ class Lua : public UiState { auto ShowAlert(lua_State*) -> int; auto HideAlert(lua_State*) -> int; + auto Ticks(lua_State*) -> int; + auto SetPlaying(const lua::LuaValue&) -> bool; auto SetRandom(const lua::LuaValue&) -> bool; auto SetRepeat(const lua::LuaValue&) -> bool; diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 3e85c36e..d98e435d 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -24,6 +24,7 @@ #include "core/lv_obj_tree.h" #include "database.hpp" #include "esp_heap_caps.h" +#include "esp_timer.h" #include "haptics.hpp" #include "lauxlib.h" #include "lua_thread.hpp" @@ -36,6 +37,7 @@ #include "encoder_input.hpp" #include "event_queue.hpp" #include "gpios.hpp" +#include "lua_registry.hpp" #include "lvgl_task.hpp" #include "nvs.hpp" #include "property.hpp" @@ -142,29 +144,29 @@ lua::Property UiState::sPlaybackPosition{0, [](const lua::LuaValue& val) { lua::Property UiState::sQueuePosition{0}; lua::Property UiState::sQueueSize{0}; lua::Property UiState::sQueueRepeat{false, [](const lua::LuaValue& val) { - if (!std::holds_alternative(val)) { - return false; - } - bool new_val = std::get(val); - sServices->track_queue().repeat(new_val); - return true; -}}; + if (!std::holds_alternative(val)) { + return false; + } + bool new_val = std::get(val); + sServices->track_queue().repeat(new_val); + return true; + }}; lua::Property UiState::sQueueReplay{false, [](const lua::LuaValue& val) { - if (!std::holds_alternative(val)) { - return false; - } - bool new_val = std::get(val); - sServices->track_queue().replay(new_val); - return true; -}}; + if (!std::holds_alternative(val)) { + return false; + } + bool new_val = std::get(val); + sServices->track_queue().replay(new_val); + return true; + }}; lua::Property UiState::sQueueRandom{false, [](const lua::LuaValue& val) { - if (!std::holds_alternative(val)) { - return false; - } - bool new_val = std::get(val); - sServices->track_queue().random(new_val); - return true; -}}; + if (!std::holds_alternative(val)) { + return false; + } + bool new_val = std::get(val); + sServices->track_queue().random(new_val); + return true; + }}; lua::Property UiState::sVolumeCurrentPct{ 0, [](const lua::LuaValue& val) { @@ -456,27 +458,26 @@ void Lua::entry() { alert_timer_callback); sAlertContainer = lv_obj_create(sCurrentScreen->alert()); - sLua.reset(lua::LuaThread::Start(*sServices, sCurrentScreen->content())); - sLua->bridge().AddPropertyModule("power", - { - {"battery_pct", &sBatteryPct}, - {"battery_millivolts", &sBatteryMv}, - {"plugged_in", &sBatteryCharging}, - }); - sLua->bridge().AddPropertyModule( - "bluetooth", { - {"enabled", &sBluetoothEnabled}, - {"connected", &sBluetoothConnected}, - {"paired_device", &sBluetoothPairedDevice}, - {"devices", &sBluetoothDevices}, - }); - sLua->bridge().AddPropertyModule("playback", - { - {"playing", &sPlaybackPlaying}, - {"track", &sPlaybackTrack}, - {"position", &sPlaybackPosition}, - }); - sLua->bridge().AddPropertyModule( + auto& registry = lua::Registry::instance(*sServices); + sLua = registry.uiThread(); + registry.AddPropertyModule("power", { + {"battery_pct", &sBatteryPct}, + {"battery_millivolts", &sBatteryMv}, + {"plugged_in", &sBatteryCharging}, + }); + registry.AddPropertyModule("bluetooth", + { + {"enabled", &sBluetoothEnabled}, + {"connected", &sBluetoothConnected}, + {"paired_device", &sBluetoothPairedDevice}, + {"devices", &sBluetoothDevices}, + }); + registry.AddPropertyModule("playback", { + {"playing", &sPlaybackPlaying}, + {"track", &sPlaybackTrack}, + {"position", &sPlaybackPosition}, + }); + registry.AddPropertyModule( "queue", { {"next", [&](lua_State* s) { return QueueNext(s); }}, @@ -487,40 +488,44 @@ void Lua::entry() { {"repeat_track", &sQueueRepeat}, {"random", &sQueueRandom}, }); - sLua->bridge().AddPropertyModule("volume", - { - {"current_pct", &sVolumeCurrentPct}, - {"current_db", &sVolumeCurrentDb}, - {"left_bias", &sVolumeLeftBias}, - {"limit_db", &sVolumeLimit}, - }); - - sLua->bridge().AddPropertyModule("display", - { - {"brightness", &sDisplayBrightness}, - }); - - sLua->bridge().AddPropertyModule("controls", - { - {"scheme", &sControlsScheme}, - {"scroll_sensitivity", &sScrollSensitivity}, - }); - - sLua->bridge().AddPropertyModule( + registry.AddPropertyModule("volume", + { + {"current_pct", &sVolumeCurrentPct}, + {"current_db", &sVolumeCurrentDb}, + {"left_bias", &sVolumeLeftBias}, + {"limit_db", &sVolumeLimit}, + }); + + registry.AddPropertyModule("display", + { + {"brightness", &sDisplayBrightness}, + }); + + registry.AddPropertyModule("controls", + { + {"scheme", &sControlsScheme}, + {"scroll_sensitivity", &sScrollSensitivity}, + }); + + registry.AddPropertyModule( "backstack", { {"push", [&](lua_State* s) { return PushLuaScreen(s); }}, {"pop", [&](lua_State* s) { return PopLuaScreen(s); }}, }); - sLua->bridge().AddPropertyModule( + registry.AddPropertyModule( "alerts", { {"show", [&](lua_State* s) { return ShowAlert(s); }}, {"hide", [&](lua_State* s) { return HideAlert(s); }}, }); - sLua->bridge().AddPropertyModule("database", - { - {"updating", &sDatabaseUpdating}, - }); + + registry.AddPropertyModule( + "time", { + {"ticks", [&](lua_State* s) { return Ticks(s); }}, + }); + registry.AddPropertyModule("database", { + {"updating", &sDatabaseUpdating}, + }); auto bt = sServices->bluetooth(); sBluetoothEnabled.Update(bt.IsEnabled()); @@ -579,6 +584,11 @@ auto Lua::PopLuaScreen(lua_State* s) -> int { return 0; } +auto Lua::Ticks(lua_State* s) -> int { + lua_pushinteger(s, esp_timer_get_time()/1000); + return 1; +} + auto Lua::ShowAlert(lua_State* s) -> int { if (!sCurrentScreen) { return 0; diff --git a/tools/cmake/common.cmake b/tools/cmake/common.cmake index 501e0dc0..158933d1 100644 --- a/tools/cmake/common.cmake +++ b/tools/cmake/common.cmake @@ -5,7 +5,7 @@ # For more information about build system see # https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html -set(PROJECT_VER "0.5.2") +set(PROJECT_VER "0.6.0") # esp-idf sets the C++ standard weird. Set cmake vars to match. set(CMAKE_CXX_STANDARD 23)