Adds the primitive the ports were missing: wada.ui.chart (two shifting series, line or bar, settable range) plus rx_events/rx_dropped/tx_pkts/ freq/bw/sf/duty_pct on wada.mesh.stats. RF Monitor 1.1: live RSSI + noise-floor chart, peak with tap-to-reset, link-margin grade, RX/min, radio params, colour-coded heard feed, narrow and wide layouts. Airtime 1.1: live + average utilization bars, peak, rx/tx air split, duty ceiling, tx budget, packet counters, tap to restart the window. Native versions stay compiled in until these are field-proven. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
912 lines
36 KiB
C++
912 lines
36 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Lua app host — see LuaAppHost.h + LUA_APPS.md. Structure:
|
|
// 1. PSRAM-capped allocator + instruction-budget hook (containment)
|
|
// 2. the wada.* bindings (ui/input/timer/sys/store) — API v1
|
|
// 3. lifecycle: launch -> run chunk -> callbacks via guarded pcall -> dismiss
|
|
#include "LuaAppHost.h"
|
|
|
|
#if CAP_LUA_APPS
|
|
#include <Arduino.h>
|
|
#include <lvgl.h>
|
|
#include <esp_heap_caps.h>
|
|
#include <FS.h>
|
|
#include "AppPage.h"
|
|
|
|
extern "C" {
|
|
#include "lua.h"
|
|
#include "lualib.h"
|
|
#include "lauxlib.h"
|
|
}
|
|
|
|
// UITask-owned services the host borrows (all pre-existing, fwd-declared here
|
|
// to keep this TU decoupled from the 47k-line UITask.cpp).
|
|
extern const lv_font_t* luaHostFontForSize(int size_class); // 12/14/16 -> g_font_*
|
|
extern void luaHostToast(const char* msg, int ms); // showAlert passthrough
|
|
extern fs::FS* luaHostAppFs(); // /apps storage root FS (may be null)
|
|
extern void luaHostAppPath(char* out, size_t cap, const char* rel); // prefixes the store root
|
|
extern int luaHostContactAt(int idx, char* name, size_t name_cap, int* type, uint32_t* secs_ago);
|
|
extern int luaHostRxLogAt(int idx, uint32_t* ms_ago, int* ptype, int* rssi, float* snr, int* hops);
|
|
extern void luaHostRadioStats(float* rssi, float* noise, uint32_t* rx_air_s, uint32_t* tx_air_s,
|
|
uint32_t* rx_pkts, uint32_t* rx_err, int* budget_ms);
|
|
extern void luaHostRadioStats2(uint32_t* rx_evt, uint32_t* rx_drop, uint32_t* tx_pkts,
|
|
float* freq, float* bw, int* sf, int* duty_pct);
|
|
extern void luaHostSelfInfo(char* name, size_t name_cap, double* lat, double* lon);
|
|
class WiFiClient; class HTTPClient;
|
|
extern int luaStoreHttpGet(WiFiClient& client, HTTPClient& http, const char* url, char* buf, size_t cap);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1) allocator + budget
|
|
// ---------------------------------------------------------------------------
|
|
namespace {
|
|
|
|
struct LuaHeap { size_t used = 0, peak = 0, cap = 0; };
|
|
|
|
void* psAllocCb(void* ud, void* ptr, size_t osize, size_t nsize) {
|
|
LuaHeap* h = (LuaHeap*)ud;
|
|
if (!ptr) osize = 0;
|
|
if (nsize == 0) {
|
|
if (ptr) { h->used -= osize; heap_caps_free(ptr); }
|
|
return nullptr;
|
|
}
|
|
if (h->used - osize + nsize > h->cap) return nullptr; // hard per-app cap -> Lua OOM error
|
|
void* np = heap_caps_realloc(ptr, nsize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
|
if (!np) return nullptr;
|
|
h->used += nsize - osize;
|
|
if (h->used > h->peak) h->peak = h->used;
|
|
return np;
|
|
}
|
|
|
|
// 100k VM instructions per callback (~<2 ms of straight-line Lua on the S3 —
|
|
// the Phase 0 spike measured containment of a hostile loop in 30 ms at this
|
|
// cadence with negligible overhead on real code).
|
|
constexpr int kInstrBudget = 100000;
|
|
|
|
void budgetHook(lua_State* L, lua_Debug*) {
|
|
luaL_error(L, "instruction budget exceeded (app tick too long)");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// host state
|
|
// ---------------------------------------------------------------------------
|
|
constexpr size_t kHeapCap = 256 * 1024; // per-app PSRAM cap
|
|
constexpr size_t kMaxSrc = 64 * 1024; // app source size limit
|
|
constexpr size_t kStoreMax = 2048; // per-app persisted KV budget (bytes, serialized)
|
|
constexpr int kMinTickMs = 33; // fastest on_tick cadence (~30 fps)
|
|
|
|
struct Host {
|
|
lua_State* L = nullptr;
|
|
LuaHeap heap;
|
|
lv_obj_t* root = nullptr; // full-screen overlay on lv_layer_top
|
|
lv_obj_t* body = nullptr; // app content area (below the tall bar)
|
|
lv_timer_t* timer = nullptr;
|
|
int body_w = 0, body_h = 0; // set at creation — lv_obj_get_width() reads 0 pre-layout
|
|
uint32_t last_tick = 0;
|
|
int ref_app = LUA_NOREF; // the table the chunk returned
|
|
int ref_btncb = LUA_NOREF; // { [btn lightuserdata] = fn } button callbacks
|
|
bool store_dirty = false;
|
|
bool in_lua = false; // re-entrancy guard (dismiss from inside a callback)
|
|
bool want_close = false;
|
|
char id[24] = "";
|
|
char title[32] = "";
|
|
};
|
|
Host* s_h = nullptr;
|
|
|
|
char s_bar_title[40]; // appPageBegin keeps the pointer — must outlive the page
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// guarded callback invocation
|
|
// ---------------------------------------------------------------------------
|
|
int tracebackMsgh(lua_State* L) {
|
|
const char* msg = lua_tostring(L, 1);
|
|
luaL_traceback(L, L, msg ? msg : "(non-string error)", 1);
|
|
return 1;
|
|
}
|
|
|
|
// Push app.<name>; returns false if the app has no such callback.
|
|
bool pushCallback(Host* h, const char* name) {
|
|
lua_rawgeti(h->L, LUA_REGISTRYINDEX, h->ref_app);
|
|
lua_getfield(h->L, -1, name);
|
|
lua_remove(h->L, -2);
|
|
if (!lua_isfunction(h->L, -1)) { lua_pop(h->L, 1); return false; }
|
|
return true;
|
|
}
|
|
|
|
// Run the function at the top of the stack with nargs args under budget+pcall.
|
|
// On error: toast + schedule close. Returns true on clean completion.
|
|
bool guardedCall(Host* h, int nargs) {
|
|
lua_State* L = h->L;
|
|
int base = lua_gettop(L) - nargs; // function slot
|
|
lua_pushcfunction(L, tracebackMsgh);
|
|
lua_insert(L, base);
|
|
lua_sethook(L, budgetHook, LUA_MASKCOUNT, kInstrBudget); // (re)arms the counter
|
|
h->in_lua = true;
|
|
int rc = lua_pcall(L, nargs, 0, base);
|
|
h->in_lua = false;
|
|
lua_sethook(L, nullptr, 0, 0);
|
|
lua_remove(L, base); // the traceback handler
|
|
if (rc != LUA_OK) {
|
|
const char* err = lua_tostring(L, -1);
|
|
Serial.printf("[LUAAPP] %s error: %s\n", h->id, err ? err : "?");
|
|
char msg[96];
|
|
snprintf(msg, sizeof msg, "App error: %.60s", err ? err : "unknown");
|
|
luaHostToast(msg, 2600);
|
|
lua_pop(L, 1);
|
|
h->want_close = true; // unwind AFTER we leave Lua
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void serviceDeferredClose() {
|
|
if (s_h && s_h->want_close && !s_h->in_lua) luaAppDismiss();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2) wada.* bindings
|
|
// ---------------------------------------------------------------------------
|
|
uint32_t argColor(lua_State* L, int idx, uint32_t def = 0xFFFFFF) {
|
|
return (uint32_t)luaL_optinteger(L, idx, (lua_Integer)def) & 0xFFFFFF;
|
|
}
|
|
|
|
// ---- canvas userdata ----
|
|
struct CanvasUd { lv_obj_t* obj; lv_color_t* buf; int w, h; };
|
|
|
|
CanvasUd* checkCanvas(lua_State* L) {
|
|
return (CanvasUd*)luaL_checkudata(L, 1, "wada.canvas");
|
|
}
|
|
|
|
int cvFill(lua_State* L) {
|
|
CanvasUd* c = checkCanvas(L);
|
|
if (c->obj) lv_canvas_fill_bg(c->obj, lv_color_hex(argColor(L, 2, 0x000000)), LV_OPA_COVER);
|
|
return 0;
|
|
}
|
|
int cvRect(lua_State* L) {
|
|
CanvasUd* c = checkCanvas(L);
|
|
if (!c->obj) return 0;
|
|
lv_draw_rect_dsc_t d; lv_draw_rect_dsc_init(&d);
|
|
uint32_t col = argColor(L, 6);
|
|
bool filled = lua_isnoneornil(L, 7) ? true : lua_toboolean(L, 7);
|
|
d.bg_color = lv_color_hex(col); d.bg_opa = filled ? LV_OPA_COVER : LV_OPA_TRANSP;
|
|
d.border_color = lv_color_hex(col); d.border_width = filled ? 0 : 1; d.border_opa = LV_OPA_COVER;
|
|
d.radius = (lv_coord_t)luaL_optinteger(L, 8, 0);
|
|
lv_canvas_draw_rect(c->obj, (lv_coord_t)luaL_checkinteger(L, 2), (lv_coord_t)luaL_checkinteger(L, 3),
|
|
(lv_coord_t)luaL_checkinteger(L, 4), (lv_coord_t)luaL_checkinteger(L, 5), &d);
|
|
return 0;
|
|
}
|
|
int cvLine(lua_State* L) {
|
|
CanvasUd* c = checkCanvas(L);
|
|
if (!c->obj) return 0;
|
|
lv_point_t pts[2] = {
|
|
{ (lv_coord_t)luaL_checkinteger(L, 2), (lv_coord_t)luaL_checkinteger(L, 3) },
|
|
{ (lv_coord_t)luaL_checkinteger(L, 4), (lv_coord_t)luaL_checkinteger(L, 5) } };
|
|
lv_draw_line_dsc_t d; lv_draw_line_dsc_init(&d);
|
|
d.color = lv_color_hex(argColor(L, 6));
|
|
d.width = (lv_coord_t)luaL_optinteger(L, 7, 1);
|
|
lv_canvas_draw_line(c->obj, pts, 2, &d);
|
|
return 0;
|
|
}
|
|
int cvCircle(lua_State* L) {
|
|
CanvasUd* c = checkCanvas(L);
|
|
if (!c->obj) return 0;
|
|
lv_coord_t x = (lv_coord_t)luaL_checkinteger(L, 2), y = (lv_coord_t)luaL_checkinteger(L, 3);
|
|
lv_coord_t r = (lv_coord_t)luaL_checkinteger(L, 4);
|
|
lv_draw_arc_dsc_t d; lv_draw_arc_dsc_init(&d);
|
|
d.color = lv_color_hex(argColor(L, 5));
|
|
bool filled = lua_isnoneornil(L, 6) ? true : lua_toboolean(L, 6);
|
|
d.width = filled ? r : (lv_coord_t)luaL_optinteger(L, 7, 1);
|
|
lv_canvas_draw_arc(c->obj, x, y, r, 0, 360, &d);
|
|
return 0;
|
|
}
|
|
int cvText(lua_State* L) {
|
|
CanvasUd* c = checkCanvas(L);
|
|
if (!c->obj) return 0;
|
|
lv_draw_label_dsc_t d; lv_draw_label_dsc_init(&d);
|
|
d.color = lv_color_hex(argColor(L, 5));
|
|
d.font = luaHostFontForSize((int)luaL_optinteger(L, 6, 14));
|
|
lv_canvas_draw_text(c->obj, (lv_coord_t)luaL_checkinteger(L, 2), (lv_coord_t)luaL_checkinteger(L, 3),
|
|
c->w, &d, luaL_checkstring(L, 4));
|
|
return 0;
|
|
}
|
|
int cvPos(lua_State* L) {
|
|
CanvasUd* c = checkCanvas(L);
|
|
if (c->obj) lv_obj_set_pos(c->obj, (lv_coord_t)luaL_checkinteger(L, 2), (lv_coord_t)luaL_checkinteger(L, 3));
|
|
return 0;
|
|
}
|
|
int cvGc(lua_State* L) {
|
|
// The buffer is heap_caps memory owned by the userdata; the lv object dies
|
|
// with the app root. Free the pixels only when Lua collects the handle AND
|
|
// the app is closing (root gone) — during app lifetime the canvas holds it.
|
|
CanvasUd* c = (CanvasUd*)luaL_checkudata(L, 1, "wada.canvas");
|
|
if (c->buf) { heap_caps_free(c->buf); c->buf = nullptr; }
|
|
c->obj = nullptr;
|
|
return 0;
|
|
}
|
|
|
|
int uiCanvas(lua_State* L) {
|
|
if (!s_h || !s_h->body) return luaL_error(L, "no app body");
|
|
int w = (int)luaL_checkinteger(L, 1), h = (int)luaL_checkinteger(L, 2);
|
|
luaL_argcheck(L, w > 0 && w <= 480, 1, "width 1..480");
|
|
luaL_argcheck(L, h > 0 && h <= 480, 2, "height 1..480");
|
|
size_t bytes = (size_t)w * h * sizeof(lv_color_t);
|
|
lv_color_t* buf = (lv_color_t*)heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
|
if (!buf) return luaL_error(L, "canvas alloc failed");
|
|
lv_obj_t* cv = lv_canvas_create(s_h->body);
|
|
lv_canvas_set_buffer(cv, buf, w, h, LV_IMG_CF_TRUE_COLOR);
|
|
lv_canvas_fill_bg(cv, lv_color_hex(0x000000), LV_OPA_COVER);
|
|
CanvasUd* ud = (CanvasUd*)lua_newuserdatauv(L, sizeof(CanvasUd), 0);
|
|
ud->obj = cv; ud->buf = buf; ud->w = w; ud->h = h;
|
|
luaL_setmetatable(L, "wada.canvas");
|
|
return 1;
|
|
}
|
|
|
|
// ---- label userdata ----
|
|
struct WidgetUd { lv_obj_t* obj; };
|
|
WidgetUd* checkLabel(lua_State* L) { return (WidgetUd*)luaL_checkudata(L, 1, "wada.label"); }
|
|
|
|
int lbSet(lua_State* L) {
|
|
WidgetUd* u = checkLabel(L);
|
|
if (u->obj) lv_label_set_text(u->obj, luaL_checkstring(L, 2));
|
|
return 0;
|
|
}
|
|
int lbPos(lua_State* L) {
|
|
WidgetUd* u = checkLabel(L);
|
|
if (u->obj) lv_obj_set_pos(u->obj, (lv_coord_t)luaL_checkinteger(L, 2), (lv_coord_t)luaL_checkinteger(L, 3));
|
|
return 0;
|
|
}
|
|
int lbColor(lua_State* L) {
|
|
WidgetUd* u = checkLabel(L);
|
|
if (u->obj) lv_obj_set_style_text_color(u->obj, lv_color_hex(argColor(L, 2)), LV_PART_MAIN);
|
|
return 0;
|
|
}
|
|
|
|
int uiLabel(lua_State* L) {
|
|
if (!s_h || !s_h->body) return luaL_error(L, "no app body");
|
|
lv_obj_t* l = lv_label_create(s_h->body);
|
|
lv_label_set_text(l, luaL_checkstring(L, 1));
|
|
lv_obj_set_pos(l, (lv_coord_t)luaL_optinteger(L, 2, 0), (lv_coord_t)luaL_optinteger(L, 3, 0));
|
|
lv_obj_set_style_text_font(l, luaHostFontForSize((int)luaL_optinteger(L, 4, 14)), LV_PART_MAIN);
|
|
lv_obj_set_style_text_color(l, lv_color_hex(argColor(L, 5, 0xE6E9ED)), LV_PART_MAIN);
|
|
WidgetUd* ud = (WidgetUd*)lua_newuserdatauv(L, sizeof(WidgetUd), 0);
|
|
ud->obj = l;
|
|
luaL_setmetatable(L, "wada.label");
|
|
return 1;
|
|
}
|
|
|
|
// ---- chart userdata (the primitive the native Monitor/Airtime pages use) ----
|
|
struct ChartUd { lv_obj_t* obj; lv_chart_series_t* ser[2]; int n_ser; };
|
|
ChartUd* checkChart(lua_State* L) { return (ChartUd*)luaL_checkudata(L, 1, "wada.chart"); }
|
|
|
|
int chPush(lua_State* L) { // chart:push(series_idx, value)
|
|
ChartUd* c = checkChart(L);
|
|
int si = (int)luaL_checkinteger(L, 2) - 1;
|
|
if (!c->obj || si < 0 || si >= c->n_ser) return 0;
|
|
lv_chart_set_next_value(c->obj, c->ser[si], (lv_coord_t)luaL_checkinteger(L, 3));
|
|
return 0;
|
|
}
|
|
int chSetAll(lua_State* L) { // chart:fill(series_idx, value) — reset a series
|
|
ChartUd* c = checkChart(L);
|
|
int si = (int)luaL_checkinteger(L, 2) - 1;
|
|
if (!c->obj || si < 0 || si >= c->n_ser) return 0;
|
|
lv_chart_set_all_value(c->obj, c->ser[si], (lv_coord_t)luaL_checkinteger(L, 3));
|
|
return 0;
|
|
}
|
|
int chRange(lua_State* L) { // chart:range(min, max)
|
|
ChartUd* c = checkChart(L);
|
|
if (c->obj) lv_chart_set_range(c->obj, LV_CHART_AXIS_PRIMARY_Y,
|
|
(lv_coord_t)luaL_checkinteger(L, 2),
|
|
(lv_coord_t)luaL_checkinteger(L, 3));
|
|
return 0;
|
|
}
|
|
int chPos(lua_State* L) {
|
|
ChartUd* c = checkChart(L);
|
|
if (c->obj) lv_obj_set_pos(c->obj, (lv_coord_t)luaL_checkinteger(L, 2), (lv_coord_t)luaL_checkinteger(L, 3));
|
|
return 0;
|
|
}
|
|
|
|
int uiChart(lua_State* L) { // wada.ui.chart(w, h, points, color1 [, color2] [, bars])
|
|
if (!s_h || !s_h->body) return luaL_error(L, "no app body");
|
|
int w = (int)luaL_checkinteger(L, 1), h = (int)luaL_checkinteger(L, 2);
|
|
int pts = (int)luaL_optinteger(L, 3, 60);
|
|
if (pts < 2) pts = 2;
|
|
if (pts > 240) pts = 240;
|
|
lv_obj_t* ch = lv_chart_create(s_h->body);
|
|
lv_obj_set_size(ch, w, h);
|
|
lv_chart_set_type(ch, lua_toboolean(L, 6) ? LV_CHART_TYPE_BAR : LV_CHART_TYPE_LINE);
|
|
lv_chart_set_point_count(ch, pts);
|
|
lv_chart_set_update_mode(ch, LV_CHART_UPDATE_MODE_SHIFT);
|
|
lv_chart_set_div_line_count(ch, 3, 0);
|
|
lv_obj_set_style_size(ch, 0, LV_PART_INDICATOR); // no point dots
|
|
lv_obj_set_style_bg_color(ch, lv_color_hex(0x101418), LV_PART_MAIN);
|
|
lv_obj_set_style_border_width(ch, 0, LV_PART_MAIN);
|
|
lv_obj_set_style_line_color(ch, lv_color_hex(0x2A3038), LV_PART_ITEMS);
|
|
ChartUd* ud = (ChartUd*)lua_newuserdatauv(L, sizeof(ChartUd), 0);
|
|
ud->obj = ch;
|
|
ud->n_ser = 0;
|
|
ud->ser[0] = lv_chart_add_series(ch, lv_color_hex(argColor(L, 4, 0x15B6A6)), LV_CHART_AXIS_PRIMARY_Y);
|
|
ud->n_ser = 1;
|
|
if (!lua_isnoneornil(L, 5)) {
|
|
ud->ser[1] = lv_chart_add_series(ch, lv_color_hex(argColor(L, 5, 0x7A7F87)), LV_CHART_AXIS_PRIMARY_Y);
|
|
ud->n_ser = 2;
|
|
}
|
|
luaL_setmetatable(L, "wada.chart");
|
|
return 1;
|
|
}
|
|
|
|
// ---- buttons: Lua callback dispatched through the guarded path ----
|
|
void btnEventCb(lv_event_t* e) {
|
|
if (lv_event_get_code(e) != LV_EVENT_CLICKED || !s_h || !s_h->L) return;
|
|
lua_State* L = s_h->L;
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, s_h->ref_btncb);
|
|
lua_pushlightuserdata(L, lv_event_get_target(e));
|
|
lua_rawget(L, -2);
|
|
lua_remove(L, -2);
|
|
if (lua_isfunction(L, -1)) guardedCall(s_h, 0);
|
|
else lua_pop(L, 1);
|
|
serviceDeferredClose();
|
|
}
|
|
|
|
int uiButton(lua_State* L) {
|
|
if (!s_h || !s_h->body) return luaL_error(L, "no app body");
|
|
const char* txt = luaL_checkstring(L, 1);
|
|
lv_obj_t* b = lv_btn_create(s_h->body);
|
|
lv_obj_set_pos(b, (lv_coord_t)luaL_checkinteger(L, 2), (lv_coord_t)luaL_checkinteger(L, 3));
|
|
lv_obj_set_size(b, (lv_coord_t)luaL_optinteger(L, 4, 90), (lv_coord_t)luaL_optinteger(L, 5, 34));
|
|
lv_obj_t* bl = lv_label_create(b);
|
|
lv_label_set_text(bl, txt);
|
|
lv_obj_center(bl);
|
|
if (lua_isfunction(L, 6)) {
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, s_h->ref_btncb);
|
|
lua_pushlightuserdata(L, b);
|
|
lua_pushvalue(L, 6);
|
|
lua_rawset(L, -3);
|
|
lua_pop(L, 1);
|
|
lv_obj_add_event_cb(b, btnEventCb, LV_EVENT_CLICKED, nullptr);
|
|
}
|
|
WidgetUd* ud = (WidgetUd*)lua_newuserdatauv(L, sizeof(WidgetUd), 0);
|
|
ud->obj = b;
|
|
luaL_setmetatable(L, "wada.label"); // shares set/pos/color methods
|
|
return 1;
|
|
}
|
|
|
|
// ---- sys / timer / store ----
|
|
int sysMillis(lua_State* L) { lua_pushinteger(L, (lua_Integer)millis()); return 1; }
|
|
int sysToast(lua_State* L) { luaHostToast(luaL_checkstring(L, 1), (int)luaL_optinteger(L, 2, 1500)); return 0; }
|
|
int sysRandom(lua_State* L) {
|
|
// esp_random-backed: apps should not have to seed math.random for games
|
|
lua_Integer lo = luaL_optinteger(L, 1, 0), hi = luaL_optinteger(L, 2, 0);
|
|
if (hi <= lo) { lua_pushinteger(L, (lua_Integer)esp_random()); return 1; }
|
|
lua_pushinteger(L, lo + (lua_Integer)(esp_random() % (uint32_t)(hi - lo + 1)));
|
|
return 1;
|
|
}
|
|
int sysBoard(lua_State* L) {
|
|
lua_createtable(L, 0, 6);
|
|
lua_pushinteger(L, s_h ? s_h->body_w : lv_disp_get_hor_res(nullptr));
|
|
lua_setfield(L, -2, "w");
|
|
lua_pushinteger(L, s_h ? s_h->body_h : lv_disp_get_ver_res(nullptr));
|
|
lua_setfield(L, -2, "h");
|
|
#if CAP_TOUCH
|
|
lua_pushboolean(L, 1);
|
|
#else
|
|
lua_pushboolean(L, 0);
|
|
#endif
|
|
lua_setfield(L, -2, "touch");
|
|
return 1;
|
|
}
|
|
|
|
void tickTimerCb(lv_timer_t* t) {
|
|
(void)t;
|
|
if (!s_h || !s_h->L) return;
|
|
uint32_t now = millis();
|
|
uint32_t dt = s_h->last_tick ? now - s_h->last_tick : 0;
|
|
s_h->last_tick = now;
|
|
if (pushCallback(s_h, "on_tick")) {
|
|
lua_pushinteger(s_h->L, (lua_Integer)dt);
|
|
guardedCall(s_h, 1);
|
|
}
|
|
serviceDeferredClose();
|
|
}
|
|
|
|
int timerEvery(lua_State* L) {
|
|
if (!s_h) return 0;
|
|
int ms = (int)luaL_checkinteger(L, 1);
|
|
if (ms < kMinTickMs) ms = kMinTickMs;
|
|
if (s_h->timer) lv_timer_set_period(s_h->timer, ms);
|
|
else s_h->timer = lv_timer_create(tickTimerCb, ms, nullptr);
|
|
return 0;
|
|
}
|
|
int timerStop(lua_State* L) {
|
|
(void)L;
|
|
if (s_h && s_h->timer) { lv_timer_del(s_h->timer); s_h->timer = nullptr; s_h->last_tick = 0; }
|
|
return 0;
|
|
}
|
|
|
|
// Store: a Lua table mirrored to /apps/<id>.sav as "k=v" lines (strings and
|
|
// numbers only). Loaded at launch, flushed on dismiss when dirty — tiny, rare,
|
|
// and off the hot path (touch_perf: never spam flash from the UI thread).
|
|
int storeGet(lua_State* L) {
|
|
lua_getfield(L, LUA_REGISTRYINDEX, "wada.storetab");
|
|
lua_pushvalue(L, 1);
|
|
lua_rawget(L, -2);
|
|
if (lua_isnil(L, -1) && !lua_isnoneornil(L, 2)) { lua_pop(L, 1); lua_pushvalue(L, 2); }
|
|
return 1;
|
|
}
|
|
int storeSet(lua_State* L) {
|
|
luaL_checkstring(L, 1);
|
|
if (!lua_isnumber(L, 2) && !lua_isstring(L, 2) && !lua_isnil(L, 2))
|
|
return luaL_error(L, "store values must be strings or numbers");
|
|
lua_getfield(L, LUA_REGISTRYINDEX, "wada.storetab");
|
|
lua_pushvalue(L, 1);
|
|
lua_pushvalue(L, 2);
|
|
lua_rawset(L, -3);
|
|
if (s_h) s_h->store_dirty = true;
|
|
return 0;
|
|
}
|
|
|
|
void storePath(char* out, size_t cap) {
|
|
char rel[40];
|
|
snprintf(rel, sizeof rel, "/apps/%s.sav", s_h->id);
|
|
luaHostAppPath(out, cap, rel);
|
|
}
|
|
|
|
void storeLoad() {
|
|
lua_State* L = s_h->L;
|
|
lua_newtable(L);
|
|
lua_setfield(L, LUA_REGISTRYINDEX, "wada.storetab");
|
|
fs::FS* fs = luaHostAppFs();
|
|
if (!fs) return;
|
|
char path[64]; storePath(path, sizeof path);
|
|
File f = fs->open(path, "r");
|
|
if (!f) return;
|
|
lua_getfield(L, LUA_REGISTRYINDEX, "wada.storetab");
|
|
char line[192];
|
|
size_t n;
|
|
while ((n = f.readBytesUntil('\n', line, sizeof line - 1)) > 0) {
|
|
line[n] = 0;
|
|
char* eq = strchr(line, '=');
|
|
if (!eq) continue;
|
|
*eq = 0;
|
|
lua_pushstring(L, line);
|
|
lua_Number num;
|
|
char* end = nullptr;
|
|
num = strtod(eq + 1, &end);
|
|
if (end && *end == 0 && end != eq + 1) lua_pushnumber(L, num);
|
|
else lua_pushstring(L, eq + 1);
|
|
lua_rawset(L, -3);
|
|
}
|
|
lua_pop(L, 1);
|
|
f.close();
|
|
}
|
|
|
|
void storeFlush() {
|
|
if (!s_h || !s_h->store_dirty) return;
|
|
fs::FS* fs = luaHostAppFs();
|
|
if (!fs) return;
|
|
lua_State* L = s_h->L;
|
|
char path[64]; storePath(path, sizeof path);
|
|
char appsdir[48]; luaHostAppPath(appsdir, sizeof appsdir, "/apps");
|
|
fs->mkdir(appsdir);
|
|
File f = fs->open(path, "w");
|
|
if (!f) return;
|
|
size_t written = 0;
|
|
lua_getfield(L, LUA_REGISTRYINDEX, "wada.storetab");
|
|
lua_pushnil(L);
|
|
while (lua_next(L, -2)) {
|
|
if (lua_type(L, -2) == LUA_TSTRING && (lua_isstring(L, -1) || lua_isnumber(L, -1))) {
|
|
char lbuf[192];
|
|
int m = snprintf(lbuf, sizeof lbuf, "%s=%s\n", lua_tostring(L, -2), luaL_tolstring(L, -1, nullptr));
|
|
lua_pop(L, 1); // luaL_tolstring's copy
|
|
if (m > 0 && written + (size_t)m <= kStoreMax) { f.write((const uint8_t*)lbuf, m); written += m; }
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
lua_pop(L, 1);
|
|
f.close();
|
|
s_h->store_dirty = false;
|
|
}
|
|
|
|
// ---- input plumbing ----
|
|
void sendInput(const char* type, const char* dir, int x, int y) {
|
|
if (!s_h || !s_h->L) return;
|
|
if (!pushCallback(s_h, "on_input")) return;
|
|
lua_State* L = s_h->L;
|
|
lua_createtable(L, 0, 4);
|
|
lua_pushstring(L, type); lua_setfield(L, -2, "type");
|
|
if (dir) { lua_pushstring(L, dir); lua_setfield(L, -2, "dir"); }
|
|
lua_pushinteger(L, x); lua_setfield(L, -2, "x");
|
|
lua_pushinteger(L, y); lua_setfield(L, -2, "y");
|
|
guardedCall(s_h, 1);
|
|
serviceDeferredClose();
|
|
}
|
|
|
|
void gestureCb(lv_event_t* e) {
|
|
lv_dir_t d = lv_indev_get_gesture_dir(lv_indev_get_act());
|
|
const char* dir = (d == LV_DIR_TOP) ? "up" : (d == LV_DIR_BOTTOM) ? "down"
|
|
: (d == LV_DIR_LEFT) ? "left" : (d == LV_DIR_RIGHT) ? "right" : nullptr;
|
|
if (dir) sendInput("swipe", dir, 0, 0);
|
|
(void)e;
|
|
}
|
|
|
|
void pressCb(lv_event_t* e) {
|
|
lv_indev_t* indev = lv_indev_get_act();
|
|
lv_point_t p{0, 0};
|
|
if (indev) lv_indev_get_point(indev, &p);
|
|
lv_area_t a;
|
|
lv_obj_get_coords(s_h->body, &a);
|
|
sendInput(lv_event_get_code(e) == LV_EVENT_PRESSED ? "down" : "up", nullptr,
|
|
p.x - a.x1, p.y - a.y1);
|
|
}
|
|
|
|
// ---- wada.mesh (read-only) ----
|
|
int meshContacts(lua_State* L) {
|
|
lua_newtable(L);
|
|
char name[36]; int type; uint32_t ago;
|
|
for (int i = 0, out = 0; i < 200 && out < 100; i++) {
|
|
if (!luaHostContactAt(i, name, sizeof name, &type, &ago)) break;
|
|
lua_createtable(L, 0, 3);
|
|
lua_pushstring(L, name); lua_setfield(L, -2, "name");
|
|
lua_pushinteger(L, type); lua_setfield(L, -2, "type");
|
|
lua_pushinteger(L, (lua_Integer)ago); lua_setfield(L, -2, "ago_s");
|
|
lua_rawseti(L, -2, ++out);
|
|
}
|
|
return 1;
|
|
}
|
|
int meshRxLog(lua_State* L) {
|
|
lua_newtable(L);
|
|
uint32_t ms_ago; int ptype, rssi, hops; float snr;
|
|
for (int i = 0, out = 0; i < 64; i++) {
|
|
if (!luaHostRxLogAt(i, &ms_ago, &ptype, &rssi, &snr, &hops)) break;
|
|
lua_createtable(L, 0, 5);
|
|
lua_pushinteger(L, (lua_Integer)ms_ago); lua_setfield(L, -2, "ago_ms");
|
|
lua_pushinteger(L, ptype); lua_setfield(L, -2, "type");
|
|
lua_pushinteger(L, rssi); lua_setfield(L, -2, "rssi");
|
|
lua_pushnumber(L, snr); lua_setfield(L, -2, "snr");
|
|
lua_pushinteger(L, hops); lua_setfield(L, -2, "hops");
|
|
lua_rawseti(L, -2, ++out);
|
|
}
|
|
return 1;
|
|
}
|
|
int meshStats(lua_State* L) {
|
|
float rssi, noise; uint32_t rx_air, tx_air, rx_pkts, rx_err; int budget;
|
|
luaHostRadioStats(&rssi, &noise, &rx_air, &tx_air, &rx_pkts, &rx_err, &budget);
|
|
lua_createtable(L, 0, 7);
|
|
lua_pushnumber(L, rssi); lua_setfield(L, -2, "rssi");
|
|
lua_pushnumber(L, noise); lua_setfield(L, -2, "noise");
|
|
lua_pushinteger(L, (lua_Integer)rx_air); lua_setfield(L, -2, "rx_air_s");
|
|
lua_pushinteger(L, (lua_Integer)tx_air); lua_setfield(L, -2, "tx_air_s");
|
|
lua_pushinteger(L, (lua_Integer)rx_pkts); lua_setfield(L, -2, "rx_pkts");
|
|
lua_pushinteger(L, (lua_Integer)rx_err); lua_setfield(L, -2, "rx_err");
|
|
lua_pushinteger(L, budget); lua_setfield(L, -2, "tx_budget_ms");
|
|
uint32_t rx_evt, rx_drop, tx_pkts; float freq, bw; int sf, duty;
|
|
luaHostRadioStats2(&rx_evt, &rx_drop, &tx_pkts, &freq, &bw, &sf, &duty);
|
|
lua_pushinteger(L, (lua_Integer)rx_evt); lua_setfield(L, -2, "rx_events");
|
|
lua_pushinteger(L, (lua_Integer)rx_drop); lua_setfield(L, -2, "rx_dropped");
|
|
lua_pushinteger(L, (lua_Integer)tx_pkts); lua_setfield(L, -2, "tx_pkts");
|
|
lua_pushnumber(L, freq); lua_setfield(L, -2, "freq");
|
|
lua_pushnumber(L, bw); lua_setfield(L, -2, "bw");
|
|
lua_pushinteger(L, sf); lua_setfield(L, -2, "sf");
|
|
lua_pushinteger(L, duty); lua_setfield(L, -2, "duty_pct");
|
|
return 1;
|
|
}
|
|
int meshSelf(lua_State* L) {
|
|
char name[36]; double lat, lon;
|
|
luaHostSelfInfo(name, sizeof name, &lat, &lon);
|
|
lua_createtable(L, 0, 3);
|
|
lua_pushstring(L, name); lua_setfield(L, -2, "name");
|
|
lua_pushnumber(L, lat); lua_setfield(L, -2, "lat");
|
|
lua_pushnumber(L, lon); lua_setfield(L, -2, "lon");
|
|
return 1;
|
|
}
|
|
|
|
// ---- wada.net: one in-flight async http_get per app ----
|
|
constexpr size_t kNetCapMax = 32 * 1024;
|
|
char s_net_url[160];
|
|
char* s_net_buf = nullptr;
|
|
size_t s_net_cap = 0;
|
|
int s_net_result = -1;
|
|
volatile bool s_net_pending = false; // worker should run it
|
|
volatile bool s_net_done = false; // result ready for the UI thread
|
|
int s_net_cb = LUA_NOREF;
|
|
lv_timer_t* s_net_poll = nullptr;
|
|
|
|
void netDeliver(lv_timer_t* t) {
|
|
(void)t;
|
|
if (!s_net_done) return;
|
|
s_net_done = false;
|
|
if (s_net_poll) { lv_timer_del(s_net_poll); s_net_poll = nullptr; }
|
|
if (!s_h || !s_h->L || s_net_cb == LUA_NOREF) { s_net_cb = LUA_NOREF; return; }
|
|
lua_State* L = s_h->L;
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, s_net_cb);
|
|
luaL_unref(L, LUA_REGISTRYINDEX, s_net_cb);
|
|
s_net_cb = LUA_NOREF;
|
|
lua_pushinteger(L, s_net_result >= 0 ? 200 : -1);
|
|
if (s_net_result >= 0 && s_net_buf) lua_pushlstring(L, s_net_buf, (size_t)s_net_result);
|
|
else lua_pushnil(L);
|
|
guardedCall(s_h, 2);
|
|
serviceDeferredClose();
|
|
}
|
|
|
|
int netHttpGet(lua_State* L) {
|
|
const char* url = luaL_checkstring(L, 1);
|
|
luaL_checktype(L, 2, LUA_TFUNCTION);
|
|
size_t maxb = (size_t)luaL_optinteger(L, 3, 16 * 1024);
|
|
if (maxb > kNetCapMax) maxb = kNetCapMax;
|
|
if (strncmp(url, "http://", 7) != 0) return luaL_error(L, "http:// urls only");
|
|
if (strlen(url) >= sizeof(s_net_url)) return luaL_error(L, "url too long");
|
|
if (s_net_pending || s_net_cb != LUA_NOREF) return luaL_error(L, "a fetch is already running");
|
|
if (!s_net_buf || s_net_cap < maxb + 1) {
|
|
if (s_net_buf) heap_caps_free(s_net_buf);
|
|
s_net_buf = (char*)heap_caps_malloc(maxb + 1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
|
s_net_cap = s_net_buf ? maxb + 1 : 0;
|
|
if (!s_net_buf) return luaL_error(L, "no memory for the fetch buffer");
|
|
}
|
|
snprintf(s_net_url, sizeof s_net_url, "%s", url);
|
|
lua_pushvalue(L, 2);
|
|
s_net_cb = luaL_ref(L, LUA_REGISTRYINDEX);
|
|
s_net_done = false;
|
|
s_net_pending = true;
|
|
if (!s_net_poll) s_net_poll = lv_timer_create(netDeliver, 120, nullptr);
|
|
return 0;
|
|
}
|
|
|
|
// ---- module registration ----
|
|
void openWada(lua_State* L) {
|
|
lua_newtable(L); // wada
|
|
|
|
lua_newtable(L); // wada.ui
|
|
lua_pushcfunction(L, uiCanvas); lua_setfield(L, -2, "canvas");
|
|
lua_pushcfunction(L, uiLabel); lua_setfield(L, -2, "label");
|
|
lua_pushcfunction(L, uiButton); lua_setfield(L, -2, "button");
|
|
lua_pushcfunction(L, uiChart); lua_setfield(L, -2, "chart");
|
|
lua_createtable(L, 0, 6); // wada.ui.colors (theme)
|
|
lua_pushinteger(L, 0x15B6A6); lua_setfield(L, -2, "accent");
|
|
lua_pushinteger(L, 0xE6E9ED); lua_setfield(L, -2, "text");
|
|
lua_pushinteger(L, 0x7A7F87); lua_setfield(L, -2, "sub");
|
|
lua_pushinteger(L, 0x0E1216); lua_setfield(L, -2, "bg");
|
|
lua_pushinteger(L, 0xD7574E); lua_setfield(L, -2, "bad");
|
|
lua_pushinteger(L, 0x53C06B); lua_setfield(L, -2, "good");
|
|
lua_setfield(L, -2, "colors");
|
|
lua_setfield(L, -2, "ui");
|
|
|
|
lua_newtable(L); // wada.sys
|
|
lua_pushcfunction(L, sysMillis); lua_setfield(L, -2, "millis");
|
|
lua_pushcfunction(L, sysToast); lua_setfield(L, -2, "toast");
|
|
lua_pushcfunction(L, sysBoard); lua_setfield(L, -2, "board");
|
|
lua_pushcfunction(L, sysRandom); lua_setfield(L, -2, "random");
|
|
lua_setfield(L, -2, "sys");
|
|
|
|
lua_newtable(L); // wada.timer
|
|
lua_pushcfunction(L, timerEvery); lua_setfield(L, -2, "every");
|
|
lua_pushcfunction(L, timerStop); lua_setfield(L, -2, "stop");
|
|
lua_setfield(L, -2, "timer");
|
|
|
|
lua_newtable(L); // wada.store
|
|
lua_pushcfunction(L, storeGet); lua_setfield(L, -2, "get");
|
|
lua_pushcfunction(L, storeSet); lua_setfield(L, -2, "set");
|
|
lua_setfield(L, -2, "store");
|
|
|
|
lua_newtable(L); // wada.mesh (read-only)
|
|
lua_pushcfunction(L, meshContacts); lua_setfield(L, -2, "contacts");
|
|
lua_pushcfunction(L, meshRxLog); lua_setfield(L, -2, "rx_log");
|
|
lua_pushcfunction(L, meshStats); lua_setfield(L, -2, "stats");
|
|
lua_pushcfunction(L, meshSelf); lua_setfield(L, -2, "self");
|
|
lua_setfield(L, -2, "mesh");
|
|
|
|
lua_newtable(L); // wada.net
|
|
lua_pushcfunction(L, netHttpGet); lua_setfield(L, -2, "http_get");
|
|
lua_setfield(L, -2, "net");
|
|
|
|
lua_setglobal(L, "wada");
|
|
|
|
// metatables
|
|
luaL_newmetatable(L, "wada.canvas");
|
|
lua_newtable(L);
|
|
lua_pushcfunction(L, cvFill); lua_setfield(L, -2, "fill");
|
|
lua_pushcfunction(L, cvRect); lua_setfield(L, -2, "rect");
|
|
lua_pushcfunction(L, cvLine); lua_setfield(L, -2, "line");
|
|
lua_pushcfunction(L, cvCircle); lua_setfield(L, -2, "circle");
|
|
lua_pushcfunction(L, cvText); lua_setfield(L, -2, "text");
|
|
lua_pushcfunction(L, cvPos); lua_setfield(L, -2, "pos");
|
|
lua_setfield(L, -2, "__index");
|
|
lua_pushcfunction(L, cvGc); lua_setfield(L, -2, "__gc");
|
|
lua_pop(L, 1);
|
|
|
|
luaL_newmetatable(L, "wada.chart");
|
|
lua_newtable(L);
|
|
lua_pushcfunction(L, chPush); lua_setfield(L, -2, "push");
|
|
lua_pushcfunction(L, chSetAll); lua_setfield(L, -2, "fill");
|
|
lua_pushcfunction(L, chRange); lua_setfield(L, -2, "range");
|
|
lua_pushcfunction(L, chPos); lua_setfield(L, -2, "pos");
|
|
lua_setfield(L, -2, "__index");
|
|
lua_pop(L, 1);
|
|
|
|
luaL_newmetatable(L, "wada.label");
|
|
lua_newtable(L);
|
|
lua_pushcfunction(L, lbSet); lua_setfield(L, -2, "set");
|
|
lua_pushcfunction(L, lbPos); lua_setfield(L, -2, "pos");
|
|
lua_pushcfunction(L, lbColor); lua_setfield(L, -2, "color");
|
|
lua_setfield(L, -2, "__index");
|
|
lua_pop(L, 1);
|
|
}
|
|
|
|
void openSandbox(lua_State* L) {
|
|
luaL_requiref(L, LUA_GNAME, luaopen_base, 1); lua_pop(L, 1);
|
|
luaL_requiref(L, LUA_MATHLIBNAME, luaopen_math, 1); lua_pop(L, 1);
|
|
luaL_requiref(L, LUA_STRLIBNAME, luaopen_string, 1); lua_pop(L, 1);
|
|
luaL_requiref(L, LUA_TABLIBNAME, luaopen_table, 1); lua_pop(L, 1);
|
|
static const char* const kill[] = { "dofile", "loadfile", "load", "require", "collectgarbage" };
|
|
for (const char* k : kill) { lua_pushnil(L); lua_setglobal(L, k); }
|
|
}
|
|
|
|
void hostTeardown(); // fwd
|
|
|
|
} // namespace
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 3) lifecycle
|
|
// ---------------------------------------------------------------------------
|
|
bool luaAppIsOpen() { return s_h != nullptr; }
|
|
|
|
void luaAppSteer(int dx, int dy) {
|
|
if (!s_h) return;
|
|
const char* dir = (dy < 0) ? "up" : (dy > 0) ? "down" : (dx < 0) ? "left" : (dx > 0) ? "right" : nullptr;
|
|
if (dir) sendInput("swipe", dir, 0, 0);
|
|
}
|
|
|
|
static void luaAppRootDeletedCb(lv_event_t* e) {
|
|
(void)e;
|
|
// Root died (async delete completed) — the lv objects are gone; the Lua
|
|
// state was already closed in hostTeardown().
|
|
}
|
|
|
|
namespace {
|
|
void hostTeardown() {
|
|
Host* h = s_h;
|
|
if (!h) return;
|
|
s_h = nullptr; // bindings see "closed" from here on
|
|
if (h->timer) { lv_timer_del(h->timer); h->timer = nullptr; }
|
|
if (s_net_poll) { lv_timer_del(s_net_poll); s_net_poll = nullptr; }
|
|
if (h->L && s_net_cb != LUA_NOREF) { luaL_unref(h->L, LUA_REGISTRYINDEX, s_net_cb); }
|
|
s_net_cb = LUA_NOREF; // a worker fetch may still land; netDeliver sees no app and drops it
|
|
if (h->L) {
|
|
// best-effort on_close + store flush before the state dies
|
|
lua_State* L = h->L;
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, h->ref_app);
|
|
if (lua_istable(L, -1)) {
|
|
lua_getfield(L, -1, "on_close");
|
|
if (lua_isfunction(L, -1)) { lua_sethook(L, budgetHook, LUA_MASKCOUNT, kInstrBudget); lua_pcall(L, 0, 0, 0); lua_sethook(L, nullptr, 0, 0); }
|
|
else lua_pop(L, 1);
|
|
}
|
|
lua_pop(L, 1);
|
|
s_h = h; // storeFlush needs the id + dirty flag
|
|
storeFlush();
|
|
s_h = nullptr;
|
|
lua_close(L); // runs canvas __gc -> frees pixel buffers
|
|
Serial.printf("[LUAAPP] %s closed, leaked=%u peak=%u\n", h->id,
|
|
(unsigned)h->heap.used, (unsigned)h->heap.peak);
|
|
}
|
|
if (h->root) appPageDeleteRootAsync(h->root);
|
|
appPageEnd(&luaAppDismiss);
|
|
delete h;
|
|
}
|
|
} // namespace
|
|
|
|
void luaAppDismiss() {
|
|
if (!s_h) return;
|
|
if (s_h->in_lua) { s_h->want_close = true; return; } // unwind first
|
|
hostTeardown();
|
|
}
|
|
|
|
bool luaAppLaunch(const char* id, const char* title, const char* src, size_t len) {
|
|
if (s_h || !id || !src || len == 0 || len > kMaxSrc) return false;
|
|
|
|
Host* h = new Host();
|
|
h->heap.cap = kHeapCap;
|
|
snprintf(h->id, sizeof h->id, "%s", id);
|
|
snprintf(h->title, sizeof h->title, "%s", title ? title : id);
|
|
|
|
h->L = lua_newstate(psAllocCb, &h->heap);
|
|
if (!h->L) { delete h; luaHostToast("App: out of memory", 1800); return false; }
|
|
openSandbox(h->L);
|
|
openWada(h->L);
|
|
|
|
// UI scaffold: full-screen overlay + tall "< title" bar via AppPage, exactly
|
|
// like SnakeGame. Body = the content area apps build into.
|
|
h->root = lv_obj_create(lv_layer_top());
|
|
lv_obj_remove_style_all(h->root);
|
|
lv_obj_set_size(h->root, lv_disp_get_hor_res(nullptr), lv_disp_get_ver_res(nullptr));
|
|
lv_obj_set_style_bg_color(h->root, lv_color_hex(0x0E1216), LV_PART_MAIN);
|
|
lv_obj_set_style_bg_opa(h->root, LV_OPA_COVER, LV_PART_MAIN);
|
|
lv_obj_clear_flag(h->root, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_add_event_cb(h->root, luaAppRootDeletedCb, LV_EVENT_DELETE, nullptr);
|
|
|
|
h->body = lv_obj_create(h->root);
|
|
lv_obj_remove_style_all(h->body);
|
|
const int bar_h = 44; // matches the AppPage tall bar band
|
|
h->body_w = lv_disp_get_hor_res(nullptr);
|
|
h->body_h = lv_disp_get_ver_res(nullptr) - bar_h;
|
|
lv_obj_set_pos(h->body, 0, bar_h);
|
|
lv_obj_set_size(h->body, h->body_w, h->body_h);
|
|
lv_obj_clear_flag(h->body, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_add_flag(h->body, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_add_event_cb(h->body, gestureCb, LV_EVENT_GESTURE, nullptr);
|
|
lv_obj_add_event_cb(h->body, pressCb, LV_EVENT_PRESSED, nullptr);
|
|
lv_obj_add_event_cb(h->body, pressCb, LV_EVENT_RELEASED, nullptr);
|
|
|
|
snprintf(s_bar_title, sizeof s_bar_title, "%s", h->title);
|
|
appPageBegin(s_bar_title, &luaAppDismiss);
|
|
|
|
s_h = h;
|
|
storeLoad();
|
|
|
|
// button-callback table
|
|
lua_newtable(h->L);
|
|
h->ref_btncb = luaL_ref(h->L, LUA_REGISTRYINDEX);
|
|
|
|
// compile + run the chunk (must return the app callback table)
|
|
lua_pushcfunction(h->L, tracebackMsgh);
|
|
int rc = luaL_loadbuffer(h->L, src, len, id);
|
|
if (rc == LUA_OK) {
|
|
lua_sethook(h->L, budgetHook, LUA_MASKCOUNT, kInstrBudget * 5); // init gets a bigger budget
|
|
h->in_lua = true;
|
|
rc = lua_pcall(h->L, 0, 1, -2);
|
|
h->in_lua = false;
|
|
lua_sethook(h->L, nullptr, 0, 0);
|
|
}
|
|
if (rc != LUA_OK || !lua_istable(h->L, -1)) {
|
|
const char* err = rc != LUA_OK ? lua_tostring(h->L, -1) : "app did not return a table";
|
|
Serial.printf("[LUAAPP] %s load failed: %s\n", id, err ? err : "?");
|
|
char msg[96];
|
|
snprintf(msg, sizeof msg, "App failed: %.60s", err ? err : "load error");
|
|
luaHostToast(msg, 2600);
|
|
hostTeardown();
|
|
return false;
|
|
}
|
|
h->ref_app = luaL_ref(h->L, LUA_REGISTRYINDEX);
|
|
lua_pop(h->L, 1); // traceback handler
|
|
|
|
if (pushCallback(h, "on_open")) {
|
|
lua_pushinteger(h->L, h->body_w);
|
|
lua_pushinteger(h->L, h->body_h);
|
|
guardedCall(h, 2);
|
|
}
|
|
serviceDeferredClose();
|
|
if (s_h) Serial.printf("[LUAAPP] %s open, heap=%u\n", id, (unsigned)h->heap.used);
|
|
return s_h != nullptr;
|
|
}
|
|
|
|
bool luaAppLaunchFile(const char* id, const char* title, const char* embedded, size_t embedded_len) {
|
|
fs::FS* fs = luaHostAppFs();
|
|
if (fs) {
|
|
char rel[40], path[64];
|
|
snprintf(rel, sizeof rel, "/apps/%s.lua", id);
|
|
luaHostAppPath(path, sizeof path, rel);
|
|
File f = fs->open(path, "r");
|
|
if (f) {
|
|
size_t sz = f.size();
|
|
if (sz > 0 && sz <= kMaxSrc) {
|
|
char* buf = (char*)heap_caps_malloc(sz, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
|
if (buf) {
|
|
size_t rd = f.read((uint8_t*)buf, sz);
|
|
f.close();
|
|
bool ok = rd == sz && luaAppLaunch(id, title, buf, sz);
|
|
heap_caps_free(buf);
|
|
if (ok) return true;
|
|
// fall through to the embedded copy on any failure
|
|
} else f.close();
|
|
} else f.close();
|
|
}
|
|
}
|
|
if (embedded && embedded_len) return luaAppLaunch(id, title, embedded, embedded_len);
|
|
return false;
|
|
}
|
|
|
|
// ---- net worker entry points (UITask's tile/net worker calls these) --------
|
|
bool luaNetWorkerPending() { return s_net_pending; }
|
|
void luaNetWorkerService(WiFiClient& client, HTTPClient& http) {
|
|
s_net_pending = false;
|
|
s_net_result = s_net_buf ? luaStoreHttpGet(client, http, s_net_url, s_net_buf, s_net_cap) : -1;
|
|
s_net_done = true;
|
|
}
|
|
|
|
#endif // CAP_LUA_APPS
|