chore: lsp + format

This commit is contained in:
2026-08-04 10:21:18 -04:00
parent fa35c3c326
commit 4533c7f61f
30 changed files with 1151 additions and 678 deletions
+4 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+38 -27
View File
@@ -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