mirror of
https://github.com/wisplite/cardstock.git
synced 2026-06-27 14:07:07 -05:00
add more graphics bindings, M5Canvas, and keyboard bindings to support the features required by Paper.
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
Cardstock is a lightweight Lua-based OS for the M5Stack Cardputer.
|
||||
|
||||
The project currently builds a Lua VM and loads a lua entrypoint (which will eventually be a launcher)
|
||||
This repository contains all the C/C++ infrastructure and bindings required to support the Lua portion of the OS.
|
||||
|
||||
I am currently writing Lua bindings for all the M5Cardputer libraries, starting with the GFX library right now.
|
||||
Currently supported modules are:
|
||||
- M5GFX
|
||||
- M5Canvas
|
||||
- Keyboard
|
||||
|
||||
DISCLAIMER: A decent portion of the C++ is written by AI right now. The plan is to go back and check over/rewrite this later as I add more complex functionality to the "bootloader".
|
||||
The eventual goal is for every API to be built and passed through to Lua, with certain things like wireless being controlled globally across apps.
|
||||
@@ -1,6 +1,33 @@
|
||||
#include "lua_gfx.h"
|
||||
|
||||
#include "services/GfxService.h"
|
||||
#include "M5Cardputer.h"
|
||||
|
||||
// -------------------------------
|
||||
// gfx.sprite userdata (M5Canvas)
|
||||
// -------------------------------
|
||||
|
||||
static const char* kSpriteMT = "gfx.sprite";
|
||||
|
||||
struct LuaSprite {
|
||||
M5Canvas* canvas = nullptr;
|
||||
};
|
||||
|
||||
static LuaSprite* lua_check_sprite(lua_State* L, int idx) {
|
||||
return static_cast<LuaSprite*>(luaL_checkudata(L, idx, kSpriteMT));
|
||||
}
|
||||
|
||||
static M5Canvas* lua_sprite_require_alive(lua_State* L, LuaSprite* s) {
|
||||
if (!s || !s->canvas) luaL_error(L, "sprite is freed");
|
||||
return s->canvas;
|
||||
}
|
||||
|
||||
static void lua_sprite_free(LuaSprite* s) {
|
||||
if (!s || !s->canvas) return;
|
||||
s->canvas->deleteSprite();
|
||||
delete s->canvas;
|
||||
s->canvas = nullptr;
|
||||
}
|
||||
|
||||
static uint16_t lua_check_u16(lua_State* L, int idx) {
|
||||
lua_Integer v = luaL_checkinteger(L, idx);
|
||||
@@ -9,6 +36,114 @@ static uint16_t lua_check_u16(lua_State* L, int idx) {
|
||||
return static_cast<uint16_t>(v);
|
||||
}
|
||||
|
||||
static int l_sprite_gc(lua_State* L) {
|
||||
LuaSprite* s = lua_check_sprite(L, 1);
|
||||
lua_sprite_free(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_sprite_free(lua_State* L) {
|
||||
LuaSprite* s = lua_check_sprite(L, 1);
|
||||
lua_sprite_free(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_sprite_clear(lua_State* L) {
|
||||
LuaSprite* s = lua_check_sprite(L, 1);
|
||||
M5Canvas* c = lua_sprite_require_alive(L, s);
|
||||
uint16_t color = 0x0000;
|
||||
if (lua_gettop(L) >= 2 && !lua_isnil(L, 2)) color = lua_check_u16(L, 2);
|
||||
c->fillScreen(color);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_sprite_set_text_color(lua_State* L) {
|
||||
LuaSprite* s = lua_check_sprite(L, 1);
|
||||
M5Canvas* c = lua_sprite_require_alive(L, s);
|
||||
uint16_t fg = lua_check_u16(L, 2);
|
||||
if (lua_gettop(L) >= 3 && !lua_isnil(L, 3)) {
|
||||
uint16_t bg = lua_check_u16(L, 3);
|
||||
c->setTextColor(fg, bg);
|
||||
} else {
|
||||
c->setTextColor(fg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_sprite_set_text_size(lua_State* L) {
|
||||
LuaSprite* s = lua_check_sprite(L, 1);
|
||||
M5Canvas* c = lua_sprite_require_alive(L, s);
|
||||
lua_Integer size = luaL_checkinteger(L, 2);
|
||||
if (size < 1) size = 1;
|
||||
if (size > 255) size = 255;
|
||||
c->setTextSize(static_cast<uint8_t>(size));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_sprite_draw_string(lua_State* L) {
|
||||
LuaSprite* s = lua_check_sprite(L, 1);
|
||||
M5Canvas* c = lua_sprite_require_alive(L, s);
|
||||
const char* text = luaL_checkstring(L, 2);
|
||||
int32_t x = static_cast<int32_t>(luaL_checkinteger(L, 3));
|
||||
int32_t y = static_cast<int32_t>(luaL_checkinteger(L, 4));
|
||||
int32_t w = c->drawString(text, x, y);
|
||||
lua_pushinteger(L, static_cast<lua_Integer>(w));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_sprite_draw_center_string(lua_State* L) {
|
||||
LuaSprite* s = lua_check_sprite(L, 1);
|
||||
M5Canvas* c = lua_sprite_require_alive(L, s);
|
||||
const char* text = luaL_checkstring(L, 2);
|
||||
int32_t x = static_cast<int32_t>(luaL_checkinteger(L, 3));
|
||||
int32_t y = static_cast<int32_t>(luaL_checkinteger(L, 4));
|
||||
int32_t w = c->drawCenterString(text, x, y);
|
||||
lua_pushinteger(L, static_cast<lua_Integer>(w));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_sprite_push(lua_State* L) {
|
||||
LuaSprite* s = lua_check_sprite(L, 1);
|
||||
M5Canvas* c = lua_sprite_require_alive(L, s);
|
||||
int32_t x = static_cast<int32_t>(luaL_checkinteger(L, 2));
|
||||
int32_t y = static_cast<int32_t>(luaL_checkinteger(L, 3));
|
||||
c->pushSprite(x, y);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const luaL_Reg kSpriteMethods[] = {
|
||||
{"clear", l_sprite_clear},
|
||||
{"setTextColor", l_sprite_set_text_color},
|
||||
{"setTextSize", l_sprite_set_text_size},
|
||||
{"drawString", l_sprite_draw_string},
|
||||
{"drawCenterString", l_sprite_draw_center_string},
|
||||
{"push", l_sprite_push},
|
||||
{"free", l_sprite_free},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
|
||||
static int l_gfx_new_sprite(lua_State* L) {
|
||||
int32_t w = static_cast<int32_t>(luaL_checkinteger(L, 1));
|
||||
int32_t h = static_cast<int32_t>(luaL_checkinteger(L, 2));
|
||||
if (w <= 0 || h <= 0) luaL_error(L, "newSprite: width and height must be > 0");
|
||||
|
||||
LuaSprite* ud = static_cast<LuaSprite*>(lua_newuserdatauv(L, sizeof(LuaSprite), 0));
|
||||
new (ud) LuaSprite();
|
||||
|
||||
// Create canvas (parented to the real display).
|
||||
ud->canvas = new (std::nothrow) M5Canvas(&M5Cardputer.Display);
|
||||
if (!ud->canvas) luaL_error(L, "newSprite: OOM allocating canvas");
|
||||
|
||||
bool ok = ud->canvas->createSprite(w, h);
|
||||
if (!ok) {
|
||||
lua_sprite_free(ud);
|
||||
luaL_error(L, "newSprite: createSprite failed");
|
||||
}
|
||||
|
||||
luaL_setmetatable(L, kSpriteMT);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_gfx_clear(lua_State* L) {
|
||||
uint16_t color = 0x0000;
|
||||
if (lua_gettop(L) >= 1 && !lua_isnil(L, 1)) color = lua_check_u16(L, 1);
|
||||
@@ -63,7 +198,41 @@ static int l_gfx_draw_string(lua_State* L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_gfx_fill_rect(lua_State* L) {
|
||||
int32_t x = static_cast<int32_t>(luaL_checkinteger(L, 1));
|
||||
int32_t y = static_cast<int32_t>(luaL_checkinteger(L, 2));
|
||||
int32_t w = static_cast<int32_t>(luaL_checkinteger(L, 3));
|
||||
int32_t h = static_cast<int32_t>(luaL_checkinteger(L, 4));
|
||||
uint16_t color = lua_check_u16(L, 5);
|
||||
GfxService::fillRect(x, y, w, h, color);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_gfx_draw_center_string(lua_State* L) {
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
int32_t x = static_cast<int32_t>(luaL_checkinteger(L, 2));
|
||||
int32_t y = static_cast<int32_t>(luaL_checkinteger(L, 3));
|
||||
int32_t font = -1;
|
||||
if (lua_gettop(L) >= 4 && !lua_isnil(L, 4)) font = static_cast<int32_t>(luaL_checkinteger(L, 4));
|
||||
int32_t w = GfxService::drawCenterString(s, x, y, font);
|
||||
lua_pushinteger(L, static_cast<lua_Integer>(w));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_gfx_width(lua_State* L) {
|
||||
int32_t w = GfxService::width();
|
||||
lua_pushinteger(L, static_cast<lua_Integer>(w));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_gfx_height(lua_State* L) {
|
||||
int32_t h = GfxService::height();
|
||||
lua_pushinteger(L, static_cast<lua_Integer>(h));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static const luaL_Reg kGfxLib[] = {
|
||||
{"newSprite", l_gfx_new_sprite},
|
||||
{"clear", l_gfx_clear},
|
||||
{"setCursor", l_gfx_set_cursor},
|
||||
{"setTextSize", l_gfx_set_text_size},
|
||||
@@ -71,10 +240,26 @@ static const luaL_Reg kGfxLib[] = {
|
||||
{"print", l_gfx_print},
|
||||
{"println", l_gfx_println},
|
||||
{"drawString", l_gfx_draw_string},
|
||||
{"fillRect", l_gfx_fill_rect},
|
||||
{"drawCenterString", l_gfx_draw_center_string},
|
||||
{"width", l_gfx_width},
|
||||
{"height", l_gfx_height},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
|
||||
int luaopen_gfx(lua_State* L) {
|
||||
// Create gfx.sprite metatable.
|
||||
if (luaL_newmetatable(L, kSpriteMT)) {
|
||||
// metatable.__index = methods table
|
||||
luaL_newlib(L, kSpriteMethods);
|
||||
lua_setfield(L, -2, "__index");
|
||||
|
||||
// metatable.__gc = l_sprite_gc
|
||||
lua_pushcfunction(L, l_sprite_gc);
|
||||
lua_setfield(L, -2, "__gc");
|
||||
}
|
||||
lua_pop(L, 1); // pop metatable
|
||||
|
||||
luaL_newlib(L, kGfxLib);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "lua_keyboard.h"
|
||||
|
||||
#include "services/KeyboardService.h"
|
||||
|
||||
static int l_keyboard_is_changed(lua_State* L) {
|
||||
lua_pushboolean(L, KeyboardService::isChanged());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_keyboard_is_pressed(lua_State* L) {
|
||||
lua_pushinteger(L, KeyboardService::isPressed());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_keyboard_is_key_pressed(lua_State* L) {
|
||||
const char* c = luaL_checkstring(L, 1);
|
||||
lua_pushboolean(L, KeyboardService::isKeyPressed(c[0]));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_keyboard_get_key(lua_State* L) {
|
||||
int32_t x = static_cast<int32_t>(luaL_checkinteger(L, 1));
|
||||
int32_t y = static_cast<int32_t>(luaL_checkinteger(L, 2));
|
||||
lua_pushinteger(L, KeyboardService::getKey(x, y));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static const luaL_Reg kKeyboardLib[] = {
|
||||
{"isChanged", l_keyboard_is_changed},
|
||||
{"isPressed", l_keyboard_is_pressed},
|
||||
{"isKeyPressed", l_keyboard_is_key_pressed},
|
||||
{"getKey", l_keyboard_get_key},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
|
||||
int luaopen_keyboard(lua_State* L) {
|
||||
luaL_newlib(L, kKeyboardLib);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "lua.hpp"
|
||||
|
||||
// Lua module entrypoint: local keyboard = require("keyboard")
|
||||
int luaopen_keyboard(lua_State* L);
|
||||
@@ -0,0 +1,217 @@
|
||||
#include "require_sd.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include <new>
|
||||
#include <memory>
|
||||
|
||||
namespace {
|
||||
|
||||
// Registry key for current app root.
|
||||
static const char* kAppRootKey = "cardstock.app_root";
|
||||
static const char* kSysLibRoot = "/syslib";
|
||||
|
||||
static String normalize_abs_path(const String& p) {
|
||||
if (!p.length()) return "";
|
||||
String out = p;
|
||||
if (out[0] != '/') out = String("/") + out;
|
||||
// Strip trailing slashes (except for "/")
|
||||
while (out.length() > 1 && out[out.length() - 1] == '/') out.remove(out.length() - 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool contains_path_traversal(const char* s) {
|
||||
if (!s) return false;
|
||||
// Disallow any ".." path segment.
|
||||
// (Accepts module names like "foo..bar"? That would be weird; still blocked.)
|
||||
return strstr(s, "..") != nullptr;
|
||||
}
|
||||
|
||||
static String module_name_to_rel_path(const char* modname) {
|
||||
// Convert dotted module name to path: "a.b.c" -> "a/b/c"
|
||||
String out;
|
||||
if (!modname) return out;
|
||||
for (const char* p = modname; *p; ++p) {
|
||||
char c = *p;
|
||||
if (c == '.') out += '/';
|
||||
else if (c == '\\') out += '/';
|
||||
else out += c;
|
||||
}
|
||||
// Strip any leading slashes to keep it relative.
|
||||
while (out.length() && out[0] == '/') out.remove(0, 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool read_entire_file(const String& path, std::unique_ptr<uint8_t[]>& out_buf, size_t& out_len, String& out_err) {
|
||||
const String normalized = normalize_abs_path(path);
|
||||
File f = SD.open(normalized.c_str(), FILE_READ);
|
||||
if (!f) {
|
||||
out_err = "open failed: " + normalized;
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t sz = static_cast<size_t>(f.size());
|
||||
if (sz == 0) {
|
||||
out_err = "empty file: " + normalized;
|
||||
f.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<uint8_t[]> buf(new (std::nothrow) uint8_t[sz]);
|
||||
if (!buf) {
|
||||
out_err = "OOM reading: " + normalized;
|
||||
f.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t read_total = 0;
|
||||
while (read_total < sz) {
|
||||
int n = f.read(buf.get() + read_total, sz - read_total);
|
||||
if (n <= 0) break;
|
||||
read_total += static_cast<size_t>(n);
|
||||
}
|
||||
f.close();
|
||||
|
||||
if (read_total != sz) {
|
||||
out_err = "short read: " + normalized;
|
||||
return false;
|
||||
}
|
||||
|
||||
out_buf = std::move(buf);
|
||||
out_len = sz;
|
||||
return true;
|
||||
}
|
||||
|
||||
static String get_app_root(lua_State* L) {
|
||||
lua_pushstring(L, kAppRootKey);
|
||||
lua_gettable(L, LUA_REGISTRYINDEX); // registry[kAppRootKey]
|
||||
const char* s = lua_tostring(L, -1);
|
||||
String out = s ? String(s) : String("");
|
||||
lua_pop(L, 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void push_searcher_error(lua_State* L, const String& msg) {
|
||||
// Match Lua's style: searchers return a string starting with "\n\t..."
|
||||
String m = "\n\t";
|
||||
m += msg;
|
||||
lua_pushstring(L, m.c_str());
|
||||
}
|
||||
|
||||
static int l_cardstock_searcher(lua_State* L) {
|
||||
const char* modname = luaL_checkstring(L, 1);
|
||||
if (contains_path_traversal(modname)) {
|
||||
push_searcher_error(L, String("invalid module name: ") + modname);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const String rel = module_name_to_rel_path(modname);
|
||||
if (!rel.length()) {
|
||||
push_searcher_error(L, "invalid module name");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const String app_root = get_app_root(L);
|
||||
bool loaded = false;
|
||||
|
||||
// Build candidates.
|
||||
String candidates[4];
|
||||
int n = 0;
|
||||
|
||||
if (app_root.length()) {
|
||||
candidates[n++] = app_root + "/" + rel + ".lua";
|
||||
candidates[n++] = app_root + "/" + rel + "/init.lua";
|
||||
}
|
||||
|
||||
candidates[n++] = String(kSysLibRoot) + "/" + rel + ".lua";
|
||||
candidates[n++] = String(kSysLibRoot) + "/" + rel + "/init.lua";
|
||||
|
||||
// Try candidates in order.
|
||||
for (int i = 0; i < n; i++) {
|
||||
const String& p = candidates[i];
|
||||
String err;
|
||||
std::unique_ptr<uint8_t[]> buf;
|
||||
size_t len = 0;
|
||||
|
||||
// Load full file into chunk.
|
||||
if (!read_entire_file(p, buf, len, err)) continue;
|
||||
|
||||
int rc = luaL_loadbuffer(L, reinterpret_cast<const char*>(buf.get()), len, p.c_str());
|
||||
if (rc != LUA_OK) {
|
||||
const char* emsg = lua_tostring(L, -1);
|
||||
String m = "load error in ";
|
||||
m += p;
|
||||
m += ": ";
|
||||
m += emsg ? emsg : "(unknown)";
|
||||
lua_pop(L, 1);
|
||||
push_searcher_error(L, m);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_pushstring(L, normalize_abs_path(p).c_str());
|
||||
loaded = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (loaded) return 2;
|
||||
|
||||
// Not found. Provide a single aggregated message.
|
||||
String msg = "module '";
|
||||
msg += modname;
|
||||
msg += "' not found (searched ";
|
||||
if (app_root.length()) {
|
||||
msg += app_root;
|
||||
msg += " and ";
|
||||
}
|
||||
msg += kSysLibRoot;
|
||||
msg += ")";
|
||||
push_searcher_error(L, msg);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void insert_searcher(lua_State* L) {
|
||||
// package.searchers is a table (Lua 5.2+).
|
||||
lua_getglobal(L, "package");
|
||||
if (!lua_istable(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
lua_getfield(L, -1, "searchers");
|
||||
if (!lua_istable(L, -1)) {
|
||||
lua_pop(L, 2);
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert at index 2 (after preload searcher).
|
||||
const int insert_at = 2;
|
||||
int len = static_cast<int>(luaL_len(L, -1));
|
||||
for (int i = len + 1; i > insert_at; --i) {
|
||||
lua_rawgeti(L, -1, i - 1);
|
||||
lua_rawseti(L, -2, i);
|
||||
}
|
||||
|
||||
lua_pushcfunction(L, l_cardstock_searcher);
|
||||
lua_rawseti(L, -2, insert_at);
|
||||
|
||||
lua_pop(L, 2); // pop searchers, package
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void lua_cardstock_install_require(lua_State* L) {
|
||||
if (!L) return;
|
||||
insert_searcher(L);
|
||||
}
|
||||
|
||||
void lua_cardstock_set_app_root(lua_State* L, const char* app_root) {
|
||||
if (!L) return;
|
||||
String root = app_root ? String(app_root) : String("");
|
||||
root = normalize_abs_path(root);
|
||||
lua_pushstring(L, kAppRootKey);
|
||||
lua_pushstring(L, root.c_str());
|
||||
lua_settable(L, LUA_REGISTRYINDEX);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
// Custom SD-backed module loader ("require") for Cardstock.
|
||||
//
|
||||
// Installs a Lua `package.searcher` that loads modules from:
|
||||
// 1) <app_root>/<module>.lua and <app_root>/<module>/init.lua
|
||||
// 2) /syslib/<module>.lua and /syslib/<module>/init.lua
|
||||
//
|
||||
// The app root is set by the host (C++) per loaded entrypoint.
|
||||
|
||||
#include "lua.hpp"
|
||||
|
||||
// Install the Cardstock SD-backed searcher into `package.searchers`.
|
||||
void lua_cardstock_install_require(lua_State* L);
|
||||
|
||||
// Set the current app root used by the searcher (absolute, e.g. "/apps/foo").
|
||||
// Pass "" or nullptr to disable app-scoped lookup.
|
||||
void lua_cardstock_set_app_root(lua_State* L, const char* app_root);
|
||||
|
||||
|
||||
+45
-6
@@ -26,13 +26,15 @@ extern "C" {
|
||||
|
||||
#include "M5Cardputer.h"
|
||||
#include "lua/bindings/lua_gfx.h"
|
||||
#include "lua/require_sd.h"
|
||||
#include "lua/bindings/lua_keyboard.h"
|
||||
|
||||
// -------------------------------
|
||||
// Build-time configuration knobs
|
||||
// -------------------------------
|
||||
// Default Lua entrypoint on SD. Override with: -DCARDSTOCK_LUA_ENTRY=\"/my.lua\"
|
||||
#ifndef CARDSTOCK_LUA_ENTRY
|
||||
#define CARDSTOCK_LUA_ENTRY "/app.lua"
|
||||
#define CARDSTOCK_LUA_ENTRY "/launcher/main.lua"
|
||||
#endif
|
||||
|
||||
// SD init pins. If you don't define these, the code falls back to SD.begin(CS).
|
||||
@@ -57,6 +59,7 @@ struct LuaHost {
|
||||
String pending_path;
|
||||
bool reload_requested = false;
|
||||
uint32_t last_ms = 0;
|
||||
String app_root;
|
||||
};
|
||||
|
||||
static LuaHost g_host;
|
||||
@@ -100,22 +103,25 @@ static bool init_sd_card() {
|
||||
}
|
||||
|
||||
static bool read_entire_file_from_sd(const String& path, std::unique_ptr<uint8_t[]>& out_buf, size_t& out_len, String& out_err) {
|
||||
File f = SD.open(path.c_str(), FILE_READ);
|
||||
// ESP32 SD/FS paths are effectively absolute (no working-directory semantics).
|
||||
// Root files often work without a leading '/', but subdirectories commonly won't.
|
||||
const String normalized = (path.length() && path[0] == '/') ? path : (String("/") + path);
|
||||
File f = SD.open(normalized.c_str(), FILE_READ);
|
||||
if (!f) {
|
||||
out_err = "SD.open failed: " + path;
|
||||
out_err = "SD.open failed: " + normalized;
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t sz = static_cast<size_t>(f.size());
|
||||
if (sz == 0) {
|
||||
out_err = "Empty file: " + path;
|
||||
out_err = "Empty file: " + normalized;
|
||||
f.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<uint8_t[]> buf(new (std::nothrow) uint8_t[sz]);
|
||||
if (!buf) {
|
||||
out_err = "OOM reading: " + path;
|
||||
out_err = "OOM reading: " + normalized;
|
||||
f.close();
|
||||
return false;
|
||||
}
|
||||
@@ -129,7 +135,7 @@ static bool read_entire_file_from_sd(const String& path, std::unique_ptr<uint8_t
|
||||
f.close();
|
||||
|
||||
if (read_total != sz) {
|
||||
out_err = "Short read: " + path;
|
||||
out_err = "Short read: " + normalized;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -138,6 +144,32 @@ static bool read_entire_file_from_sd(const String& path, std::unique_ptr<uint8_t
|
||||
return true;
|
||||
}
|
||||
|
||||
static String normalize_abs_path(const String& p) {
|
||||
if (!p.length()) return "";
|
||||
String out = p;
|
||||
if (out[0] != '/') out = String("/") + out;
|
||||
while (out.length() > 1 && out[out.length() - 1] == '/') out.remove(out.length() - 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
static String compute_app_root_for_entrypoint(const String& entry_script_path) {
|
||||
const String p = normalize_abs_path(entry_script_path);
|
||||
|
||||
// Preferred: apps live at /apps/<appID>/...
|
||||
if (p.startsWith("/apps/")) {
|
||||
const int app_id_start = 6; // strlen("/apps/")
|
||||
const int slash_after_app_id = p.indexOf('/', app_id_start);
|
||||
if (slash_after_app_id > app_id_start) {
|
||||
return p.substring(0, slash_after_app_id); // "/apps/<appID>"
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use directory of the entrypoint (useful for launcher scripts too).
|
||||
const int last_slash = p.lastIndexOf('/');
|
||||
if (last_slash > 0) return p.substring(0, last_slash);
|
||||
return "";
|
||||
}
|
||||
|
||||
static int l_print_serial(lua_State* L) {
|
||||
int n = lua_gettop(L);
|
||||
String line;
|
||||
@@ -217,9 +249,16 @@ static bool lua_boot_and_load(LuaHost& host, const String& script_path) {
|
||||
host_set_for_lua(host.L, &host);
|
||||
luaL_openlibs(host.L);
|
||||
|
||||
// Install SD-backed require searcher and set app root scope for this entrypoint.
|
||||
lua_cardstock_install_require(host.L);
|
||||
host.app_root = compute_app_root_for_entrypoint(script_path);
|
||||
lua_cardstock_set_app_root(host.L, host.app_root.c_str());
|
||||
|
||||
// Register built-in modules (Lua: local gfx = require("gfx")).
|
||||
luaL_requiref(host.L, "gfx", luaopen_gfx, 1);
|
||||
lua_pop(host.L, 1); // pop returned module table
|
||||
luaL_requiref(host.L, "keyboard", luaopen_keyboard, 1);
|
||||
lua_pop(host.L, 1); // pop returned module table
|
||||
|
||||
// Override print() to go to Serial (handy on embedded).
|
||||
lua_pushcfunction(host.L, l_print_serial);
|
||||
|
||||
+33
-14
@@ -4,47 +4,66 @@
|
||||
|
||||
namespace GfxService {
|
||||
|
||||
void clear(uint16_t color) {
|
||||
void clear(uint16_t color) {
|
||||
M5Cardputer.Display.fillScreen(color);
|
||||
}
|
||||
}
|
||||
|
||||
void setCursor(int32_t x, int32_t y) {
|
||||
void setCursor(int32_t x, int32_t y) {
|
||||
M5Cardputer.Display.setCursor(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void setTextSize(uint8_t size) {
|
||||
void setTextSize(uint8_t size) {
|
||||
M5Cardputer.Display.setTextSize(size);
|
||||
}
|
||||
}
|
||||
|
||||
void setTextColor(uint16_t fg, int32_t bg) {
|
||||
void setTextColor(uint16_t fg, int32_t bg) {
|
||||
if (bg < 0) {
|
||||
M5Cardputer.Display.setTextColor(fg);
|
||||
} else {
|
||||
M5Cardputer.Display.setTextColor(fg, static_cast<uint16_t>(bg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void print(const char* s) {
|
||||
void print(const char* s) {
|
||||
if (!s) return;
|
||||
M5Cardputer.Display.print(s);
|
||||
}
|
||||
}
|
||||
|
||||
void println(const char* s) {
|
||||
void println(const char* s) {
|
||||
if (!s) {
|
||||
M5Cardputer.Display.println();
|
||||
return;
|
||||
}
|
||||
M5Cardputer.Display.println(s);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t drawString(const char* s, int32_t x, int32_t y, int32_t font) {
|
||||
int32_t drawString(const char* s, int32_t x, int32_t y, int32_t font) {
|
||||
if (!s) return 0;
|
||||
if (font < 0) {
|
||||
return M5Cardputer.Display.drawString(s, x, y);
|
||||
}
|
||||
return M5Cardputer.Display.drawString(s, x, y, font);
|
||||
}
|
||||
}
|
||||
|
||||
void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t color) {
|
||||
M5Cardputer.Display.fillRect(x, y, w, h, color);
|
||||
}
|
||||
|
||||
int32_t width() {
|
||||
return M5Cardputer.Display.width();
|
||||
}
|
||||
|
||||
int32_t height() {
|
||||
return M5Cardputer.Display.height();
|
||||
}
|
||||
|
||||
int32_t drawCenterString(const char* s, int32_t x, int32_t y, int32_t font) {
|
||||
if (!s) return 0;
|
||||
if (font < 0) {
|
||||
return M5Cardputer.Display.drawCenterString(s, x, y);
|
||||
}
|
||||
return M5Cardputer.Display.drawCenterString(s, x, y, font);
|
||||
}
|
||||
} // namespace GfxService
|
||||
|
||||
|
||||
|
||||
@@ -11,10 +11,14 @@ void setTextSize(uint8_t size);
|
||||
void setTextColor(uint16_t fg, int32_t bg = -1); // bg < 0 => don't set background
|
||||
void print(const char* s);
|
||||
void println(const char* s);
|
||||
void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t color = 0x0000 /* BLACK */);
|
||||
|
||||
// Returns the pixel width of the rendered string (per M5GFX/LovyanGFX convention).
|
||||
int32_t drawString(const char* s, int32_t x, int32_t y, int32_t font = -1);
|
||||
int32_t drawCenterString(const char* s, int32_t x, int32_t y, int32_t font = -1);
|
||||
|
||||
int32_t width();
|
||||
int32_t height();
|
||||
} // namespace GfxService
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "KeyboardService.h"
|
||||
|
||||
#include "M5Cardputer.h"
|
||||
|
||||
namespace KeyboardService {
|
||||
bool isChanged() {
|
||||
return M5Cardputer.Keyboard.isChange();
|
||||
}
|
||||
uint8_t isPressed() {
|
||||
return M5Cardputer.Keyboard.isPressed();
|
||||
}
|
||||
bool isKeyPressed(char c) {
|
||||
return M5Cardputer.Keyboard.isKeyPressed(c);
|
||||
}
|
||||
uint8_t getKey(int32_t x, int32_t y) {
|
||||
Point2D_t keyCoord;
|
||||
keyCoord.x = static_cast<int>(x);
|
||||
keyCoord.y = static_cast<int>(y);
|
||||
return M5Cardputer.Keyboard.getKey(keyCoord);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace KeyboardService {
|
||||
bool isChanged();
|
||||
uint8_t isPressed(); // returns number of pressed keys
|
||||
bool isKeyPressed(char c); // returns true if the key is pressed
|
||||
uint8_t getKey(int32_t x, int32_t y); // returns the key code of the pressed key at (x,y)
|
||||
}
|
||||
Reference in New Issue
Block a user