chore: lsp + format

This commit is contained in:
2026-08-04 10:21:18 -04:00
parent fa35c3c326
commit 4533c7f61f
30 changed files with 1151 additions and 678 deletions
+15 -9
View File
@@ -13,7 +13,8 @@ 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));
return pushStatus(
state, Runtime::from(state)->ble().init(hasName ? &name : nullptr));
}
int deinit(lua_State* state) {
@@ -27,7 +28,8 @@ int scan(lua_State* state) {
std::vector<BleDevice> devices;
const Status status = Runtime::from(state)->ble().scan(durationMs, devices);
if (!status.ok) return pushError(state, status.error);
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++) {
@@ -59,8 +61,10 @@ 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);
const Status status =
Runtime::from(state)->ble().read(service, characteristic, value);
if (!status.ok)
return pushError(state, status.error);
pushString(state, value);
return 1;
}
@@ -69,13 +73,15 @@ 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));
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));
return pushStatus(state, Runtime::from(state)->ble().startAdvertising(
hasName ? &name : nullptr));
}
int stopAdvertising(lua_State* state) {
@@ -129,12 +135,12 @@ const luaL_Reg FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
} // namespace
void registerBle(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "ble");
}
} // namespace bindings
} // namespace esp32lua
} // namespace bindings
} // namespace esp32lua
+39 -21
View File
@@ -1,5 +1,6 @@
// @lua-module fs FsLib
// @lua-const MAX_READ_BYTES integer 65536 Largest portable whole-file or line read.
// @lua-const MAX_READ_BYTES integer 65536 Largest portable whole-file or line
// read.
#include "../helpers.h"
@@ -11,7 +12,8 @@ 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");
luaL_argcheck(state, maxBytes >= 0 && maxBytes <= MAX_READ_BYTES, index,
"exceeds fs.MAX_READ_BYTES");
return maxBytes;
}
@@ -25,7 +27,8 @@ 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);
if (!status.ok)
return pushError(state, status.error);
lua_pushinteger(state, size);
return 1;
}
@@ -34,8 +37,10 @@ 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);
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;
}
@@ -52,8 +57,10 @@ 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);
const Status status =
Runtime::from(state)->fs().readFile(path, maxBytes, content);
if (!status.ok)
return pushError(state, status.error);
pushString(state, content);
return 1;
}
@@ -67,7 +74,8 @@ int readLineAt(lua_State* state) {
bool found = false;
int32_t nextOffset = 0;
std::string line;
const Status status = Runtime::from(state)->fs().readLineAt(path, offset, maxBytes, found, line, nextOffset);
const Status status = Runtime::from(state)->fs().readLineAt(
path, offset, maxBytes, found, line, nextOffset);
if (!status.ok) {
lua_pushnil(state);
lua_pushnil(state);
@@ -96,7 +104,8 @@ int removeTree(lua_State* state) {
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));
return pushStatus(state,
Runtime::from(state)->fs().rename(source, destination));
}
int writeFile(lua_State* state) {
@@ -107,7 +116,8 @@ int writeFile(lua_State* state) {
const luaL_Reg FUNCTIONS[] = {
// --- Whether a path exists.
// @param path string Absolute SD-card path; traversal components are rejected.
// @param path string Absolute SD-card path; traversal components are
// rejected.
// @return boolean
{"exists", exists},
// --- Returns the size of a file in bytes.
@@ -132,35 +142,43 @@ const luaL_Reg FUNCTIONS[] = {
{"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.
// @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.
// @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.
// @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.
// @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.
// @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.
// @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.
// --- Atomically replaces the destination or leaves its previous contents
// intact.
// @param path string Absolute file path.
// @param content string
// @return true|nil ok
@@ -169,7 +187,7 @@ const luaL_Reg FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
} // namespace
void registerFs(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
@@ -178,5 +196,5 @@ void registerFs(lua_State* state) {
lua_setglobal(state, "fs");
}
} // namespace bindings
} // namespace esp32lua
} // namespace bindings
} // namespace esp32lua
+47 -25
View File
@@ -34,7 +34,8 @@ int getRotation(lua_State* state) {
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");
luaL_argcheck(state, degrees >= 0 && degrees <= 270 && degrees % 90 == 0, 1,
"expected 0, 90, 180, or 270");
provider(state).setRotation(degrees);
return 0;
}
@@ -59,30 +60,35 @@ int clear(lua_State* state) {
}
int fillRect(lua_State* state) {
provider(state).fillRect(checkInt(state, 1), checkInt(state, 2), checkInt(state, 3), checkInt(state, 4),
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),
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),
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));
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),
provider(state).drawCircle(checkInt(state, 1), checkInt(state, 2),
checkInt(state, 3), checkInt(state, 4),
optionalInt(state, 5, 1));
return 0;
}
@@ -94,7 +100,8 @@ int fillCircle(lua_State* state) {
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);
provider(state).fillCircle(x, y, radius, fill,
hasBackground ? &background : nullptr);
return 0;
}
@@ -111,8 +118,10 @@ int roundRect(lua_State* state) {
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);
provider(state).roundRect(x, y, w, h, radius, background,
hasTop ? &top : nullptr,
hasBottom ? &bottom : (hasTop ? &top : nullptr),
hasBorder ? &border : nullptr);
return 0;
}
@@ -141,7 +150,8 @@ int fillPolygon(lua_State* state) {
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.
// The vectors are destroyed before any Lua error is raised, because
// luaL_error longjmps.
bool usable = false;
{
std::vector<int32_t> xs;
@@ -149,26 +159,31 @@ int fillPolygon(lua_State* state) {
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)
provider(state).fillPolygon(xs.data(), ys.data(), xs.size(), fill);
}
if (!usable) return luaL_error(state, "expected matching non-empty integer arrays");
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]);
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);
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 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;
@@ -176,7 +191,10 @@ int getTextWidth(lua_State* state) {
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)));
lua_pushinteger(
state,
provider(state).fontHeight(
font, optionalInt(state, 2, provider(state).fonts().styleNormal)));
return 1;
}
@@ -191,7 +209,8 @@ int drawText(lua_State* state) {
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);
gui.drawText(font, x, y, text, textColor, style,
hasBackground ? &background : nullptr);
return 0;
}
@@ -208,7 +227,8 @@ const luaL_Reg FUNCTIONS[] = {
// --- 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.
// --- 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.
@@ -258,7 +278,8 @@ const luaL_Reg FUNCTIONS[] = {
// @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.
// ---Draws an anti-aliased rounded fill, optional gradient, and optional
// border in one pass.
// @param x integer
// @param y integer
// @param w integer
@@ -266,7 +287,8 @@ const luaL_Reg FUNCTIONS[] = {
// @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 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.
@@ -309,7 +331,7 @@ const luaL_Reg FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
} // namespace
void registerGui(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
@@ -323,5 +345,5 @@ void registerGui(lua_State* state) {
lua_setglobal(state, "gui");
}
} // namespace bindings
} // namespace esp32lua
} // namespace bindings
} // namespace esp32lua
+40 -22
View File
@@ -1,17 +1,22 @@
// @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 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 ---@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 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.
// @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"
@@ -31,12 +36,14 @@ int32_t checkResponseLimit(lua_State* state, int index) {
return maxBytes;
}
void readHeaders(lua_State* state, int index, std::vector<HttpHeader>& headers) {
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) {
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);
@@ -59,8 +66,10 @@ int request(lua_State* state, const char* method, bool withBody) {
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);
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);
@@ -79,14 +88,18 @@ int download(lua_State* state) {
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");
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));
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");
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;
@@ -97,8 +110,10 @@ int download(lua_State* state) {
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);
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;
}
@@ -112,8 +127,9 @@ int urlencode(lua_State* state) {
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 == '~';
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 {
@@ -131,7 +147,8 @@ const luaL_Reg FUNCTIONS[] = {
// @param url string
// @param options HttpRequestOptions
// @return HttpResponse|nil response
// @return string|nil error Transport failure or response body exceeding maxBytes.
// @return string|nil error Transport failure or response body exceeding
// maxBytes.
{"get", get},
// --- Performs a HEAD request.
// @param url string
@@ -159,7 +176,8 @@ const luaL_Reg FUNCTIONS[] = {
// @return HttpResponse|nil response
// @return string|nil error
{"patch", patch},
// --- Streams authenticated HTTPS to a new file and removes partial or unverified output.
// --- 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
@@ -173,7 +191,7 @@ const luaL_Reg FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
} // namespace
void registerHttp(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
@@ -182,5 +200,5 @@ void registerHttp(lua_State* state) {
lua_setglobal(state, "http");
}
} // namespace bindings
} // namespace esp32lua
} // namespace bindings
} // namespace esp32lua
+3 -3
View File
@@ -33,12 +33,12 @@ const luaL_Reg FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
} // namespace
void registerLog(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "log");
}
} // namespace bindings
} // namespace esp32lua
} // namespace bindings
} // namespace esp32lua
+112 -63
View File
@@ -20,13 +20,15 @@
// @lua-preamble
// @lua-preamble ---@class NodeStyle
// @lua-preamble ---@field color? GuiColor
// @lua-preamble ---@field background? GuiColor Background offered to descendants.
// @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 focusColor? GuiColor Distinct outline for directional
// focus.
// @lua-preamble ---@field radius? integer
// @lua-preamble ---@field font? GuiFont
// @lua-preamble ---@field textStyle? GuiTextStyle
@@ -46,26 +48,32 @@ 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");
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.
// 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 (!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;
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));
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) {
@@ -75,9 +83,12 @@ ui::Size readSize(lua_State* state, int index, const char* key, bool& present) {
return size;
}
int16_t readNumber(lua_State* state, int index, const char* key, int16_t fallback) {
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));
const int16_t value = lua_isnoneornil(state, -1)
? fallback
: static_cast<int16_t>(luaL_checknumber(state, -1));
lua_pop(state, 1);
return value;
}
@@ -100,8 +111,8 @@ ui::Align readAlign(lua_State* state, int index, const char* key) {
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.
// "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);
@@ -160,17 +171,22 @@ int create(lua_State* state) {
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;
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));
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);
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;
@@ -179,7 +195,8 @@ int create(lua_State* state) {
}
const uint16_t id = tree(state).add(parent, spec, type, flags);
if (label) tree(state).setLabel(id, label);
if (label)
tree(state).setLabel(id, label);
lua_pop(state, 1);
lua_pushinteger(state, id);
return 1;
@@ -188,22 +205,26 @@ int create(lua_State* state) {
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");
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");
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;
if (hasW)
spec.w = w;
if (hasH)
spec.h = h;
return 0;
}
@@ -214,7 +235,8 @@ int layout(lua_State* state) {
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");
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;
@@ -231,8 +253,10 @@ int dropScratch(lua_State* state) {
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;
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;
}
@@ -256,19 +280,22 @@ int setLabel(lua_State* state) {
int getLabel(lua_State* state) {
const char* label = tree(state).label(checkNode(state, 1));
if (!label) return 0;
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;
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) {
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));
@@ -287,11 +314,15 @@ int setStyle(lua_State* state) {
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, "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);
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);
@@ -321,30 +352,37 @@ int setPressed(lua_State* state) {
}
int isPressed(lua_State* state) {
lua_pushboolean(state, (tree(state).nodes[checkNode(state, 1)].flags & ui::PRESSED) != 0);
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) {
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;
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;
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;
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;
if (found == ui::NONE)
return 0;
lua_pushinteger(state, found);
return 1;
}
@@ -357,16 +395,17 @@ int setFocus(lua_State* state) {
int getFocus(lua_State* state) {
const uint16_t focus = tree(state).focus;
if (focus == ui::NONE) return 0;
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) {
// 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);
@@ -399,7 +438,8 @@ void collectCandidate(const ui::Tree& nodes, uint16_t id, const ui::Node& from,
}
}
}
for (uint16_t child = nodes.nodes[id].first; child != ui::NONE; child = nodes.nodes[child].next) {
for (uint16_t child = nodes.nodes[id].first; child != ui::NONE;
child = nodes.nodes[child].next) {
collectCandidate(nodes, child, from, direction, best, bestScore);
}
}
@@ -407,18 +447,22 @@ void collectCandidate(const ui::Tree& nodes, uint16_t id, const ui::Node& from,
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");
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);
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);
collectCandidate(nodes, root, nodes.nodes[nodes.focus], direction, best,
bestScore);
if (best != ui::NONE)
setFocusTo(nodes, best);
lua_pushinteger(state, nodes.focus);
return 1;
}
@@ -436,7 +480,9 @@ void callPainter(void* context, uint16_t id, int x, int y, int w, int h) {
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");
Runtime::from(state)->log().write(
LogLevel::Error,
lua_tostring(state, -1) ? lua_tostring(state, -1) : "painter");
lua_pop(state, 1);
}
}
@@ -545,15 +591,18 @@ const luaL_Reg FUNCTIONS[] = {
// --- Returns the focused node.
// @return NodeId|nil
{"getFocus", getFocus},
// --- Moves to the nearest interactive node in the requested direction without wrapping.
// --- 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)
// @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.
// --- Paints dirty nodes; the firmware owns publication to the physical
// display.
// @param root NodeId
{"draw", draw},
// --- Returns the number of nodes in the tree.
@@ -565,12 +614,12 @@ const luaL_Reg FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
} // namespace
void registerNode(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "node");
}
} // namespace bindings
} // namespace esp32lua
} // namespace bindings
} // namespace esp32lua
+7 -5
View File
@@ -30,7 +30,8 @@ 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));
return pushStatus(state,
Runtime::from(state)->settings().setRotation(degrees));
}
int getTimezone(lua_State* state) {
@@ -42,7 +43,8 @@ int getTimezone(lua_State* state) {
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}));
return pushStatus(
state, Runtime::from(state)->settings().setTimezone({value, length}));
}
const luaL_Reg FUNCTIONS[] = {
@@ -65,12 +67,12 @@ const luaL_Reg FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
} // namespace
void registerSettings(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "settings");
}
} // namespace bindings
} // namespace esp32lua
} // namespace bindings
} // namespace esp32lua
+50 -16
View File
@@ -7,23 +7,49 @@ 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 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);
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 back(lua_State* state) {
Runtime::from(state)->requestBack();
return 0;
}
int getMemory(lua_State* state) {
const MemoryInfo memory = Runtime::from(state)->sys().memory();
@@ -32,7 +58,10 @@ int getMemory(lua_State* state) {
lua_pushinteger(state, memory.largestFreeBlock);
return 3;
}
int isClockSynced(lua_State* state) { lua_pushboolean(state, Runtime::from(state)->sys().isClockSynced()); return 1; }
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.
@@ -51,8 +80,10 @@ const luaL_Reg FUNCTIONS[] = {
// --- 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.
// --- 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
@@ -78,7 +109,10 @@ const luaL_Reg FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
void registerSys(lua_State* state) { luaL_newlib(state, FUNCTIONS); lua_setglobal(state, "sys"); }
} // namespace bindings
} // namespace esp32lua
} // namespace
void registerSys(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "sys");
}
} // namespace bindings
} // namespace esp32lua
+11 -7
View File
@@ -15,8 +15,10 @@ int schedule(lua_State* state, bool repeating) {
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");
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;
}
@@ -31,12 +33,14 @@ int cancel(lua_State* state) {
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 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 intervalMs integer Positive interval; callback timing is best
// effort and never early.
// @param callback TimerCallback Retained until cancelled.
// @return TimerId
{"every", every},
@@ -47,12 +51,12 @@ const luaL_Reg FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
} // namespace
void registerTimer(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "timer");
}
} // namespace bindings
} // namespace esp32lua
} // namespace bindings
} // namespace esp32lua
+16 -10
View File
@@ -4,7 +4,8 @@
// @lua-preamble ---@field rssi integer
// @lua-preamble ---@field secure boolean
// @lua-preamble
// @lua-preamble ---@alias WifiState "disconnected"|"connecting"|"connected"|"not_found"|"failed"
// @lua-preamble ---@alias WifiState
// "disconnected"|"connecting"|"connected"|"not_found"|"failed"
// @lua-preamble
// @lua-preamble ---@class WifiStatus
// @lua-preamble ---@field state WifiState
@@ -21,7 +22,8 @@ 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);
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++) {
@@ -39,8 +41,8 @@ int connect(lua_State* state) {
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);
const Status status = Runtime::from(state)->wifi().connect(
hasSsid ? &ssid : nullptr, hasPassword ? &password : nullptr);
return pushStatus(state, status);
}
@@ -55,7 +57,8 @@ int getStatus(lua_State* state) {
}
int isConnected(lua_State* state) {
lua_pushboolean(state, Runtime::from(state)->wifi().status().state == "connected");
lua_pushboolean(state,
Runtime::from(state)->wifi().status().state == "connected");
return 1;
}
@@ -70,14 +73,17 @@ int disconnect(lua_State* state) {
return 0;
}
int forget(lua_State* state) { return pushStatus(state, Runtime::from(state)->wifi().forget()); }
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.
// --- 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
@@ -101,12 +107,12 @@ const luaL_Reg FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
} // namespace
void registerWifi(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "wifi");
}
} // namespace bindings
} // namespace esp32lua
} // namespace bindings
} // namespace esp32lua
+12 -7
View File
@@ -35,10 +35,13 @@ int wasReleased(lua_State* state) {
// @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.
// @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.
// ---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.
@@ -59,12 +62,14 @@ const luaL_Reg INPUT_FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
} // namespace
void registerButtons(lua_State* state) { augmentGlobal(state, "input", INPUT_FUNCTIONS); }
void registerButtons(lua_State* state) {
augmentGlobal(state, "input", INPUT_FUNCTIONS);
}
} // namespace bindings
} // namespace esp32lua
} // namespace bindings
} // namespace esp32lua
// @lua-global
// ---Fired when a button goes down.
+13 -8
View File
@@ -9,11 +9,13 @@ int setCalibration(lua_State* state) {
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));
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;
if (!touched)
return 0;
lua_pushinteger(state, x);
lua_pushinteger(state, y);
return 2;
@@ -53,11 +55,13 @@ const luaL_Reg SETTINGS_FUNCTIONS[] = {
// @lua-augment input InputLib
const luaL_Reg INPUT_FUNCTIONS[] = {
// --- Returns the calibrated touch point, or nothing when the panel is not touched.
// --- 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.
// --- Returns the uncalibrated touch reading, or nothing when the panel is
// not touched.
// @return integer|nil x
// @return integer|nil y
{"getRawTouch", getRawTouch},
@@ -67,22 +71,23 @@ const luaL_Reg INPUT_FUNCTIONS[] = {
{nullptr, nullptr},
};
} // namespace
} // namespace
void registerTouch(lua_State* state) {
augmentGlobal(state, "settings", SETTINGS_FUNCTIONS);
augmentGlobal(state, "input", INPUT_FUNCTIONS);
}
} // namespace bindings
} // namespace esp32lua
} // 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.
// ---Fired when the finger moves while down, after the firmware's jitter
// filter.
// @param x integer
// @param y integer
// @lua-fn on_touch_move
+19 -12
View File
@@ -1,8 +1,8 @@
#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.
// 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>
@@ -29,7 +29,8 @@ inline std::string checkString(lua_State* state, int index) {
}
inline bool optionalString(lua_State* state, int index, std::string& out) {
if (lua_isnoneornil(state, index)) return false;
if (lua_isnoneornil(state, index))
return false;
out = checkString(state, index);
return true;
}
@@ -43,12 +44,14 @@ inline int32_t optionalInt(lua_State* state, int index, int32_t fallback) {
}
inline bool optionalColor(lua_State* state, int index, int32_t& out) {
if (lua_isnoneornil(state, index)) return false;
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.
// `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);
@@ -65,7 +68,8 @@ inline int pushError(lua_State* state, const std::string& error) {
return 2;
}
inline void pushStrings(lua_State* state, const std::vector<std::string>& values) {
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]);
@@ -73,7 +77,8 @@ inline void pushStrings(lua_State* state, const std::vector<std::string>& values
}
}
inline void setField(lua_State* state, const char* key, const std::string& value) {
inline void setField(lua_State* state, const char* key,
const std::string& value) {
pushString(state, value);
lua_setfield(state, -2, key);
}
@@ -88,8 +93,10 @@ inline void setField(lua_State* state, const char* key, bool 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) {
// 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);
@@ -101,5 +108,5 @@ inline void augmentGlobal(lua_State* state, const char* name, const luaL_Reg* fu
lua_pop(state, 1);
}
} // namespace bindings
} // namespace esp32lua
} // namespace bindings
} // namespace esp32lua
+68 -51
View File
@@ -1,9 +1,10 @@
#pragma once
// Painting the node tree through GuiProvider, so the same walk drives an LCD and an
// e-ink panel. A dirty node paints itself and dirties its children, because a parent's
// fill lands on top of whatever they drew; nothing tracks sub-regions, and a widget that
// wants to repaint part of itself is a CUSTOM node painting through the gui bindings.
// Painting the node tree through GuiProvider, so the same walk drives an LCD
// and an e-ink panel. A dirty node paints itself and dirties its children,
// because a parent's fill lands on top of whatever they drew; nothing tracks
// sub-regions, and a widget that wants to repaint part of itself is a CUSTOM
// node painting through the gui bindings.
#include <lua/layout.h>
#include <lua/providers.h>
@@ -11,10 +12,11 @@
namespace esp32lua {
namespace ui {
typedef void (*CustomPainter)(void* context, uint16_t id, int x, int y, int w, int h);
typedef void (*CustomPainter)(void* context, uint16_t id, int x, int y, int w,
int h);
class Painter {
public:
public:
Painter(GuiProvider& gui, Tree& tree) : gui(gui), tree(tree) {}
CustomPainter custom = nullptr;
@@ -24,51 +26,58 @@ class Painter {
if (tree.nodes[id].flags & DIRTY) {
paint(id);
tree.nodes[id].flags &= ~DIRTY;
for (uint16_t c = tree.nodes[id].first; c != NONE; c = tree.nodes[c].next) {
for (uint16_t c = tree.nodes[id].first; c != NONE;
c = tree.nodes[c].next) {
tree.nodes[c].flags |= DIRTY;
}
}
for (uint16_t c = tree.nodes[id].first; c != NONE; c = tree.nodes[c].next) draw(c);
for (uint16_t c = tree.nodes[id].first; c != NONE; c = tree.nodes[c].next)
draw(c);
}
private:
private:
GuiProvider& gui;
Tree& tree;
// What a node sits on, which is not what it fills. Derived rather than stored, because
// a node cannot be told what is behind it: a dialog layer paints nothing, so its card
// blends into the dimmed content two levels up, not into the lit palette the layer
// hands its children.
// What a node sits on, which is not what it fills. Derived rather than
// stored, because a node cannot be told what is behind it: a dialog layer
// paints nothing, so its card blends into the dimmed content two levels up,
// not into the lit palette the layer hands its children.
int32_t surfaceOf(uint16_t id) const {
for (uint16_t n = tree.nodes[id].parent; n != NONE; n = tree.nodes[n].parent) {
for (uint16_t n = tree.nodes[id].parent; n != NONE;
n = tree.nodes[n].parent) {
const Style* style = tree.styleOf(n);
if (style && (style->set & S_FILL)) return style->fill;
if (style && (style->set & S_FILL))
return style->fill;
}
uint16_t root = id;
while (tree.nodes[root].parent != NONE) root = tree.nodes[root].parent;
while (tree.nodes[root].parent != NONE)
root = tree.nodes[root].parent;
return tree.inherited(root, S_BG).bg;
}
void paint(uint16_t id) {
const Node& n = tree.nodes[id];
switch (n.type) {
case BUTTON:
paintButton(id);
break;
case TEXT:
paintText(id);
break;
case CUSTOM:
// Cleared first, because a custom painter draws what it wants and nothing knows
// what it drew last time.
gui.fillRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_BG).bg);
if (custom) custom(context, id, n.x, n.y, n.w, n.h);
break;
default:
paintBox(id);
break;
case BUTTON:
paintButton(id);
break;
case TEXT:
paintText(id);
break;
case CUSTOM:
// Cleared first, because a custom painter draws what it wants and nothing
// knows what it drew last time.
gui.fillRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_BG).bg);
if (custom)
custom(context, id, n.x, n.y, n.w, n.h);
break;
default:
paintBox(id);
break;
}
if (id == tree.focus) paintFocus(id);
if (id == tree.focus)
paintFocus(id);
}
void paintBox(uint16_t id) {
@@ -76,39 +85,44 @@ class Painter {
const Style* own = tree.styleOf(id);
const bool hasBorder = own && (own->set & S_BORDER);
const bool hasFill = own && (own->set & S_FILL);
if (!hasFill && !hasBorder) return;
if (!hasFill && !hasBorder)
return;
// Only a bordered box rounds its corners. Filling a square first would leave corners
// outside the border, and rounding an unbordered fill puts a seam where a plain panel
// background was expected.
// Only a bordered box rounds its corners. Filling a square first would
// leave corners outside the border, and rounding an unbordered fill puts a
// seam where a plain panel background was expected.
if (!hasBorder) {
gui.fillRect(n.x, n.y, n.w, n.h, own->fill);
return;
}
const int32_t fill = hasFill ? own->fill : tree.inherited(id, S_BG).bg;
gui.roundRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_RADIUS).radius, surfaceOf(id), &fill, &fill,
&own->border);
gui.roundRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_RADIUS).radius,
surfaceOf(id), &fill, &fill, &own->border);
}
void paintButton(uint16_t id) {
const Node& n = tree.nodes[id];
const bool pressed = (n.flags & PRESSED) != 0;
const Style& faceStyle = tree.inherited(id, pressed ? S_PRESSED_FACE : S_FACE);
const Style& faceStyle =
tree.inherited(id, pressed ? S_PRESSED_FACE : S_FACE);
const int32_t face = pressed ? faceStyle.pressedFace : faceStyle.face;
const int32_t radius = tree.inherited(id, S_RADIUS).radius;
const Style* own = tree.styleOf(id);
const bool hasBorder = own && (own->set & S_BORDER);
gui.roundRect(n.x, n.y, n.w, n.h, radius, surfaceOf(id), &face, &face, hasBorder ? &own->border : nullptr);
gui.roundRect(n.x, n.y, n.w, n.h, radius, surfaceOf(id), &face, &face,
hasBorder ? &own->border : nullptr);
}
// Glyphs over a button are transparent: an opaque fill is one flat colour, and the
// face is repainted whenever it changes, so the label has nothing to erase. Elsewhere
// the whole box is cleared, because a label replaced by a shorter one would otherwise
// leave the tail of the old text standing next to the new.
// Glyphs over a button are transparent: an opaque fill is one flat colour,
// and the face is repainted whenever it changes, so the label has nothing to
// erase. Elsewhere the whole box is cleared, because a label replaced by a
// shorter one would otherwise leave the tail of the old text standing next to
// the new.
void paintText(uint16_t id) {
const Node& n = tree.nodes[id];
const char* label = tree.label(id);
if (!label) return;
if (!label)
return;
const uint16_t parent = n.parent;
const bool onButton = parent != NONE && tree.nodes[parent].type == BUTTON;
@@ -117,7 +131,9 @@ class Painter {
const int32_t textStyle = tree.inherited(id, S_TEXT_STYLE).textStyle;
if (pressed) {
gui.drawText(font, n.x, n.y, label, tree.inherited(id, S_PRESSED_COLOR).pressedColor, textStyle, nullptr);
gui.drawText(font, n.x, n.y, label,
tree.inherited(id, S_PRESSED_COLOR).pressedColor, textStyle,
nullptr);
return;
}
const int32_t color = tree.inherited(id, S_COLOR).color;
@@ -134,11 +150,12 @@ class Painter {
void paintFocus(uint16_t id) {
const Node& n = tree.nodes[id];
const Style& style = tree.inherited(id, S_FOCUS_COLOR);
if (!(style.set & S_FOCUS_COLOR)) return;
gui.roundRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_RADIUS).radius, surfaceOf(id), nullptr, nullptr,
&style.focusColor);
if (!(style.set & S_FOCUS_COLOR))
return;
gui.roundRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_RADIUS).radius,
surfaceOf(id), nullptr, nullptr, &style.focusColor);
}
};
} // namespace ui
} // namespace esp32lua
} // namespace ui
} // namespace esp32lua
+39 -25
View File
@@ -1,8 +1,9 @@
// Loading Lua off the SD card. Lua's stock loaders go through stdio, which cannot see the
// mount, so every path into the filesystem goes through FsProvider instead.
// Loading Lua off the SD card. Lua's stock loaders go through stdio, which
// cannot see the mount, so every path into the filesystem goes through
// FsProvider instead.
#include <lua/runtime.h>
#include <lua/embedded_modules.h>
#include <lua/runtime.h>
#include <cstring>
@@ -21,12 +22,14 @@ struct ChunkReader {
const char* readChunk(lua_State*, void* context, size_t* size) {
ChunkReader* reader = static_cast<ChunkReader*>(context);
const int32_t read = reader->file->read(reader->buffer, sizeof(reader->buffer));
const int32_t read =
reader->file->read(reader->buffer, sizeof(reader->buffer));
*size = read > 0 ? static_cast<size_t>(read) : 0;
return read > 0 ? reader->buffer : nullptr;
}
// Loads a path onto the stack as a chunk, or pushes nothing and returns a Lua status.
// Loads a path onto the stack as a chunk, or pushes nothing and returns a Lua
// status.
int load(lua_State* state, FsProvider& fs, const std::string& path) {
ChunkReader reader;
reader.file = fs.openRead(path);
@@ -35,24 +38,26 @@ int load(lua_State* state, FsProvider& fs, const std::string& path) {
return LUA_ERRFILE;
}
const std::string chunkname = "@" + path;
const int status = lua_load(state, readChunk, &reader, chunkname.c_str(), "t");
const int status =
lua_load(state, readChunk, &reader, chunkname.c_str(), "t");
delete reader.file;
return status;
}
} // namespace
} // namespace
bool Runtime::loadScript(const std::string& path) {
return load(state_, *providers_.fs, path) == LUA_OK;
}
// Resolves a module name against package.path, reporting every path tried the way the stock
// searcher does.
// Resolves a module name against package.path, reporting every path tried the
// way the stock searcher does.
int Runtime::searchModule(lua_State* state) {
Runtime* runtime = Runtime::from(state);
std::string name = luaL_checkstring(state, 1);
for (size_t at = 0; at < name.size(); at++) {
if (name[at] == '.') name[at] = '/';
if (name[at] == '.')
name[at] = '/';
}
lua_getglobal(state, "package");
@@ -64,19 +69,23 @@ int Runtime::searchModule(lua_State* state) {
size_t start = 0;
while (start <= templates.size()) {
const size_t end = templates.find(';', start);
std::string candidate = templates.substr(start, end == std::string::npos ? std::string::npos : end - start);
std::string candidate = templates.substr(
start, end == std::string::npos ? std::string::npos : end - start);
start = end == std::string::npos ? templates.size() + 1 : end + 1;
if (candidate.empty()) continue;
if (candidate.empty())
continue;
const size_t mark = candidate.find('?');
if (mark != std::string::npos) candidate.replace(mark, 1, name);
if (mark != std::string::npos)
candidate.replace(mark, 1, name);
if (!runtime->providers_.fs->exists(candidate)) {
tried += "\n\tno file '" + candidate + "'";
continue;
}
if (load(state, *runtime->providers_.fs, candidate) != LUA_OK) {
return luaL_error(state, "error loading module '%s' from '%s':\n\t%s", luaL_checkstring(state, 1),
candidate.c_str(), lua_tostring(state, -1));
return luaL_error(state, "error loading module '%s' from '%s':\n\t%s",
luaL_checkstring(state, 1), candidate.c_str(),
lua_tostring(state, -1));
}
lua_pushstring(state, candidate.c_str());
return 2;
@@ -88,11 +97,14 @@ int Runtime::searchModule(lua_State* state) {
int Runtime::searchEmbedded(lua_State* state) {
const char* name = luaL_checkstring(state, 1);
for (size_t i = 0; i < embedded_modules_count; i++) {
if (std::strcmp(name, embedded_modules[i].name) != 0) continue;
if (std::strcmp(name, embedded_modules[i].name) != 0)
continue;
const std::string chunkname = std::string("=") + name;
if (luaL_loadbuffer(state, reinterpret_cast<const char*>(embedded_modules[i].data),
embedded_modules[i].size, chunkname.c_str()) != LUA_OK) {
return luaL_error(state, "error loading embedded module '%s': %s", name, lua_tostring(state, -1));
if (luaL_loadbuffer(
state, reinterpret_cast<const char*>(embedded_modules[i].data),
embedded_modules[i].size, chunkname.c_str()) != LUA_OK) {
return luaL_error(state, "error loading embedded module '%s': %s", name,
lua_tostring(state, -1));
}
lua_pushstring(state, name);
return 2;
@@ -103,7 +115,9 @@ int Runtime::searchEmbedded(lua_State* state) {
int Runtime::loadFile(lua_State* state) {
Runtime* runtime = Runtime::from(state);
if (load(state, *runtime->providers_.fs, luaL_checkstring(state, 1)) == LUA_OK) return 1;
if (load(state, *runtime->providers_.fs, luaL_checkstring(state, 1)) ==
LUA_OK)
return 1;
lua_pushnil(state);
lua_insert(state, -2);
return 2;
@@ -116,13 +130,13 @@ void Runtime::installLoader(const std::string& appDir) {
lua_pushlstring(state_, path.data(), path.size());
lua_setfield(state_, -2, "path");
// Keep the preload searcher, drop the C loaders: they can only report misleading errors
// about shared objects that were never there.
// Keep the preload searcher, drop the C loaders: they can only report
// misleading errors about shared objects that were never there.
lua_getfield(state_, -1, "searchers");
lua_pushcfunction(state_, searchModule);
lua_rawseti(state_, -2, 2);
// Embedded bytecode is the fallback after the SD card, so a local /.lua/lib/ui.lua shadows
// the packaged one without reflashing.
// Embedded bytecode is the fallback after the SD card, so a local
// /.lua/lib/ui.lua shadows the packaged one without reflashing.
lua_pushcfunction(state_, searchEmbedded);
lua_rawseti(state_, -2, 3);
lua_pushnil(state_);
@@ -133,4 +147,4 @@ void Runtime::installLoader(const std::string& appDir) {
lua_setglobal(state_, "loadfile");
}
} // namespace esp32lua
} // namespace esp32lua
+117 -62
View File
@@ -20,39 +20,48 @@ void registerSys(lua_State* state);
void registerTimer(lua_State* state);
void registerTouch(lua_State* state);
void registerWifi(lua_State* state);
} // namespace bindings
} // namespace bindings
namespace {
// App routes are relative and stay inside the apps root, so a traversal component is a hard no.
// App routes are relative and stay inside the apps root, so a traversal
// component is a hard no.
bool isSafeRoute(const std::string& path) {
if (path.empty() || path[0] == '/') return false;
if (path.empty() || path[0] == '/')
return false;
size_t start = 0;
while (start <= path.size()) {
const size_t end = path.find('/', start);
const std::string part = path.substr(start, end == std::string::npos ? std::string::npos : end - start);
if (part.empty() || part == "." || part == "..") return false;
if (end == std::string::npos) break;
const std::string part = path.substr(
start, end == std::string::npos ? std::string::npos : end - start);
if (part.empty() || part == "." || part == "..")
return false;
if (end == std::string::npos)
break;
start = end + 1;
}
return true;
}
} // namespace
} // namespace
Runtime::Runtime(const Providers& providers, const Paths& paths) : providers_(providers), paths_(paths) {}
Runtime::Runtime(const Providers& providers, const Paths& paths)
: providers_(providers), paths_(paths) {}
Runtime::~Runtime() { close(); }
bool Runtime::open() {
if (state_) return true;
if (!providers_.log || !providers_.settings || !providers_.sys || !providers_.fs || !providers_.gui ||
!providers_.http || !providers_.timer || !providers_.wifi || !providers_.ble) {
if (state_)
return true;
if (!providers_.log || !providers_.settings || !providers_.sys ||
!providers_.fs || !providers_.gui || !providers_.http ||
!providers_.timer || !providers_.wifi || !providers_.ble) {
return false;
}
state_ = luaL_newstate();
if (!state_) return false;
if (!state_)
return false;
*static_cast<Runtime**>(lua_getextraspace(state_)) = this;
luaL_openlibs(state_);
@@ -67,57 +76,74 @@ bool Runtime::open() {
bindings::registerTimer(state_);
bindings::registerWifi(state_);
// Feature namespaces extend the tables the core registrations just created, so they
// always follow them.
if (providers_.touch) bindings::registerTouch(state_);
if (providers_.buttons) bindings::registerButtons(state_);
// Feature namespaces extend the tables the core registrations just created,
// so they always follow them.
if (providers_.touch)
bindings::registerTouch(state_);
if (providers_.buttons)
bindings::registerButtons(state_);
return true;
}
void Runtime::close() {
if (!state_) return;
if (!state_)
return;
cancelAllTimers();
lua_close(state_);
state_ = nullptr;
// Node handles mean nothing to the next lua_State, so an app that inherited the previous
// tree would build onto its nodes.
// Node handles mean nothing to the next lua_State, so an app that inherited
// the previous tree would build onto its nodes.
tree_.reset();
appPath_.clear();
appTitle_.clear();
}
Runtime::Batch::Batch(Runtime& runtime) : runtime_(runtime) { runtime_.batchDepth_++; }
Runtime::Batch::Batch(Runtime& runtime) : runtime_(runtime) {
runtime_.batchDepth_++;
}
Runtime::Batch::~Batch() {
if (--runtime_.batchDepth_ == 0) runtime_.providers_.gui->commit();
if (--runtime_.batchDepth_ == 0)
runtime_.providers_.gui->commit();
}
bool Runtime::beginCall(const char* name) {
lua_getglobal(state_, name);
if (lua_isfunction(state_, -1)) return true;
if (lua_isfunction(state_, -1))
return true;
lua_pop(state_, 1);
return false;
}
bool Runtime::finishCall(const char* name, int argc) {
if (lua_pcall(state_, argc, 0, 0) == LUA_OK) return true;
if (lua_pcall(state_, argc, 0, 0) == LUA_OK)
return true;
const char* message = lua_tostring(state_, -1);
providers_.log->write(LogLevel::Error, std::string(name) + ": " + (message ? message : "failed"));
providers_.log->write(LogLevel::Error, std::string(name) + ": " +
(message ? message : "failed"));
lua_pop(state_, 1);
return false;
}
// @lua-global core/runtime
// @lua-preamble -- Runtime layout:
// @lua-preamble -- /.lua/apps/<AppId>/main.lua application entry point
// @lua-preamble -- /.lua/apps/<AppId>/<Subapp>/main.lua nested route, omitted from the launcher
// @lua-preamble -- /.lua/data/<AppId>/ persistent app data, preserved across updates
// @lua-preamble -- /.lua/lib/<module>.lua shared require() modules
// @lua-preamble -- require() also searches the running application's directory
// @lua-preamble -- /.lua/apps/<AppId>/main.lua application entry
// point
// @lua-preamble -- /.lua/apps/<AppId>/<Subapp>/main.lua nested route,
// omitted from the launcher
// @lua-preamble -- /.lua/data/<AppId>/ persistent app
// data, preserved across updates
// @lua-preamble -- /.lua/lib/<module>.lua shared require()
// modules
// @lua-preamble -- require() also searches the running application's
// directory
// @lua-preamble --
// @lua-preamble -- The firmware does not clear the frame before calling draw(), and commits changed
// @lua-preamble -- display content after each callback batch using the panel's own refresh policy.
// @lua-preamble -- Timer callbacks are registered directly with timer.after/every.
// @lua-preamble -- The firmware does not clear the frame before calling draw(),
// and commits changed
// @lua-preamble -- display content after each callback batch using the panel's
// own refresh policy.
// @lua-preamble -- Timer callbacks are registered directly with
// timer.after/every.
// ---Required. Runs once before the first draw; failing here stops the app.
// @param arg string|nil The string passed to sys.launch or sys.replace.
@@ -132,32 +158,41 @@ bool Runtime::callInit(const std::string& arg) {
return finishCall("init", 1);
}
// ---Optional frame loop, called once after init and then at most 30 FPS, best effort.
// @param deltaMs integer Monotonic milliseconds since the previous draw; zero on the first.
// ---Optional frame loop, called once after init and then at most 30 FPS, best
// effort.
// @param deltaMs integer Monotonic milliseconds since the previous draw; zero
// on the first.
// @lua-fn draw
void Runtime::callDraw(int32_t deltaMs) {
const Batch batch(*this);
if (!beginCall("draw")) return;
if (!beginCall("draw"))
return;
lua_pushinteger(state_, deltaMs);
finishCall("draw", 1);
}
void Runtime::callTouch(TouchPhase phase, int32_t x, int32_t y) {
if (!providers_.touch) {
providers_.log->write(LogLevel::Error, "callTouch without a touch provider");
providers_.log->write(LogLevel::Error,
"callTouch without a touch provider");
return;
}
const Batch batch(*this);
const char* name = phase == TouchPhase::Down ? "on_touch_down" : (phase == TouchPhase::Move ? "on_touch_move"
: "on_touch_up");
const char* name =
phase == TouchPhase::Down
? "on_touch_down"
: (phase == TouchPhase::Move ? "on_touch_move" : "on_touch_up");
for (int pass = 0; pass < 2; pass++) {
// The tap alias is ordering, not policy: a release always fires on_touch_up and then
// on_touch, so both firmwares agree without either of them deciding anything.
// The tap alias is ordering, not policy: a release always fires on_touch_up
// and then on_touch, so both firmwares agree without either of them
// deciding anything.
if (pass == 1) {
if (phase != TouchPhase::Up) return;
if (phase != TouchPhase::Up)
return;
name = "on_touch";
}
if (!beginCall(name)) continue;
if (!beginCall(name))
continue;
lua_pushinteger(state_, x);
lua_pushinteger(state_, y);
finishCall(name, 2);
@@ -166,17 +201,20 @@ void Runtime::callTouch(TouchPhase phase, int32_t x, int32_t y) {
void Runtime::callButton(const std::string& button, bool pressed) {
if (!providers_.buttons) {
providers_.log->write(LogLevel::Error, "callButton without a buttons provider");
providers_.log->write(LogLevel::Error,
"callButton without a buttons provider");
return;
}
const Batch batch(*this);
const char* name = pressed ? "on_button_down" : "on_button_up";
for (int pass = 0; pass < 2; pass++) {
if (pass == 1) {
if (pressed) return;
if (pressed)
return;
name = "on_button";
}
if (!beginCall(name)) continue;
if (!beginCall(name))
continue;
lua_pushlstring(state_, button.data(), button.size());
finishCall(name, 1);
}
@@ -196,7 +234,8 @@ TimerId Runtime::addTimer(int callbackRef, int32_t intervalMs, bool repeating) {
bool Runtime::cancelTimer(TimerId id) {
const std::map<TimerId, Timer>::iterator found = timers_.find(id);
if (found == timers_.end()) return false;
if (found == timers_.end())
return false;
providers_.timer->cancel(id);
luaL_unref(state_, LUA_REGISTRYINDEX, found->second.callbackRef);
timers_.erase(found);
@@ -205,25 +244,31 @@ bool Runtime::cancelTimer(TimerId id) {
void Runtime::callTimer(TimerId id) {
const std::map<TimerId, Timer>::iterator found = timers_.find(id);
if (found == timers_.end()) return;
if (found == timers_.end())
return;
const Batch batch(*this);
const int callbackRef = found->second.callbackRef;
const bool repeating = found->second.repeating;
// A one-shot is forgotten before it runs, so a callback that cancels itself or starts a
// new timer sees a consistent table.
if (!repeating) timers_.erase(found);
// A one-shot is forgotten before it runs, so a callback that cancels itself
// or starts a new timer sees a consistent table.
if (!repeating)
timers_.erase(found);
lua_rawgeti(state_, LUA_REGISTRYINDEX, callbackRef);
if (lua_pcall(state_, 0, 0, 0) != LUA_OK) {
providers_.log->write(LogLevel::Error, lua_tostring(state_, -1) ? lua_tostring(state_, -1) : "timer failed");
providers_.log->write(LogLevel::Error, lua_tostring(state_, -1)
? lua_tostring(state_, -1)
: "timer failed");
lua_pop(state_, 1);
}
if (!repeating) luaL_unref(state_, LUA_REGISTRYINDEX, callbackRef);
if (!repeating)
luaL_unref(state_, LUA_REGISTRYINDEX, callbackRef);
}
void Runtime::cancelAllTimers() {
for (std::map<TimerId, Timer>::iterator it = timers_.begin(); it != timers_.end(); ++it) {
for (std::map<TimerId, Timer>::iterator it = timers_.begin();
it != timers_.end(); ++it) {
providers_.timer->cancel(it->first);
luaL_unref(state_, LUA_REGISTRYINDEX, it->second.callbackRef);
}
@@ -238,8 +283,10 @@ std::string Runtime::appId() const {
std::string Runtime::appDataPath() const { return paths_.data + "/" + appId(); }
bool Runtime::hasFeature(const std::string& feature) const {
if (feature == "touch") return providers_.touch != nullptr;
if (feature == "buttons") return providers_.buttons != nullptr;
if (feature == "touch")
return providers_.touch != nullptr;
if (feature == "buttons")
return providers_.buttons != nullptr;
return false;
}
@@ -250,18 +297,22 @@ bool Runtime::startApp(const std::string& path, const std::string& arg) {
}
close();
if (!open()) return false;
if (!open())
return false;
appPath_ = path;
appTitle_ = appId();
const std::string directory = paths_.apps + "/" + path;
installLoader(directory);
if (!loadScript(directory + "/main.lua")) {
providers_.log->write(LogLevel::Error, lua_tostring(state_, -1) ? lua_tostring(state_, -1) : "load failed");
providers_.log->write(LogLevel::Error, lua_tostring(state_, -1)
? lua_tostring(state_, -1)
: "load failed");
close();
return false;
}
// The chunk body runs first, then init(), so an app that fails either way leaves nothing behind.
// The chunk body runs first, then init(), so an app that fails either way
// leaves nothing behind.
if (!finishCall("main.lua", 0) || !callInit(arg)) {
close();
return false;
@@ -269,7 +320,8 @@ bool Runtime::startApp(const std::string& path, const std::string& arg) {
return true;
}
void Runtime::requestLaunch(const std::string& path, const std::string& arg, bool replace) {
void Runtime::requestLaunch(const std::string& path, const std::string& arg,
bool replace) {
pending_.kind = replace ? Pending::Replace : Pending::Launch;
pending_.route.path = path;
pending_.route.arg = arg;
@@ -283,7 +335,8 @@ void Runtime::requestBack() {
bool Runtime::applyPendingNavigation() {
const Pending pending = pending_;
pending_ = Pending();
if (pending.kind == Pending::None) return hasApp();
if (pending.kind == Pending::None)
return hasApp();
if (pending.kind == Pending::Back) {
// An empty history means the launcher, which is an app like any other.
@@ -304,6 +357,8 @@ bool Runtime::applyPendingNavigation() {
return startApp(pending.route.path, pending.route.arg);
}
Runtime* Runtime::from(lua_State* state) { return *static_cast<Runtime**>(lua_getextraspace(state)); }
Runtime* Runtime::from(lua_State* state) {
return *static_cast<Runtime**>(lua_getextraspace(state));
}
} // namespace esp32lua
} // namespace esp32lua