refactor(api)!: one namespace per feature, screen split out

Namespaces were shared across features: `settings` was written by core, the
panel and touch, and `input` by touch and buttons. That made "does this
firmware implement the whole feature?" a question no pointer could answer.

Each namespace now belongs to exactly one feature or to core, so a feature is
a provider pointer and the compiler validates completeness:

  gui, node       -> screen, tree, under the screen feature
  settings        -> screen (rotation, theme), sys (timezone),
                     touch (calibration)
  input           -> touch, buttons

Runtime::open() no longer requires a GuiProvider; a firmware without one runs
with no screen/tree globals and reports sys.hasFeature("screen") false.
Rotation is one value again: GuiProvider::setRotation applies and persists, so
an app rotating the panel transiently puts the old value back itself.
This commit is contained in:
2026-08-05 10:26:38 -04:00
parent 445a9b2b8f
commit 75b3a2c490
26 changed files with 556 additions and 626 deletions
+9 -14
View File
@@ -35,19 +35,6 @@ struct MemoryInfo {
int32_t largestFreeBlock;
};
class SettingsProvider {
public:
virtual ~SettingsProvider() = default;
virtual int32_t rotation() const = 0;
virtual Status setRotation(int32_t degrees) = 0;
virtual std::string timezone() const = 0;
virtual Status setTimezone(const std::string& timezone) = 0;
// Only the name of a palette; the colours themselves live in Lua, so the
// firmware can read the saved theme before a lua_State exists.
virtual std::string theme() const = 0;
virtual Status setTheme(const std::string& theme) = 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.
@@ -57,6 +44,8 @@ public:
virtual int32_t millis() const = 0;
virtual MemoryInfo memory() const = 0;
virtual bool isClockSynced() const = 0;
virtual std::string timezone() const = 0;
virtual Status setTimezone(const std::string& timezone) = 0;
};
// Scripts are streamed rather than slurped: a whole module in one buffer needs
@@ -115,7 +104,13 @@ public:
virtual int32_t width() const = 0;
virtual int32_t height() const = 0;
virtual int32_t rotation() const = 0;
virtual void setRotation(int32_t degrees) = 0;
// Applies the rotation and persists it, so the panel comes back the way the
// user left it with no second call to forget.
virtual Status setRotation(int32_t degrees) = 0;
// Only the name of a palette; the colours themselves live in Lua, so the
// firmware can read the saved theme before a lua_State exists.
virtual std::string theme() const = 0;
virtual Status setTheme(const std::string& theme) = 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,
+1 -3
View File
@@ -25,15 +25,14 @@ constexpr const char* MAIN_PATH = "/.lua/main.lua";
// registered.
struct Providers {
LogProvider* log = nullptr;
SettingsProvider* settings = nullptr;
SysProvider* sys = nullptr;
FsProvider* fs = nullptr;
GuiProvider* gui = nullptr;
HttpProvider* http = nullptr;
TimerProvider* timer = nullptr;
WifiProvider* wifi = nullptr;
BleProvider* ble = nullptr;
GuiProvider* gui = nullptr;
TouchProvider* touch = nullptr;
ButtonsProvider* buttons = nullptr;
};
@@ -82,7 +81,6 @@ public:
bool applyPendingNavigation();
LogProvider& log() const { return *providers_.log; }
SettingsProvider& settings() const { return *providers_.settings; }
SysProvider& sys() const { return *providers_.sys; }
FsProvider& fs() const { return *providers_.fs; }
GuiProvider& gui() const { return *providers_.gui; }
-101
View File
@@ -1,101 +0,0 @@
// @lua-module settings SettingsLib
#include <lua/runtime.h>
extern "C" {
#include "lauxlib.h"
#include "lua.h"
}
namespace esp32lua {
namespace bindings {
namespace {
int pushStatus(lua_State* state, const Status& status) {
if (status.ok) {
lua_pushboolean(state, true);
return 1;
}
lua_pushnil(state);
lua_pushlstring(state, status.error.data(), status.error.size());
return 2;
}
int getRotation(lua_State* state) {
lua_pushinteger(state, Runtime::from(state)->settings().rotation());
return 1;
}
int setRotation(lua_State* state) {
const lua_Integer degrees = luaL_checkinteger(state, 1);
luaL_argcheck(state, degrees >= 0 && degrees <= 270 && degrees % 90 == 0, 1,
"expected 0, 90, 180, or 270");
return pushStatus(state,
Runtime::from(state)->settings().setRotation(degrees));
}
int getTimezone(lua_State* state) {
const std::string timezone = Runtime::from(state)->settings().timezone();
lua_pushlstring(state, timezone.data(), timezone.size());
return 1;
}
int setTimezone(lua_State* state) {
size_t length = 0;
const char* value = luaL_checklstring(state, 1, &length);
return pushStatus(
state, Runtime::from(state)->settings().setTimezone({value, length}));
}
int getTheme(lua_State* state) {
const std::string theme = Runtime::from(state)->settings().theme();
lua_pushlstring(state, theme.data(), theme.size());
return 1;
}
int setTheme(lua_State* state) {
size_t length = 0;
const char* value = luaL_checklstring(state, 1, &length);
return pushStatus(state,
Runtime::from(state)->settings().setTheme({value, length}));
}
const luaL_Reg FUNCTIONS[] = {
// --- Returns the saved rotation in degrees clockwise.
// @return integer
{"getRotation", getRotation},
// --- Applies and persists the screen rotation.
// @param degrees integer 0, 90, 180, or 270 clockwise.
// @return true|nil ok
// @return string|nil error
{"setRotation", setRotation},
// --- Returns the active POSIX timezone rule.
// @return string
{"getTimezone", getTimezone},
// --- Applies and persists a POSIX timezone rule.
// @param timezone string
// @return true|nil ok
// @return string|nil error
{"setTimezone", setTimezone},
// --- Returns the saved palette name. Apps read ui.getTheme() instead; this
// --- is the stored value, which only ui.setTheme() knows how to apply.
// @return string
{"getTheme", getTheme},
// --- Persists a palette name without applying it. Call ui.setTheme(), which
// --- writes through here and then rebuilds the palette and repaints.
// @param theme string
// @return true|nil ok
// @return string|nil error
{"setTheme", setTheme},
{nullptr, nullptr},
};
} // namespace
void registerSettings(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "settings");
}
} // namespace bindings
} // namespace esp32lua
+17 -1
View File
@@ -1,5 +1,5 @@
// @lua-module sys SysLib
// @lua-preamble ---@alias Feature "touch"|"buttons"
// @lua-preamble ---@alias Feature "screen"|"touch"|"buttons"
#include "../helpers.h"
@@ -66,6 +66,14 @@ int isClockSynced(lua_State* state) {
lua_pushboolean(state, Runtime::from(state)->sys().isClockSynced());
return 1;
}
int getTimezone(lua_State* state) {
pushString(state, Runtime::from(state)->sys().timezone());
return 1;
}
int setTimezone(lua_State* state) {
const std::string timezone = checkString(state, 1);
return pushStatus(state, Runtime::from(state)->sys().setTimezone(timezone));
}
const luaL_Reg FUNCTIONS[] = {
// --- Returns the implemented API contract version.
@@ -116,6 +124,14 @@ const luaL_Reg FUNCTIONS[] = {
// --- Whether network time synchronization has completed.
// @return boolean
{"isClockSynced", isClockSynced},
// --- Returns the active POSIX timezone rule.
// @return string
{"getTimezone", getTimezone},
// --- Applies and persists a POSIX timezone rule.
// @param timezone string
// @return true|nil ok
// @return string|nil error
{"setTimezone", setTimezone},
{nullptr, nullptr},
};
+6 -5
View File
@@ -32,18 +32,18 @@ int wasReleased(lua_State* state) {
return 1;
}
// @lua-augment input InputLib
// @lua-module buttons ButtonsLib
// @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
// @lua-preamble -- up/down/left/right are the directions tree.moveFocus already
// takes.
const luaL_Reg INPUT_FUNCTIONS[] = {
const luaL_Reg FUNCTIONS[] = {
// ---Returns the roles this device reports, so an app can label only the
// actions it has.
// @return Button[]
{"getButtons", getButtons},
{"getAll", getButtons},
// ---Whether any button is held.
// @return boolean
{"isAnyPressed", isAnyPressed},
@@ -65,7 +65,8 @@ const luaL_Reg INPUT_FUNCTIONS[] = {
} // namespace
void registerButtons(lua_State* state) {
augmentGlobal(state, "input", INPUT_FUNCTIONS);
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "buttons");
}
} // namespace bindings
@@ -1,15 +1,17 @@
// @lua-module gui GuiLib
// @lua-preamble ---@alias GuiColor integer
// @lua-preamble ---@alias GuiFont integer
// @lua-preamble ---@alias GuiTextStyle integer
// @lua-const FONT_SMALL GuiFont 0 Small auxiliary text.
// @lua-const FONT_UI GuiFont 0 Normal controls and labels.
// @lua-const FONT_BODY GuiFont 0 Normal reading text.
// @lua-const FONT_LARGE GuiFont 0 Headings and prominent values.
// @lua-const STYLE_NORMAL GuiTextStyle 0
// @lua-const STYLE_BOLD GuiTextStyle 0
// @lua-module screen ScreenLib
// @lua-preamble -- The panel itself; the widget tree it paints is `tree`, and
// @lua-preamble -- sys.hasFeature("screen") covers both.
// @lua-preamble ---@alias ScreenColor integer
// @lua-preamble ---@alias ScreenFont integer
// @lua-preamble ---@alias ScreenTextStyle integer
// @lua-const FONT_SMALL ScreenFont 0 Small auxiliary text.
// @lua-const FONT_UI ScreenFont 0 Normal controls and labels.
// @lua-const FONT_BODY ScreenFont 0 Normal reading text.
// @lua-const FONT_LARGE ScreenFont 0 Headings and prominent values.
// @lua-const STYLE_NORMAL ScreenTextStyle 0
// @lua-const STYLE_BOLD ScreenTextStyle 0
#include "../helpers.h"
#include "../../helpers.h"
namespace esp32lua {
namespace bindings {
@@ -36,8 +38,17 @@ int setRotation(lua_State* state) {
const int32_t degrees = checkInt(state, 1);
luaL_argcheck(state, degrees >= 0 && degrees <= 270 && degrees % 90 == 0, 1,
"expected 0, 90, 180, or 270");
provider(state).setRotation(degrees);
return 0;
return pushStatus(state, provider(state).setRotation(degrees));
}
int getTheme(lua_State* state) {
pushString(state, provider(state).theme());
return 1;
}
int setTheme(lua_State* state) {
const std::string theme = checkString(state, 1);
return pushStatus(state, provider(state).setTheme(theme));
}
int color(lua_State* state) {
@@ -215,62 +226,76 @@ const luaL_Reg FUNCTIONS[] = {
// --- Returns the live frame height.
// @return integer
{"getHeight", getHeight},
// --- Rotates the live frame without changing the saved preference.
// --- Rotates the panel and persists the choice, so there is one rotation
// --- rather than a live one and a saved one to reconcile.
// @param degrees integer 0, 90, 180, or 270 clockwise.
// @return true|nil ok
// @return string|nil error
{"setRotation", setRotation},
// --- Returns the rotation of the live frame.
// @return integer Degrees clockwise for the live frame.
// --- Returns the rotation in degrees clockwise.
// @return integer
{"getRotation", getRotation},
// --- Returns the saved palette name. Apps read ui.getTheme() instead; this
// --- is the stored value, which only ui.setTheme() knows how to apply.
// @return string
{"getTheme", getTheme},
// --- Persists a palette name without applying it. Call ui.setTheme(),
// which
// --- writes through here and then rebuilds the palette and repaints.
// @param theme string
// @return true|nil ok
// @return string|nil error
{"setTheme", setTheme},
// --- Returns an opaque native color. E-ink implementations quantize RGB to
// available grayscale.
// @param r integer 0 through 255.
// @param g integer 0 through 255.
// @param b integer 0 through 255.
// @return GuiColor
// @return ScreenColor
{"color", color},
// --- Clears the frame.
// @param color GuiColor|nil Defaults to white.
// @param color ScreenColor|nil Defaults to white.
{"clear", clear},
// --- Fills a rectangle.
// @param x integer
// @param y integer
// @param w integer
// @param h integer
// @param color GuiColor
// @param color ScreenColor
{"fillRect", fillRect},
// --- Outlines a rectangle.
// @param x integer
// @param y integer
// @param w integer
// @param h integer
// @param color GuiColor
// @param color ScreenColor
{"drawRect", drawRect},
// --- Draws a line.
// @param x1 integer
// @param y1 integer
// @param x2 integer
// @param y2 integer
// @param color GuiColor
// @param color ScreenColor
// @param width integer|nil Defaults to one pixel.
{"drawLine", drawLine},
// --- Draws a single pixel.
// @param x integer
// @param y integer
// @param color GuiColor
// @param color ScreenColor
{"drawPixel", drawPixel},
// --- Outlines a circle.
// @param x integer Center.
// @param y integer Center.
// @param radius integer
// @param color GuiColor
// @param color ScreenColor
// @param width integer|nil Defaults to one pixel.
{"drawCircle", drawCircle},
// --- Fills a circle.
// @param x integer Center.
// @param y integer Center.
// @param radius integer
// @param color GuiColor
// @param background GuiColor|nil Surface behind an anti-aliased edge.
// @param color ScreenColor
// @param background ScreenColor|nil Surface behind an anti-aliased edge.
{"fillCircle", fillCircle},
// ---Draws an anti-aliased rounded fill, optional gradient, and optional
// border in one pass.
@@ -279,16 +304,16 @@ const luaL_Reg FUNCTIONS[] = {
// @param w integer
// @param h integer
// @param radius integer
// @param background GuiColor Surface behind the anti-aliased edge.
// @param top GuiColor|nil Fill, or gradient top; omitted for no fill.
// @param bottom GuiColor|nil Gradient bottom; defaults to top. Panels
// @param background ScreenColor Surface behind the anti-aliased edge.
// @param top ScreenColor|nil Fill, or gradient top; omitted for no fill.
// @param bottom ScreenColor|nil Gradient bottom; defaults to top. Panels
// without a gradient use top.
// @param border GuiColor|nil Omitted for no border.
// @param border ScreenColor|nil Omitted for no border.
{"roundRect", roundRect},
// --- Fills a polygon.
// @param xs integer[]
// @param ys integer[]
// @param color GuiColor
// @param color ScreenColor
{"fillPolygon", fillPolygon},
// --- Draws a bitmap.
// @param path string Absolute BMP path.
@@ -300,31 +325,31 @@ const luaL_Reg FUNCTIONS[] = {
// @return string|nil error
{"drawBmp", drawBmp},
// --- Measures a text run.
// @param font GuiFont Use a named gui.FONT_* role.
// @param font ScreenFont Use a named screen.FONT_* role.
// @param text string
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
// @param style ScreenTextStyle|nil Defaults to screen.STYLE_NORMAL.
// @return integer
{"getTextWidth", getTextWidth},
// --- Returns the line height of a font role.
// @param font GuiFont Use a named gui.FONT_* role.
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
// @param font ScreenFont Use a named screen.FONT_* role.
// @param style ScreenTextStyle|nil Defaults to screen.STYLE_NORMAL.
// @return integer
{"getFontHeight", getFontHeight},
// --- Draws a text run with its top-left corner at x, y.
// @param font GuiFont Use a named gui.FONT_* role.
// @param font ScreenFont Use a named screen.FONT_* role.
// @param x integer Left edge.
// @param y integer Top edge.
// @param text string
// @param color GuiColor|nil Defaults to black.
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
// @param background GuiColor|nil Omitted for transparent text.
// @param color ScreenColor|nil Defaults to black.
// @param style ScreenTextStyle|nil Defaults to screen.STYLE_NORMAL.
// @param background ScreenColor|nil Omitted for transparent text.
{"drawText", drawText},
{nullptr, nullptr},
};
} // namespace
void registerGui(lua_State* state) {
void registerScreen(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
const FontIds fonts = Runtime::from(state)->gui().fonts();
setField(state, "FONT_SMALL", fonts.small);
@@ -333,7 +358,7 @@ void registerGui(lua_State* state) {
setField(state, "FONT_LARGE", fonts.large);
setField(state, "STYLE_NORMAL", fonts.styleNormal);
setField(state, "STYLE_BOLD", fonts.styleBold);
lua_setglobal(state, "gui");
lua_setglobal(state, "screen");
}
} // namespace bindings
@@ -1,4 +1,4 @@
// @lua-module node NodeLib
// @lua-module tree TreeLib
// @lua-preamble ---@alias NodeId integer
// @lua-preamble ---@alias NodeType "box"|"text"|"button"|"custom"
// @lua-preamble ---@alias NodeDirection "up"|"down"|"left"|"right"
@@ -16,27 +16,27 @@
// @lua-preamble ---@field capture? boolean
// @lua-preamble ---@field interactive? boolean
// @lua-preamble ---@field label? string
// @lua-preamble ---@field font? GuiFont
// @lua-preamble ---@field font? ScreenFont
// @lua-preamble
// @lua-preamble ---@class NodeStyle
// @lua-preamble ---@field color? GuiColor
// @lua-preamble ---@field background? GuiColor Background offered to
// @lua-preamble ---@field color? ScreenColor
// @lua-preamble ---@field background? ScreenColor 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 fill? ScreenColor Surface painted by a box.
// @lua-preamble ---@field border? ScreenColor
// @lua-preamble ---@field face? ScreenColor Default button surface.
// @lua-preamble ---@field pressedFace? ScreenColor Pressed button surface.
// @lua-preamble ---@field pressedColor? ScreenColor Pressed button text.
// @lua-preamble ---@field focusColor? ScreenColor Distinct outline for
// directional focus.
// @lua-preamble ---@field radius? integer
// @lua-preamble ---@field font? GuiFont
// @lua-preamble ---@field textStyle? GuiTextStyle
// @lua-preamble ---@field font? ScreenFont
// @lua-preamble ---@field textStyle? ScreenTextStyle
#include <cstdlib>
#include "../../node/painter.h"
#include "../helpers.h"
#include "../../../node/painter.h"
#include "../../helpers.h"
namespace esp32lua {
namespace bindings {
@@ -616,9 +616,9 @@ const luaL_Reg FUNCTIONS[] = {
} // namespace
void registerNode(lua_State* state) {
void registerTree(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "node");
lua_setglobal(state, "tree");
}
} // namespace bindings
+17 -22
View File
@@ -40,8 +40,21 @@ int isTouched(lua_State* state) {
return 1;
}
// @lua-augment settings SettingsLib
const luaL_Reg SETTINGS_FUNCTIONS[] = {
// @lua-module touch TouchLib
const luaL_Reg FUNCTIONS[] = {
// --- Returns the calibrated touch point, or nothing when the panel is not
// touched.
// @return integer|nil x
// @return integer|nil y
{"getPoint", getTouch},
// --- Returns the uncalibrated touch reading, or nothing when the panel is
// not touched.
// @return integer|nil x
// @return integer|nil y
{"getRawPoint", getRawTouch},
// --- Whether the panel is currently touched.
// @return boolean
{"isTouched", isTouched},
// --- Persists the panel's touch calibration.
// @param x0 integer Raw reading at the left edge.
// @param y0 integer Raw reading at the top edge.
@@ -53,29 +66,11 @@ const luaL_Reg SETTINGS_FUNCTIONS[] = {
{nullptr, nullptr},
};
// @lua-augment input InputLib
const luaL_Reg INPUT_FUNCTIONS[] = {
// --- Returns the calibrated touch point, or nothing when the panel is not
// touched.
// @return integer|nil x
// @return integer|nil y
{"getTouch", getTouch},
// --- Returns the uncalibrated touch reading, or nothing when the panel is
// not touched.
// @return integer|nil x
// @return integer|nil y
{"getRawTouch", getRawTouch},
// --- Whether the panel is currently touched.
// @return boolean
{"isTouched", isTouched},
{nullptr, nullptr},
};
} // namespace
void registerTouch(lua_State* state) {
augmentGlobal(state, "settings", SETTINGS_FUNCTIONS);
augmentGlobal(state, "input", INPUT_FUNCTIONS);
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "touch");
}
} // namespace bindings
File diff suppressed because one or more lines are too long
+15 -15
View File
@@ -11,12 +11,11 @@ namespace bindings {
void registerBle(lua_State* state);
void registerButtons(lua_State* state);
void registerFs(lua_State* state);
void registerGui(lua_State* state);
void registerHttp(lua_State* state);
void registerLog(lua_State* state);
void registerNode(lua_State* state);
void registerSettings(lua_State* state);
void registerScreen(lua_State* state);
void registerSys(lua_State* state);
void registerTree(lua_State* state);
void registerTimer(lua_State* state);
void registerTouch(lua_State* state);
void registerWifi(lua_State* state);
@@ -52,9 +51,9 @@ 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 (!providers_.log || !providers_.sys || !providers_.fs ||
!providers_.http || !providers_.timer || !providers_.wifi ||
!providers_.ble) {
return false;
}
@@ -64,19 +63,20 @@ bool Runtime::open() {
*static_cast<Runtime**>(lua_getextraspace(state_)) = this;
luaL_openlibs(state_);
// Primary Namespaces
bindings::registerBle(state_);
bindings::registerFs(state_);
bindings::registerGui(state_);
bindings::registerHttp(state_);
bindings::registerLog(state_);
bindings::registerNode(state_);
bindings::registerSettings(state_);
bindings::registerSys(state_);
bindings::registerTimer(state_);
bindings::registerWifi(state_);
// Feature namespaces extend the tables the core registrations just created,
// so they always follow them.
// Feature Namespaces
if (providers_.gui) {
bindings::registerScreen(state_);
bindings::registerTree(state_);
}
if (providers_.touch)
bindings::registerTouch(state_);
if (providers_.buttons)
@@ -90,9 +90,7 @@ void Runtime::close() {
cancelAllTimers();
lua_close(state_);
state_ = nullptr;
mainRef_ = 0; // the registry it referenced went with the state
// Node handles mean nothing to the next lua_State, so an app that inherited
// the previous tree would build onto its nodes.
mainRef_ = 0;
tree_.reset();
appPath_.clear();
appTitle_.clear();
@@ -103,7 +101,7 @@ Runtime::Batch::Batch(Runtime& runtime) : runtime_(runtime) {
}
Runtime::Batch::~Batch() {
if (--runtime_.batchDepth_ == 0)
if (--runtime_.batchDepth_ == 0 && runtime_.providers_.gui)
runtime_.providers_.gui->commit();
}
@@ -319,6 +317,8 @@ std::string Runtime::appDataPath() const {
}
bool Runtime::hasFeature(const std::string& feature) const {
if (feature == "screen")
return providers_.gui != nullptr;
if (feature == "touch")
return providers_.touch != nullptr;
if (feature == "buttons")
+16 -24
View File
@@ -23,34 +23,19 @@ struct Log : LogProvider {
}
};
struct Settings : SettingsProvider {
int32_t degrees = 0;
std::string tz = "UTC0";
std::string themeName = "light";
int32_t rotation() const override { return degrees; }
Status setRotation(int32_t value) override {
degrees = value;
return Status::success();
}
std::string timezone() const override { return tz; }
Status setTimezone(const std::string& value) override {
tz = value;
return Status::success();
}
std::string theme() const override { return themeName; }
Status setTheme(const std::string& value) override {
themeName = value;
return Status::success();
}
};
struct Sys : SysProvider {
std::string tz = "UTC0";
int32_t millis() const override { return 1234; }
MemoryInfo memory() const override {
const MemoryInfo info = {100, 200, 50};
return info;
}
bool isClockSynced() const override { return true; }
std::string timezone() const override { return tz; }
Status setTimezone(const std::string& value) override {
tz = value;
return Status::success();
}
};
struct Fs : FsProvider {
@@ -154,6 +139,7 @@ struct Fs : FsProvider {
struct Gui : GuiProvider {
std::string trace;
int32_t degrees = 0;
std::string themeName = "light";
bool gradient = false;
FontIds fonts() const override {
@@ -163,7 +149,15 @@ struct Gui : GuiProvider {
int32_t width() const override { return 320; }
int32_t height() const override { return 240; }
int32_t rotation() const override { return degrees; }
void setRotation(int32_t value) override { degrees = value; }
Status setRotation(int32_t value) override {
degrees = value;
return Status::success();
}
std::string theme() const override { return themeName; }
Status setTheme(const std::string& value) override {
themeName = value;
return Status::success();
}
int32_t color(int32_t r, int32_t g, int32_t b) const override {
return (r << 16) | (g << 8) | b;
}
@@ -357,7 +351,6 @@ struct Buttons : ButtonsProvider {
// Every provider a Runtime needs, so a test names only what it asserts on.
struct Bench {
Log log;
Settings settings;
Sys sys;
Fs fs;
Gui gui;
@@ -371,7 +364,6 @@ struct Bench {
Providers providers() {
Providers providers;
providers.log = &log;
providers.settings = &settings;
providers.sys = &sys;
providers.fs = &fs;
providers.gui = &gui;
+67 -49
View File
@@ -37,13 +37,17 @@ 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'))");
assert(bench.settings.degrees == 90);
assert(bench.settings.tz == "EST5EDT");
expectError(state, "settings.setRotation(45)");
run(state, "assert(sys.hasFeature('screen'))\n"
"assert(screen.getRotation() == 0)\n"
"assert(screen.setRotation(90))\n"
"assert(screen.getTheme() == 'light')\n"
"assert(screen.setTheme('dark'))\n"
"assert(sys.getTimezone() == 'UTC0')\n"
"assert(sys.setTimezone('EST5EDT'))");
assert(bench.gui.degrees == 90);
assert(bench.gui.themeName == "dark");
assert(bench.sys.tz == "EST5EDT");
expectError(state, "screen.setRotation(45)");
run(state, "assert(sys.getAPIVersion() == 1)\n"
"assert(sys.hasFeature('touch') and sys.hasFeature('buttons'))\n"
@@ -66,17 +70,17 @@ int main() {
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(screen.getWidth() == 320 and screen.getHeight() == 240)\n"
"assert(screen.FONT_UI == 2 and screen.STYLE_BOLD == 1)\n"
"assert(screen.color(255, 0, 0) == 0xFF0000)\n"
"assert(screen.setRotation(180))\n"
"screen.clear()\n"
"screen.fillPolygon({1, 2, 3}, {4, 5, 6}, 0)\n"
"screen.drawText(screen.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)");
expectError(state, "screen.fillPolygon({1, 2}, {3}, 0)");
expectError(state, "screen.color(300, 0, 0)");
run(state, "local response = http.get('https://example.test', {maxBytes = "
"16, headers = {Accept = 'text/plain'}})\n"
@@ -120,60 +124,60 @@ int main() {
expectError(state, "timer.after(0, function() end)");
run(state,
"assert(settings.setCalibration(100, 200, 300, 400))\n"
"local x, y = input.getTouch()\n"
"assert(touch.setCalibration(100, 200, 300, 400))\n"
"local x, y = touch.getPoint()\n"
"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] == "
"assert(touch.isTouched())\n"
"assert(buttons.isPressed('confirm') and not buttons.isPressed('back'))\n"
"assert(#buttons.getAll() == 4 and buttons.getAll()[3] == "
"'confirm')");
assert(bench.touch.calibration[3] == 400);
// Gradients are core: an e-ink provider flattens what it cannot show.
run(state, "gui.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
"gui.roundRect(0, 0, 10, 10, 4, 0, nil, nil, 0xFF)");
run(state, "screen.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
"screen.roundRect(0, 0, 10, 10, 4, 0, nil, nil, 0xFF)");
assert(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', "
"tree.reset()\n"
"local root = tree.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 "
"local label = tree.create(root, {type = 'text', label = 'Hello'})\n"
"local button = tree.create(root, {type = 'button', h = 40, interactive "
"= true})\n"
"node.setStyle(root, {background = 0xFFFFFF, fill = 0xFFFFFF, color = 0, "
"tree.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"
"assert(tree.layout(root, 0, 0, 320, 240))\n"
"local x, y, w, h = tree.getRect(label)\n"
"assert(x == 4 and y == 4 and w == 312 and h == 16)\n"
"assert(node.getLabel(label) == 'Hello')\n"
"assert(node.hit(root, 10, 40) == button)\n"
"assert(node.hit(root, 10, 200) == nil)\n"
"assert(node.focusFirst(root) == button)\n"
"assert(node.getFocus() == button)\n"
"assert(node.moveFocus(root, 'up') == button)\n"
"node.setPressed(button, true)\n"
"assert(node.isPressed(button))\n"
"assert(node.getCount() == 3 and node.getFootprint() > 0)\n"
"node.draw(root)\n"
"node.dropScratch()");
"assert(tree.getLabel(label) == 'Hello')\n"
"assert(tree.hit(root, 10, 40) == button)\n"
"assert(tree.hit(root, 10, 200) == nil)\n"
"assert(tree.focusFirst(root) == button)\n"
"assert(tree.getFocus() == button)\n"
"assert(tree.moveFocus(root, 'up') == button)\n"
"tree.setPressed(button, true)\n"
"assert(tree.isPressed(button))\n"
"assert(tree.getCount() == 3 and tree.getFootprint() > 0)\n"
"tree.draw(root)\n"
"tree.dropScratch()");
assert(bench.gui.trace.find("drawText(Hello);") != std::string::npos);
// The bordered box rounds its corners; the button fills without one.
assert(bench.gui.trace.find("roundRect(fill,border);") != std::string::npos);
assert(bench.gui.trace.find("roundRect(fill,-);") != std::string::npos);
expectError(state, "node.create(nil, {type = 'nope'})");
expectError(state, "tree.create(nil, {type = 'nope'})");
run(state,
"node.reset()\n"
"local root = node.create(nil, {type = 'custom', w = 'fill', h = "
"tree.reset()\n"
"local root = tree.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"
"tree.setPainter(function(id, x, y, w, h) painted = painted + w end)\n"
"assert(tree.layout(root, 0, 0, 320, 240))\n"
"tree.draw(root)\n"
"assert(painted == 320)");
// A timer firing inside draw is still one batch.
@@ -245,8 +249,22 @@ 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(touch == nil and buttons.isPressed ~= nil)");
}
// A screenless firmware still runs: no gui provider, no screen/tree
// namespaces, but a callback batch still completes.
{
fake::Bench screenless;
esp32lua::Providers providers = screenless.providers();
providers.gui = nullptr;
esp32lua::Runtime noScreen(providers);
assert(noScreen.open());
assert(!noScreen.hasFeature("screen"));
run(noScreen.state(), "assert(not sys.hasFeature('screen'))\n"
"assert(screen == nil and tree == nil)\n"
"assert(sys.getTimezone() == 'UTC0')");
noScreen.callDraw(0);
}
// App loading: a fresh state per app, main.lua deciding where apps and