initial commit

This commit is contained in:
2026-08-03 16:09:07 -04:00
commit 95fa512047
109 changed files with 35023 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
// @lua-module ble BleLib
// @lua-preamble ---@class BleDevice
// @lua-preamble ---@field name string
// @lua-preamble ---@field address string
// @lua-preamble ---@field rssi integer
#include "../helpers.h"
namespace esp32lua {
namespace bindings {
namespace {
int init(lua_State* state) {
std::string name;
const bool hasName = optionalString(state, 1, name);
return pushStatus(state, Runtime::from(state)->ble().init(hasName ? &name : nullptr));
}
int deinit(lua_State* state) {
Runtime::from(state)->ble().deinit();
return 0;
}
int scan(lua_State* state) {
const int32_t durationMs = optionalInt(state, 1, 3000);
luaL_argcheck(state, durationMs > 0, 1, "must be positive");
std::vector<BleDevice> devices;
const Status status = Runtime::from(state)->ble().scan(durationMs, devices);
if (!status.ok) return pushError(state, status.error);
lua_createtable(state, static_cast<int>(devices.size()), 0);
for (size_t at = 0; at < devices.size(); at++) {
lua_createtable(state, 0, 3);
setField(state, "name", devices[at].name);
setField(state, "address", devices[at].address);
setField(state, "rssi", devices[at].rssi);
lua_rawseti(state, -2, static_cast<lua_Integer>(at + 1));
}
return 1;
}
int connect(lua_State* state) {
const std::string address = checkString(state, 1);
return pushStatus(state, Runtime::from(state)->ble().connect(address));
}
int disconnect(lua_State* state) {
Runtime::from(state)->ble().disconnect();
return 0;
}
int isConnected(lua_State* state) {
lua_pushboolean(state, Runtime::from(state)->ble().isConnected());
return 1;
}
int read(lua_State* state) {
const std::string service = checkString(state, 1);
const std::string characteristic = checkString(state, 2);
std::string value;
const Status status = Runtime::from(state)->ble().read(service, characteristic, value);
if (!status.ok) return pushError(state, status.error);
pushString(state, value);
return 1;
}
int write(lua_State* state) {
const std::string service = checkString(state, 1);
const std::string characteristic = checkString(state, 2);
const std::string value = checkString(state, 3);
return pushStatus(state, Runtime::from(state)->ble().write(service, characteristic, value));
}
int startAdvertising(lua_State* state) {
std::string name;
const bool hasName = optionalString(state, 1, name);
return pushStatus(state, Runtime::from(state)->ble().startAdvertising(hasName ? &name : nullptr));
}
int stopAdvertising(lua_State* state) {
Runtime::from(state)->ble().stopAdvertising();
return 0;
}
const luaL_Reg FUNCTIONS[] = {
// --- Starts the BLE stack.
// @param name string|nil Local device name.
// @return true|nil ok
// @return string|nil error
{"init", init},
// --- Stops the BLE stack and releases its memory.
{"deinit", deinit},
// --- Scans for advertising devices.
// @param durationMs integer|nil Defaults to 3000.
// @return BleDevice[]|nil devices
// @return string|nil error
{"scan", scan},
// --- Connects to a peripheral.
// @param address string
// @return true|nil ok
// @return string|nil error
{"connect", connect},
// --- Disconnects from the connected peripheral.
{"disconnect", disconnect},
// --- Whether a peripheral is connected.
// @return boolean
{"isConnected", isConnected},
// --- Reads a characteristic value.
// @param serviceUuid string
// @param characteristicUuid string
// @return string|nil value
// @return string|nil error
{"read", read},
// --- Writes a characteristic value.
// @param serviceUuid string
// @param characteristicUuid string
// @param value string
// @return true|nil ok
// @return string|nil error
{"write", write},
// --- Starts advertising.
// @param name string|nil Advertised device name.
// @return true|nil ok
// @return string|nil error
{"startAdvertising", startAdvertising},
// --- Stops advertising.
{"stopAdvertising", stopAdvertising},
{nullptr, nullptr},
};
} // namespace
void registerBle(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "ble");
}
} // namespace bindings
} // namespace esp32lua
+182
View File
@@ -0,0 +1,182 @@
// @lua-module fs FsLib
// @lua-const MAX_READ_BYTES integer 65536 Largest portable whole-file or line read.
#include "../helpers.h"
namespace esp32lua {
namespace bindings {
namespace {
constexpr int32_t MAX_READ_BYTES = 65536;
int32_t checkReadLimit(lua_State* state, int index) {
const int32_t maxBytes = checkInt(state, index);
luaL_argcheck(state, maxBytes >= 0 && maxBytes <= MAX_READ_BYTES, index, "exceeds fs.MAX_READ_BYTES");
return maxBytes;
}
int exists(lua_State* state) {
const std::string path = checkString(state, 1);
lua_pushboolean(state, Runtime::from(state)->fs().exists(path));
return 1;
}
int fileSize(lua_State* state) {
const std::string path = checkString(state, 1);
int32_t size = 0;
const Status status = Runtime::from(state)->fs().fileSize(path, size);
if (!status.ok) return pushError(state, status.error);
lua_pushinteger(state, size);
return 1;
}
int list(lua_State* state, bool directories) {
const std::string path = checkString(state, 1);
std::vector<std::string> names;
const FsProvider& fs = Runtime::from(state)->fs();
const Status status = directories ? fs.listDirs(path, names) : fs.listFiles(path, names);
if (!status.ok) return pushError(state, status.error);
pushStrings(state, names);
return 1;
}
int listDirs(lua_State* state) { return list(state, true); }
int listFiles(lua_State* state) { return list(state, false); }
int mkdir(lua_State* state) {
const std::string path = checkString(state, 1);
return pushStatus(state, Runtime::from(state)->fs().mkdir(path));
}
int readFile(lua_State* state) {
const int32_t maxBytes = checkReadLimit(state, 2);
const std::string path = checkString(state, 1);
std::string content;
const Status status = Runtime::from(state)->fs().readFile(path, maxBytes, content);
if (!status.ok) return pushError(state, status.error);
pushString(state, content);
return 1;
}
int readLineAt(lua_State* state) {
const int32_t offset = checkInt(state, 2);
const int32_t maxBytes = checkReadLimit(state, 3);
luaL_argcheck(state, offset >= 0, 2, "must not be negative");
const std::string path = checkString(state, 1);
bool found = false;
int32_t nextOffset = 0;
std::string line;
const Status status = Runtime::from(state)->fs().readLineAt(path, offset, maxBytes, found, line, nextOffset);
if (!status.ok) {
lua_pushnil(state);
lua_pushnil(state);
pushString(state, status.error);
return 3;
}
if (!found) {
lua_pushnil(state);
return 1;
}
pushString(state, line);
lua_pushinteger(state, nextOffset);
return 2;
}
int remove(lua_State* state) {
const std::string path = checkString(state, 1);
return pushStatus(state, Runtime::from(state)->fs().remove(path));
}
int removeTree(lua_State* state) {
const std::string path = checkString(state, 1);
return pushStatus(state, Runtime::from(state)->fs().removeTree(path));
}
int rename(lua_State* state) {
const std::string source = checkString(state, 1);
const std::string destination = checkString(state, 2);
return pushStatus(state, Runtime::from(state)->fs().rename(source, destination));
}
int writeFile(lua_State* state) {
const std::string path = checkString(state, 1);
const std::string content = checkString(state, 2);
return pushStatus(state, Runtime::from(state)->fs().writeFile(path, content));
}
const luaL_Reg FUNCTIONS[] = {
// --- Whether a path exists.
// @param path string Absolute SD-card path; traversal components are rejected.
// @return boolean
{"exists", exists},
// --- Returns the size of a file in bytes.
// @param path string Absolute SD-card path.
// @return integer|nil size
// @return string|nil error Present when the path is missing or not a file.
{"fileSize", fileSize},
// --- Lists the directories in a directory.
// @param path string Absolute directory path.
// @return string[]|nil names Sorted names, excluding hidden entries.
// @return string|nil error
{"listDirs", listDirs},
// --- Lists the files in a directory.
// @param path string Absolute directory path.
// @return string[]|nil names Sorted names, excluding hidden entries.
// @return string|nil error
{"listFiles", listFiles},
// --- Creates a directory.
// @param path string Absolute directory path.
// @return true|nil ok
// @return string|nil error
{"mkdir", mkdir},
// --- Reads a whole file.
// @param path string Absolute file path.
// @param maxBytes integer Maximum bytes to allocate, up to fs.MAX_READ_BYTES; oversized files fail rather than truncate.
// @return string|nil content
// @return string|nil error
{"readFile", readFile},
// --- Reads one line starting at a byte offset.
// @param path string Absolute file path.
// @param offset integer Zero-based byte offset; a mid-line offset advances to the next line.
// @param maxBytes integer Maximum line bytes to allocate, up to fs.MAX_READ_BYTES.
// @return string|nil line Nil at end of file or on failure.
// @return integer|nil nextOffset Byte offset of the following line.
// @return string|nil error Present when the file cannot be read or the line exceeds maxBytes.
{"readLineAt", readLineAt},
// --- Removes a file.
// @param path string Absolute file path. Firmware-protected roots cannot be removed.
// @return true|nil ok
// @return string|nil error
{"remove", remove},
// --- Removes a directory and everything below it.
// @param path string Absolute directory path. Firmware-protected roots cannot be removed.
// @return true|nil ok
// @return string|nil error
{"removeTree", removeTree},
// --- Renames a file or directory.
// @param source string Absolute source path.
// @param destination string Absolute destination path, which must not exist.
// @return true|nil ok
// @return string|nil error
{"rename", rename},
// --- Atomically replaces the destination or leaves its previous contents intact.
// @param path string Absolute file path.
// @param content string
// @return true|nil ok
// @return string|nil error
{"writeFile", writeFile},
{nullptr, nullptr},
};
} // namespace
void registerFs(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_pushinteger(state, MAX_READ_BYTES);
lua_setfield(state, -2, "MAX_READ_BYTES");
lua_setglobal(state, "fs");
}
} // namespace bindings
} // namespace esp32lua
+327
View File
@@ -0,0 +1,327 @@
// @lua-module gui GuiLib
// @lua-preamble ---@alias GuiColor integer
// @lua-preamble ---@alias GuiFont integer
// @lua-preamble ---@alias GuiTextStyle integer
// @lua-const FONT_SMALL GuiFont 0 Small auxiliary text.
// @lua-const FONT_UI GuiFont 0 Normal controls and labels.
// @lua-const FONT_BODY GuiFont 0 Normal reading text.
// @lua-const FONT_LARGE GuiFont 0 Headings and prominent values.
// @lua-const STYLE_NORMAL GuiTextStyle 0
// @lua-const STYLE_BOLD GuiTextStyle 0
#include "../helpers.h"
namespace esp32lua {
namespace bindings {
namespace {
GuiProvider& provider(lua_State* state) { return Runtime::from(state)->gui(); }
int getWidth(lua_State* state) {
lua_pushinteger(state, provider(state).width());
return 1;
}
int getHeight(lua_State* state) {
lua_pushinteger(state, provider(state).height());
return 1;
}
int getRotation(lua_State* state) {
lua_pushinteger(state, provider(state).rotation());
return 1;
}
int setRotation(lua_State* state) {
const int32_t degrees = checkInt(state, 1);
luaL_argcheck(state, degrees >= 0 && degrees <= 270 && degrees % 90 == 0, 1, "expected 0, 90, 180, or 270");
provider(state).setRotation(degrees);
return 0;
}
int color(lua_State* state) {
const int32_t r = checkInt(state, 1);
const int32_t g = checkInt(state, 2);
const int32_t b = checkInt(state, 3);
luaL_argcheck(state, r >= 0 && r <= 255, 1, "expected 0 through 255");
luaL_argcheck(state, g >= 0 && g <= 255, 2, "expected 0 through 255");
luaL_argcheck(state, b >= 0 && b <= 255, 3, "expected 0 through 255");
lua_pushinteger(state, provider(state).color(r, g, b));
return 1;
}
int clear(lua_State* state) {
GuiProvider& gui = provider(state);
int32_t fill = gui.color(255, 255, 255);
optionalColor(state, 1, fill);
gui.clear(fill);
return 0;
}
int fillRect(lua_State* state) {
provider(state).fillRect(checkInt(state, 1), checkInt(state, 2), checkInt(state, 3), checkInt(state, 4),
checkInt(state, 5));
return 0;
}
int drawRect(lua_State* state) {
provider(state).drawRect(checkInt(state, 1), checkInt(state, 2), checkInt(state, 3), checkInt(state, 4),
checkInt(state, 5));
return 0;
}
int drawLine(lua_State* state) {
provider(state).drawLine(checkInt(state, 1), checkInt(state, 2), checkInt(state, 3), checkInt(state, 4),
checkInt(state, 5), optionalInt(state, 6, 1));
return 0;
}
int drawPixel(lua_State* state) {
provider(state).drawPixel(checkInt(state, 1), checkInt(state, 2), checkInt(state, 3));
return 0;
}
int drawCircle(lua_State* state) {
provider(state).drawCircle(checkInt(state, 1), checkInt(state, 2), checkInt(state, 3), checkInt(state, 4),
optionalInt(state, 5, 1));
return 0;
}
int fillCircle(lua_State* state) {
const int32_t x = checkInt(state, 1);
const int32_t y = checkInt(state, 2);
const int32_t radius = checkInt(state, 3);
const int32_t fill = checkInt(state, 4);
int32_t background = 0;
const bool hasBackground = optionalColor(state, 5, background);
provider(state).fillCircle(x, y, radius, fill, hasBackground ? &background : nullptr);
return 0;
}
int roundRect(lua_State* state) {
const int32_t x = checkInt(state, 1);
const int32_t y = checkInt(state, 2);
const int32_t w = checkInt(state, 3);
const int32_t h = checkInt(state, 4);
const int32_t radius = checkInt(state, 5);
const int32_t background = checkInt(state, 6);
int32_t top = 0;
int32_t bottom = 0;
int32_t border = 0;
const bool hasTop = optionalColor(state, 7, top);
const bool hasBottom = optionalColor(state, 8, bottom);
const bool hasBorder = optionalColor(state, 9, border);
provider(state).roundRect(x, y, w, h, radius, background, hasTop ? &top : nullptr,
hasBottom ? &bottom : (hasTop ? &top : nullptr), hasBorder ? &border : nullptr);
return 0;
}
int setFullscreen(lua_State* state) {
luaL_checkany(state, 1);
provider(state).setFullscreen(lua_toboolean(state, 1) != 0);
return 0;
}
void readIntegers(lua_State* state, int index, std::vector<int32_t>& out) {
const lua_Integer count = luaL_len(state, index);
for (lua_Integer at = 1; at <= count; at++) {
lua_rawgeti(state, index, at);
if (!lua_isinteger(state, -1)) {
lua_pop(state, 1);
out.clear();
return;
}
out.push_back(static_cast<int32_t>(lua_tointeger(state, -1)));
lua_pop(state, 1);
}
}
int fillPolygon(lua_State* state) {
luaL_checktype(state, 1, LUA_TTABLE);
luaL_checktype(state, 2, LUA_TTABLE);
const int32_t fill = checkInt(state, 3);
// The vectors are destroyed before any Lua error is raised, because luaL_error longjmps.
bool usable = false;
{
std::vector<int32_t> xs;
std::vector<int32_t> ys;
readIntegers(state, 1, xs);
readIntegers(state, 2, ys);
usable = !xs.empty() && xs.size() == ys.size();
if (usable) provider(state).fillPolygon(xs.data(), ys.data(), xs.size(), fill);
}
if (!usable) return luaL_error(state, "expected matching non-empty integer arrays");
return 0;
}
int drawBmp(lua_State* state) {
int32_t values[4] = {0, 0, 0, 0};
bool present[4] = {false, false, false, false};
for (int at = 0; at < 4; at++) present[at] = optionalColor(state, at + 2, values[at]);
const std::string path = checkString(state, 1);
const Status status = provider(state).drawBmp(path, present[0] ? &values[0] : nullptr,
present[1] ? &values[1] : nullptr, present[2] ? &values[2] : nullptr,
present[3] ? &values[3] : nullptr);
return pushStatus(state, status);
}
int getTextWidth(lua_State* state) {
const int32_t font = checkInt(state, 1);
const int32_t style = optionalInt(state, 3, provider(state).fonts().styleNormal);
const std::string text = checkString(state, 2);
lua_pushinteger(state, provider(state).textWidth(font, text, style));
return 1;
}
int getFontHeight(lua_State* state) {
const int32_t font = checkInt(state, 1);
lua_pushinteger(state, provider(state).fontHeight(font, optionalInt(state, 2, provider(state).fonts().styleNormal)));
return 1;
}
int drawText(lua_State* state) {
GuiProvider& gui = provider(state);
const int32_t font = checkInt(state, 1);
const int32_t x = checkInt(state, 2);
const int32_t y = checkInt(state, 3);
int32_t textColor = gui.color(0, 0, 0);
optionalColor(state, 5, textColor);
const int32_t style = optionalInt(state, 6, gui.fonts().styleNormal);
int32_t background = 0;
const bool hasBackground = optionalColor(state, 7, background);
const std::string text = checkString(state, 4);
gui.drawText(font, x, y, text, textColor, style, hasBackground ? &background : nullptr);
return 0;
}
const luaL_Reg FUNCTIONS[] = {
// --- Returns the live frame width.
// @return integer
{"getWidth", getWidth},
// --- Returns the live frame height.
// @return integer
{"getHeight", getHeight},
// --- Rotates the live frame without changing the saved preference.
// @param degrees integer 0, 90, 180, or 270 clockwise.
{"setRotation", setRotation},
// --- Returns the rotation of the live frame.
// @return integer Degrees clockwise for the live frame.
{"getRotation", getRotation},
// --- Returns an opaque native color. E-ink implementations quantize RGB to available grayscale.
// @param r integer 0 through 255.
// @param g integer 0 through 255.
// @param b integer 0 through 255.
// @return GuiColor
{"color", color},
// --- Clears the frame.
// @param color GuiColor|nil Defaults to white.
{"clear", clear},
// --- Fills a rectangle.
// @param x integer
// @param y integer
// @param w integer
// @param h integer
// @param color GuiColor
{"fillRect", fillRect},
// --- Outlines a rectangle.
// @param x integer
// @param y integer
// @param w integer
// @param h integer
// @param color GuiColor
{"drawRect", drawRect},
// --- Draws a line.
// @param x1 integer
// @param y1 integer
// @param x2 integer
// @param y2 integer
// @param color GuiColor
// @param width integer|nil Defaults to one pixel.
{"drawLine", drawLine},
// --- Draws a single pixel.
// @param x integer
// @param y integer
// @param color GuiColor
{"drawPixel", drawPixel},
// --- Outlines a circle.
// @param x integer Center.
// @param y integer Center.
// @param radius integer
// @param color GuiColor
// @param width integer|nil Defaults to one pixel.
{"drawCircle", drawCircle},
// --- Fills a circle.
// @param x integer Center.
// @param y integer Center.
// @param radius integer
// @param color GuiColor
// @param background GuiColor|nil Surface behind an anti-aliased edge.
{"fillCircle", fillCircle},
// ---Draws an anti-aliased rounded fill, optional gradient, and optional border in one pass.
// @param x integer
// @param y integer
// @param w integer
// @param h integer
// @param radius integer
// @param background GuiColor Surface behind the anti-aliased edge.
// @param top GuiColor|nil Fill, or gradient top; omitted for no fill.
// @param bottom GuiColor|nil Gradient bottom; defaults to top. Panels without a gradient use top.
// @param border GuiColor|nil Omitted for no border.
{"roundRect", roundRect},
// ---Temporarily gives the app the full panel, including firmware chrome.
// @param on boolean
{"setFullscreen", setFullscreen},
// --- Fills a polygon.
// @param xs integer[]
// @param ys integer[]
// @param color GuiColor
{"fillPolygon", fillPolygon},
// --- Draws a bitmap.
// @param path string Absolute BMP path.
// @param x integer|nil Left edge; defaults to centered.
// @param y integer|nil Top edge; defaults to centered.
// @param maxWidth integer|nil Defaults to panel width.
// @param maxHeight integer|nil Defaults to panel height.
// @return true|nil ok
// @return string|nil error
{"drawBmp", drawBmp},
// --- Measures a text run.
// @param font GuiFont Use a named gui.FONT_* role.
// @param text string
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
// @return integer
{"getTextWidth", getTextWidth},
// --- Returns the line height of a font role.
// @param font GuiFont Use a named gui.FONT_* role.
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
// @return integer
{"getFontHeight", getFontHeight},
// --- Draws a text run with its top-left corner at x, y.
// @param font GuiFont Use a named gui.FONT_* role.
// @param x integer Left edge.
// @param y integer Top edge.
// @param text string
// @param color GuiColor|nil Defaults to black.
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
// @param background GuiColor|nil Omitted for transparent text.
{"drawText", drawText},
{nullptr, nullptr},
};
} // namespace
void registerGui(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
const FontIds fonts = Runtime::from(state)->gui().fonts();
setField(state, "FONT_SMALL", fonts.small);
setField(state, "FONT_UI", fonts.ui);
setField(state, "FONT_BODY", fonts.body);
setField(state, "FONT_LARGE", fonts.large);
setField(state, "STYLE_NORMAL", fonts.styleNormal);
setField(state, "STYLE_BOLD", fonts.styleBold);
lua_setglobal(state, "gui");
}
} // namespace bindings
} // namespace esp32lua
+186
View File
@@ -0,0 +1,186 @@
// @lua-module http HttpLib
// @lua-preamble ---@class HttpRequestOptions
// @lua-preamble ---@field maxBytes integer Maximum response-body bytes to allocate, up to http.MAX_RESPONSE_BYTES; zero is valid for HEAD.
// @lua-preamble ---@field headers? table<string, string>
// @lua-preamble
// @lua-preamble ---@class HttpResponse
// @lua-preamble ---@field status integer HTTP status code.
// @lua-preamble ---@field body string Response body, including for non-2xx responses.
// @lua-preamble
// @lua-preamble ---@class HttpDownloadOptions
// @lua-preamble ---@field maxBytes integer Required maximum, from 1 through 16777216.
// @lua-preamble ---@field expectedSize? integer Exact expected byte count.
// @lua-preamble ---@field sha256? string Exact expected SHA-256 as 64 hexadecimal characters.
// @lua-const MAX_RESPONSE_BYTES integer 65536 Largest portable in-memory response body.
#include "../helpers.h"
namespace esp32lua {
namespace bindings {
namespace {
constexpr int32_t MAX_RESPONSE_BYTES = 65536;
constexpr int32_t MAX_DOWNLOAD_BYTES = 16777216;
int32_t checkResponseLimit(lua_State* state, int index) {
lua_getfield(state, index, "maxBytes");
const int32_t maxBytes = static_cast<int32_t>(luaL_checkinteger(state, -1));
lua_pop(state, 1);
luaL_argcheck(state, maxBytes >= 0 && maxBytes <= MAX_RESPONSE_BYTES, index,
"maxBytes exceeds http.MAX_RESPONSE_BYTES");
return maxBytes;
}
void readHeaders(lua_State* state, int index, std::vector<HttpHeader>& headers) {
lua_getfield(state, index, "headers");
if (lua_istable(state, -1)) {
lua_pushnil(state);
while (lua_next(state, -2)) {
if (lua_type(state, -2) == LUA_TSTRING && lua_type(state, -1) == LUA_TSTRING) {
HttpHeader header;
header.name = lua_tostring(state, -2);
header.value = lua_tostring(state, -1);
headers.push_back(header);
}
lua_pop(state, 1);
}
}
lua_pop(state, 1);
}
int request(lua_State* state, const char* method, bool withBody) {
const int optionsIndex = withBody ? 3 : 2;
luaL_checktype(state, optionsIndex, LUA_TTABLE);
const int32_t maxBytes = checkResponseLimit(state, optionsIndex);
const std::string url = checkString(state, 1);
const std::string body = withBody ? checkString(state, 2) : std::string();
std::vector<HttpHeader> headers;
readHeaders(state, optionsIndex, headers);
HttpResponse response;
const Status status = Runtime::from(state)->http().request(method, url, body, headers, maxBytes, response);
if (!status.ok) return pushError(state, status.error);
lua_createtable(state, 0, 2);
setField(state, "status", response.status);
setField(state, "body", response.body);
return 1;
}
int get(lua_State* state) { return request(state, "GET", false); }
int head(lua_State* state) { return request(state, "HEAD", false); }
int remove(lua_State* state) { return request(state, "DELETE", false); }
int post(lua_State* state) { return request(state, "POST", true); }
int patch(lua_State* state) { return request(state, "PATCH", true); }
int download(lua_State* state) {
luaL_checktype(state, 3, LUA_TTABLE);
lua_getfield(state, 3, "maxBytes");
const int32_t maxBytes = static_cast<int32_t>(luaL_checkinteger(state, -1));
lua_pop(state, 1);
luaL_argcheck(state, maxBytes >= 1 && maxBytes <= MAX_DOWNLOAD_BYTES, 3, "maxBytes must be 1 through 16777216");
lua_getfield(state, 3, "expectedSize");
const int32_t expectedSize = static_cast<int32_t>(luaL_optinteger(state, -1, 0));
lua_pop(state, 1);
lua_getfield(state, 3, "sha256");
const char* sha256 = lua_isnil(state, -1) ? nullptr : luaL_checkstring(state, -1);
luaL_argcheck(state, !sha256 || lua_rawlen(state, -1) == 64, 3, "sha256 must be 64 hexadecimal characters");
HttpDownload options;
options.maxBytes = maxBytes;
options.expectedSize = expectedSize;
options.sha256 = sha256 ? sha256 : "";
lua_pop(state, 1);
const std::string url = checkString(state, 1);
const std::string destination = checkString(state, 2);
int32_t bytesWritten = 0;
const Status status = Runtime::from(state)->http().download(url, destination, options, bytesWritten);
if (!status.ok) return pushError(state, status.error);
lua_pushinteger(state, bytesWritten);
return 1;
}
int urlencode(lua_State* state) {
static const char* HEX = "0123456789ABCDEF";
size_t length = 0;
const char* input = luaL_checklstring(state, 1, &length);
luaL_Buffer buffer;
luaL_buffinit(state, &buffer);
for (size_t at = 0; at < length; at++) {
const unsigned char c = static_cast<unsigned char>(input[at]);
const bool unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' ||
c == '_' || c == '.' || c == '~';
if (unreserved) {
luaL_addchar(&buffer, static_cast<char>(c));
} else {
luaL_addchar(&buffer, '%');
luaL_addchar(&buffer, HEX[c >> 4]);
luaL_addchar(&buffer, HEX[c & 0x0F]);
}
}
luaL_pushresult(&buffer);
return 1;
}
const luaL_Reg FUNCTIONS[] = {
// --- Performs a GET request.
// @param url string
// @param options HttpRequestOptions
// @return HttpResponse|nil response
// @return string|nil error Transport failure or response body exceeding maxBytes.
{"get", get},
// --- Performs a HEAD request.
// @param url string
// @param options HttpRequestOptions
// @return HttpResponse|nil response Body is empty.
// @return string|nil error
{"head", head},
// --- Performs a DELETE request.
// @param url string
// @param options HttpRequestOptions
// @return HttpResponse|nil response
// @return string|nil error
{"delete", remove},
// --- Performs a POST request.
// @param url string
// @param body string
// @param options HttpRequestOptions
// @return HttpResponse|nil response
// @return string|nil error
{"post", post},
// --- Performs a PATCH request.
// @param url string
// @param body string
// @param options HttpRequestOptions
// @return HttpResponse|nil response
// @return string|nil error
{"patch", patch},
// --- Streams authenticated HTTPS to a new file and removes partial or unverified output.
// @param url string HTTPS URL.
// @param destination string Absolute path which must not exist.
// @param options HttpDownloadOptions
// @return integer|nil bytesWritten
// @return string|nil error
{"download", download},
// --- Percent-encodes a string for use in a URL.
// @param input string
// @return string
{"urlencode", urlencode},
{nullptr, nullptr},
};
} // namespace
void registerHttp(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_pushinteger(state, MAX_RESPONSE_BYTES);
lua_setfield(state, -2, "MAX_RESPONSE_BYTES");
lua_setglobal(state, "http");
}
} // namespace bindings
} // namespace esp32lua
+44
View File
@@ -0,0 +1,44 @@
// @lua-module log LogLib
#include <lua/runtime.h>
extern "C" {
#include "lauxlib.h"
#include "lua.h"
}
namespace esp32lua {
namespace bindings {
namespace {
int write(lua_State* state, LogLevel level) {
Runtime::from(state)->log().write(level, luaL_checkstring(state, 1));
return 0;
}
int debug(lua_State* state) { return write(state, LogLevel::Debug); }
int info(lua_State* state) { return write(state, LogLevel::Info); }
int error(lua_State* state) { return write(state, LogLevel::Error); }
const luaL_Reg FUNCTIONS[] = {
// --- Writes a debug message to the firmware log.
// @param message string
{"debug", debug},
// --- Writes an informational message to the firmware log.
// @param message string
{"info", info},
// --- Writes an error message to the firmware log.
// @param message string
{"error", error},
{nullptr, nullptr},
};
} // namespace
void registerLog(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "log");
}
} // namespace bindings
} // namespace esp32lua
+576
View File
@@ -0,0 +1,576 @@
// @lua-module node NodeLib
// @lua-preamble ---@alias NodeId integer
// @lua-preamble ---@alias NodeType "box"|"text"|"button"|"custom"
// @lua-preamble ---@alias NodeDirection "up"|"down"|"left"|"right"
// @lua-preamble
// @lua-preamble ---@class NodeSpec
// @lua-preamble ---@field type NodeType
// @lua-preamble ---@field w? number|"fill"|"auto"
// @lua-preamble ---@field h? number|"fill"|"auto"
// @lua-preamble ---@field pad? number
// @lua-preamble ---@field gap? number
// @lua-preamble ---@field align? "start"|"center"|"end"|"stretch"
// @lua-preamble ---@field justify? "start"|"center"|"end"|"between"
// @lua-preamble ---@field row? boolean
// @lua-preamble ---@field at? table Absolute-position fields.
// @lua-preamble ---@field capture? boolean
// @lua-preamble ---@field interactive? boolean
// @lua-preamble ---@field label? string
// @lua-preamble ---@field font? GuiFont
// @lua-preamble
// @lua-preamble ---@class NodeStyle
// @lua-preamble ---@field color? GuiColor
// @lua-preamble ---@field background? GuiColor Background offered to descendants.
// @lua-preamble ---@field fill? GuiColor Surface painted by a box.
// @lua-preamble ---@field border? GuiColor
// @lua-preamble ---@field face? GuiColor Default button surface.
// @lua-preamble ---@field pressedFace? GuiColor Pressed button surface.
// @lua-preamble ---@field pressedColor? GuiColor Pressed button text.
// @lua-preamble ---@field focusColor? GuiColor Distinct outline for directional focus.
// @lua-preamble ---@field radius? integer
// @lua-preamble ---@field font? GuiFont
// @lua-preamble ---@field textStyle? GuiTextStyle
#include <cstdlib>
#include "../../node/painter.h"
#include "../helpers.h"
namespace esp32lua {
namespace bindings {
namespace {
const char* const PAINTER_KEY = "esp32lua.node.painter";
ui::Tree& tree(lua_State* state) { return Runtime::from(state)->tree(); }
uint16_t checkNode(lua_State* state, int index) {
const lua_Integer id = luaL_checkinteger(state, index);
luaL_argcheck(state, id >= 0 && static_cast<size_t>(id) < tree(state).nodes.size(), index, "unknown node");
return static_cast<uint16_t>(id);
}
// A number below one is a fraction of the parent, kept as per-mille so the firmware and
// the host round a layout identically.
ui::Size sizeAt(lua_State* state, int index, bool& present) {
present = !lua_isnoneornil(state, index);
ui::Size size;
if (!present) return size;
if (lua_type(state, index) == LUA_TSTRING) {
const char* mode = lua_tostring(state, index);
if (strcmp(mode, "fill") == 0) return ui::Size::fill();
if (strcmp(mode, "auto") == 0) return size;
luaL_error(state, "size must be a number, 'fill', or 'auto'");
return size;
}
const lua_Number value = luaL_checknumber(state, index);
return value > 0.0 && value < 1.0 ? ui::Size::fraction(static_cast<int16_t>(value * 1000.0))
: ui::Size::px(static_cast<int16_t>(value));
}
ui::Size readSize(lua_State* state, int index, const char* key, bool& present) {
lua_getfield(state, index, key);
const ui::Size size = sizeAt(state, lua_gettop(state), present);
lua_pop(state, 1);
return size;
}
int16_t readNumber(lua_State* state, int index, const char* key, int16_t fallback) {
lua_getfield(state, index, key);
const int16_t value = lua_isnoneornil(state, -1) ? fallback : static_cast<int16_t>(luaL_checknumber(state, -1));
lua_pop(state, 1);
return value;
}
bool readFlag(lua_State* state, int index, const char* key) {
lua_getfield(state, index, key);
const bool value = lua_toboolean(state, -1) != 0;
lua_pop(state, 1);
return value;
}
ui::Align readAlign(lua_State* state, int index, const char* key) {
lua_getfield(state, index, key);
ui::Align align = ui::START;
if (!lua_isnoneornil(state, -1)) {
const char* value = luaL_checkstring(state, -1);
if (strcmp(value, "center") == 0) {
align = ui::CENTER;
} else if (strcmp(value, "end") == 0) {
align = ui::END;
} else if (strcmp(value, "between") == 0) {
align = ui::BETWEEN;
// "stretch" is the default cross-axis behaviour: a child without its own size
// already fills the line.
} else if (strcmp(value, "start") != 0 && strcmp(value, "stretch") != 0) {
lua_pop(state, 1);
luaL_error(state, "unknown alignment '%s'", value);
}
}
lua_pop(state, 1);
return align;
}
uint8_t readType(lua_State* state, int index) {
lua_getfield(state, index, "type");
const char* value = luaL_checkstring(state, -1);
uint8_t type = ui::BOX;
if (strcmp(value, "text") == 0) {
type = ui::TEXT;
} else if (strcmp(value, "button") == 0) {
type = ui::BUTTON;
} else if (strcmp(value, "custom") == 0) {
type = ui::CUSTOM;
} else if (strcmp(value, "box") != 0) {
lua_pop(state, 1);
luaL_error(state, "unknown node type '%s'", value);
}
lua_pop(state, 1);
return type;
}
int reset(lua_State* state) {
tree(state).reset();
return 0;
}
int create(lua_State* state) {
const bool hasParent = !lua_isnoneornil(state, 1);
const uint16_t parent = hasParent ? checkNode(state, 1) : ui::NONE;
luaL_checktype(state, 2, LUA_TTABLE);
const uint8_t type = readType(state, 2);
ui::Spec spec;
bool present = false;
spec.w = readSize(state, 2, "w", present);
spec.h = readSize(state, 2, "h", present);
const int16_t pad = readNumber(state, 2, "pad", 0);
spec.padT = spec.padR = spec.padB = spec.padL = static_cast<uint8_t>(pad);
spec.gap = static_cast<uint8_t>(readNumber(state, 2, "gap", 0));
spec.align = readAlign(state, 2, "align");
spec.justify = readAlign(state, 2, "justify");
lua_getfield(state, 2, "at");
if (lua_istable(state, -1)) {
const int at = lua_gettop(state);
spec.atX = readSize(state, at, "x", present);
spec.atY = readSize(state, at, "y", present);
spec.absolute = true;
}
lua_pop(state, 1);
uint8_t flags = 0;
if (readFlag(state, 2, "row")) flags |= ui::ROW;
if (readFlag(state, 2, "capture")) flags |= ui::CAPTURE;
if (readFlag(state, 2, "interactive")) flags |= ui::INTERACTIVE;
lua_getfield(state, 2, "font");
const int32_t font = lua_isnoneornil(state, -1) ? Runtime::from(state)->gui().fonts().ui
: static_cast<int32_t>(luaL_checkinteger(state, -1));
lua_pop(state, 1);
lua_getfield(state, 2, "label");
const char* label = lua_isnoneornil(state, -1) ? nullptr : luaL_checkstring(state, -1);
if (label && type == ui::TEXT) {
GuiProvider& gui = Runtime::from(state)->gui();
const int32_t style = gui.fonts().styleNormal;
spec.intrinsicW = static_cast<int16_t>(gui.textWidth(font, label, style));
spec.intrinsicH = static_cast<int16_t>(gui.fontHeight(font, style));
}
const uint16_t id = tree(state).add(parent, spec, type, flags);
if (label) tree(state).setLabel(id, label);
lua_pop(state, 1);
lua_pushinteger(state, id);
return 1;
}
int attach(lua_State* state) {
const uint16_t parent = checkNode(state, 1);
const uint16_t child = checkNode(state, 2);
luaL_argcheck(state, tree(state).nodes[child].parent == ui::NONE, 2, "already attached");
tree(state).attach(parent, child);
return 0;
}
int setSize(lua_State* state) {
const uint16_t id = checkNode(state, 1);
luaL_argcheck(state, id < tree(state).specs.size(), 1, "layout scratch has been dropped");
bool hasW = false;
bool hasH = false;
const ui::Size w = sizeAt(state, 2, hasW);
const ui::Size h = sizeAt(state, 3, hasH);
ui::Spec& spec = tree(state).specs[id];
if (hasW) spec.w = w;
if (hasH) spec.h = h;
return 0;
}
int layout(lua_State* state) {
const uint16_t root = checkNode(state, 1);
const int32_t x = checkInt(state, 2);
const int32_t y = checkInt(state, 3);
const int32_t w = checkInt(state, 4);
const int32_t h = checkInt(state, 5);
ui::Tree& nodes = tree(state);
luaL_argcheck(state, root < nodes.specs.size(), 1, "layout scratch has been dropped");
if (nodes.layout(root, x, y, w, h)) {
lua_pushboolean(state, true);
return 1;
}
lua_pushnil(state);
lua_pushstring(state, nodes.error ? nodes.error : "layout failed");
return 2;
}
int dropScratch(lua_State* state) {
tree(state).dropScratch();
return 0;
}
int hit(lua_State* state) {
const uint16_t root = checkNode(state, 1);
const uint16_t found = tree(state).hit(root, checkInt(state, 2), checkInt(state, 3));
if (found == ui::NONE) return 0;
lua_pushinteger(state, found);
return 1;
}
int getRect(lua_State* state) {
const ui::Node& node = tree(state).nodes[checkNode(state, 1)];
lua_pushinteger(state, node.x);
lua_pushinteger(state, node.y);
lua_pushinteger(state, node.w);
lua_pushinteger(state, node.h);
return 4;
}
int setLabel(lua_State* state) {
const uint16_t id = checkNode(state, 1);
const char* text = luaL_checkstring(state, 2);
tree(state).setLabel(id, text);
tree(state).nodes[id].flags |= ui::DIRTY;
return 0;
}
int getLabel(lua_State* state) {
const char* label = tree(state).label(checkNode(state, 1));
if (!label) return 0;
lua_pushstring(state, label);
return 1;
}
int getParent(lua_State* state) {
const uint16_t parent = tree(state).nodes[checkNode(state, 1)].parent;
if (parent == ui::NONE) return 0;
lua_pushinteger(state, parent);
return 1;
}
void readStyleColor(lua_State* state, int index, const char* key, int32_t& field, uint16_t flag, uint16_t& set) {
lua_getfield(state, index, key);
if (!lua_isnoneornil(state, -1)) {
field = static_cast<int32_t>(luaL_checkinteger(state, -1));
set |= flag;
}
lua_pop(state, 1);
}
int setStyle(lua_State* state) {
const uint16_t id = checkNode(state, 1);
luaL_checktype(state, 2, LUA_TTABLE);
ui::Style& style = tree(state).styleFor(id);
readStyleColor(state, 2, "color", style.color, ui::S_COLOR, style.set);
readStyleColor(state, 2, "background", style.bg, ui::S_BG, style.set);
readStyleColor(state, 2, "fill", style.fill, ui::S_FILL, style.set);
readStyleColor(state, 2, "border", style.border, ui::S_BORDER, style.set);
readStyleColor(state, 2, "face", style.face, ui::S_FACE, style.set);
readStyleColor(state, 2, "pressedFace", style.pressedFace, ui::S_PRESSED_FACE, style.set);
readStyleColor(state, 2, "pressedColor", style.pressedColor, ui::S_PRESSED_COLOR, style.set);
readStyleColor(state, 2, "focusColor", style.focusColor, ui::S_FOCUS_COLOR, style.set);
readStyleColor(state, 2, "font", style.font, ui::S_FONT, style.set);
readStyleColor(state, 2, "textStyle", style.textStyle, ui::S_TEXT_STYLE, style.set);
int32_t radius = style.radius;
readStyleColor(state, 2, "radius", radius, ui::S_RADIUS, style.set);
style.radius = static_cast<uint8_t>(radius);
tree(state).nodes[id].flags |= ui::DIRTY;
return 0;
}
int invalidate(lua_State* state) {
tree(state).nodes[checkNode(state, 1)].flags |= ui::DIRTY;
return 0;
}
int setPressed(lua_State* state) {
const uint16_t id = checkNode(state, 1);
luaL_checkany(state, 2);
ui::Node& node = tree(state).nodes[id];
const bool pressed = lua_toboolean(state, 2) != 0;
if (pressed) {
node.flags |= ui::PRESSED;
} else {
node.flags &= ~ui::PRESSED;
}
node.flags |= ui::DIRTY;
return 0;
}
int isPressed(lua_State* state) {
lua_pushboolean(state, (tree(state).nodes[checkNode(state, 1)].flags & ui::PRESSED) != 0);
return 1;
}
uint16_t firstInteractive(const ui::Tree& nodes, uint16_t id) {
if (nodes.nodes[id].flags & ui::INTERACTIVE) return id;
for (uint16_t child = nodes.nodes[id].first; child != ui::NONE; child = nodes.nodes[child].next) {
const uint16_t found = firstInteractive(nodes, child);
if (found != ui::NONE) return found;
}
return ui::NONE;
}
void setFocusTo(ui::Tree& nodes, uint16_t id) {
if (nodes.focus != ui::NONE) nodes.nodes[nodes.focus].flags |= ui::DIRTY;
nodes.focus = id;
if (id != ui::NONE) nodes.nodes[id].flags |= ui::DIRTY;
}
int focusFirst(lua_State* state) {
ui::Tree& nodes = tree(state);
const uint16_t found = firstInteractive(nodes, checkNode(state, 1));
setFocusTo(nodes, found);
if (found == ui::NONE) return 0;
lua_pushinteger(state, found);
return 1;
}
int setFocus(lua_State* state) {
ui::Tree& nodes = tree(state);
setFocusTo(nodes, lua_isnoneornil(state, 1) ? ui::NONE : checkNode(state, 1));
return 0;
}
int getFocus(lua_State* state) {
const uint16_t focus = tree(state).focus;
if (focus == ui::NONE) return 0;
lua_pushinteger(state, focus);
return 1;
}
// Nearest candidate strictly beyond the current node's edge, scored by the gap along the
// travel axis plus the misalignment across it, so a directly adjacent control always
// beats a distant one that happens to line up.
void collectCandidate(const ui::Tree& nodes, uint16_t id, const ui::Node& from, const char* direction,
uint16_t& best, long& bestScore) {
const ui::Node& node = nodes.nodes[id];
if ((node.flags & ui::INTERACTIVE) && id != nodes.focus) {
const long dx = (node.x + node.w / 2) - (from.x + from.w / 2);
const long dy = (node.y + node.h / 2) - (from.y + from.h / 2);
bool forward = false;
long along = 0;
long across = 0;
if (strcmp(direction, "left") == 0) {
forward = node.x + node.w <= from.x;
along = -dx;
across = labs(dy);
} else if (strcmp(direction, "right") == 0) {
forward = node.x >= from.x + from.w;
along = dx;
across = labs(dy);
} else if (strcmp(direction, "up") == 0) {
forward = node.y + node.h <= from.y;
along = -dy;
across = labs(dx);
} else {
forward = node.y >= from.y + from.h;
along = dy;
across = labs(dx);
}
if (forward) {
const long score = along + across * 2;
if (best == ui::NONE || score < bestScore) {
best = id;
bestScore = score;
}
}
}
for (uint16_t child = nodes.nodes[id].first; child != ui::NONE; child = nodes.nodes[child].next) {
collectCandidate(nodes, child, from, direction, best, bestScore);
}
}
int moveFocus(lua_State* state) {
const uint16_t root = checkNode(state, 1);
const char* direction = luaL_checkstring(state, 2);
luaL_argcheck(state,
strcmp(direction, "up") == 0 || strcmp(direction, "down") == 0 || strcmp(direction, "left") == 0 ||
strcmp(direction, "right") == 0,
2, "expected up, down, left, or right");
ui::Tree& nodes = tree(state);
if (nodes.focus == ui::NONE) return focusFirst(state);
uint16_t best = ui::NONE;
long bestScore = 0;
collectCandidate(nodes, root, nodes.nodes[nodes.focus], direction, best, bestScore);
if (best != ui::NONE) setFocusTo(nodes, best);
lua_pushinteger(state, nodes.focus);
return 1;
}
void callPainter(void* context, uint16_t id, int x, int y, int w, int h) {
lua_State* state = static_cast<lua_State*>(context);
lua_getfield(state, LUA_REGISTRYINDEX, PAINTER_KEY);
if (!lua_isfunction(state, -1)) {
lua_pop(state, 1);
return;
}
lua_pushinteger(state, id);
lua_pushinteger(state, x);
lua_pushinteger(state, y);
lua_pushinteger(state, w);
lua_pushinteger(state, h);
if (lua_pcall(state, 5, 0, 0) != LUA_OK) {
Runtime::from(state)->log().write(LogLevel::Error, lua_tostring(state, -1) ? lua_tostring(state, -1) : "painter");
lua_pop(state, 1);
}
}
int setPainter(lua_State* state) {
luaL_checktype(state, 1, LUA_TFUNCTION);
lua_pushvalue(state, 1);
lua_setfield(state, LUA_REGISTRYINDEX, PAINTER_KEY);
return 0;
}
int draw(lua_State* state) {
const uint16_t root = checkNode(state, 1);
ui::Painter painter(Runtime::from(state)->gui(), tree(state));
painter.custom = callPainter;
painter.context = state;
painter.draw(root);
return 0;
}
int getCount(lua_State* state) {
lua_pushinteger(state, static_cast<lua_Integer>(tree(state).nodes.size()));
return 1;
}
int getFootprint(lua_State* state) {
lua_pushinteger(state, static_cast<lua_Integer>(tree(state).footprint()));
return 1;
}
const luaL_Reg FUNCTIONS[] = {
// --- Drops the current tree; all existing IDs become invalid.
{"reset", reset},
// --- Creates a node, optionally as a child of an existing one.
// @param parent NodeId|nil Nil creates a root.
// @param spec NodeSpec
// @return NodeId
{"create", create},
// --- Adopts an existing root as a child.
// @param parent NodeId
// @param child NodeId Existing root without a parent.
{"attach", attach},
// --- Changes a node's requested size before layout.
// @param id NodeId
// @param w number|"fill"|"auto"|nil
// @param h number|"fill"|"auto"|nil
{"setSize", setSize},
// --- Measures and places a subtree.
// @param root NodeId
// @param x integer
// @param y integer
// @param w integer
// @param h integer
// @return true|nil ok
// @return string|nil error
{"layout", layout},
// --- Releases temporary measurement and placement inputs after layout.
{"dropScratch", dropScratch},
// --- Returns the deepest interactive node under a point.
// @param root NodeId
// @param x integer
// @param y integer
// @return NodeId|nil
{"hit", hit},
// --- Returns a node's placed rectangle.
// @param id NodeId
// @return integer x
// @return integer y
// @return integer w
// @return integer h
{"getRect", getRect},
// --- Replaces a node's text and marks it for repaint.
// @param id NodeId
// @param text string
{"setLabel", setLabel},
// --- Returns a node's text.
// @param id NodeId
// @return string|nil
{"getLabel", getLabel},
// --- Returns a node's parent.
// @param id NodeId
// @return NodeId|nil
{"getParent", getParent},
// --- Sets the style roles a subtree inherits.
// @param id NodeId
// @param style NodeStyle
{"setStyle", setStyle},
// --- Marks a node for repaint.
// @param id NodeId
{"invalidate", invalidate},
// --- Sets a node's pressed state.
// @param id NodeId
// @param pressed boolean
{"setPressed", setPressed},
// --- Whether a node is pressed.
// @param id NodeId
// @return boolean
{"isPressed", isPressed},
// --- Focuses the first interactive node in layout order.
// @param root NodeId
// @return NodeId|nil focused
{"focusFirst", focusFirst},
// --- Changes focus and invalidates the previously and newly focused nodes.
// @param id NodeId|nil Nil clears focus.
{"setFocus", setFocus},
// --- Returns the focused node.
// @return NodeId|nil
{"getFocus", getFocus},
// --- Moves to the nearest interactive node in the requested direction without wrapping.
// @param root NodeId
// @param direction NodeDirection
// @return NodeId|nil focused Current focus when no candidate exists.
{"moveFocus", moveFocus},
// --- Registers the painter every custom node calls.
// @param painter fun(id: NodeId, x: integer, y: integer, w: integer, h: integer)
{"setPainter", setPainter},
// --- Paints dirty nodes; the firmware owns publication to the physical display.
// @param root NodeId
{"draw", draw},
// --- Returns the number of nodes in the tree.
// @return integer
{"getCount", getCount},
// --- Returns the tree's memory use.
// @return integer bytes
{"getFootprint", getFootprint},
{nullptr, nullptr},
};
} // namespace
void registerNode(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "node");
}
} // namespace bindings
} // namespace esp32lua
+76
View File
@@ -0,0 +1,76 @@
// @lua-module settings SettingsLib
#include <lua/runtime.h>
extern "C" {
#include "lauxlib.h"
#include "lua.h"
}
namespace esp32lua {
namespace bindings {
namespace {
int pushStatus(lua_State* state, const Status& status) {
if (status.ok) {
lua_pushboolean(state, true);
return 1;
}
lua_pushnil(state);
lua_pushlstring(state, status.error.data(), status.error.size());
return 2;
}
int getRotation(lua_State* state) {
lua_pushinteger(state, Runtime::from(state)->settings().rotation());
return 1;
}
int setRotation(lua_State* state) {
const lua_Integer degrees = luaL_checkinteger(state, 1);
luaL_argcheck(state, degrees >= 0 && degrees <= 270 && degrees % 90 == 0, 1,
"expected 0, 90, 180, or 270");
return pushStatus(state, Runtime::from(state)->settings().setRotation(degrees));
}
int getTimezone(lua_State* state) {
const std::string timezone = Runtime::from(state)->settings().timezone();
lua_pushlstring(state, timezone.data(), timezone.size());
return 1;
}
int setTimezone(lua_State* state) {
size_t length = 0;
const char* value = luaL_checklstring(state, 1, &length);
return pushStatus(state, Runtime::from(state)->settings().setTimezone({value, length}));
}
const luaL_Reg FUNCTIONS[] = {
// --- Returns the saved rotation in degrees clockwise.
// @return integer
{"getRotation", getRotation},
// --- Applies and persists the screen rotation.
// @param degrees integer 0, 90, 180, or 270 clockwise.
// @return true|nil ok
// @return string|nil error
{"setRotation", setRotation},
// --- Returns the active POSIX timezone rule.
// @return string
{"getTimezone", getTimezone},
// --- Applies and persists a POSIX timezone rule.
// @param timezone string
// @return true|nil ok
// @return string|nil error
{"setTimezone", setTimezone},
{nullptr, nullptr},
};
} // namespace
void registerSettings(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "settings");
}
} // namespace bindings
} // namespace esp32lua
+84
View File
@@ -0,0 +1,84 @@
// @lua-module sys SysLib
// @lua-preamble ---@alias Feature "touch"|"buttons"
#include "../helpers.h"
namespace esp32lua {
namespace bindings {
namespace {
int getAPIVersion(lua_State* state) { lua_pushinteger(state, API_VERSION); return 1; }
int hasFeature(lua_State* state) { lua_pushboolean(state, Runtime::from(state)->hasFeature(luaL_checkstring(state, 1))); return 1; }
int getMillis(lua_State* state) { lua_pushinteger(state, Runtime::from(state)->sys().millis()); return 1; }
int getAppID(lua_State* state) { pushString(state, Runtime::from(state)->appId()); return 1; }
int getAppTitle(lua_State* state) { pushString(state, Runtime::from(state)->appTitle()); return 1; }
int getAppDataPath(lua_State* state) { pushString(state, Runtime::from(state)->appDataPath()); return 1; }
int setAppTitle(lua_State* state) { Runtime::from(state)->setAppTitle(luaL_checkstring(state, 1)); return 0; }
int navigate(lua_State* state, bool replace) {
const std::string path = checkString(state, 1);
const std::string arg = lua_isnoneornil(state, 2) ? std::string() : checkString(state, 2);
Runtime::from(state)->requestLaunch(path, arg, replace);
return 0;
}
int launch(lua_State* state) { return navigate(state, false); }
int replace(lua_State* state) { return navigate(state, true); }
int back(lua_State* state) { Runtime::from(state)->requestBack(); return 0; }
int getMemory(lua_State* state) {
const MemoryInfo memory = Runtime::from(state)->sys().memory();
lua_pushinteger(state, memory.freeBytes);
lua_pushinteger(state, memory.totalBytes);
lua_pushinteger(state, memory.largestFreeBlock);
return 3;
}
int isClockSynced(lua_State* state) { lua_pushboolean(state, Runtime::from(state)->sys().isClockSynced()); return 1; }
const luaL_Reg FUNCTIONS[] = {
// --- Returns the implemented API contract version.
// @return integer
{"getAPIVersion", getAPIVersion},
// --- Whether the firmware implements a complete optional feature contract.
// @param feature Feature
// @return boolean
{"hasFeature", hasFeature},
// --- Returns monotonic milliseconds since boot.
// @return integer
{"getMillis", getMillis},
// --- Returns the immutable first path component of the running app.
// @return string
{"getAppID", getAppID},
// --- Returns the running app title, initially the app ID.
// @return string
{"getAppTitle", getAppTitle},
// --- Returns the current app's guaranteed-existing persistent data directory.
// @return string Absolute path under /.lua/data, preserved across app updates.
{"getAppDataPath", getAppDataPath},
// --- Changes the running app's display title.
// @param title string
{"setAppTitle", setAppTitle},
// --- Launches /.lua/apps/<path>/main.lua and pushes the current route.
// @param path string App-relative directory path; traversal is rejected.
// @param arg string|nil Passed to init(arg).
{"launch", launch},
// --- Launches an app path without retaining the current route.
// @param path string App-relative directory path; traversal is rejected.
// @param arg string|nil Passed to init(arg).
{"replace", replace},
// --- Returns to the previous app, or the launcher when history is empty.
{"back", back},
// --- Returns heap statistics.
// @return integer freeBytes
// @return integer totalBytes
// @return integer largestFreeBlock
{"getMemory", getMemory},
// --- Whether network time synchronization has completed.
// @return boolean
{"isClockSynced", isClockSynced},
{nullptr, nullptr},
};
} // namespace
void registerSys(lua_State* state) { luaL_newlib(state, FUNCTIONS); lua_setglobal(state, "sys"); }
} // namespace bindings
} // namespace esp32lua
+58
View File
@@ -0,0 +1,58 @@
// @lua-module timer TimerLib
// @lua-preamble ---@alias TimerId integer
// @lua-preamble ---@alias TimerCallback fun()
#include "../helpers.h"
namespace esp32lua {
namespace bindings {
namespace {
int schedule(lua_State* state, bool repeating) {
const int32_t intervalMs = checkInt(state, 1);
luaL_argcheck(state, intervalMs > 0, 1, "must be positive");
luaL_checktype(state, 2, LUA_TFUNCTION);
lua_pushvalue(state, 2);
const int callbackRef = luaL_ref(state, LUA_REGISTRYINDEX);
const TimerId id = Runtime::from(state)->addTimer(callbackRef, intervalMs, repeating);
if (id == 0) return luaL_error(state, "no timer slot available");
lua_pushinteger(state, id);
return 1;
}
int after(lua_State* state) { return schedule(state, false); }
int every(lua_State* state) { return schedule(state, true); }
int cancel(lua_State* state) {
lua_pushboolean(state, Runtime::from(state)->cancelTimer(checkInt(state, 1)));
return 1;
}
const luaL_Reg FUNCTIONS[] = {
// --- Runs a callback once after a delay.
// @param intervalMs integer Positive delay; callback timing is best effort and never early.
// @param callback TimerCallback Retained until it fires or is cancelled.
// @return TimerId
{"after", after},
// --- Runs a callback repeatedly.
// @param intervalMs integer Positive interval; callback timing is best effort and never early.
// @param callback TimerCallback Retained until cancelled.
// @return TimerId
{"every", every},
// --- Cancels a timer and releases its callback.
// @param id TimerId
// @return boolean Whether an active timer was cancelled.
{"cancel", cancel},
{nullptr, nullptr},
};
} // namespace
void registerTimer(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "timer");
}
} // namespace bindings
} // namespace esp32lua
+112
View File
@@ -0,0 +1,112 @@
// @lua-module wifi WifiLib
// @lua-preamble ---@class WifiNetwork
// @lua-preamble ---@field ssid string
// @lua-preamble ---@field rssi integer
// @lua-preamble ---@field secure boolean
// @lua-preamble
// @lua-preamble ---@alias WifiState "disconnected"|"connecting"|"connected"|"not_found"|"failed"
// @lua-preamble
// @lua-preamble ---@class WifiStatus
// @lua-preamble ---@field state WifiState
// @lua-preamble ---@field ssid string
// @lua-preamble ---@field ip string
// @lua-preamble ---@field rssi integer
#include "../helpers.h"
namespace esp32lua {
namespace bindings {
namespace {
int scan(lua_State* state) {
std::vector<WifiNetwork> networks;
const Status status = Runtime::from(state)->wifi().scan(networks);
if (!status.ok) return pushError(state, status.error);
lua_createtable(state, static_cast<int>(networks.size()), 0);
for (size_t at = 0; at < networks.size(); at++) {
lua_createtable(state, 0, 3);
setField(state, "ssid", networks[at].ssid);
setField(state, "rssi", networks[at].rssi);
setField(state, "secure", networks[at].secure);
lua_rawseti(state, -2, static_cast<lua_Integer>(at + 1));
}
return 1;
}
int connect(lua_State* state) {
std::string ssid;
std::string password;
const bool hasSsid = optionalString(state, 1, ssid);
const bool hasPassword = optionalString(state, 2, password);
const Status status =
Runtime::from(state)->wifi().connect(hasSsid ? &ssid : nullptr, hasPassword ? &password : nullptr);
return pushStatus(state, status);
}
int getStatus(lua_State* state) {
const WifiStatus status = Runtime::from(state)->wifi().status();
lua_createtable(state, 0, 4);
setField(state, "state", status.state);
setField(state, "ssid", status.ssid);
setField(state, "ip", status.ip);
setField(state, "rssi", status.rssi);
return 1;
}
int isConnected(lua_State* state) {
lua_pushboolean(state, Runtime::from(state)->wifi().status().state == "connected");
return 1;
}
int getLocalIP(lua_State* state) {
const WifiStatus status = Runtime::from(state)->wifi().status();
pushString(state, status.ip.empty() ? "0.0.0.0" : status.ip);
return 1;
}
int disconnect(lua_State* state) {
Runtime::from(state)->wifi().disconnect();
return 0;
}
int forget(lua_State* state) { return pushStatus(state, Runtime::from(state)->wifi().forget()); }
const luaL_Reg FUNCTIONS[] = {
// --- Scans for visible networks.
// @return WifiNetwork[]|nil networks
// @return string|nil error
{"scan", scan},
// --- With credentials, saves and joins that network. Without them, reconnects saved credentials.
// @param ssid string|nil
// @param password string|nil Omit for an open network.
// @return true|nil ok
// @return string|nil error
{"connect", connect},
// --- Returns the current connection state.
// @return WifiStatus
{"getStatus", getStatus},
// --- Whether the station is associated and has an address.
// @return boolean
{"isConnected", isConnected},
// --- Returns the station address.
// @return string IPv4 address, or 0.0.0.0 when disconnected.
{"getLocalIP", getLocalIP},
// --- Disconnects while retaining saved credentials.
{"disconnect", disconnect},
// --- Disconnects and erases saved credentials.
// @return true|nil ok
// @return string|nil error
{"forget", forget},
{nullptr, nullptr},
};
} // namespace
void registerWifi(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "wifi");
}
} // namespace bindings
} // namespace esp32lua
+78
View File
@@ -0,0 +1,78 @@
#include "../helpers.h"
namespace esp32lua {
namespace bindings {
namespace {
int getButtons(lua_State* state) {
pushStrings(state, Runtime::from(state)->buttons().buttons());
return 1;
}
int isAnyPressed(lua_State* state) {
lua_pushboolean(state, Runtime::from(state)->buttons().isAnyPressed());
return 1;
}
int isPressed(lua_State* state) {
const std::string button = checkString(state, 1);
lua_pushboolean(state, Runtime::from(state)->buttons().isPressed(button));
return 1;
}
int wasPressed(lua_State* state) {
const std::string button = checkString(state, 1);
lua_pushboolean(state, Runtime::from(state)->buttons().wasPressed(button));
return 1;
}
int wasReleased(lua_State* state) {
const std::string button = checkString(state, 1);
lua_pushboolean(state, Runtime::from(state)->buttons().wasReleased(button));
return 1;
}
// @lua-augment input InputLib
// @lua-preamble ---@alias Button "up"|"down"|"left"|"right"|"confirm"|"back"
// @lua-preamble
// @lua-preamble -- Roles, not physical buttons: a device maps whatever hardware it has onto them, and
// @lua-preamble -- up/down/left/right are the directions node.moveFocus already takes.
const luaL_Reg INPUT_FUNCTIONS[] = {
// ---Returns the roles this device reports, so an app can label only the actions it has.
// @return Button[]
{"getButtons", getButtons},
// ---Whether any button is held.
// @return boolean
{"isAnyPressed", isAnyPressed},
// ---Whether a button is held.
// @param button Button
// @return boolean
{"isPressed", isPressed},
// ---Whether a button went down since the last poll.
// @param button Button
// @return boolean
{"wasPressed", wasPressed},
// ---Whether a button came up since the last poll.
// @param button Button
// @return boolean
{"wasReleased", wasReleased},
{nullptr, nullptr},
};
} // namespace
void registerButtons(lua_State* state) { augmentGlobal(state, "input", INPUT_FUNCTIONS); }
} // namespace bindings
} // namespace esp32lua
// @lua-global
// ---Fired when a button goes down.
// @param button Button
// @lua-fn on_button_down
// ---Fired when a button comes up.
// @param button Button
// @lua-fn on_button_up
// ---Tap alias, fired on release like a click, after on_button_up.
// @param button Button
// @lua-fn on_button
+96
View File
@@ -0,0 +1,96 @@
#include "../helpers.h"
namespace esp32lua {
namespace bindings {
namespace {
int setCalibration(lua_State* state) {
const int32_t x0 = checkInt(state, 1);
const int32_t y0 = checkInt(state, 2);
const int32_t x1 = checkInt(state, 3);
const int32_t y1 = checkInt(state, 4);
return pushStatus(state, Runtime::from(state)->touch().setCalibration(x0, y0, x1, y1));
}
int pushPoint(lua_State* state, bool touched, int32_t x, int32_t y) {
if (!touched) return 0;
lua_pushinteger(state, x);
lua_pushinteger(state, y);
return 2;
}
int getTouch(lua_State* state) {
int32_t x = 0;
int32_t y = 0;
const bool touched = Runtime::from(state)->touch().touch(x, y);
return pushPoint(state, touched, x, y);
}
int getRawTouch(lua_State* state) {
int32_t x = 0;
int32_t y = 0;
const bool touched = Runtime::from(state)->touch().rawTouch(x, y);
return pushPoint(state, touched, x, y);
}
int isTouched(lua_State* state) {
lua_pushboolean(state, Runtime::from(state)->touch().isTouched());
return 1;
}
// @lua-augment settings SettingsLib
const luaL_Reg SETTINGS_FUNCTIONS[] = {
// --- Persists the panel's touch calibration.
// @param x0 integer Raw reading at the left edge.
// @param y0 integer Raw reading at the top edge.
// @param x1 integer Raw reading at the right edge.
// @param y1 integer Raw reading at the bottom edge.
// @return true|nil ok
// @return string|nil error
{"setCalibration", setCalibration},
{nullptr, nullptr},
};
// @lua-augment input InputLib
const luaL_Reg INPUT_FUNCTIONS[] = {
// --- Returns the calibrated touch point, or nothing when the panel is not touched.
// @return integer|nil x
// @return integer|nil y
{"getTouch", getTouch},
// --- Returns the uncalibrated touch reading, or nothing when the panel is not touched.
// @return integer|nil x
// @return integer|nil y
{"getRawTouch", getRawTouch},
// --- Whether the panel is currently touched.
// @return boolean
{"isTouched", isTouched},
{nullptr, nullptr},
};
} // namespace
void registerTouch(lua_State* state) {
augmentGlobal(state, "settings", SETTINGS_FUNCTIONS);
augmentGlobal(state, "input", INPUT_FUNCTIONS);
}
} // namespace bindings
} // namespace esp32lua
// @lua-global
// ---Fired when the finger lands.
// @param x integer
// @param y integer
// @lua-fn on_touch_down
// ---Fired when the finger moves while down, after the firmware's jitter filter.
// @param x integer
// @param y integer
// @lua-fn on_touch_move
// ---Fired when the finger lifts.
// @param x integer
// @param y integer
// @lua-fn on_touch_up
// ---Tap alias, fired on release like a click, after on_touch_up.
// @param x integer
// @param y integer
// @lua-fn on_touch
+105
View File
@@ -0,0 +1,105 @@
#pragma once
// Argument and result marshalling shared by the binding sources. Every luaL_check* call
// happens before a non-trivial C++ local exists, because a Lua error longjmps past
// destructors.
#include <string>
#include <vector>
#include <lua/providers.h>
#include <lua/runtime.h>
extern "C" {
#include "lauxlib.h"
#include "lua.h"
}
namespace esp32lua {
namespace bindings {
inline void pushString(lua_State* state, const std::string& value) {
lua_pushlstring(state, value.data(), value.size());
}
inline std::string checkString(lua_State* state, int index) {
size_t length = 0;
const char* value = luaL_checklstring(state, index, &length);
return std::string(value, length);
}
inline bool optionalString(lua_State* state, int index, std::string& out) {
if (lua_isnoneornil(state, index)) return false;
out = checkString(state, index);
return true;
}
inline int32_t checkInt(lua_State* state, int index) {
return static_cast<int32_t>(luaL_checkinteger(state, index));
}
inline int32_t optionalInt(lua_State* state, int index, int32_t fallback) {
return static_cast<int32_t>(luaL_optinteger(state, index, fallback));
}
inline bool optionalColor(lua_State* state, int index, int32_t& out) {
if (lua_isnoneornil(state, index)) return false;
out = checkInt(state, index);
return true;
}
// `true` on success, `nil, error` on failure: the shape every mutating binding returns.
inline int pushStatus(lua_State* state, const Status& status) {
if (status.ok) {
lua_pushboolean(state, true);
return 1;
}
lua_pushnil(state);
pushString(state, status.error);
return 2;
}
inline int pushError(lua_State* state, const std::string& error) {
lua_pushnil(state);
pushString(state, error);
return 2;
}
inline void pushStrings(lua_State* state, const std::vector<std::string>& values) {
lua_createtable(state, static_cast<int>(values.size()), 0);
for (size_t index = 0; index < values.size(); index++) {
pushString(state, values[index]);
lua_rawseti(state, -2, static_cast<lua_Integer>(index + 1));
}
}
inline void setField(lua_State* state, const char* key, const std::string& value) {
pushString(state, value);
lua_setfield(state, -2, key);
}
inline void setField(lua_State* state, const char* key, int32_t value) {
lua_pushinteger(state, value);
lua_setfield(state, -2, key);
}
inline void setField(lua_State* state, const char* key, bool value) {
lua_pushboolean(state, value);
lua_setfield(state, -2, key);
}
// Feature contracts add functions to namespaces the core registrations already created.
inline void augmentGlobal(lua_State* state, const char* name, const luaL_Reg* functions) {
lua_getglobal(state, name);
if (!lua_istable(state, -1)) {
lua_pop(state, 1);
lua_newtable(state);
lua_pushvalue(state, -1);
lua_setglobal(state, name);
}
luaL_setfuncs(state, functions, 0);
lua_pop(state, 1);
}
} // namespace bindings
} // namespace esp32lua