feat: Lua app firmware for the E32R40T display module

Boots to a launcher that lists /apps/<name>/main.lua on the SD card and runs
the selected app in a vendored Lua 5.4 with gui, input, fs, sys and log
bindings. Settings persist as a Lua table in /settings.lua, covering touch
calibration and screen rotation, with a settings app to edit both. Rotation is
applied after mapping raw touch into the panel's rotation-0 frame, so turning
the UI never invalidates a calibration.
This commit is contained in:
2026-07-31 21:28:11 -04:00
commit ad396d3f01
75 changed files with 29804 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
#include <SD.h>
#include <SPI.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>
#include "lua/lua_app.h"
#include "settings.h"
// SD card is on a separate bus from the display/touch (board schematic)
static constexpr int SD_CS = 5;
static constexpr int SD_SCK = 18;
static constexpr int SD_MOSI = 23;
static constexpr int SD_MISO = 19;
static constexpr int TOUCH_CS = 33;
TFT_eSPI tft;
SPIClass touchSpi(HSPI);
SPIClass& sdSpi = SPI;
XPT2046_Touchscreen touch(TOUCH_CS);
LuaApp app(tft, touch);
bool sdOk = false;
String appPath;
void drawLauncher() {
tft.fillScreen(TFT_WHITE);
tft.setTextColor(TFT_BLACK, TFT_WHITE);
tft.drawString("esp32-lcd - tap an app", 10, 10);
File dir = SD.open("/apps");
int y = 40;
while (File entry = dir.openNextFile()) {
if (entry.isDirectory() && entry.name()[0] != '.') {
tft.drawRect(10, y - 4, tft.width() - 20, 28, TFT_BLACK);
tft.drawString(entry.name(), 18, y);
y += 36;
}
entry.close();
}
dir.close();
}
// ponytail: first N rows of 36px = first N app dirs, in SD order. A scrollable
// menu widget can come later; this is a launcher, not an OS.
void launcherTouch() {
if (!touch.touched()) return;
TS_Point p = touch.getPoint();
int16_t tx, ty;
LuaApp::mapTouch(tft, p, tx, ty);
int idx = (ty - 40) / 36;
if (idx < 0) return;
File dir = SD.open("/apps");
int i = 0;
while (File entry = dir.openNextFile()) {
if (entry.isDirectory() && entry.name()[0] != '.') {
if (i++ == idx) {
appPath = String("/apps/") + entry.name() + "/main.lua";
entry.close();
break;
}
}
entry.close();
}
dir.close();
}
void setup() {
Serial.begin(115200);
tft.begin();
tft.setRotation(0);
tft.fillScreen(TFT_WHITE);
touchSpi.begin(14, 12, 13, TOUCH_CS);
touch.begin(touchSpi);
sdSpi.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS, sdSpi)) {
tft.setTextColor(TFT_RED, TFT_WHITE);
tft.drawString("SD card mount failed", 10, 10);
Serial.println("SD mount failed");
return;
}
sdOk = true;
settings.load(); // absent file keeps the built-in touch defaults
tft.setRotation(settings.rotationIndex());
drawLauncher();
Serial.println("launcher ready");
}
void loop() {
if (!sdOk) return; // SD failed to mount; error already drawn
if (app.running()) {
app.loop();
if (!app.running()) {
appPath = "";
tft.setRotation(settings.rotationIndex()); // apps may have rotated the frame
drawLauncher();
Serial.println("launcher ready");
}
return;
}
if (appPath.length() > 0) {
String path = appPath;
appPath = "";
Serial.printf("launching %s\n", path.c_str());
app.load(path.c_str());
return;
}
launcherTouch();
delay(10);
}