chore: lsp + format
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
BasedOnStyle: LLVM
|
||||
DerivePointerAlignment: false
|
||||
PointerAlignment: Left
|
||||
@@ -0,0 +1,2 @@
|
||||
native/src/embedded_modules.cpp
|
||||
native/src/vendor/**
|
||||
@@ -1 +1,3 @@
|
||||
_build/
|
||||
__pycache__
|
||||
/compile_commands.json
|
||||
|
||||
@@ -14,7 +14,7 @@ LUAC32 := _build/luac32
|
||||
RUNTIME_CPP := $(sort $(wildcard native/src/runtime/*.cpp native/src/bindings/core/*.cpp \
|
||||
native/src/bindings/features/*.cpp) native/src/embedded_modules.cpp)
|
||||
|
||||
.PHONY: api test embed
|
||||
.PHONY: api test embed compiledb
|
||||
|
||||
embed: $(LUAC32)
|
||||
@$(PYTHON) tools/embed.py $(LUAC32) lua/lib native/src/embedded_modules.cpp
|
||||
@@ -22,7 +22,11 @@ embed: $(LUAC32)
|
||||
api:
|
||||
@$(PYTHON) tools/gen_api.py
|
||||
|
||||
compiledb:
|
||||
@bear -- $(MAKE) -B $(RUNTIME_TEST) $(LUA_SMOKE) $(LUAC32)
|
||||
|
||||
test: $(LUA_SMOKE) $(RUNTIME_TEST) $(LUAC32)
|
||||
@printf '%-32s ' tools/test_gen_api.py; $(PYTHON) tools/test_gen_api.py
|
||||
@$(PYTHON) tools/gen_api.py --check
|
||||
@printf '%-32s ' generated-api; echo ok
|
||||
@$(PYTHON) tools/embed.py $(LUAC32) lua/lib native/src/embedded_modules.cpp --check
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bear
|
||||
gnumake
|
||||
lua5_4
|
||||
python3
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
namespace esp32lua {
|
||||
|
||||
// A platform module compiled to bytecode and linked into the firmware. The module searcher
|
||||
// tries the SD card first, so a local copy shadows the packaged one.
|
||||
// A platform module compiled to bytecode and linked into the firmware. The
|
||||
// module searcher tries the SD card first, so a local copy shadows the packaged
|
||||
// one.
|
||||
struct EmbeddedModule {
|
||||
const char* name;
|
||||
const unsigned char* data;
|
||||
@@ -15,4 +16,4 @@ struct EmbeddedModule {
|
||||
extern const EmbeddedModule embedded_modules[];
|
||||
extern const size_t embedded_modules_count;
|
||||
|
||||
} // namespace esp32lua
|
||||
} // namespace esp32lua
|
||||
|
||||
+143
-108
@@ -1,15 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
// The widget tree: structure, block-flow layout, labels and style, over a flat node
|
||||
// arena. Free of Arduino headers so test/ui_layout_test.cpp can exercise it on the host.
|
||||
// Ported from the Lua toolkit's Component:measure/place, whose semantics the tests
|
||||
// still describe.
|
||||
// The widget tree: structure, block-flow layout, labels and style, over a flat
|
||||
// node arena. Free of Arduino headers so test/ui_layout_test.cpp can exercise
|
||||
// it on the host. Ported from the Lua toolkit's Component:measure/place, whose
|
||||
// semantics the tests still describe.
|
||||
//
|
||||
// The tree is split across two arenas because the halves have different lifetimes. A
|
||||
// Node holds what hit testing and repainting need forever: 16 bytes. A Spec holds what
|
||||
// only measure() and place() read -- requested sizes, padding, gap, alignment -- and is
|
||||
// dropped when the pass ends. Re-layout rebuilds from Lua rather than retaining ~20
|
||||
// bytes per node against a rotation nobody measures in milliseconds.
|
||||
// The tree is split across two arenas because the halves have different
|
||||
// lifetimes. A Node holds what hit testing and repainting need forever: 16
|
||||
// bytes. A Spec holds what only measure() and place() read -- requested sizes,
|
||||
// padding, gap, alignment -- and is dropped when the pass ends. Re-layout
|
||||
// rebuilds from Lua rather than retaining ~20 bytes per node against a rotation
|
||||
// nobody measures in milliseconds.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -22,17 +23,17 @@ namespace ui {
|
||||
|
||||
constexpr uint16_t NONE = 0xFFFF;
|
||||
|
||||
// Distinguishes "no constraint" from a real zero, which a plain int cannot: an auto-sized
|
||||
// parent genuinely has no width to hand down, and a child asking for a fraction of it is
|
||||
// an error rather than a zero-width silence.
|
||||
// Distinguishes "no constraint" from a real zero, which a plain int cannot: an
|
||||
// auto-sized parent genuinely has no width to hand down, and a child asking for
|
||||
// a fraction of it is an error rather than a zero-width silence.
|
||||
constexpr int UNKNOWN = INT32_MIN;
|
||||
|
||||
enum Type : uint8_t { BOX, TEXT, BUTTON, CUSTOM };
|
||||
|
||||
enum Flag : uint8_t {
|
||||
ROW = 1 << 0, // main axis is horizontal
|
||||
CAPTURE = 1 << 1, // swallows the taps its children missed
|
||||
INTERACTIVE = 1 << 2, // has an on_press
|
||||
ROW = 1 << 0, // main axis is horizontal
|
||||
CAPTURE = 1 << 1, // swallows the taps its children missed
|
||||
INTERACTIVE = 1 << 2, // has an on_press
|
||||
DIRTY = 1 << 3,
|
||||
PRESSED = 1 << 4,
|
||||
};
|
||||
@@ -40,14 +41,15 @@ enum Flag : uint8_t {
|
||||
enum SizeMode : uint8_t { AUTO, PX, FRACTION, FILL };
|
||||
enum Align : uint8_t { START, CENTER, END, BETWEEN };
|
||||
|
||||
// Which roles a style states for itself. Anything unset is answered by the nearest
|
||||
// ancestor that does state it, so styling stays in one place and a node that names no
|
||||
// colours costs no bytes at all.
|
||||
// Which roles a style states for itself. Anything unset is answered by the
|
||||
// nearest ancestor that does state it, so styling stays in one place and a node
|
||||
// that names no colours costs no bytes at all.
|
||||
enum StyleField : uint16_t {
|
||||
S_COLOR = 1 << 0,
|
||||
// The background a node offers its descendants to draw text on, which is not the same
|
||||
// as the fill it paints: a dialog layer hands the lit palette down while painting
|
||||
// nothing itself. What is physically behind a node is derived at paint time, never set.
|
||||
// The background a node offers its descendants to draw text on, which is not
|
||||
// the same as the fill it paints: a dialog layer hands the lit palette down
|
||||
// while painting nothing itself. What is physically behind a node is derived
|
||||
// at paint time, never set.
|
||||
S_BG = 1 << 1,
|
||||
S_FILL = 1 << 2,
|
||||
S_BORDER = 1 << 3,
|
||||
@@ -76,8 +78,8 @@ struct Style {
|
||||
uint16_t set = 0;
|
||||
};
|
||||
|
||||
// FRACTION is per-mille rather than a float: 0.85 of 320 is 272 either way, and the
|
||||
// firmware has no business rounding differently to the host.
|
||||
// FRACTION is per-mille rather than a float: 0.85 of 320 is 272 either way, and
|
||||
// the firmware has no business rounding differently to the host.
|
||||
struct Size {
|
||||
SizeMode mode = AUTO;
|
||||
int16_t value = 0;
|
||||
@@ -93,8 +95,8 @@ struct Size {
|
||||
static Size fill() { return make(FILL, 0); }
|
||||
};
|
||||
|
||||
// Persistent. w/h hold the measured size between measure() and place(), and the final
|
||||
// rect afterwards, because the two are never needed at once.
|
||||
// Persistent. w/h hold the measured size between measure() and place(), and the
|
||||
// final rect afterwards, because the two are never needed at once.
|
||||
struct Node {
|
||||
int16_t x = 0, y = 0, w = 0, h = 0;
|
||||
uint16_t first = NONE, next = NONE, parent = NONE;
|
||||
@@ -107,21 +109,23 @@ struct Spec {
|
||||
Size w, h;
|
||||
Size atX, atY;
|
||||
bool absolute = false;
|
||||
int16_t intrinsicW = 0, intrinsicH = 0; // content size of a leaf, e.g. a text run
|
||||
int16_t intrinsicW = 0,
|
||||
intrinsicH = 0; // content size of a leaf, e.g. a text run
|
||||
uint8_t padT = 0, padR = 0, padB = 0, padL = 0;
|
||||
uint8_t gap = 0;
|
||||
Align align = START;
|
||||
Align justify = START;
|
||||
uint16_t last = NONE; // tail of the child list, so append is not a walk
|
||||
uint16_t last = NONE; // tail of the child list, so append is not a walk
|
||||
};
|
||||
|
||||
// Resistive panels land a few pixels off, so a hit box is larger than what was painted.
|
||||
// Resistive panels land a few pixels off, so a hit box is larger than what was
|
||||
// painted.
|
||||
constexpr int SLOP = 4;
|
||||
|
||||
constexpr uint16_t NO_LABEL = 0xFFFF;
|
||||
|
||||
class Tree {
|
||||
public:
|
||||
public:
|
||||
std::vector<Node> nodes;
|
||||
std::vector<Spec> specs;
|
||||
const char* error = nullptr;
|
||||
@@ -137,11 +141,14 @@ class Tree {
|
||||
error = nullptr;
|
||||
}
|
||||
|
||||
uint16_t add(uint16_t parent, const Spec& spec, uint8_t type = BOX, uint8_t flags = 0) {
|
||||
// A tree whose scratch has been dropped cannot be extended: its layout inputs are
|
||||
// gone, so building again is a new screen by definition. Self-healing rather than
|
||||
// advisory, because the alternative is a spec list that no longer indexes the nodes.
|
||||
if (specs.size() != nodes.size()) reset();
|
||||
uint16_t add(uint16_t parent, const Spec& spec, uint8_t type = BOX,
|
||||
uint8_t flags = 0) {
|
||||
// A tree whose scratch has been dropped cannot be extended: its layout
|
||||
// inputs are gone, so building again is a new screen by definition.
|
||||
// Self-healing rather than advisory, because the alternative is a spec list
|
||||
// that no longer indexes the nodes.
|
||||
if (specs.size() != nodes.size())
|
||||
reset();
|
||||
uint16_t id = static_cast<uint16_t>(nodes.size());
|
||||
nodes.push_back(Node());
|
||||
specs.push_back(spec);
|
||||
@@ -149,13 +156,15 @@ class Tree {
|
||||
nodes[id].type = type;
|
||||
nodes[id].flags = flags;
|
||||
nodes[id].parent = parent;
|
||||
if (parent != NONE) attach(parent, id);
|
||||
if (parent != NONE)
|
||||
attach(parent, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
// Lua evaluates inner constructors first, so a child exists before the box that holds
|
||||
// it. Adopting afterwards is what lets each spec table be garbage the moment its node
|
||||
// is created, instead of a whole screen's worth of them living until the end of a build.
|
||||
// Lua evaluates inner constructors first, so a child exists before the box
|
||||
// that holds it. Adopting afterwards is what lets each spec table be garbage
|
||||
// the moment its node is created, instead of a whole screen's worth of them
|
||||
// living until the end of a build.
|
||||
void attach(uint16_t parent, uint16_t child) {
|
||||
nodes[child].parent = parent;
|
||||
uint16_t tail = specs[parent].last;
|
||||
@@ -170,42 +179,49 @@ class Tree {
|
||||
bool layout(uint16_t root, int x, int y, int w, int h) {
|
||||
error = nullptr;
|
||||
measure(root, w, h);
|
||||
if (error) return false;
|
||||
if (error)
|
||||
return false;
|
||||
place(root, x, y, w, h);
|
||||
return error == nullptr;
|
||||
}
|
||||
|
||||
// Deepest interactive node wins, so a tappable child beats its tappable parent. The
|
||||
// list is singly linked, so "last match walking forward" stands in for "first match
|
||||
// walking backward"; they name the same node.
|
||||
// Deepest interactive node wins, so a tappable child beats its tappable
|
||||
// parent. The list is singly linked, so "last match walking forward" stands
|
||||
// in for "first match walking backward"; they name the same node.
|
||||
uint16_t hit(uint16_t id, int px, int py) const {
|
||||
const Node& n = nodes[id];
|
||||
if (px < n.x - SLOP || px >= n.x + n.w + SLOP) return NONE;
|
||||
if (py < n.y - SLOP || py >= n.y + n.h + SLOP) return NONE;
|
||||
if (px < n.x - SLOP || px >= n.x + n.w + SLOP)
|
||||
return NONE;
|
||||
if (py < n.y - SLOP || py >= n.y + n.h + SLOP)
|
||||
return NONE;
|
||||
|
||||
uint16_t found = NONE;
|
||||
for (uint16_t c = n.first; c != NONE; c = nodes[c].next) {
|
||||
uint16_t inner = hit(c, px, py);
|
||||
if (inner != NONE) found = inner;
|
||||
if (inner != NONE)
|
||||
found = inner;
|
||||
}
|
||||
if (found != NONE) return found;
|
||||
if (n.flags & CAPTURE) return id;
|
||||
if (found != NONE)
|
||||
return found;
|
||||
if (n.flags & CAPTURE)
|
||||
return id;
|
||||
return (n.flags & INTERACTIVE) ? id : NONE;
|
||||
}
|
||||
|
||||
// Layout inputs are dead once place() has run. Callers drop them here rather than
|
||||
// carrying ~20 bytes a node for the lifetime of a screen.
|
||||
// Layout inputs are dead once place() has run. Callers drop them here rather
|
||||
// than carrying ~20 bytes a node for the lifetime of a screen.
|
||||
void dropScratch() {
|
||||
specs.clear();
|
||||
specs.shrink_to_fit();
|
||||
}
|
||||
|
||||
// Labels share one NUL-separated arena, so a text node costs two bytes plus its
|
||||
// characters rather than a string object.
|
||||
// Labels share one NUL-separated arena, so a text node costs two bytes plus
|
||||
// its characters rather than a string object.
|
||||
//
|
||||
// ponytail: a longer label appends and abandons the old bytes. The clock repaints
|
||||
// every second at a fixed width, which overwrites in place, so the arena only grows
|
||||
// when a label genuinely gets longer. Compact on rebuild if some app proves otherwise.
|
||||
// ponytail: a longer label appends and abandons the old bytes. The clock
|
||||
// repaints every second at a fixed width, which overwrites in place, so the
|
||||
// arena only grows when a label genuinely gets longer. Compact on rebuild if
|
||||
// some app proves otherwise.
|
||||
void setLabel(uint16_t id, const char* text) {
|
||||
size_t length = strlen(text);
|
||||
uint16_t at = labelAt[id];
|
||||
@@ -223,14 +239,14 @@ class Tree {
|
||||
}
|
||||
|
||||
size_t footprint() const {
|
||||
return nodes.size() * sizeof(Node) + labelAt.size() * sizeof(uint16_t) + labels.size() +
|
||||
styles.size() * sizeof(styles[0]);
|
||||
return nodes.size() * sizeof(Node) + labelAt.size() * sizeof(uint16_t) +
|
||||
labels.size() + styles.size() * sizeof(styles[0]);
|
||||
}
|
||||
|
||||
// Styles are sparse because inheritance means almost every node states nothing: a
|
||||
// screen's root carries the palette and a handful of nodes override one role. Ids are
|
||||
// handed out in increasing order during a build, so appends keep the list sorted and
|
||||
// lookup is a binary search.
|
||||
// Styles are sparse because inheritance means almost every node states
|
||||
// nothing: a screen's root carries the palette and a handful of nodes
|
||||
// override one role. Ids are handed out in increasing order during a build,
|
||||
// so appends keep the list sorted and lookup is a binary search.
|
||||
Style& styleFor(uint16_t id) {
|
||||
size_t low = 0, high = styles.size();
|
||||
while (low < high) {
|
||||
@@ -241,17 +257,21 @@ class Tree {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
if (low < styles.size() && styles[low].first == id) return styles[low].second;
|
||||
return styles.insert(styles.begin() + low, std::make_pair(id, Style()))->second;
|
||||
if (low < styles.size() && styles[low].first == id)
|
||||
return styles[low].second;
|
||||
return styles.insert(styles.begin() + low, std::make_pair(id, Style()))
|
||||
->second;
|
||||
}
|
||||
|
||||
// The nearest style at or above `id` that states `field`, or a default one if nothing
|
||||
// does. Returning the whole style lets a caller read the pair a role comes in.
|
||||
// The nearest style at or above `id` that states `field`, or a default one if
|
||||
// nothing does. Returning the whole style lets a caller read the pair a role
|
||||
// comes in.
|
||||
const Style& inherited(uint16_t id, uint16_t field) const {
|
||||
static const Style fallback;
|
||||
for (uint16_t n = id; n != NONE; n = nodes[n].parent) {
|
||||
const Style* style = styleOf(n);
|
||||
if (style && (style->set & field)) return *style;
|
||||
if (style && (style->set & field))
|
||||
return *style;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
@@ -266,69 +286,78 @@ class Tree {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
return (low < styles.size() && styles[low].first == id) ? &styles[low].second : nullptr;
|
||||
return (low < styles.size() && styles[low].first == id)
|
||||
? &styles[low].second
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
private:
|
||||
std::vector<uint16_t> labelAt;
|
||||
std::vector<char> labels;
|
||||
std::vector<std::pair<uint16_t, Style> > styles;
|
||||
std::vector<std::pair<uint16_t, Style>> styles;
|
||||
|
||||
void fail(const char* message) {
|
||||
if (!error) error = message;
|
||||
if (!error)
|
||||
error = message;
|
||||
}
|
||||
|
||||
int resolve(Size size, int span) {
|
||||
switch (size.mode) {
|
||||
case AUTO:
|
||||
return UNKNOWN;
|
||||
case PX:
|
||||
return size.value;
|
||||
case FILL:
|
||||
if (span == UNKNOWN) {
|
||||
fail("fill inside an auto-sized parent");
|
||||
return 0;
|
||||
}
|
||||
return span;
|
||||
case FRACTION:
|
||||
if (span == UNKNOWN) {
|
||||
fail("fraction inside an auto-sized parent");
|
||||
return 0;
|
||||
}
|
||||
return span * size.value / 1000;
|
||||
case AUTO:
|
||||
return UNKNOWN;
|
||||
case PX:
|
||||
return size.value;
|
||||
case FILL:
|
||||
if (span == UNKNOWN) {
|
||||
fail("fill inside an auto-sized parent");
|
||||
return 0;
|
||||
}
|
||||
return span;
|
||||
case FRACTION:
|
||||
if (span == UNKNOWN) {
|
||||
fail("fraction inside an auto-sized parent");
|
||||
return 0;
|
||||
}
|
||||
return span * size.value / 1000;
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
void measure(uint16_t id, int availW, int availH) {
|
||||
// By value, and re-index after recursion: measuring a child can push nodes, and a
|
||||
// vector that grows moves every reference taken before it.
|
||||
// By value, and re-index after recursion: measuring a child can push nodes,
|
||||
// and a vector that grows moves every reference taken before it.
|
||||
const Spec s = specs[id];
|
||||
const bool row = (nodes[id].flags & ROW) != 0;
|
||||
int w = resolve(s.w, availW);
|
||||
int h = resolve(s.h, availH);
|
||||
|
||||
int innerW = w != UNKNOWN ? w - s.padL - s.padR
|
||||
: (availW != UNKNOWN ? availW - s.padL - s.padR : UNKNOWN);
|
||||
// Height is only handed down when this node was given one. A parent sized by its
|
||||
// content cannot tell a child what fraction of it to take.
|
||||
int innerW = w != UNKNOWN
|
||||
? w - s.padL - s.padR
|
||||
: (availW != UNKNOWN ? availW - s.padL - s.padR : UNKNOWN);
|
||||
// Height is only handed down when this node was given one. A parent sized
|
||||
// by its content cannot tell a child what fraction of it to take.
|
||||
int innerH = h != UNKNOWN ? h - s.padT - s.padB : UNKNOWN;
|
||||
|
||||
int main = 0, cross = 0, count = 0;
|
||||
for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next) {
|
||||
measure(c, innerW, innerH);
|
||||
// Absolutely placed children are measured, because they still need a size, but they
|
||||
// take no part in the flow their siblings share. The Lua original counted them here
|
||||
// and excluded them in place(); nothing depended on the disagreement.
|
||||
if (specs[c].absolute) continue;
|
||||
// Absolutely placed children are measured, because they still need a
|
||||
// size, but they take no part in the flow their siblings share. The Lua
|
||||
// original counted them here and excluded them in place(); nothing
|
||||
// depended on the disagreement.
|
||||
if (specs[c].absolute)
|
||||
continue;
|
||||
const Node& child = nodes[c];
|
||||
if (count) main += s.gap;
|
||||
if (count)
|
||||
main += s.gap;
|
||||
if (row) {
|
||||
main += child.w;
|
||||
if (child.h > cross) cross = child.h;
|
||||
if (child.h > cross)
|
||||
cross = child.h;
|
||||
} else {
|
||||
main += child.h;
|
||||
if (child.w > cross) cross = child.w;
|
||||
if (child.w > cross)
|
||||
cross = child.w;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
@@ -364,17 +393,20 @@ class Tree {
|
||||
int cx = x + s.padL, cy = y + s.padT;
|
||||
int cw = w - s.padL - s.padR, ch = h - s.padT - s.padB;
|
||||
|
||||
// Main-axis distribution, CSS justify-content minus the modes nothing asks for.
|
||||
// Absolutely placed children take no part in the flow.
|
||||
// Main-axis distribution, CSS justify-content minus the modes nothing asks
|
||||
// for. Absolutely placed children take no part in the flow.
|
||||
int flowing = 0, used = 0;
|
||||
for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next) {
|
||||
if (specs[c].absolute) continue;
|
||||
if (specs[c].absolute)
|
||||
continue;
|
||||
flowing++;
|
||||
used += row ? nodes[c].w : nodes[c].h;
|
||||
}
|
||||
if (flowing > 1) used += (flowing - 1) * s.gap;
|
||||
if (flowing > 1)
|
||||
used += (flowing - 1) * s.gap;
|
||||
int slack = (row ? cw : ch) - used;
|
||||
if (slack < 0) slack = 0;
|
||||
if (slack < 0)
|
||||
slack = 0;
|
||||
|
||||
int offset = 0, spread = 0;
|
||||
if (s.justify == END) {
|
||||
@@ -388,11 +420,14 @@ class Tree {
|
||||
for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next) {
|
||||
const Spec cs = specs[c];
|
||||
int childW = nodes[c].w, childH = nodes[c].h;
|
||||
// Cross axis fills the parent unless the child asked for a size, like CSS blocks.
|
||||
// Cross axis fills the parent unless the child asked for a size, like CSS
|
||||
// blocks.
|
||||
if (row) {
|
||||
if (cs.h.mode == AUTO) childH = ch;
|
||||
if (cs.h.mode == AUTO)
|
||||
childH = ch;
|
||||
} else {
|
||||
if (cs.w.mode == AUTO) childW = cw;
|
||||
if (cs.w.mode == AUTO)
|
||||
childW = cw;
|
||||
}
|
||||
|
||||
int px, py;
|
||||
@@ -423,5 +458,5 @@ class Tree {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace esp32lua
|
||||
} // namespace ui
|
||||
} // namespace esp32lua
|
||||
|
||||
+101
-65
@@ -7,11 +7,12 @@
|
||||
|
||||
namespace esp32lua {
|
||||
|
||||
// Every value crossing this seam is int32_t because the interpreter is built with
|
||||
// LUA_32BITS: the ESP32 is a 32-bit core with no hardware float64, so a wider lua_Integer
|
||||
// would cost size and speed on every value in the heap. The ceilings that follows from it
|
||||
// are sys.getMillis() wrapping after ~24.9 days of uptime and file offsets stopping at
|
||||
// 2 GB, neither of which a battery-powered handheld reading an SD card reaches.
|
||||
// Every value crossing this seam is int32_t because the interpreter is built
|
||||
// with LUA_32BITS: the ESP32 is a 32-bit core with no hardware float64, so a
|
||||
// wider lua_Integer would cost size and speed on every value in the heap. The
|
||||
// ceilings that follows from it are sys.getMillis() wrapping after ~24.9 days
|
||||
// of uptime and file offsets stopping at 2 GB, neither of which a
|
||||
// battery-powered handheld reading an SD card reaches.
|
||||
struct Status {
|
||||
bool ok;
|
||||
std::string error;
|
||||
@@ -23,7 +24,7 @@ struct Status {
|
||||
enum class LogLevel { Debug, Info, Error };
|
||||
|
||||
class LogProvider {
|
||||
public:
|
||||
public:
|
||||
virtual ~LogProvider() = default;
|
||||
virtual void write(LogLevel level, const std::string& message) = 0;
|
||||
};
|
||||
@@ -35,7 +36,7 @@ struct MemoryInfo {
|
||||
};
|
||||
|
||||
class SettingsProvider {
|
||||
public:
|
||||
public:
|
||||
virtual ~SettingsProvider() = default;
|
||||
virtual int32_t rotation() const = 0;
|
||||
virtual Status setRotation(int32_t degrees) = 0;
|
||||
@@ -43,46 +44,57 @@ class SettingsProvider {
|
||||
virtual Status setTimezone(const std::string& timezone) = 0;
|
||||
};
|
||||
|
||||
// Only what the firmware alone can answer. App identity, titles, data paths, feature reporting
|
||||
// and navigation are the runtime's, because it owns app loading and knows which providers exist.
|
||||
// Only what the firmware alone can answer. App identity, titles, data paths,
|
||||
// feature reporting and navigation are the runtime's, because it owns app
|
||||
// loading and knows which providers exist.
|
||||
class SysProvider {
|
||||
public:
|
||||
public:
|
||||
virtual ~SysProvider() = default;
|
||||
virtual int32_t millis() const = 0;
|
||||
virtual MemoryInfo memory() const = 0;
|
||||
virtual bool isClockSynced() const = 0;
|
||||
};
|
||||
|
||||
// Scripts are streamed rather than slurped: a whole module in one buffer needs that many bytes
|
||||
// contiguous, and once WiFi is up the largest free block is far smaller than the free heap.
|
||||
// Scripts are streamed rather than slurped: a whole module in one buffer needs
|
||||
// that many bytes contiguous, and once WiFi is up the largest free block is far
|
||||
// smaller than the free heap.
|
||||
class FileReader {
|
||||
public:
|
||||
public:
|
||||
virtual ~FileReader() = default;
|
||||
// Bytes read, zero at end of file, negative on failure.
|
||||
virtual int32_t read(char* out, int32_t maxBytes) = 0;
|
||||
};
|
||||
|
||||
class FsProvider {
|
||||
public:
|
||||
public:
|
||||
virtual ~FsProvider() = default;
|
||||
// Null when the path is missing or is a directory. The caller owns the reader.
|
||||
// Null when the path is missing or is a directory. The caller owns the
|
||||
// reader.
|
||||
virtual FileReader* openRead(const std::string& path) = 0;
|
||||
virtual bool exists(const std::string& path) const = 0;
|
||||
virtual Status fileSize(const std::string& path, int32_t& size) const = 0;
|
||||
virtual Status listDirs(const std::string& path, std::vector<std::string>& names) const = 0;
|
||||
virtual Status listFiles(const std::string& path, std::vector<std::string>& names) const = 0;
|
||||
virtual Status listDirs(const std::string& path,
|
||||
std::vector<std::string>& names) const = 0;
|
||||
virtual Status listFiles(const std::string& path,
|
||||
std::vector<std::string>& names) const = 0;
|
||||
virtual Status mkdir(const std::string& path) = 0;
|
||||
virtual Status readFile(const std::string& path, int32_t maxBytes, std::string& content) const = 0;
|
||||
// A line past the end reports ok with `found` false, which the binding returns as a bare nil.
|
||||
virtual Status readLineAt(const std::string& path, int32_t offset, int32_t maxBytes, bool& found,
|
||||
std::string& line, int32_t& nextOffset) const = 0;
|
||||
virtual Status readFile(const std::string& path, int32_t maxBytes,
|
||||
std::string& content) const = 0;
|
||||
// A line past the end reports ok with `found` false, which the binding
|
||||
// returns as a bare nil.
|
||||
virtual Status readLineAt(const std::string& path, int32_t offset,
|
||||
int32_t maxBytes, bool& found, std::string& line,
|
||||
int32_t& nextOffset) const = 0;
|
||||
virtual Status remove(const std::string& path) = 0;
|
||||
virtual Status removeTree(const std::string& path) = 0;
|
||||
virtual Status rename(const std::string& source, const std::string& destination) = 0;
|
||||
virtual Status writeFile(const std::string& path, const std::string& content) = 0;
|
||||
virtual Status rename(const std::string& source,
|
||||
const std::string& destination) = 0;
|
||||
virtual Status writeFile(const std::string& path,
|
||||
const std::string& content) = 0;
|
||||
};
|
||||
|
||||
// Native identifiers the firmware assigns to the portable font roles and text styles.
|
||||
// Native identifiers the firmware assigns to the portable font roles and text
|
||||
// styles.
|
||||
struct FontIds {
|
||||
int32_t small;
|
||||
int32_t ui;
|
||||
@@ -93,7 +105,7 @@ struct FontIds {
|
||||
};
|
||||
|
||||
class GuiProvider {
|
||||
public:
|
||||
public:
|
||||
virtual ~GuiProvider() = default;
|
||||
virtual FontIds fonts() const = 0;
|
||||
virtual int32_t width() const = 0;
|
||||
@@ -102,31 +114,44 @@ class GuiProvider {
|
||||
virtual void setRotation(int32_t degrees) = 0;
|
||||
virtual int32_t color(int32_t r, int32_t g, int32_t b) const = 0;
|
||||
virtual void clear(int32_t color) = 0;
|
||||
virtual void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color) = 0;
|
||||
virtual void drawRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color) = 0;
|
||||
virtual void drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t color, int32_t width) = 0;
|
||||
virtual void fillRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
||||
int32_t color) = 0;
|
||||
virtual void drawRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
||||
int32_t color) = 0;
|
||||
virtual void drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2,
|
||||
int32_t color, int32_t width) = 0;
|
||||
virtual void drawPixel(int32_t x, int32_t y, int32_t color) = 0;
|
||||
virtual void drawCircle(int32_t x, int32_t y, int32_t radius, int32_t color, int32_t width) = 0;
|
||||
virtual void fillCircle(int32_t x, int32_t y, int32_t radius, int32_t color, const int32_t* background) = 0;
|
||||
// Fill, gradient, and border come from one distance field, so they cannot disagree at the
|
||||
// corners. A null top paints no fill, a null border paints no outline, and a panel with no
|
||||
// gradient of its own is free to ignore `bottom` the way color() quantizes to grayscale.
|
||||
virtual void roundRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t radius, int32_t background,
|
||||
const int32_t* top, const int32_t* bottom, const int32_t* border) = 0;
|
||||
// Hands the app the whole panel, including whatever chrome the firmware paints.
|
||||
virtual void drawCircle(int32_t x, int32_t y, int32_t radius, int32_t color,
|
||||
int32_t width) = 0;
|
||||
virtual void fillCircle(int32_t x, int32_t y, int32_t radius, int32_t color,
|
||||
const int32_t* background) = 0;
|
||||
// Fill, gradient, and border come from one distance field, so they cannot
|
||||
// disagree at the corners. A null top paints no fill, a null border paints no
|
||||
// outline, and a panel with no gradient of its own is free to ignore `bottom`
|
||||
// the way color() quantizes to grayscale.
|
||||
virtual void roundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
||||
int32_t radius, int32_t background, const int32_t* top,
|
||||
const int32_t* bottom, const int32_t* border) = 0;
|
||||
// Hands the app the whole panel, including whatever chrome the firmware
|
||||
// paints.
|
||||
virtual void setFullscreen(bool on) = 0;
|
||||
// Applies everything drawn since the last commit. The runtime supplies only the timing -- the
|
||||
// end of a callback batch -- because that is the one fact a driver cannot know; which region to
|
||||
// touch, which waveform, and whether to clean up ghosting all stay here. A live LCD has nothing
|
||||
// pending and does nothing.
|
||||
// Applies everything drawn since the last commit. The runtime supplies only
|
||||
// the timing -- the end of a callback batch -- because that is the one fact a
|
||||
// driver cannot know; which region to touch, which waveform, and whether to
|
||||
// clean up ghosting all stay here. A live LCD has nothing pending and does
|
||||
// nothing.
|
||||
virtual void commit() = 0;
|
||||
virtual void fillPolygon(const int32_t* xs, const int32_t* ys, size_t count, int32_t color) = 0;
|
||||
virtual void fillPolygon(const int32_t* xs, const int32_t* ys, size_t count,
|
||||
int32_t color) = 0;
|
||||
// Null coordinates centre the image; null bounds fall back to the panel size.
|
||||
virtual Status drawBmp(const std::string& path, const int32_t* x, const int32_t* y, const int32_t* maxWidth,
|
||||
virtual Status drawBmp(const std::string& path, const int32_t* x,
|
||||
const int32_t* y, const int32_t* maxWidth,
|
||||
const int32_t* maxHeight) = 0;
|
||||
virtual int32_t textWidth(int32_t font, const std::string& text, int32_t style) const = 0;
|
||||
virtual int32_t textWidth(int32_t font, const std::string& text,
|
||||
int32_t style) const = 0;
|
||||
virtual int32_t fontHeight(int32_t font, int32_t style) const = 0;
|
||||
virtual void drawText(int32_t font, int32_t x, int32_t y, const std::string& text, int32_t color, int32_t style,
|
||||
virtual void drawText(int32_t font, int32_t x, int32_t y,
|
||||
const std::string& text, int32_t color, int32_t style,
|
||||
const int32_t* background) = 0;
|
||||
};
|
||||
|
||||
@@ -142,25 +167,29 @@ struct HttpResponse {
|
||||
|
||||
struct HttpDownload {
|
||||
int32_t maxBytes;
|
||||
int32_t expectedSize; // Zero when the caller did not declare one.
|
||||
std::string sha256; // Empty when the caller did not declare one.
|
||||
int32_t expectedSize; // Zero when the caller did not declare one.
|
||||
std::string sha256; // Empty when the caller did not declare one.
|
||||
};
|
||||
|
||||
class HttpProvider {
|
||||
public:
|
||||
public:
|
||||
virtual ~HttpProvider() = default;
|
||||
virtual Status request(const std::string& method, const std::string& url, const std::string& body,
|
||||
const std::vector<HttpHeader>& headers, int32_t maxBytes, HttpResponse& response) = 0;
|
||||
virtual Status download(const std::string& url, const std::string& destination, const HttpDownload& options,
|
||||
virtual Status request(const std::string& method, const std::string& url,
|
||||
const std::string& body,
|
||||
const std::vector<HttpHeader>& headers,
|
||||
int32_t maxBytes, HttpResponse& response) = 0;
|
||||
virtual Status download(const std::string& url,
|
||||
const std::string& destination,
|
||||
const HttpDownload& options,
|
||||
int32_t& bytesWritten) = 0;
|
||||
};
|
||||
|
||||
using TimerId = int32_t;
|
||||
|
||||
// The firmware schedules deadlines and calls Runtime::callTimer() on the Lua thread; the
|
||||
// runtime owns the identifiers and the retained callbacks.
|
||||
// The firmware schedules deadlines and calls Runtime::callTimer() on the Lua
|
||||
// thread; the runtime owns the identifiers and the retained callbacks.
|
||||
class TimerProvider {
|
||||
public:
|
||||
public:
|
||||
virtual ~TimerProvider() = default;
|
||||
virtual Status schedule(TimerId id, int32_t intervalMs, bool repeating) = 0;
|
||||
virtual void cancel(TimerId id) = 0;
|
||||
@@ -173,18 +202,19 @@ struct WifiNetwork {
|
||||
};
|
||||
|
||||
struct WifiStatus {
|
||||
std::string state; // One of the WifiState alias values.
|
||||
std::string state; // One of the WifiState alias values.
|
||||
std::string ssid;
|
||||
std::string ip;
|
||||
int32_t rssi;
|
||||
};
|
||||
|
||||
class WifiProvider {
|
||||
public:
|
||||
public:
|
||||
virtual ~WifiProvider() = default;
|
||||
virtual Status scan(std::vector<WifiNetwork>& networks) = 0;
|
||||
// Null credentials reconnect whatever the firmware has saved.
|
||||
virtual Status connect(const std::string* ssid, const std::string* password) = 0;
|
||||
virtual Status connect(const std::string* ssid,
|
||||
const std::string* password) = 0;
|
||||
virtual WifiStatus status() const = 0;
|
||||
virtual void disconnect() = 0;
|
||||
virtual Status forget() = 0;
|
||||
@@ -197,7 +227,7 @@ struct BleDevice {
|
||||
};
|
||||
|
||||
class BleProvider {
|
||||
public:
|
||||
public:
|
||||
virtual ~BleProvider() = default;
|
||||
virtual Status init(const std::string* name) = 0;
|
||||
virtual void deinit() = 0;
|
||||
@@ -205,8 +235,12 @@ class BleProvider {
|
||||
virtual Status connect(const std::string& address) = 0;
|
||||
virtual void disconnect() = 0;
|
||||
virtual bool isConnected() const = 0;
|
||||
virtual Status read(const std::string& service, const std::string& characteristic, std::string& value) = 0;
|
||||
virtual Status write(const std::string& service, const std::string& characteristic, const std::string& value) = 0;
|
||||
virtual Status read(const std::string& service,
|
||||
const std::string& characteristic,
|
||||
std::string& value) = 0;
|
||||
virtual Status write(const std::string& service,
|
||||
const std::string& characteristic,
|
||||
const std::string& value) = 0;
|
||||
virtual Status startAdvertising(const std::string* name) = 0;
|
||||
virtual void stopAdvertising() = 0;
|
||||
};
|
||||
@@ -214,19 +248,21 @@ class BleProvider {
|
||||
enum class TouchPhase { Down, Move, Up };
|
||||
|
||||
class TouchProvider {
|
||||
public:
|
||||
public:
|
||||
virtual ~TouchProvider() = default;
|
||||
virtual bool touch(int32_t& x, int32_t& y) const = 0;
|
||||
virtual bool rawTouch(int32_t& x, int32_t& y) const = 0;
|
||||
virtual bool isTouched() const = 0;
|
||||
virtual Status setCalibration(int32_t x0, int32_t y0, int32_t x1, int32_t y1) = 0;
|
||||
virtual Status setCalibration(int32_t x0, int32_t y0, int32_t x1,
|
||||
int32_t y1) = 0;
|
||||
};
|
||||
|
||||
class ButtonsProvider {
|
||||
public:
|
||||
public:
|
||||
virtual ~ButtonsProvider() = default;
|
||||
// The roles this device maps physical buttons onto, drawn from the Button union. Hardware
|
||||
// variety lives in the mapping; the vocabulary stays closed so an app stays portable.
|
||||
// The roles this device maps physical buttons onto, drawn from the Button
|
||||
// union. Hardware variety lives in the mapping; the vocabulary stays closed
|
||||
// so an app stays portable.
|
||||
virtual std::vector<std::string> buttons() const = 0;
|
||||
virtual bool isAnyPressed() const = 0;
|
||||
virtual bool isPressed(const std::string& button) const = 0;
|
||||
@@ -234,4 +270,4 @@ class ButtonsProvider {
|
||||
virtual bool wasReleased(const std::string& button) const = 0;
|
||||
};
|
||||
|
||||
} // namespace esp32lua
|
||||
} // namespace esp32lua
|
||||
|
||||
@@ -11,7 +11,8 @@ struct lua_State;
|
||||
|
||||
namespace esp32lua {
|
||||
|
||||
// The version sys.getAPIVersion() reports: this contract, not the firmware's build.
|
||||
// The version sys.getAPIVersion() reports: this contract, not the firmware's
|
||||
// build.
|
||||
constexpr int32_t API_VERSION = 1;
|
||||
|
||||
// Where the runtime looks for apps, their data, and shared modules.
|
||||
@@ -23,8 +24,9 @@ struct Paths {
|
||||
std::string home = "Home";
|
||||
};
|
||||
|
||||
// Firmware supplies every core provider; a null feature provider is how sys.hasFeature()
|
||||
// answers false, and its namespace additions are simply never registered.
|
||||
// Firmware supplies every core provider; a null feature provider is how
|
||||
// sys.hasFeature() answers false, and its namespace additions are simply never
|
||||
// registered.
|
||||
struct Providers {
|
||||
LogProvider* log = nullptr;
|
||||
SettingsProvider* settings = nullptr;
|
||||
@@ -41,7 +43,7 @@ struct Providers {
|
||||
};
|
||||
|
||||
class Runtime {
|
||||
public:
|
||||
public:
|
||||
explicit Runtime(const Providers& providers, const Paths& paths = Paths());
|
||||
~Runtime();
|
||||
|
||||
@@ -53,11 +55,14 @@ class Runtime {
|
||||
void close();
|
||||
lua_State* state() const { return state_; }
|
||||
|
||||
// Replaces the running app with a fresh lua_State, loads <apps>/<path>/main.lua, and calls
|
||||
// init(arg). A failure leaves no app running rather than a half-built one.
|
||||
bool startApp(const std::string& path, const std::string& arg = std::string());
|
||||
// Replaces the running app with a fresh lua_State, loads
|
||||
// <apps>/<path>/main.lua, and calls init(arg). A failure leaves no app
|
||||
// running rather than a half-built one.
|
||||
bool startApp(const std::string& path,
|
||||
const std::string& arg = std::string());
|
||||
bool hasApp() const { return !appPath_.empty(); }
|
||||
// The app-relative route, its immutable first component, and the title the app chose.
|
||||
// The app-relative route, its immutable first component, and the title the
|
||||
// app chose.
|
||||
const std::string& appPath() const { return appPath_; }
|
||||
std::string appId() const;
|
||||
std::string appDataPath() const;
|
||||
@@ -65,16 +70,19 @@ class Runtime {
|
||||
void setAppTitle(const std::string& title) { appTitle_ = title; }
|
||||
bool hasFeature(const std::string& feature) const;
|
||||
|
||||
// sys.launch/replace/back record intent and return; swapping the lua_State inside a callback
|
||||
// would free the VM that is still executing. The firmware applies it between batches.
|
||||
void requestLaunch(const std::string& path, const std::string& arg, bool replace);
|
||||
// sys.launch/replace/back record intent and return; swapping the lua_State
|
||||
// inside a callback would free the VM that is still executing. The firmware
|
||||
// applies it between batches.
|
||||
void requestLaunch(const std::string& path, const std::string& arg,
|
||||
bool replace);
|
||||
void requestBack();
|
||||
bool hasPendingNavigation() const { return pending_.kind != Pending::None; }
|
||||
// Whether sys.back() would return somewhere rather than land on the launcher, which is what
|
||||
// firmware chrome needs to decide whether to offer a back control.
|
||||
// Whether sys.back() would return somewhere rather than land on the launcher,
|
||||
// which is what firmware chrome needs to decide whether to offer a back
|
||||
// control.
|
||||
bool canGoBack() const { return !history_.empty(); }
|
||||
// Loads whatever was requested. False means the app failed to start or history ran out at the
|
||||
// launcher, in which case no app is running.
|
||||
// Loads whatever was requested. False means the app failed to start or
|
||||
// history ran out at the launcher, in which case no app is running.
|
||||
bool applyPendingNavigation();
|
||||
|
||||
LogProvider& log() const { return *providers_.log; }
|
||||
@@ -91,9 +99,10 @@ class Runtime {
|
||||
|
||||
ui::Tree& tree() { return tree_; }
|
||||
|
||||
// Entry points into the app. The firmware decides whether an event reaches the app at all --
|
||||
// jitter, chrome and debouncing are its business -- and the runtime decides what the app sees.
|
||||
// Only a failed init() stops an app; every other callback logs and carries on.
|
||||
// Entry points into the app. The firmware decides whether an event reaches
|
||||
// the app at all -- jitter, chrome and debouncing are its business -- and the
|
||||
// runtime decides what the app sees. Only a failed init() stops an app; every
|
||||
// other callback logs and carries on.
|
||||
bool callInit(const std::string& arg);
|
||||
void callDraw(int32_t deltaMs);
|
||||
// An Up phase also fires the on_touch tap alias, in that order.
|
||||
@@ -101,7 +110,8 @@ class Runtime {
|
||||
// A release also fires the on_button tap alias, in that order.
|
||||
void callButton(const std::string& button, bool pressed);
|
||||
|
||||
// Timer identity and callback retention are the runtime's; deadlines are the firmware's.
|
||||
// Timer identity and callback retention are the runtime's; deadlines are the
|
||||
// firmware's.
|
||||
TimerId addTimer(int callbackRef, int32_t intervalMs, bool repeating);
|
||||
bool cancelTimer(TimerId id);
|
||||
// Called on the Lua thread when a scheduled deadline elapses.
|
||||
@@ -109,7 +119,7 @@ class Runtime {
|
||||
|
||||
static Runtime* from(lua_State* state);
|
||||
|
||||
private:
|
||||
private:
|
||||
struct Timer {
|
||||
int callbackRef;
|
||||
bool repeating;
|
||||
@@ -131,16 +141,17 @@ class Runtime {
|
||||
static int searchEmbedded(lua_State* state);
|
||||
static int loadFile(lua_State* state);
|
||||
|
||||
// A batch is one visit to the app, however many callbacks it fans out into: a tap fires
|
||||
// on_touch_up and then the on_touch alias, and a timer can fire inside draw. Committing per
|
||||
// callback would refresh an e-ink panel twice for one visible change, so the display is
|
||||
// committed when the outermost call returns.
|
||||
// A batch is one visit to the app, however many callbacks it fans out into: a
|
||||
// tap fires on_touch_up and then the on_touch alias, and a timer can fire
|
||||
// inside draw. Committing per callback would refresh an e-ink panel twice for
|
||||
// one visible change, so the display is committed when the outermost call
|
||||
// returns.
|
||||
class Batch {
|
||||
public:
|
||||
public:
|
||||
explicit Batch(Runtime& runtime);
|
||||
~Batch();
|
||||
|
||||
private:
|
||||
private:
|
||||
Runtime& runtime_;
|
||||
};
|
||||
|
||||
@@ -163,4 +174,4 @@ class Runtime {
|
||||
Pending pending_;
|
||||
};
|
||||
|
||||
} // namespace esp32lua
|
||||
} // namespace esp32lua
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
// Host doubles for every provider, deliberately dumb: they record what a binding asked
|
||||
// for and hand back canned values, so a test asserts the marshalling rather than a device.
|
||||
// Host doubles for every provider, deliberately dumb: they record what a
|
||||
// binding asked for and hand back canned values, so a test asserts the
|
||||
// marshalling rather than a device.
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -56,7 +57,9 @@ struct Fs : FsProvider {
|
||||
size_t at = 0;
|
||||
int32_t read(char* out, int32_t maxBytes) override {
|
||||
const size_t remaining = content.size() - at;
|
||||
const size_t count = remaining < static_cast<size_t>(maxBytes) ? remaining : static_cast<size_t>(maxBytes);
|
||||
const size_t count = remaining < static_cast<size_t>(maxBytes)
|
||||
? remaining
|
||||
: static_cast<size_t>(maxBytes);
|
||||
content.copy(out, count, at);
|
||||
at += count;
|
||||
return static_cast<int32_t>(count);
|
||||
@@ -64,47 +67,65 @@ struct Fs : FsProvider {
|
||||
};
|
||||
|
||||
FileReader* openRead(const std::string& path) override {
|
||||
const std::map<std::string, std::string>::const_iterator found = files.find(path);
|
||||
if (found == files.end()) return nullptr;
|
||||
const std::map<std::string, std::string>::const_iterator found =
|
||||
files.find(path);
|
||||
if (found == files.end())
|
||||
return nullptr;
|
||||
Reader* reader = new Reader();
|
||||
reader->content = found->second;
|
||||
return reader;
|
||||
}
|
||||
|
||||
bool exists(const std::string& path) const override { return files.count(path) != 0; }
|
||||
bool exists(const std::string& path) const override {
|
||||
return files.count(path) != 0;
|
||||
}
|
||||
Status fileSize(const std::string& path, int32_t& size) const override {
|
||||
const std::map<std::string, std::string>::const_iterator found = files.find(path);
|
||||
if (found == files.end()) return Status::failure("no such file");
|
||||
const std::map<std::string, std::string>::const_iterator found =
|
||||
files.find(path);
|
||||
if (found == files.end())
|
||||
return Status::failure("no such file");
|
||||
size = static_cast<int32_t>(found->second.size());
|
||||
return Status::success();
|
||||
}
|
||||
Status listDirs(const std::string&, std::vector<std::string>& names) const override {
|
||||
Status listDirs(const std::string&,
|
||||
std::vector<std::string>& names) const override {
|
||||
names.push_back("apps");
|
||||
return Status::success();
|
||||
}
|
||||
Status listFiles(const std::string&, std::vector<std::string>& names) const override {
|
||||
Status listFiles(const std::string&,
|
||||
std::vector<std::string>& names) const override {
|
||||
names.push_back("main.lua");
|
||||
return Status::success();
|
||||
}
|
||||
Status mkdir(const std::string&) override { return Status::success(); }
|
||||
Status readFile(const std::string& path, int32_t maxBytes, std::string& content) const override {
|
||||
const std::map<std::string, std::string>::const_iterator found = files.find(path);
|
||||
if (found == files.end()) return Status::failure("no such file");
|
||||
if (static_cast<int32_t>(found->second.size()) > maxBytes) return Status::failure("too large");
|
||||
Status readFile(const std::string& path, int32_t maxBytes,
|
||||
std::string& content) const override {
|
||||
const std::map<std::string, std::string>::const_iterator found =
|
||||
files.find(path);
|
||||
if (found == files.end())
|
||||
return Status::failure("no such file");
|
||||
if (static_cast<int32_t>(found->second.size()) > maxBytes)
|
||||
return Status::failure("too large");
|
||||
content = found->second;
|
||||
return Status::success();
|
||||
}
|
||||
Status readLineAt(const std::string& path, int32_t offset, int32_t, bool& found, std::string& line,
|
||||
Status readLineAt(const std::string& path, int32_t offset, int32_t,
|
||||
bool& found, std::string& line,
|
||||
int32_t& nextOffset) const override {
|
||||
const std::map<std::string, std::string>::const_iterator file = files.find(path);
|
||||
if (file == files.end()) return Status::failure("no such file");
|
||||
const std::map<std::string, std::string>::const_iterator file =
|
||||
files.find(path);
|
||||
if (file == files.end())
|
||||
return Status::failure("no such file");
|
||||
if (offset >= static_cast<int32_t>(file->second.size())) {
|
||||
found = false;
|
||||
return Status::success();
|
||||
}
|
||||
const size_t end = file->second.find('\n', offset);
|
||||
line = file->second.substr(offset, end == std::string::npos ? std::string::npos : end - offset);
|
||||
nextOffset = end == std::string::npos ? static_cast<int32_t>(file->second.size()) : static_cast<int32_t>(end + 1);
|
||||
line = file->second.substr(
|
||||
offset, end == std::string::npos ? std::string::npos : end - offset);
|
||||
nextOffset = end == std::string::npos
|
||||
? static_cast<int32_t>(file->second.size())
|
||||
: static_cast<int32_t>(end + 1);
|
||||
found = true;
|
||||
return Status::success();
|
||||
}
|
||||
@@ -113,8 +134,11 @@ struct Fs : FsProvider {
|
||||
return Status::success();
|
||||
}
|
||||
Status removeTree(const std::string&) override { return Status::success(); }
|
||||
Status rename(const std::string&, const std::string&) override { return Status::success(); }
|
||||
Status writeFile(const std::string& path, const std::string& content) override {
|
||||
Status rename(const std::string&, const std::string&) override {
|
||||
return Status::success();
|
||||
}
|
||||
Status writeFile(const std::string& path,
|
||||
const std::string& content) override {
|
||||
files[path] = content;
|
||||
written = content;
|
||||
return Status::success();
|
||||
@@ -135,30 +159,47 @@ struct Gui : GuiProvider {
|
||||
int32_t height() const override { return 240; }
|
||||
int32_t rotation() const override { return degrees; }
|
||||
void setRotation(int32_t value) override { degrees = value; }
|
||||
int32_t color(int32_t r, int32_t g, int32_t b) const override { return (r << 16) | (g << 8) | b; }
|
||||
int32_t color(int32_t r, int32_t g, int32_t b) const override {
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
void clear(int32_t) override { trace += "clear;"; }
|
||||
void fillRect(int32_t, int32_t, int32_t, int32_t, int32_t) override { trace += "fillRect;"; }
|
||||
void drawRect(int32_t, int32_t, int32_t, int32_t, int32_t) override { trace += "drawRect;"; }
|
||||
void drawLine(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) override { trace += "drawLine;"; }
|
||||
void fillRect(int32_t, int32_t, int32_t, int32_t, int32_t) override {
|
||||
trace += "fillRect;";
|
||||
}
|
||||
void drawRect(int32_t, int32_t, int32_t, int32_t, int32_t) override {
|
||||
trace += "drawRect;";
|
||||
}
|
||||
void drawLine(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) override {
|
||||
trace += "drawLine;";
|
||||
}
|
||||
void drawPixel(int32_t, int32_t, int32_t) override { trace += "drawPixel;"; }
|
||||
void drawCircle(int32_t, int32_t, int32_t, int32_t, int32_t) override { trace += "drawCircle;"; }
|
||||
void fillCircle(int32_t, int32_t, int32_t, int32_t, const int32_t*) override { trace += "fillCircle;"; }
|
||||
void roundRect(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, const int32_t* top, const int32_t* bottom,
|
||||
void drawCircle(int32_t, int32_t, int32_t, int32_t, int32_t) override {
|
||||
trace += "drawCircle;";
|
||||
}
|
||||
void fillCircle(int32_t, int32_t, int32_t, int32_t, const int32_t*) override {
|
||||
trace += "fillCircle;";
|
||||
}
|
||||
void roundRect(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t,
|
||||
const int32_t* top, const int32_t* bottom,
|
||||
const int32_t* border) override {
|
||||
if (top && bottom && *top != *bottom) gradient = true;
|
||||
if (top && bottom && *top != *bottom)
|
||||
gradient = true;
|
||||
trace += "roundRect(";
|
||||
trace += top ? "fill" : "-";
|
||||
trace += border ? ",border" : ",-";
|
||||
trace += ");";
|
||||
}
|
||||
void setFullscreen(bool on) override { fullscreen = on; }
|
||||
// Stands in for an e-ink panel, where a second commit is a second visible refresh.
|
||||
// Stands in for an e-ink panel, where a second commit is a second visible
|
||||
// refresh.
|
||||
void commit() override { commits++; }
|
||||
int commits = 0;
|
||||
void fillPolygon(const int32_t*, const int32_t*, size_t count, int32_t) override {
|
||||
void fillPolygon(const int32_t*, const int32_t*, size_t count,
|
||||
int32_t) override {
|
||||
trace += "fillPolygon" + std::to_string(count) + ";";
|
||||
}
|
||||
Status drawBmp(const std::string&, const int32_t*, const int32_t*, const int32_t*, const int32_t*) override {
|
||||
Status drawBmp(const std::string&, const int32_t*, const int32_t*,
|
||||
const int32_t*, const int32_t*) override {
|
||||
trace += "drawBmp;";
|
||||
return Status::success();
|
||||
}
|
||||
@@ -166,7 +207,8 @@ struct Gui : GuiProvider {
|
||||
return static_cast<int32_t>(text.size()) * 8;
|
||||
}
|
||||
int32_t fontHeight(int32_t, int32_t) const override { return 16; }
|
||||
void drawText(int32_t, int32_t, int32_t, const std::string& text, int32_t, int32_t, const int32_t*) override {
|
||||
void drawText(int32_t, int32_t, int32_t, const std::string& text, int32_t,
|
||||
int32_t, const int32_t*) override {
|
||||
trace += "drawText(" + text + ");";
|
||||
}
|
||||
};
|
||||
@@ -177,8 +219,10 @@ struct Http : HttpProvider {
|
||||
std::vector<HttpHeader> headers;
|
||||
int32_t limit = 0;
|
||||
|
||||
Status request(const std::string& nextMethod, const std::string&, const std::string& nextBody,
|
||||
const std::vector<HttpHeader>& nextHeaders, int32_t maxBytes, HttpResponse& response) override {
|
||||
Status request(const std::string& nextMethod, const std::string&,
|
||||
const std::string& nextBody,
|
||||
const std::vector<HttpHeader>& nextHeaders, int32_t maxBytes,
|
||||
HttpResponse& response) override {
|
||||
method = nextMethod;
|
||||
body = nextBody;
|
||||
headers = nextHeaders;
|
||||
@@ -187,7 +231,8 @@ struct Http : HttpProvider {
|
||||
response.body = "missing";
|
||||
return Status::success();
|
||||
}
|
||||
Status download(const std::string&, const std::string&, const HttpDownload& options, int32_t& bytesWritten) override {
|
||||
Status download(const std::string&, const std::string&,
|
||||
const HttpDownload& options, int32_t& bytesWritten) override {
|
||||
bytesWritten = options.maxBytes;
|
||||
return Status::success();
|
||||
}
|
||||
@@ -242,15 +287,19 @@ struct Ble : BleProvider {
|
||||
Status connect(const std::string&) override { return Status::success(); }
|
||||
void disconnect() override {}
|
||||
bool isConnected() const override { return true; }
|
||||
Status read(const std::string&, const std::string&, std::string& out) override {
|
||||
Status read(const std::string&, const std::string&,
|
||||
std::string& out) override {
|
||||
out = std::string("a\0b", 3);
|
||||
return Status::success();
|
||||
}
|
||||
Status write(const std::string&, const std::string&, const std::string& next) override {
|
||||
Status write(const std::string&, const std::string&,
|
||||
const std::string& next) override {
|
||||
value = next;
|
||||
return Status::success();
|
||||
}
|
||||
Status startAdvertising(const std::string*) override { return Status::success(); }
|
||||
Status startAdvertising(const std::string*) override {
|
||||
return Status::success();
|
||||
}
|
||||
void stopAdvertising() override {}
|
||||
};
|
||||
|
||||
@@ -269,7 +318,8 @@ struct Touch : TouchProvider {
|
||||
return down;
|
||||
}
|
||||
bool isTouched() const override { return down; }
|
||||
Status setCalibration(int32_t x0, int32_t y0, int32_t x1, int32_t y1) override {
|
||||
Status setCalibration(int32_t x0, int32_t y0, int32_t x1,
|
||||
int32_t y1) override {
|
||||
calibration[0] = x0;
|
||||
calibration[1] = y0;
|
||||
calibration[2] = x1;
|
||||
@@ -288,7 +338,9 @@ struct Buttons : ButtonsProvider {
|
||||
return roles;
|
||||
}
|
||||
bool isAnyPressed() const override { return false; }
|
||||
bool isPressed(const std::string& button) const override { return button == "confirm"; }
|
||||
bool isPressed(const std::string& button) const override {
|
||||
return button == "confirm";
|
||||
}
|
||||
bool wasPressed(const std::string&) const override { return false; }
|
||||
bool wasReleased(const std::string&) const override { return false; }
|
||||
};
|
||||
@@ -324,4 +376,4 @@ struct Bench {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fake
|
||||
} // namespace fake
|
||||
|
||||
@@ -25,7 +25,7 @@ void expectError(lua_State* state, const char* chunk) {
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
fake::Bench bench;
|
||||
@@ -37,88 +37,82 @@ int main() {
|
||||
assert(bench.log.level == esp32lua::LogLevel::Info);
|
||||
assert(bench.log.message == "shared runtime");
|
||||
|
||||
run(state,
|
||||
"assert(settings.getRotation() == 0)\n"
|
||||
"assert(settings.setRotation(90))\n"
|
||||
"assert(settings.getTimezone() == 'UTC0')\n"
|
||||
"assert(settings.setTimezone('EST5EDT'))");
|
||||
run(state, "assert(settings.getRotation() == 0)\n"
|
||||
"assert(settings.setRotation(90))\n"
|
||||
"assert(settings.getTimezone() == 'UTC0')\n"
|
||||
"assert(settings.setTimezone('EST5EDT'))");
|
||||
assert(bench.settings.degrees == 90);
|
||||
assert(bench.settings.tz == "EST5EDT");
|
||||
expectError(state, "settings.setRotation(45)");
|
||||
|
||||
run(state,
|
||||
"assert(sys.getAPIVersion() == 1)\n"
|
||||
"assert(sys.hasFeature('touch') and sys.hasFeature('buttons'))\n"
|
||||
"assert(not sys.hasFeature('eink'))\n"
|
||||
"local free, total, largest = sys.getMemory()\n"
|
||||
"assert(free == 100 and total == 200 and largest == 50)");
|
||||
run(state, "assert(sys.getAPIVersion() == 1)\n"
|
||||
"assert(sys.hasFeature('touch') and sys.hasFeature('buttons'))\n"
|
||||
"assert(not sys.hasFeature('eink'))\n"
|
||||
"local free, total, largest = sys.getMemory()\n"
|
||||
"assert(free == 100 and total == 200 and largest == 50)");
|
||||
|
||||
bench.fs.files["/notes.txt"] = "first\nsecond";
|
||||
run(state,
|
||||
"assert(fs.MAX_READ_BYTES == 65536)\n"
|
||||
"assert(fs.exists('/notes.txt') and not fs.exists('/missing'))\n"
|
||||
"assert(fs.fileSize('/notes.txt') == 12)\n"
|
||||
"local size, err = fs.fileSize('/missing')\n"
|
||||
"assert(size == nil and err == 'no such file')\n"
|
||||
"assert(fs.readFile('/notes.txt', 64) == 'first\\nsecond')\n"
|
||||
"local line, nextOffset = fs.readLineAt('/notes.txt', 0, 64)\n"
|
||||
"assert(line == 'first' and nextOffset == 6)\n"
|
||||
"assert(fs.readLineAt('/notes.txt', 12, 64) == nil)\n"
|
||||
"assert(fs.listFiles('/')[1] == 'main.lua')\n"
|
||||
"assert(fs.writeFile('/out.bin', 'a\\0b'))");
|
||||
run(state, "assert(fs.MAX_READ_BYTES == 65536)\n"
|
||||
"assert(fs.exists('/notes.txt') and not fs.exists('/missing'))\n"
|
||||
"assert(fs.fileSize('/notes.txt') == 12)\n"
|
||||
"local size, err = fs.fileSize('/missing')\n"
|
||||
"assert(size == nil and err == 'no such file')\n"
|
||||
"assert(fs.readFile('/notes.txt', 64) == 'first\\nsecond')\n"
|
||||
"local line, nextOffset = fs.readLineAt('/notes.txt', 0, 64)\n"
|
||||
"assert(line == 'first' and nextOffset == 6)\n"
|
||||
"assert(fs.readLineAt('/notes.txt', 12, 64) == nil)\n"
|
||||
"assert(fs.listFiles('/')[1] == 'main.lua')\n"
|
||||
"assert(fs.writeFile('/out.bin', 'a\\0b'))");
|
||||
assert(bench.fs.written.size() == 3);
|
||||
expectError(state, "fs.readFile('/notes.txt', 999999)");
|
||||
|
||||
run(state,
|
||||
"assert(gui.getWidth() == 320 and gui.getHeight() == 240)\n"
|
||||
"assert(gui.FONT_UI == 2 and gui.STYLE_BOLD == 1)\n"
|
||||
"assert(gui.color(255, 0, 0) == 0xFF0000)\n"
|
||||
"gui.setRotation(180)\n"
|
||||
"gui.clear()\n"
|
||||
"gui.fillPolygon({1, 2, 3}, {4, 5, 6}, 0)\n"
|
||||
"gui.drawText(gui.FONT_UI, 0, 0, 'hi')");
|
||||
run(state, "assert(gui.getWidth() == 320 and gui.getHeight() == 240)\n"
|
||||
"assert(gui.FONT_UI == 2 and gui.STYLE_BOLD == 1)\n"
|
||||
"assert(gui.color(255, 0, 0) == 0xFF0000)\n"
|
||||
"gui.setRotation(180)\n"
|
||||
"gui.clear()\n"
|
||||
"gui.fillPolygon({1, 2, 3}, {4, 5, 6}, 0)\n"
|
||||
"gui.drawText(gui.FONT_UI, 0, 0, 'hi')");
|
||||
assert(bench.gui.degrees == 180);
|
||||
assert(bench.gui.trace == "clear;fillPolygon3;drawText(hi);");
|
||||
expectError(state, "gui.fillPolygon({1, 2}, {3}, 0)");
|
||||
expectError(state, "gui.color(300, 0, 0)");
|
||||
|
||||
run(state,
|
||||
"local response = http.get('https://example.test', {maxBytes = 16, headers = {Accept = 'text/plain'}})\n"
|
||||
"assert(response.status == 404 and response.body == 'missing')\n"
|
||||
"assert(http.urlencode('a b/c~') == 'a%20b%2Fc~')\n"
|
||||
"assert(http.download('https://example.test/f', '/f', {maxBytes = 32}) == 32)");
|
||||
run(state, "local response = http.get('https://example.test', {maxBytes = "
|
||||
"16, headers = {Accept = 'text/plain'}})\n"
|
||||
"assert(response.status == 404 and response.body == 'missing')\n"
|
||||
"assert(http.urlencode('a b/c~') == 'a%20b%2Fc~')\n"
|
||||
"assert(http.download('https://example.test/f', '/f', {maxBytes = "
|
||||
"32}) == 32)");
|
||||
assert(bench.http.method == "GET" && bench.http.limit == 16);
|
||||
assert(bench.http.headers.size() == 1 && bench.http.headers[0].name == "Accept");
|
||||
assert(bench.http.headers.size() == 1 &&
|
||||
bench.http.headers[0].name == "Accept");
|
||||
expectError(state, "http.get('https://example.test', {maxBytes = 999999})");
|
||||
|
||||
run(state,
|
||||
"assert(wifi.scan()[1].ssid == 'home')\n"
|
||||
"assert(wifi.isConnected())\n"
|
||||
"assert(wifi.getLocalIP() == '192.168.1.5')\n"
|
||||
"assert(wifi.getStatus().state == 'connected')\n"
|
||||
"assert(wifi.connect())");
|
||||
run(state, "assert(wifi.scan()[1].ssid == 'home')\n"
|
||||
"assert(wifi.isConnected())\n"
|
||||
"assert(wifi.getLocalIP() == '192.168.1.5')\n"
|
||||
"assert(wifi.getStatus().state == 'connected')\n"
|
||||
"assert(wifi.connect())");
|
||||
assert(bench.wifi.savedReconnect);
|
||||
|
||||
run(state,
|
||||
"assert(ble.scan()[1].address == 'aa:bb')\n"
|
||||
"assert(#ble.read('svc', 'chr') == 3)\n"
|
||||
"assert(ble.write('svc', 'chr', 'x\\0y'))");
|
||||
run(state, "assert(ble.scan()[1].address == 'aa:bb')\n"
|
||||
"assert(#ble.read('svc', 'chr') == 3)\n"
|
||||
"assert(ble.write('svc', 'chr', 'x\\0y'))");
|
||||
assert(bench.ble.duration == 3000);
|
||||
assert(bench.ble.value.size() == 3);
|
||||
|
||||
run(state,
|
||||
"fired = 0\n"
|
||||
"once = timer.after(10, function() fired = fired + 1 end)\n"
|
||||
"repeated = timer.every(5, function() fired = fired + 10 end)");
|
||||
run(state, "fired = 0\n"
|
||||
"once = timer.after(10, function() fired = fired + 1 end)\n"
|
||||
"repeated = timer.every(5, function() fired = fired + 10 end)");
|
||||
assert(bench.timer.scheduled.size() == 2);
|
||||
runtime.callTimer(bench.timer.scheduled[0]);
|
||||
runtime.callTimer(bench.timer.scheduled[0]); // a one-shot never fires twice
|
||||
runtime.callTimer(bench.timer.scheduled[0]); // a one-shot never fires twice
|
||||
runtime.callTimer(bench.timer.scheduled[1]);
|
||||
runtime.callTimer(bench.timer.scheduled[1]);
|
||||
run(state,
|
||||
"assert(fired == 21)\n"
|
||||
"assert(timer.cancel(repeated))\n"
|
||||
"assert(not timer.cancel(repeated))");
|
||||
run(state, "assert(fired == 21)\n"
|
||||
"assert(timer.cancel(repeated))\n"
|
||||
"assert(not timer.cancel(repeated))");
|
||||
assert(bench.timer.cancelled.size() == 1);
|
||||
expectError(state, "timer.after(0, function() end)");
|
||||
|
||||
@@ -128,24 +122,28 @@ int main() {
|
||||
"assert(x == 10 and y == 20)\n"
|
||||
"assert(input.isTouched())\n"
|
||||
"assert(input.isPressed('confirm') and not input.isPressed('back'))\n"
|
||||
"assert(#input.getButtons() == 4 and input.getButtons()[3] == 'confirm')");
|
||||
"assert(#input.getButtons() == 4 and input.getButtons()[3] == "
|
||||
"'confirm')");
|
||||
assert(bench.touch.calibration[3] == 400);
|
||||
|
||||
// Chrome control and gradients are core: an e-ink provider flattens what it cannot show.
|
||||
run(state,
|
||||
"gui.setFullscreen(true)\n"
|
||||
"gui.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
|
||||
"gui.roundRect(0, 0, 10, 10, 4, 0, nil, nil, 0xFF)");
|
||||
// Chrome control and gradients are core: an e-ink provider flattens what it
|
||||
// cannot show.
|
||||
run(state, "gui.setFullscreen(true)\n"
|
||||
"gui.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
|
||||
"gui.roundRect(0, 0, 10, 10, 4, 0, nil, nil, 0xFF)");
|
||||
assert(bench.gui.fullscreen && bench.gui.gradient);
|
||||
assert(bench.gui.trace.find("roundRect(-,border);") != std::string::npos);
|
||||
|
||||
bench.gui.trace.clear();
|
||||
run(state,
|
||||
"node.reset()\n"
|
||||
"local root = node.create(nil, {type = 'box', w = 'fill', h = 'fill', pad = 4, gap = 2})\n"
|
||||
"local root = node.create(nil, {type = 'box', w = 'fill', h = 'fill', "
|
||||
"pad = 4, gap = 2})\n"
|
||||
"local label = node.create(root, {type = 'text', label = 'Hello'})\n"
|
||||
"local button = node.create(root, {type = 'button', h = 40, interactive = true})\n"
|
||||
"node.setStyle(root, {background = 0xFFFFFF, fill = 0xFFFFFF, color = 0, face = 0xEEEEEE,\n"
|
||||
"local button = node.create(root, {type = 'button', h = 40, interactive "
|
||||
"= true})\n"
|
||||
"node.setStyle(root, {background = 0xFFFFFF, fill = 0xFFFFFF, color = 0, "
|
||||
"face = 0xEEEEEE,\n"
|
||||
" border = 0x333333, focusColor = 0xFF0000})\n"
|
||||
"assert(node.layout(root, 0, 0, 320, 240))\n"
|
||||
"local x, y, w, h = node.getRect(label)\n"
|
||||
@@ -169,17 +167,20 @@ int main() {
|
||||
|
||||
run(state,
|
||||
"node.reset()\n"
|
||||
"local root = node.create(nil, {type = 'custom', w = 'fill', h = 'fill'})\n"
|
||||
"local root = node.create(nil, {type = 'custom', w = 'fill', h = "
|
||||
"'fill'})\n"
|
||||
"painted = 0\n"
|
||||
"node.setPainter(function(id, x, y, w, h) painted = painted + w end)\n"
|
||||
"assert(node.layout(root, 0, 0, 320, 240))\n"
|
||||
"node.draw(root)\n"
|
||||
"assert(painted == 320)");
|
||||
|
||||
// Callbacks: only init failing stops an app, and a release fires the tap alias after the up.
|
||||
// Callbacks: only init failing stops an app, and a release fires the tap
|
||||
// alias after the up.
|
||||
run(state,
|
||||
"events = {}\n"
|
||||
"local function note(name) return function(a) events[#events + 1] = name .. ':' .. tostring(a) end end\n"
|
||||
"local function note(name) return function(a) events[#events + 1] = name "
|
||||
".. ':' .. tostring(a) end end\n"
|
||||
"function init(arg) events[#events + 1] = 'init:' .. tostring(arg) end\n"
|
||||
"function draw(delta) events[#events + 1] = 'draw:' .. delta end\n"
|
||||
"on_touch_down = note('down')\n"
|
||||
@@ -191,20 +192,21 @@ int main() {
|
||||
assert(runtime.callInit("book.epub"));
|
||||
runtime.callDraw(33);
|
||||
runtime.callTouch(esp32lua::TouchPhase::Down, 5, 6);
|
||||
runtime.callTouch(esp32lua::TouchPhase::Move, 5, 7); // the app defines no on_touch_move
|
||||
runtime.callTouch(esp32lua::TouchPhase::Move, 5,
|
||||
7); // the app defines no on_touch_move
|
||||
runtime.callTouch(esp32lua::TouchPhase::Up, 5, 8);
|
||||
runtime.callButton("confirm", false);
|
||||
run(state,
|
||||
"assert(table.concat(events, ' ') == "
|
||||
"'init:book.epub draw:33 down:5 up:5 tap:5 bup:confirm btap:confirm')");
|
||||
// One commit per visit to the app, so six calls and not seven: the release and its tap alias
|
||||
// are one visible change, and the move nobody handled still ends a batch.
|
||||
// One commit per visit to the app, so six calls and not seven: the release
|
||||
// and its tap alias are one visible change, and the move nobody handled still
|
||||
// ends a batch.
|
||||
assert(bench.gui.commits == 6);
|
||||
|
||||
// A timer firing inside draw is still one batch.
|
||||
run(state,
|
||||
"function draw() timer.after(1, function() end) end\n"
|
||||
"nested = timer.after(1, function() draw() end)");
|
||||
run(state, "function draw() timer.after(1, function() end) end\n"
|
||||
"nested = timer.after(1, function() draw() end)");
|
||||
bench.gui.commits = 0;
|
||||
runtime.callTimer(bench.timer.scheduled.back());
|
||||
assert(bench.gui.commits == 1);
|
||||
@@ -213,10 +215,11 @@ int main() {
|
||||
assert(!runtime.callInit(""));
|
||||
assert(bench.log.message.find("init: ") == 0);
|
||||
run(state, "function draw() error('kaboom') end");
|
||||
runtime.callDraw(1); // a failed frame logs and the app keeps running
|
||||
runtime.callDraw(1); // a failed frame logs and the app keeps running
|
||||
assert(bench.log.message.find("draw: ") == 0);
|
||||
|
||||
// A feature callback without its provider is a wiring bug, not a silent no-op.
|
||||
// A feature callback without its provider is a wiring bug, not a silent
|
||||
// no-op.
|
||||
{
|
||||
fake::Bench headless;
|
||||
esp32lua::Providers providers = headless.providers();
|
||||
@@ -225,17 +228,21 @@ int main() {
|
||||
assert(noTouch.open());
|
||||
noTouch.callTouch(esp32lua::TouchPhase::Down, 1, 1);
|
||||
assert(headless.log.message == "callTouch without a touch provider");
|
||||
run(noTouch.state(), "assert(input.getTouch == nil and input.isPressed ~= nil)");
|
||||
run(noTouch.state(),
|
||||
"assert(input.getTouch == nil and input.isPressed ~= nil)");
|
||||
}
|
||||
|
||||
// App loading: a fresh state per app, require reaching the app directory and /.lua/lib, and
|
||||
// navigation applied between batches rather than inside a callback.
|
||||
// App loading: a fresh state per app, require reaching the app directory and
|
||||
// /.lua/lib, and navigation applied between batches rather than inside a
|
||||
// callback.
|
||||
{
|
||||
fake::Bench host;
|
||||
host.fs.files["/.lua/lib/greet.lua"] = "return {hello = function() return 'hi' end}";
|
||||
host.fs.files["/.lua/lib/greet.lua"] =
|
||||
"return {hello = function() return 'hi' end}";
|
||||
host.fs.files["/.lua/apps/Home/main.lua"] =
|
||||
"local greet = require('greet')\n"
|
||||
"function init(arg) started = greet.hello() .. ':' .. tostring(arg) end";
|
||||
"function init(arg) started = greet.hello() .. ':' .. tostring(arg) "
|
||||
"end";
|
||||
host.fs.files["/.lua/apps/Reader/main.lua"] =
|
||||
"local page = require('page')\n"
|
||||
"function init(arg) started = page.name .. ':' .. arg end";
|
||||
@@ -251,7 +258,8 @@ int main() {
|
||||
// A subapp shares the app ID, so both routes share one data directory.
|
||||
run(app.state(), "sys.launch('Reader', 'book.epub')");
|
||||
assert(app.hasPendingNavigation());
|
||||
run(app.state(), "assert(started == 'hi:')"); // the current app keeps running until applied
|
||||
run(app.state(), "assert(started == 'hi:')"); // the current app keeps
|
||||
// running until applied
|
||||
assert(app.applyPendingNavigation());
|
||||
run(app.state(), "assert(started == 'page:book.epub')");
|
||||
run(app.state(), "sys.launch('Reader/Notes')");
|
||||
@@ -267,7 +275,8 @@ int main() {
|
||||
run(app.state(), "sys.back()");
|
||||
assert(app.applyPendingNavigation() && app.appPath() == "Home");
|
||||
|
||||
// sys.replace does not grow history, so back from it still reaches the launcher.
|
||||
// sys.replace does not grow history, so back from it still reaches the
|
||||
// launcher.
|
||||
run(app.state(), "sys.replace('Reader', 'other.epub')");
|
||||
assert(app.applyPendingNavigation() && app.appPath() == "Reader");
|
||||
run(app.state(), "sys.back()");
|
||||
@@ -276,7 +285,8 @@ int main() {
|
||||
// A missing app, a broken app, and a traversal all leave nothing running.
|
||||
assert(!app.startApp("Absent"));
|
||||
assert(!app.hasApp() && app.state() == nullptr);
|
||||
host.fs.files["/.lua/apps/Broken/main.lua"] = "function init() error('nope') end";
|
||||
host.fs.files["/.lua/apps/Broken/main.lua"] =
|
||||
"function init() error('nope') end";
|
||||
assert(!app.startApp("Broken"));
|
||||
assert(!app.hasApp());
|
||||
assert(!app.startApp("../secrets"));
|
||||
|
||||
+21
-1
@@ -35,6 +35,7 @@ DOC = re.compile(r"^\s*// ---\s?(?P<text>.*)$")
|
||||
PARAM = re.compile(r"^\s*// @param (?P<name>\w+) (?P<type>\S+)(?: (?P<desc>.*))?$")
|
||||
RETURN = re.compile(r"^\s*// @return (?P<type>\S+)(?: (?P<desc>.*))?$")
|
||||
TAG = re.compile(r"^\s*// @(?P<tag>[\w-]+)")
|
||||
CONTINUATION = re.compile(r"^\s*// (?P<text>(?!@|---)\S.*)$")
|
||||
TYPES = {"int": "integer", "bool": "boolean"}
|
||||
|
||||
|
||||
@@ -65,6 +66,7 @@ def parse(path):
|
||||
current = None
|
||||
doc = {"doc": [], "params": [], "returns": []}
|
||||
in_table = False
|
||||
continuation = None
|
||||
|
||||
def reset():
|
||||
return {"doc": [], "params": [], "returns": []}
|
||||
@@ -73,6 +75,18 @@ def parse(path):
|
||||
stripped = line.strip()
|
||||
where = f"{path.relative_to(ROOT)}:{number}"
|
||||
|
||||
wrapped = CONTINUATION.match(line)
|
||||
if continuation and wrapped:
|
||||
items, index, field = continuation
|
||||
if field is None:
|
||||
items[index] += " " + wrapped.group("text")
|
||||
else:
|
||||
item = list(items[index])
|
||||
item[field] += " " + wrapped.group("text")
|
||||
items[index] = tuple(item)
|
||||
continue
|
||||
continuation = None
|
||||
|
||||
module = MODULE.match(stripped)
|
||||
if module:
|
||||
pending_module = new_module(module.group("kind"), module.group("name"), module.group("class_"))
|
||||
@@ -104,12 +118,15 @@ def parse(path):
|
||||
target["consts"].append(
|
||||
(const.group("name"), lua_type(const.group("type")), const.group("value"), const.group("desc") or "")
|
||||
)
|
||||
continuation = (target["consts"], len(target["consts"]) - 1, 3)
|
||||
continue
|
||||
fix = FIX.match(stripped)
|
||||
if fix:
|
||||
if not target:
|
||||
raise SystemExit(f"{where}: @lua-{fix.group('where')} outside a module")
|
||||
target[fix.group("where")].append(fix.group("text"))
|
||||
items = target[fix.group("where")]
|
||||
items.append(fix.group("text"))
|
||||
continuation = (items, len(items) - 1, None)
|
||||
continue
|
||||
|
||||
table = TABLE.match(line)
|
||||
@@ -128,14 +145,17 @@ def parse(path):
|
||||
match = DOC.match(line)
|
||||
if match:
|
||||
doc["doc"].append(match.group("text").strip())
|
||||
continuation = (doc["doc"], len(doc["doc"]) - 1, None)
|
||||
continue
|
||||
match = PARAM.match(line)
|
||||
if match:
|
||||
doc["params"].append((match.group("name"), lua_type(match.group("type")), match.group("desc") or ""))
|
||||
continuation = (doc["params"], len(doc["params"]) - 1, 2)
|
||||
continue
|
||||
match = RETURN.match(line)
|
||||
if match:
|
||||
doc["returns"].append((lua_type(match.group("type")), match.group("desc") or ""))
|
||||
continuation = (doc["returns"], len(doc["returns"]) - 1, 1)
|
||||
continue
|
||||
match = TAG.match(line)
|
||||
if match:
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from gen_api import ROOT, parse
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(dir=ROOT) as directory:
|
||||
source = Path(directory) / "wrapped.cpp"
|
||||
source.write_text(
|
||||
"""// @lua-module fs FsLib
|
||||
// @lua-const MAX_READ_BYTES integer 65536 Largest portable
|
||||
// whole-file read.
|
||||
const luaL_Reg FUNCTIONS[] = {
|
||||
// --- Reads one complete
|
||||
// file.
|
||||
// @param path string Absolute file
|
||||
// path.
|
||||
// @return string|nil File
|
||||
// contents.
|
||||
{"readFile", readFile},
|
||||
};
|
||||
"""
|
||||
)
|
||||
module = parse(source)[0]
|
||||
|
||||
assert module["consts"][0][3] == "Largest portable whole-file read."
|
||||
doc = module["functions"][0][1]
|
||||
assert doc["doc"] == ["Reads one complete file."]
|
||||
assert doc["params"][0][2] == "Absolute file path."
|
||||
assert doc["returns"][0][1] == "File contents."
|
||||
print("ok")
|
||||
Reference in New Issue
Block a user