mirror of
https://github.com/wisplite/cardstock.git
synced 2026-06-27 22:17:07 -05:00
Initial commit
This commit is contained in:
+329
@@ -0,0 +1,329 @@
|
||||
#include <Arduino.h>
|
||||
// In some C++ modes/toolchains, LLONG_MAX/ULLONG_MAX are not exposed unless
|
||||
// <climits> is included (Lua uses LLONG_MAX as a proxy for long long support).
|
||||
#include <climits>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
|
||||
// Last-resort fallback: some strict modes may still not define LLONG_MAX.
|
||||
// GCC/Clang typically provide __LONG_LONG_MAX__.
|
||||
#if !defined(LLONG_MAX) && defined(__LONG_LONG_MAX__)
|
||||
#define LLONG_MAX __LONG_LONG_MAX__
|
||||
#define LLONG_MIN (-LLONG_MAX - 1LL)
|
||||
#endif
|
||||
#if !defined(ULLONG_MAX) && defined(LLONG_MAX)
|
||||
#define ULLONG_MAX (static_cast<unsigned long long>(LLONG_MAX) * 2ULL + 1ULL)
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
#include "lua.h"
|
||||
#include "lauxlib.h"
|
||||
#include "lualib.h"
|
||||
}
|
||||
|
||||
#include <SPI.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include "M5Cardputer.h"
|
||||
#include "lua/bindings/lua_gfx.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"
|
||||
#endif
|
||||
|
||||
// SD init pins. If you don't define these, the code falls back to SD.begin(CS).
|
||||
// Override with (example):
|
||||
// -DCARDSTOCK_SD_CS=4 -DCARDSTOCK_SD_SCK=40 -DCARDSTOCK_SD_MISO=39 -DCARDSTOCK_SD_MOSI=41
|
||||
#ifndef CARDSTOCK_SD_CS
|
||||
#define CARDSTOCK_SD_CS SS
|
||||
#endif
|
||||
|
||||
// SPI frequency for SD (Hz). Many cards are happy at 20-40MHz; drop if unstable.
|
||||
#ifndef CARDSTOCK_SD_FREQ_HZ
|
||||
#define CARDSTOCK_SD_FREQ_HZ 20000000u
|
||||
#endif
|
||||
|
||||
// -------------------------------
|
||||
// Tiny Lua host runtime
|
||||
// -------------------------------
|
||||
|
||||
struct LuaHost {
|
||||
lua_State* L = nullptr;
|
||||
String current_path;
|
||||
String pending_path;
|
||||
bool reload_requested = false;
|
||||
uint32_t last_ms = 0;
|
||||
};
|
||||
|
||||
static LuaHost g_host;
|
||||
|
||||
static void* host_from_lua(lua_State* L) {
|
||||
// Store LuaHost* in the Lua "extra space" (Lua 5.4 feature).
|
||||
void** p = reinterpret_cast<void**>(lua_getextraspace(L));
|
||||
return p ? *p : nullptr;
|
||||
}
|
||||
|
||||
static void host_set_for_lua(lua_State* L, LuaHost* host) {
|
||||
void** p = reinterpret_cast<void**>(lua_getextraspace(L));
|
||||
if (p) *p = host;
|
||||
}
|
||||
|
||||
static void ui_status(const String& line1, const String& line2 = "") {
|
||||
// Simple status overlay for early bring-up.
|
||||
M5Cardputer.Display.fillScreen(BLACK);
|
||||
M5Cardputer.Display.setTextSize(1);
|
||||
M5Cardputer.Display.setCursor(0, 0);
|
||||
M5Cardputer.Display.setTextColor(WHITE, BLACK);
|
||||
M5Cardputer.Display.println(line1);
|
||||
if (line2.length()) M5Cardputer.Display.println(line2);
|
||||
}
|
||||
|
||||
static void log_line(const String& s) {
|
||||
Serial.println(s);
|
||||
}
|
||||
|
||||
static bool init_sd_card() {
|
||||
#if defined(CARDSTOCK_SD_SCK) && defined(CARDSTOCK_SD_MISO) && defined(CARDSTOCK_SD_MOSI)
|
||||
SPI.begin(CARDSTOCK_SD_SCK, CARDSTOCK_SD_MISO, CARDSTOCK_SD_MOSI, CARDSTOCK_SD_CS);
|
||||
if (!SD.begin(CARDSTOCK_SD_CS, SPI, CARDSTOCK_SD_FREQ_HZ)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
// Fall back to default SPI pins for the selected board definition.
|
||||
return SD.begin(CARDSTOCK_SD_CS);
|
||||
#endif
|
||||
}
|
||||
|
||||
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);
|
||||
if (!f) {
|
||||
out_err = "SD.open failed: " + path;
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t sz = static_cast<size_t>(f.size());
|
||||
if (sz == 0) {
|
||||
out_err = "Empty file: " + path;
|
||||
f.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<uint8_t[]> buf(new (std::nothrow) uint8_t[sz]);
|
||||
if (!buf) {
|
||||
out_err = "OOM reading: " + path;
|
||||
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: " + path;
|
||||
return false;
|
||||
}
|
||||
|
||||
out_buf = std::move(buf);
|
||||
out_len = sz;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int l_print_serial(lua_State* L) {
|
||||
int n = lua_gettop(L);
|
||||
String line;
|
||||
for (int i = 1; i <= n; i++) {
|
||||
size_t len = 0;
|
||||
const char* s = luaL_tolstring(L, i, &len); // pushes string
|
||||
if (i > 1) line += "\t";
|
||||
if (s && len) line += String(s);
|
||||
lua_pop(L, 1); // pop tostring result
|
||||
}
|
||||
Serial.println(line);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_switch_app(lua_State* L) {
|
||||
const char* p = luaL_checkstring(L, 1);
|
||||
LuaHost* host = reinterpret_cast<LuaHost*>(host_from_lua(L));
|
||||
if (!host) return 0;
|
||||
|
||||
host->pending_path = String(p);
|
||||
host->reload_requested = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void lua_report_top_error(lua_State* L, const char* prefix) {
|
||||
const char* msg = lua_tostring(L, -1);
|
||||
String s = prefix;
|
||||
s += msg ? msg : "(unknown lua error)";
|
||||
log_line(s);
|
||||
ui_status("Lua error", s);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
static bool lua_call_optional(lua_State* L, const char* fn_name, int nargs, int nresults) {
|
||||
// Stack on entry: ... [args already pushed if nargs>0]
|
||||
// We'll move them to after the function if needed.
|
||||
lua_getglobal(L, fn_name);
|
||||
if (!lua_isfunction(L, -1)) {
|
||||
lua_pop(L, 1); // not a function
|
||||
if (nargs) lua_pop(L, nargs); // drop args if caller pushed them
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nargs > 0) {
|
||||
// Function is on top; args are below it. Rotate so stack is ... func arg1..argN
|
||||
lua_insert(L, -(nargs + 1));
|
||||
}
|
||||
|
||||
if (lua_pcall(L, nargs, nresults, 0) != LUA_OK) {
|
||||
String prefix = "pcall ";
|
||||
prefix += fn_name;
|
||||
prefix += ": ";
|
||||
lua_report_top_error(L, prefix.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void lua_close_state(LuaHost& host) {
|
||||
if (host.L) {
|
||||
lua_close(host.L);
|
||||
host.L = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static bool lua_boot_and_load(LuaHost& host, const String& script_path) {
|
||||
lua_close_state(host);
|
||||
|
||||
host.L = luaL_newstate();
|
||||
if (!host.L) {
|
||||
ui_status("Lua", "luaL_newstate failed");
|
||||
log_line("luaL_newstate failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
host_set_for_lua(host.L, &host);
|
||||
luaL_openlibs(host.L);
|
||||
|
||||
// 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
|
||||
|
||||
// Override print() to go to Serial (handy on embedded).
|
||||
lua_pushcfunction(host.L, l_print_serial);
|
||||
lua_setglobal(host.L, "print");
|
||||
|
||||
// Expose application switching API.
|
||||
lua_pushcfunction(host.L, l_switch_app);
|
||||
lua_setglobal(host.L, "switch_app");
|
||||
|
||||
std::unique_ptr<uint8_t[]> buf;
|
||||
size_t len = 0;
|
||||
String err;
|
||||
if (!read_entire_file_from_sd(script_path, buf, len, err)) {
|
||||
ui_status("SD read failed", err);
|
||||
log_line(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
int rc = luaL_loadbuffer(host.L, reinterpret_cast<const char*>(buf.get()), len, script_path.c_str());
|
||||
if (rc != LUA_OK) {
|
||||
lua_report_top_error(host.L, "load: ");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lua_pcall(host.L, 0, 0, 0) != LUA_OK) {
|
||||
lua_report_top_error(host.L, "run: ");
|
||||
return false;
|
||||
}
|
||||
|
||||
host.current_path = script_path;
|
||||
host.pending_path = "";
|
||||
host.reload_requested = false;
|
||||
|
||||
ui_status("Lua loaded", host.current_path);
|
||||
|
||||
// Call init() once if present.
|
||||
if (!lua_call_optional(host.L, "init", 0, 0)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(50);
|
||||
|
||||
auto cfg = M5.config();
|
||||
M5Cardputer.begin(cfg);
|
||||
M5Cardputer.Display.setRotation(1);
|
||||
ui_status("cardstock", "booting...");
|
||||
|
||||
if (!init_sd_card()) {
|
||||
ui_status("SD init failed",
|
||||
"Define CARDSTOCK_SD_* pins or check wiring");
|
||||
log_line("SD init failed. If you're on Cardputer, define CARDSTOCK_SD_CS/SCK/MISO/MOSI in build_flags.");
|
||||
return;
|
||||
}
|
||||
|
||||
ui_status("SD OK", String("Loading ") + CARDSTOCK_LUA_ENTRY);
|
||||
g_host.last_ms = millis();
|
||||
lua_boot_and_load(g_host, String(CARDSTOCK_LUA_ENTRY));
|
||||
}
|
||||
|
||||
void loop() {
|
||||
M5Cardputer.update();
|
||||
|
||||
// If SD failed in setup, nothing to do.
|
||||
if (!g_host.L) {
|
||||
delay(250);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = millis();
|
||||
float dt = (now - g_host.last_ms) / 1000.0f;
|
||||
g_host.last_ms = now;
|
||||
|
||||
// tick(dt)
|
||||
lua_pushnumber(g_host.L, dt);
|
||||
if (!lua_call_optional(g_host.L, "tick", 1, 0)) {
|
||||
// On error, keep Lua alive so user can see the message. (They can switch apps via reset.)
|
||||
delay(250);
|
||||
return;
|
||||
}
|
||||
|
||||
// draw()
|
||||
if (!lua_call_optional(g_host.L, "draw", 0, 0)) {
|
||||
delay(250);
|
||||
return;
|
||||
}
|
||||
|
||||
// If Lua requested a new script, reload cleanly between frames.
|
||||
if (g_host.reload_requested) {
|
||||
String next = g_host.pending_path;
|
||||
if (!next.length()) {
|
||||
// If someone passed "", just ignore.
|
||||
g_host.reload_requested = false;
|
||||
} else {
|
||||
ui_status("Switching to", next);
|
||||
log_line(String("switch_app -> ") + next);
|
||||
lua_boot_and_load(g_host, next);
|
||||
// Note: lua_boot_and_load updates g_host fields.
|
||||
}
|
||||
}
|
||||
|
||||
// Keep loop responsive; adjust later if you add a fixed framerate.
|
||||
delay(1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user