diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..44b086d --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Remove: [-mlongcalls, -fstrict-volatile-bitfields, -fno-tree-switch-conversion] diff --git a/.gitignore b/.gitignore index 3cb8047..8a477fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .pio/ .cache/ +/compile_commands.json _scratch/ # Shared Lua modules are copied in from lib/esp32-lua-api by `make sdcard`. diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..a223407 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,11 @@ +{ + "runtime.version": "Lua 5.4", + "workspace.library": [ + "./lib/esp32-lua-api/lua/api/core", + "./lib/esp32-lua-api/lua/api/features" + ], + "workspace.ignoreDir": [ + "test", + "lib/esp32-lua-api/lua/test" + ] +} diff --git a/Makefile b/Makefile index 90ee946..63a045f 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ BUILD_DIR := .pio/build/esp32-32e SHARED_LUA := lib/esp32-lua-api/lua/lib LUA_TESTS := test/keyboard.lua test/settings_calibration.lua test/statusbar_dirty.lua test/settings_busy.lua -.PHONY: test test-lua test-cpp test-shared sdcard build upload monitor clean +.PHONY: test test-lua test-cpp test-shared sdcard compiledb build upload monitor clean # The shared modules ship from the submodule rather than a copy in this repo, so an app # on the card and an app on the host resolve the same ui.lua. Run before flashing or @@ -35,6 +35,9 @@ $(BUILD_DIR)/round_rect_test: test/round_rect_test.cpp src/gfx/round_rect.h @mkdir -p $(@D) @$(CXX) $(CXXFLAGS) $< -o $@ +compiledb: + pio run -t compiledb + build: pio run diff --git a/lib/esp32-lua-api b/lib/esp32-lua-api index 6a9b082..fa35c3c 160000 --- a/lib/esp32-lua-api +++ b/lib/esp32-lua-api @@ -1 +1 @@ -Subproject commit 6a9b082f97e367325c7c2aed5a701ea60c45bd01 +Subproject commit fa35c3c3262bf5270793e9f7c41938180fcb1471 diff --git a/platformio.ini b/platformio.ini index 46c8637..e7a7277 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,6 +4,7 @@ board = esp32dev framework = arduino monitor_speed = 115200 upload_speed = 921600 +extra_scripts = pre:platformio_compiledb.py board_build.partitions = partitions.csv diff --git a/platformio_compiledb.py b/platformio_compiledb.py new file mode 100644 index 0000000..a0a75a5 --- /dev/null +++ b/platformio_compiledb.py @@ -0,0 +1,2 @@ +Import("env") +env.Replace(COMPILATIONDB_INCLUDE_TOOLCHAIN=True) diff --git a/src/gfx/round_rect.h b/src/gfx/round_rect.h index 2e598df..72d37af 100644 --- a/src/gfx/round_rect.h +++ b/src/gfx/round_rect.h @@ -1,46 +1,56 @@ #pragma once -// Pure geometry and colour maths for the rounded-rect primitive, free of Arduino -// headers so test/round_rect_test.cpp can exercise it on the host. The previous -// corner arc was wrong for a year's worth of pixels because nothing but the eye -// ever checked it. +// Pure geometry and colour maths for the rounded-rect primitive, free of +// Arduino headers so test/round_rect_test.cpp can exercise it on the host. The +// previous corner arc was wrong for a year's worth of pixels because nothing +// but the eye ever checked it. #include #include namespace gfx { -// Signed distance to a rounded rectangle centred on the origin: negative inside, -// and near the edge its magnitude is the distance in pixels, so coverage falls out. -inline float roundRectDistance(float px, float py, float halfWidth, float halfHeight, float radius) { +// Signed distance to a rounded rectangle centred on the origin: negative +// inside, and near the edge its magnitude is the distance in pixels, so +// coverage falls out. +inline float roundRectDistance(float px, float py, float halfWidth, + float halfHeight, float radius) { float qx = std::fabs(px) - (halfWidth - radius); float qy = std::fabs(py) - (halfHeight - radius); float outsideX = std::fmax(qx, 0.0f), outsideY = std::fmax(qy, 0.0f); - return std::sqrt(outsideX * outsideX + outsideY * outsideY) + std::fmin(std::fmax(qx, qy), 0.0f) - radius; + return std::sqrt(outsideX * outsideX + outsideY * outsideY) + + std::fmin(std::fmax(qx, qy), 0.0f) - radius; } -// Pixel centres sit half a unit inside their cell, which is what makes a straight -// edge land on exactly full coverage instead of a half-lit fringe. +// Pixel centres sit half a unit inside their cell, which is what makes a +// straight edge land on exactly full coverage instead of a half-lit fringe. inline float coverage(float distance) { return std::fmin(std::fmax(0.5f - distance, 0.0f), 1.0f); } inline uint16_t blend565(uint16_t under, uint16_t over, float amount) { - if (amount <= 0.0f) return under; - if (amount >= 1.0f) return over; + if (amount <= 0.0f) + return under; + if (amount >= 1.0f) + return over; int a = static_cast(amount * 255.0f + 0.5f), inverse = 255 - a; - int r = (((over >> 11) & 0x1F) * a + ((under >> 11) & 0x1F) * inverse + 127) / 255; - int g = (((over >> 5) & 0x3F) * a + ((under >> 5) & 0x3F) * inverse + 127) / 255; + int r = (((over >> 11) & 0x1F) * a + ((under >> 11) & 0x1F) * inverse + 127) / + 255; + int g = + (((over >> 5) & 0x3F) * a + ((under >> 5) & 0x3F) * inverse + 127) / 255; int b = ((over & 0x1F) * a + (under & 0x1F) * inverse + 127) / 255; return static_cast((r << 11) | (g << 5) | b); } inline uint16_t lerp565(uint16_t from, uint16_t to, int step, int steps) { - if (steps <= 0) return from; - int r = ((from >> 11) & 0x1F) + (((to >> 11) & 0x1F) - ((from >> 11) & 0x1F)) * step / steps; - int g = ((from >> 5) & 0x3F) + (((to >> 5) & 0x3F) - ((from >> 5) & 0x3F)) * step / steps; + if (steps <= 0) + return from; + int r = ((from >> 11) & 0x1F) + + (((to >> 11) & 0x1F) - ((from >> 11) & 0x1F)) * step / steps; + int g = ((from >> 5) & 0x3F) + + (((to >> 5) & 0x3F) - ((from >> 5) & 0x3F)) * step / steps; int b = (from & 0x1F) + ((to & 0x1F) - (from & 0x1F)) * step / steps; return static_cast((r << 11) | (g << 5) | b); } -} // namespace gfx +} // namespace gfx diff --git a/src/host/lua_host.cpp b/src/host/lua_host.cpp index 32fe771..22ae72f 100644 --- a/src/host/lua_host.cpp +++ b/src/host/lua_host.cpp @@ -21,17 +21,19 @@ esp32lua::Paths slatePaths() { } // Reads an optional positive integer field off the bar module on the stack top. -lua_Integer barField(lua_State* state, const char* key, lua_Integer fallback) { +lua_Integer barField(lua_State *state, const char *key, lua_Integer fallback) { lua_getfield(state, -1, key); - const lua_Integer value = lua_isinteger(state, -1) ? lua_tointeger(state, -1) : fallback; + const lua_Integer value = + lua_isinteger(state, -1) ? lua_tointeger(state, -1) : fallback; lua_pop(state, 1); return value > 0 ? value : fallback; } -} // namespace +} // namespace -// Called from the initializer list once every provider member exists, which declaration order -// guarantees: the runtime holds pointers to them for its whole life. +// Called from the initializer list once every provider member exists, which +// declaration order guarantees: the runtime holds pointers to them for its +// whole life. esp32lua::Providers LuaHost::wire() { esp32lua::Providers providers; providers.log = &logProvider; @@ -44,126 +46,140 @@ esp32lua::Providers LuaHost::wire() { providers.wifi = &wifiProvider; providers.ble = &bleProvider; providers.touch = &touchProvider; - // No buttons on this board, so sys.hasFeature("buttons") is false and input gains nothing. + // No buttons on this board, so sys.hasFeature("buttons") is false and input + // gains nothing. return providers; } -LuaHost::LuaHost(TFT_eSPI& tft, XPT2046_Touchscreen& touch) - : tft(tft), - touchPanel(touch), - settingsProvider(*this), - guiProvider(tft, *this), - touchProvider(touch, *this), +LuaHost::LuaHost(TFT_eSPI &tft, XPT2046_Touchscreen &touch) + : tft(tft), touchPanel(touch), settingsProvider(*this), + guiProvider(tft, *this), touchProvider(touch, *this), runtime(wire(), slatePaths()) {} -void LuaHost::mapTouch(const TS_Point& point, int16_t& x, int16_t& y) const { - const int16_t nx = constrain(map(point.y, settings.touchX0, settings.touchX1, 0, PANEL_W), 0, PANEL_W - 1); - const int16_t ny = constrain(map(point.x, settings.touchY0, settings.touchY1, 0, PANEL_H), 0, PANEL_H - 1); +void LuaHost::mapTouch(const TS_Point &point, int16_t &x, int16_t &y) const { + const int16_t nx = + constrain(map(point.y, settings.touchX0, settings.touchX1, 0, PANEL_W), 0, + PANEL_W - 1); + const int16_t ny = + constrain(map(point.x, settings.touchY0, settings.touchY1, 0, PANEL_H), 0, + PANEL_H - 1); switch (tft.getRotation() & 3) { - case 1: - x = ny; - y = PANEL_W - 1 - nx; - break; - case 2: - x = PANEL_W - 1 - nx; - y = PANEL_H - 1 - ny; - break; - case 3: - x = PANEL_H - 1 - ny; - y = nx; - break; - default: - x = nx; - y = ny; - break; + case 1: + x = ny; + y = PANEL_W - 1 - nx; + break; + case 2: + x = PANEL_W - 1 - nx; + y = PANEL_H - 1 - ny; + break; + case 3: + x = PANEL_H - 1 - ny; + y = nx; + break; + default: + x = nx; + y = ny; + break; } - // App space starts below the bar, matching the viewport apps draw into. A tap on the bar - // itself lands at a negative y, which pollTouch() drops. + // App space starts below the bar, matching the viewport apps draw into. A tap + // on the bar itself lands at a negative y, which pollTouch() drops. y -= barInset(); } -// resetViewport() first: width()/height() report the viewport once one is set, so re-applying -// over an existing one would shrink the app area again every time. setRotation() leaves the old -// viewport metrics behind, so every rotation needs this too. +// resetViewport() first: width()/height() report the viewport once one is set, +// so re-applying over an existing one would shrink the app area again every +// time. setRotation() leaves the old viewport metrics behind, so every rotation +// needs this too. void LuaHost::applyViewport() { const int16_t inset = barInset(); tft.resetViewport(); - if (inset > 0) tft.setViewport(0, inset, tft.width(), tft.height() - inset, true); + if (inset > 0) + tft.setViewport(0, inset, tft.width(), tft.height() - inset, true); } void LuaHost::applyRotation() { tft.setRotation(settings.rotationIndex()); applyViewport(); - refreshStatusBar(); // every slot in the bar just moved + refreshStatusBar(); // every slot in the bar just moved } void LuaHost::setFullscreen(bool on) { fullscreen = on; applyViewport(); - nextBarMs = 0; // leaving fullscreen left the bar's rows painted by the app + nextBarMs = 0; // leaving fullscreen left the bar's rows painted by the app } bool LuaHost::begin() { prepareForApp(); - if (!runtime.startApp("Home")) return false; + if (!runtime.startApp("Home")) + return false; settleNewApp(); return true; } void LuaHost::prepareForApp() { - tft.setRotation(settings.rotationIndex()); // the previous app may have rotated the frame + tft.setRotation( + settings.rotationIndex()); // the previous app may have rotated the frame fullscreen = false; - // Applied before the app loads, because init() measures the panel it was given. The height - // is the last app's, which is the same module, and settleNewApp() corrects it if that changes. + // Applied before the app loads, because init() measures the panel it was + // given. The height is the last app's, which is the same module, and + // settleNewApp() corrects it if that changes. applyViewport(); - lastTouched = true; // the tap that launched this app may still be down - ignoreRelease = true; // and its release is not this app's gesture + lastTouched = true; // the tap that launched this app may still be down + ignoreRelease = true; // and its release is not this app's gesture backArmed = false; - Serial.printf("[lua] launching free=%u largest=%u\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + Serial.printf("[lua] launching free=%u largest=%u\n", ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); } void LuaHost::settleNewApp() { - hasBack = runtime.canGoBack(); // the runtime keeps the history; the bar only offers the control + hasBack = runtime.canGoBack(); // the runtime keeps the history; the bar only + // offers the control loadStatusBar(); applyViewport(); - if (barInset() > 0 && !barBroken) drawStatusBar(); + if (barInset() > 0 && !barBroken) + drawStatusBar(); const uint32_t now = millis(); nextDrawMs = now; lastDrawMs = now; Serial.printf("[lua] running %s\n", runtime.appPath().c_str()); } -// The bar is a Lua module like any other, loaded per app because the state is too. +// The bar is a Lua module like any other, loaded per app because the state is +// too. void LuaHost::loadStatusBar() { - lua_State* state = runtime.state(); + lua_State *state = runtime.state(); barBroken = false; barIntervalMs = BAR_INTERVAL_MS; lua_getglobal(state, "require"); lua_pushstring(state, "statusbar"); if (lua_pcall(state, 1, 1, 0) != LUA_OK || !lua_istable(state, -1)) { - Serial.printf("[statusbar] unavailable: %s\n", luaL_tolstring(state, -1, nullptr)); - // Nothing has measured the panel yet, so surrendering the strip here is free. + Serial.printf("[statusbar] unavailable: %s\n", + luaL_tolstring(state, -1, nullptr)); + // Nothing has measured the panel yet, so surrendering the strip here is + // free. barBroken = true; barHeight = 0; lua_pop(state, 2); return; } barHeight = barField(state, "height", DEFAULT_BAR_H); - barIntervalMs = std::max(DRAW_INTERVAL_MS, barField(state, "interval", BAR_INTERVAL_MS)); + barIntervalMs = std::max( + DRAW_INTERVAL_MS, barField(state, "interval", BAR_INTERVAL_MS)); lua_setglobal(state, "__statusbar"); nextBarMs = 0; } -// Errors here disable the bar rather than killing the app: chrome that fails should not take -// the running program with it. +// Errors here disable the bar rather than killing the app: chrome that fails +// should not take the running program with it. void LuaHost::drawStatusBar() { - lua_State* state = runtime.state(); + lua_State *state = runtime.state(); lua_getglobal(state, "__statusbar"); lua_getfield(state, -1, "draw"); lua_remove(state, -2); lua_pushboolean(state, hasBack); - tft.resetViewport(); // the bar paints in panel coordinates, the app does not + tft.resetViewport(); // the bar paints in panel coordinates, the app does not if (lua_pcall(state, 1, 0, 0) != LUA_OK) { Serial.printf("[statusbar] %s\n", luaL_tolstring(state, -1, nullptr)); lua_pop(state, 2); @@ -172,9 +188,9 @@ void LuaHost::drawStatusBar() { applyViewport(); } -// Deliberately not themed: this can fire when the settings naming the theme are themselves -// unreadable, so it stays high contrast. -void LuaHost::fail(const char* message) { +// Deliberately not themed: this can fire when the settings naming the theme are +// themselves unreadable, so it stays high contrast. +void LuaHost::fail(const char *message) { Serial.printf("[lua] %s\n", message ? message : "(unknown)"); tft.resetViewport(); tft.fillScreen(TFT_WHITE); @@ -187,10 +203,12 @@ void LuaHost::fail(const char* message) { void LuaHost::pollTouch() { bool touched = touchPanel.touched(); - if (touched) mapTouch(touchPanel.getPoint(), lastX, lastY); + if (touched) + mapTouch(touchPanel.getPoint(), lastX, lastY); - // The bar is the host's: apps never see a touch in it, and its one control acts on release, - // so a press that slides off into the app cancels like any other button. + // The bar is the host's: apps never see a touch in it, and its one control + // acts on release, so a press that slides off into the app cancels like any + // other button. if (touched && lastY < 0) { backArmed = backArmed || inBackButton(); touched = false; @@ -207,8 +225,9 @@ void LuaHost::pollTouch() { movedY = lastY; runtime.callTouch(esp32lua::TouchPhase::Down, lastX, lastY); } else if (touched && lastTouched) { - // The XPT2046 jitters a pixel or two under a still finger. Below this every poll would - // fire a move, and a painting app would draw a blur where nothing moved. + // The XPT2046 jitters a pixel or two under a still finger. Below this every + // poll would fire a move, and a painting app would draw a blur where + // nothing moved. if (abs(lastX - movedX) + abs(lastY - movedY) >= MOVE_EPSILON_PX) { movedX = lastX; movedY = lastY; @@ -228,7 +247,8 @@ void LuaHost::pollTimers() { timerProvider.collectDue(millis(), due); for (size_t at = 0; at < due.size(); at++) { runtime.callTimer(due[at]); - if (runtime.hasPendingNavigation()) return; // the app is leaving; its other timers can wait + if (runtime.hasPendingNavigation()) + return; // the app is leaving; its other timers can wait } } @@ -242,10 +262,12 @@ void LuaHost::navigate() { } void LuaHost::loop() { - if (!runtime.hasApp()) return; + if (!runtime.hasApp()) + return; pollTouch(); - if (!runtime.hasPendingNavigation()) pollTimers(); + if (!runtime.hasPendingNavigation()) + pollTimers(); const uint32_t now = millis(); if (!runtime.hasPendingNavigation() && now >= nextDrawMs) { @@ -254,12 +276,14 @@ void LuaHost::loop() { lastDrawMs = now; } - if (!runtime.hasPendingNavigation() && barInset() > 0 && !barBroken && now >= nextBarMs) { + if (!runtime.hasPendingNavigation() && barInset() > 0 && !barBroken && + now >= nextBarMs) { nextBarMs = now + barIntervalMs; drawStatusBar(); } - // Between batches, never inside one: swapping the lua_State mid-callback would free the VM - // that is still executing. - if (runtime.hasPendingNavigation()) navigate(); + // Between batches, never inside one: swapping the lua_State mid-callback + // would free the VM that is still executing. + if (runtime.hasPendingNavigation()) + navigate(); } diff --git a/src/host/lua_host.h b/src/host/lua_host.h index 90a9191..b984b79 100644 --- a/src/host/lua_host.h +++ b/src/host/lua_host.h @@ -1,8 +1,8 @@ #pragma once -// The firmware side of running a Lua app: the panel, the touch controller, the status bar -// and the frame pacing. The lua_State, the bindings, the node tree, app loading and -// navigation history all belong to esp32lua::Runtime. +// The firmware side of running a Lua app: the panel, the touch controller, the +// status bar and the frame pacing. The lua_State, the bindings, the node tree, +// app loading and navigation history all belong to esp32lua::Runtime. #include #include @@ -11,8 +11,8 @@ #include "providers.h" class LuaHost { - public: - LuaHost(TFT_eSPI& tft, XPT2046_Touchscreen& touch); +public: + LuaHost(TFT_eSPI &tft, XPT2046_Touchscreen &touch); // Starts the launcher, or reports the failure that stopped it. bool begin(); @@ -24,9 +24,9 @@ class LuaHost { void applyRotation(); void setFullscreen(bool on); void refreshStatusBar() { nextBarMs = 0; } - void mapTouch(const TS_Point& point, int16_t& x, int16_t& y) const; + void mapTouch(const TS_Point &point, int16_t &x, int16_t &y) const; - private: +private: static constexpr uint32_t DRAW_INTERVAL_MS = 33; static constexpr uint32_t BAR_INTERVAL_MS = 1000; static constexpr int16_t DEFAULT_BAR_H = 22; @@ -37,12 +37,14 @@ class LuaHost { int16_t barInset() const { return fullscreen ? 0 : barHeight; } bool inBackButton() const { - return hasBack && lastY < 0 && lastY >= -barInset() && lastX >= 0 && lastX < barInset(); + return hasBack && lastY < 0 && lastY >= -barInset() && lastX >= 0 && + lastX < barInset(); } esp32lua::Providers wire(); - // Every app starts the same way, whether it is the launcher at boot or a route the - // running app asked for: prepare the panel, load, then re-clip around the new bar. + // Every app starts the same way, whether it is the launcher at boot or a + // route the running app asked for: prepare the panel, load, then re-clip + // around the new bar. void prepareForApp(); void settleNewApp(); void navigate(); @@ -50,10 +52,10 @@ class LuaHost { void pollTimers(); void loadStatusBar(); void drawStatusBar(); - void fail(const char* message); + void fail(const char *message); - TFT_eSPI& tft; - XPT2046_Touchscreen& touchPanel; + TFT_eSPI &tft; + XPT2046_Touchscreen &touchPanel; slate::Log logProvider; slate::Settings settingsProvider; @@ -71,8 +73,8 @@ class LuaHost { uint32_t lastDrawMs = 0; uint32_t nextBarMs = 0; uint32_t barIntervalMs = BAR_INTERVAL_MS; - // Kept across apps: the bar is the same module for every one of them, and the viewport has - // to be right before init() measures the panel it was given. + // Kept across apps: the bar is the same module for every one of them, and the + // viewport has to be right before init() measures the panel it was given. int16_t barHeight = DEFAULT_BAR_H; bool barBroken = false; bool fullscreen = false; diff --git a/src/host/providers.h b/src/host/providers.h index b3e1633..65a6810 100644 --- a/src/host/providers.h +++ b/src/host/providers.h @@ -1,8 +1,9 @@ #pragma once -// The firmware half of the shared Lua platform: hardware behind the interfaces in -// , and nothing else. Everything above these -- bindings, argument -// checking, the node tree, app loading, navigation -- lives in lib/esp32-lua-api. +// The firmware half of the shared Lua platform: hardware behind the interfaces +// in , and nothing else. Everything above these -- bindings, +// argument checking, the node tree, app loading, navigation -- lives in +// lib/esp32-lua-api. #include #include @@ -13,51 +14,59 @@ class LuaHost; namespace slate { class Log : public esp32lua::LogProvider { - public: - void write(esp32lua::LogLevel level, const std::string& message) override; +public: + void write(esp32lua::LogLevel level, const std::string &message) override; }; class Settings : public esp32lua::SettingsProvider { - public: - explicit Settings(LuaHost& host) : host(host) {} +public: + explicit Settings(LuaHost &host) : host(host) {} int32_t rotation() const override; esp32lua::Status setRotation(int32_t degrees) override; std::string timezone() const override; - esp32lua::Status setTimezone(const std::string& timezone) override; + esp32lua::Status setTimezone(const std::string &timezone) override; - private: - LuaHost& host; +private: + LuaHost &host; }; class Sys : public esp32lua::SysProvider { - public: +public: int32_t millis() const override; esp32lua::MemoryInfo memory() const override; bool isClockSynced() const override; }; class Fs : public esp32lua::FsProvider { - public: - esp32lua::FileReader* openRead(const std::string& path) override; - bool exists(const std::string& path) const override; - esp32lua::Status fileSize(const std::string& path, int32_t& size) const override; - esp32lua::Status listDirs(const std::string& path, std::vector& names) const override; - esp32lua::Status listFiles(const std::string& path, std::vector& names) const override; - esp32lua::Status mkdir(const std::string& path) override; - esp32lua::Status readFile(const std::string& path, int32_t maxBytes, std::string& content) const override; - esp32lua::Status readLineAt(const std::string& path, int32_t offset, int32_t maxBytes, bool& found, - std::string& line, int32_t& nextOffset) const override; - esp32lua::Status remove(const std::string& path) override; - esp32lua::Status removeTree(const std::string& path) override; - esp32lua::Status rename(const std::string& source, const std::string& destination) override; - esp32lua::Status writeFile(const std::string& path, const std::string& content) override; +public: + esp32lua::FileReader *openRead(const std::string &path) override; + bool exists(const std::string &path) const override; + esp32lua::Status fileSize(const std::string &path, + int32_t &size) const override; + esp32lua::Status listDirs(const std::string &path, + std::vector &names) const override; + esp32lua::Status listFiles(const std::string &path, + std::vector &names) const override; + esp32lua::Status mkdir(const std::string &path) override; + esp32lua::Status readFile(const std::string &path, int32_t maxBytes, + std::string &content) const override; + esp32lua::Status readLineAt(const std::string &path, int32_t offset, + int32_t maxBytes, bool &found, std::string &line, + int32_t &nextOffset) const override; + esp32lua::Status remove(const std::string &path) override; + esp32lua::Status removeTree(const std::string &path) override; + esp32lua::Status rename(const std::string &source, + const std::string &destination) override; + esp32lua::Status writeFile(const std::string &path, + const std::string &content) override; }; -// Font roles map onto the one built-in font at whole-number scales, which is all TFT_eSPI -// offers here; STYLE_BOLD has no glyphs of its own and renders as normal. +// Font roles map onto the one built-in font at whole-number scales, which is +// all TFT_eSPI offers here; STYLE_BOLD has no glyphs of its own and renders as +// normal. class Gui : public esp32lua::GuiProvider { - public: - Gui(TFT_eSPI& tft, LuaHost& host) : tft(tft), host(host) {} +public: + Gui(TFT_eSPI &tft, LuaHost &host) : tft(tft), host(host) {} esp32lua::FontIds fonts() const override; int32_t width() const override; @@ -66,94 +75,124 @@ class Gui : public esp32lua::GuiProvider { void setRotation(int32_t degrees) override; int32_t color(int32_t r, int32_t g, int32_t b) const override; void clear(int32_t color) override; - void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color) override; - void drawRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color) override; - void drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t color, int32_t width) override; + void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t color) override; + void drawRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t color) override; + void drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t color, + int32_t width) override; void drawPixel(int32_t x, int32_t y, int32_t color) override; - void drawCircle(int32_t x, int32_t y, int32_t radius, int32_t color, int32_t width) override; - void fillCircle(int32_t x, int32_t y, int32_t radius, int32_t color, const int32_t* background) override; - 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) override; - void fillPolygon(const int32_t* xs, const int32_t* ys, size_t count, int32_t color) override; - esp32lua::Status drawBmp(const std::string& path, const int32_t* x, const int32_t* y, const int32_t* maxWidth, - const int32_t* maxHeight) override; - int32_t textWidth(int32_t font, const std::string& text, int32_t style) const override; + void drawCircle(int32_t x, int32_t y, int32_t radius, int32_t color, + int32_t width) override; + void fillCircle(int32_t x, int32_t y, int32_t radius, int32_t color, + const int32_t *background) override; + 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) override; + void fillPolygon(const int32_t *xs, const int32_t *ys, size_t count, + int32_t color) override; + esp32lua::Status drawBmp(const std::string &path, const int32_t *x, + const int32_t *y, const int32_t *maxWidth, + const int32_t *maxHeight) override; + int32_t textWidth(int32_t font, const std::string &text, + int32_t style) const override; int32_t fontHeight(int32_t font, int32_t style) const override; - 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) override; + 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) override; void setFullscreen(bool on) override; // The panel is live, so there is nothing pending to apply. void commit() override {} - private: - TFT_eSPI& tft; - LuaHost& host; +private: + TFT_eSPI &tft; + LuaHost &host; }; class Http : public esp32lua::HttpProvider { - public: - esp32lua::Status request(const std::string& method, const std::string& url, const std::string& body, - const std::vector& headers, int32_t maxBytes, - esp32lua::HttpResponse& response) override; - esp32lua::Status download(const std::string& url, const std::string& destination, - const esp32lua::HttpDownload& options, int32_t& bytesWritten) override; +public: + esp32lua::Status request(const std::string &method, const std::string &url, + const std::string &body, + const std::vector &headers, + int32_t maxBytes, + esp32lua::HttpResponse &response) override; + esp32lua::Status download(const std::string &url, + const std::string &destination, + const esp32lua::HttpDownload &options, + int32_t &bytesWritten) override; }; class Wifi : public esp32lua::WifiProvider { - public: - esp32lua::Status scan(std::vector& networks) override; - esp32lua::Status connect(const std::string* ssid, const std::string* password) override; +public: + esp32lua::Status scan(std::vector &networks) override; + esp32lua::Status connect(const std::string *ssid, + const std::string *password) override; esp32lua::WifiStatus status() const override; void disconnect() override; esp32lua::Status forget() override; }; -// This board has no radio for it. The namespace stays core so an app fails with a reason -// rather than a nil global it has to feature-test. +// This board has no radio for it. The namespace stays core so an app fails with +// a reason rather than a nil global it has to feature-test. class Ble : public esp32lua::BleProvider { - public: - esp32lua::Status init(const std::string*) override { return unsupported(); } +public: + esp32lua::Status init(const std::string *) override { return unsupported(); } void deinit() override {} - esp32lua::Status scan(int32_t, std::vector&) override { return unsupported(); } - esp32lua::Status connect(const std::string&) override { return unsupported(); } - void disconnect() override {} - bool isConnected() const override { return false; } - esp32lua::Status read(const std::string&, const std::string&, std::string&) override { return unsupported(); } - esp32lua::Status write(const std::string&, const std::string&, const std::string&) override { + esp32lua::Status scan(int32_t, std::vector &) override { + return unsupported(); + } + esp32lua::Status connect(const std::string &) override { + return unsupported(); + } + void disconnect() override {} + bool isConnected() const override { return false; } + esp32lua::Status read(const std::string &, const std::string &, + std::string &) override { + return unsupported(); + } + esp32lua::Status write(const std::string &, const std::string &, + const std::string &) override { + return unsupported(); + } + esp32lua::Status startAdvertising(const std::string *) override { return unsupported(); } - esp32lua::Status startAdvertising(const std::string*) override { return unsupported(); } void stopAdvertising() override {} - private: - static esp32lua::Status unsupported() { return esp32lua::Status::failure("this device has no Bluetooth"); } +private: + static esp32lua::Status unsupported() { + return esp32lua::Status::failure("this device has no Bluetooth"); + } }; class Touch : public esp32lua::TouchProvider { - public: - Touch(XPT2046_Touchscreen& panel, LuaHost& host) : panel(panel), host(host) {} - bool touch(int32_t& x, int32_t& y) const override; - bool rawTouch(int32_t& x, int32_t& y) const override; +public: + Touch(XPT2046_Touchscreen &panel, LuaHost &host) : panel(panel), host(host) {} + bool touch(int32_t &x, int32_t &y) const override; + bool rawTouch(int32_t &x, int32_t &y) const override; bool isTouched() const override; - esp32lua::Status setCalibration(int32_t x0, int32_t y0, int32_t x1, int32_t y1) override; + esp32lua::Status setCalibration(int32_t x0, int32_t y0, int32_t x1, + int32_t y1) override; - private: - XPT2046_Touchscreen& panel; - LuaHost& host; +private: + XPT2046_Touchscreen &panel; + LuaHost &host; }; -// Deadlines only: identity and the retained Lua callbacks belong to the runtime, which is -// called back through Runtime::callTimer() from the firmware loop. +// Deadlines only: identity and the retained Lua callbacks belong to the +// runtime, which is called back through Runtime::callTimer() from the firmware +// loop. class Timer : public esp32lua::TimerProvider { - public: +public: static constexpr int SLOTS = 8; - esp32lua::Status schedule(esp32lua::TimerId id, int32_t intervalMs, bool repeating) override; + esp32lua::Status schedule(esp32lua::TimerId id, int32_t intervalMs, + bool repeating) override; void cancel(esp32lua::TimerId id) override; // Ids whose deadline has passed, rescheduling the repeating ones. - void collectDue(uint32_t nowMs, std::vector& due); + void collectDue(uint32_t nowMs, std::vector &due); - private: +private: struct Slot { esp32lua::TimerId id = 0; uint32_t dueMs = 0; @@ -163,4 +202,4 @@ class Timer : public esp32lua::TimerProvider { Slot slots[SLOTS]; }; -} // namespace slate +} // namespace slate diff --git a/src/host/providers_fs.cpp b/src/host/providers_fs.cpp index 076aafa..54c9f25 100644 --- a/src/host/providers_fs.cpp +++ b/src/host/providers_fs.cpp @@ -7,30 +7,35 @@ namespace { using esp32lua::Status; -// Streamed rather than slurped: holding a whole module as one buffer needs that many bytes -// contiguous, and once WiFi is up the largest free block is far below the free heap. +// Streamed rather than slurped: holding a whole module as one buffer needs that +// many bytes contiguous, and once WiFi is up the largest free block is far +// below the free heap. class SdReader : public esp32lua::FileReader { - public: +public: explicit SdReader(File file) : file(file) {} ~SdReader() override { - if (file) file.close(); + if (file) + file.close(); } - int32_t read(char* out, int32_t maxBytes) override { - return file.read(reinterpret_cast(out), maxBytes); + int32_t read(char *out, int32_t maxBytes) override { + return file.read(reinterpret_cast(out), maxBytes); } - private: +private: File file; }; -Status listEntries(const std::string& path, bool directories, std::vector& names) { +Status listEntries(const std::string &path, bool directories, + std::vector &names) { File dir = SD.open(path.c_str()); if (!dir || !dir.isDirectory()) { - if (dir) dir.close(); + if (dir) + dir.close(); return Status::failure("not a directory"); } for (File entry = dir.openNextFile(); entry; entry = dir.openNextFile()) { - if (entry.isDirectory() == directories && entry.name()[0] != '.') names.push_back(entry.name()); + if (entry.isDirectory() == directories && entry.name()[0] != '.') + names.push_back(entry.name()); entry.close(); } dir.close(); @@ -38,9 +43,10 @@ Status listEntries(const std::string& path, bool directories, std::vector(file.size()); @@ -84,23 +96,29 @@ Status Fs::fileSize(const std::string& path, int32_t& size) const { return Status::success(); } -Status Fs::listDirs(const std::string& path, std::vector& names) const { +Status Fs::listDirs(const std::string &path, + std::vector &names) const { return listEntries(path, true, names); } -Status Fs::listFiles(const std::string& path, std::vector& names) const { +Status Fs::listFiles(const std::string &path, + std::vector &names) const { return listEntries(path, false, names); } -Status Fs::mkdir(const std::string& path) { - if (SD.exists(path.c_str())) return Status::success(); - return SD.mkdir(path.c_str()) ? Status::success() : Status::failure("cannot create directory"); +Status Fs::mkdir(const std::string &path) { + if (SD.exists(path.c_str())) + return Status::success(); + return SD.mkdir(path.c_str()) ? Status::success() + : Status::failure("cannot create directory"); } -Status Fs::readFile(const std::string& path, int32_t maxBytes, std::string& content) const { +Status Fs::readFile(const std::string &path, int32_t maxBytes, + std::string &content) const { File file = SD.open(path.c_str()); if (!file || file.isDirectory()) { - if (file) file.close(); + if (file) + file.close(); return Status::failure("not a file"); } const size_t size = file.size(); @@ -109,17 +127,21 @@ Status Fs::readFile(const std::string& path, int32_t maxBytes, std::string& cont return Status::failure("file exceeds maxBytes"); } content.resize(size); - const size_t read = size ? file.read(reinterpret_cast(&content[0]), size) : 0; + const size_t read = + size ? file.read(reinterpret_cast(&content[0]), size) : 0; file.close(); - if (read != size) return Status::failure("short read"); + if (read != size) + return Status::failure("short read"); return Status::success(); } -Status Fs::readLineAt(const std::string& path, int32_t offset, int32_t maxBytes, bool& found, std::string& line, - int32_t& nextOffset) const { +Status Fs::readLineAt(const std::string &path, int32_t offset, int32_t maxBytes, + bool &found, std::string &line, + int32_t &nextOffset) const { File file = SD.open(path.c_str()); if (!file || file.isDirectory()) { - if (file) file.close(); + if (file) + file.close(); return Status::failure("not a file"); } if (!file.seek(offset)) { @@ -127,9 +149,12 @@ Status Fs::readLineAt(const std::string& path, int32_t offset, int32_t maxBytes, return Status::failure("cannot seek"); } if (offset > 0) { - // A mid-line offset advances to the next line, so a caller can resume from any byte. - while (file.available() && file.peek() != '\n') file.read(); - if (file.available()) file.read(); + // A mid-line offset advances to the next line, so a caller can resume from + // any byte. + while (file.available() && file.peek() != '\n') + file.read(); + if (file.available()) + file.read(); } if (!file.available()) { file.close(); @@ -140,8 +165,10 @@ Status Fs::readLineAt(const std::string& path, int32_t offset, int32_t maxBytes, line.clear(); while (file.available()) { const int c = file.read(); - if (c == '\n') break; - if (c != '\r') line.push_back(static_cast(c)); + if (c == '\n') + break; + if (c != '\r') + line.push_back(static_cast(c)); if (static_cast(line.size()) > maxBytes) { file.close(); return Status::failure("line exceeds maxBytes"); @@ -153,30 +180,40 @@ Status Fs::readLineAt(const std::string& path, int32_t offset, int32_t maxBytes, return Status::success(); } -Status Fs::remove(const std::string& path) { - return SD.remove(path.c_str()) ? Status::success() : Status::failure("cannot remove"); +Status Fs::remove(const std::string &path) { + return SD.remove(path.c_str()) ? Status::success() + : Status::failure("cannot remove"); } -Status Fs::removeTree(const std::string& path) { - return removeRecursive(String(path.c_str())) ? Status::success() : Status::failure("cannot remove tree"); +Status Fs::removeTree(const std::string &path) { + return removeRecursive(String(path.c_str())) + ? Status::success() + : Status::failure("cannot remove tree"); } -Status Fs::rename(const std::string& source, const std::string& destination) { - if (SD.exists(destination.c_str())) return Status::failure("destination exists"); - return SD.rename(source.c_str(), destination.c_str()) ? Status::success() : Status::failure("cannot rename"); +Status Fs::rename(const std::string &source, const std::string &destination) { + if (SD.exists(destination.c_str())) + return Status::failure("destination exists"); + return SD.rename(source.c_str(), destination.c_str()) + ? Status::success() + : Status::failure("cannot rename"); } -// Written to a temporary first: a half-written file that replaced a good one is worse than a -// failed write that left the old contents alone. -Status Fs::writeFile(const std::string& path, const std::string& content) { +// Written to a temporary first: a half-written file that replaced a good one is +// worse than a failed write that left the old contents alone. +Status Fs::writeFile(const std::string &path, const std::string &content) { const String target(path.c_str()); const String temporary = target + ".tmp"; SD.remove(temporary); File file = SD.open(temporary, FILE_WRITE); - if (!file) return Status::failure("cannot open for writing"); - const size_t written = content.empty() ? 0 : file.write(reinterpret_cast(content.data()), - content.size()); + if (!file) + return Status::failure("cannot open for writing"); + const size_t written = + content.empty() + ? 0 + : file.write(reinterpret_cast(content.data()), + content.size()); file.close(); if (written != content.size()) { SD.remove(temporary); @@ -190,4 +227,4 @@ Status Fs::writeFile(const std::string& path, const std::string& content) { return Status::success(); } -} // namespace slate +} // namespace slate diff --git a/src/host/providers_gui.cpp b/src/host/providers_gui.cpp index dfd48b0..5971301 100644 --- a/src/host/providers_gui.cpp +++ b/src/host/providers_gui.cpp @@ -8,18 +8,24 @@ namespace slate { namespace { -constexpr int MAX_SPAN = 480; // longest panel edge, so one row buffer covers any shape +constexpr int MAX_SPAN = + 480; // longest panel edge, so one row buffer covers any shape -// One primitive draws the whole surface: fill (solid or vertical gradient) and border derive -// from the same distance field, so they cannot disagree at the corners the way two separate -// rounded-rect algorithms did. The panel has no alpha, so edge pixels blend against `surface`. -void paintRoundRect(TFT_eSPI& tft, int x, int y, int w, int h, float radius, uint16_t surface, bool hasFill, - uint16_t top, uint16_t bottom, bool hasBorder, uint16_t border) { - if (w <= 0 || h <= 0 || w > MAX_SPAN) return; +// One primitive draws the whole surface: fill (solid or vertical gradient) and +// border derive from the same distance field, so they cannot disagree at the +// corners the way two separate rounded-rect algorithms did. The panel has no +// alpha, so edge pixels blend against `surface`. +void paintRoundRect(TFT_eSPI &tft, int x, int y, int w, int h, float radius, + uint16_t surface, bool hasFill, uint16_t top, + uint16_t bottom, bool hasBorder, uint16_t border) { + if (w <= 0 || h <= 0 || w > MAX_SPAN) + return; float halfWidth = w * 0.5f, halfHeight = h * 0.5f; - if (radius < 0.0f) radius = 0.0f; + if (radius < 0.0f) + radius = 0.0f; const float limit = (w < h ? w : h) / 2.0f; - if (radius > limit) radius = limit; + if (radius > limit) + radius = limit; const int ir = static_cast(radius + 0.5f); // Gradient fills need a per-pixel row blend that DMA cannot produce. @@ -31,12 +37,14 @@ void paintRoundRect(TFT_eSPI& tft, int x, int y, int w, int h, float radius, uin const uint16_t fill = gfx::lerp565(top, bottom, row, h - 1); const float py = row + 0.5f - halfHeight; for (int column = 0; column < w; column++) { - const float distance = gfx::roundRectDistance(column + 0.5f - halfWidth, py, halfWidth, halfHeight, radius); + const float distance = gfx::roundRectDistance( + column + 0.5f - halfWidth, py, halfWidth, halfHeight, radius); const float outer = gfx::coverage(distance); const float inner = hasBorder ? gfx::coverage(distance + 1.0f) : outer; uint16_t pixel = surface; pixel = gfx::blend565(pixel, fill, outer); - if (hasBorder) pixel = gfx::blend565(pixel, border, outer - inner); + if (hasBorder) + pixel = gfx::blend565(pixel, border, outer - inner); span[column] = pixel; } tft.pushImage(x, y + row, w, 1, span); @@ -45,10 +53,11 @@ void paintRoundRect(TFT_eSPI& tft, int x, int y, int w, int h, float radius, uin return; } - // Flat fill: DMA the interior and straight edges, compute the distance field only at corners. - // The cost drops from O(w*h) to O(radius^2) per-pixel work. + // Flat fill: DMA the interior and straight edges, compute the distance field + // only at corners. The cost drops from O(w*h) to O(radius^2) per-pixel work. const uint16_t fill = hasFill ? top : surface; - if (h > 2 * ir) tft.fillRect(x, y + ir, w, h - 2 * ir, fill); + if (h > 2 * ir) + tft.fillRect(x, y + ir, w, h - 2 * ir, fill); if (w > 2 * ir) { tft.fillRect(x + ir, y, w - 2 * ir, ir, fill); tft.fillRect(x + ir, y + h - ir, w - 2 * ir, ir, fill); @@ -75,12 +84,16 @@ void paintRoundRect(TFT_eSPI& tft, int x, int y, int w, int h, float radius, uin const float py = (rowStart + row) + 0.5f - halfHeight; for (int col = 0; col < ir; col++) { const float px = (colStart + col) + 0.5f - halfWidth; - const float distance = gfx::roundRectDistance(px, py, halfWidth, halfHeight, radius); + const float distance = + gfx::roundRectDistance(px, py, halfWidth, halfHeight, radius); const float outer = gfx::coverage(distance); - const float inner = hasBorder ? gfx::coverage(distance + 1.0f) : outer; + const float inner = + hasBorder ? gfx::coverage(distance + 1.0f) : outer; uint16_t pixel = surface; - if (hasFill) pixel = gfx::blend565(pixel, top, outer); - if (hasBorder) pixel = gfx::blend565(pixel, border, outer - inner); + if (hasFill) + pixel = gfx::blend565(pixel, top, outer); + if (hasBorder) + pixel = gfx::blend565(pixel, border, outer - inner); span[col] = pixel; } tft.pushImage(x + colStart, y + rowStart + row, ir, 1, span); @@ -90,10 +103,13 @@ void paintRoundRect(TFT_eSPI& tft, int x, int y, int w, int h, float radius, uin } } -// Font roles are whole-number scales of the one built-in font, which is what this driver has. -uint8_t scaleFor(int32_t font) { return static_cast(font < 1 ? 1 : (font > 8 ? 8 : font)); } +// Font roles are whole-number scales of the one built-in font, which is what +// this driver has. +uint8_t scaleFor(int32_t font) { + return static_cast(font < 1 ? 1 : (font > 8 ? 8 : font)); +} -} // namespace +} // namespace esp32lua::FontIds Gui::fonts() const { esp32lua::FontIds ids; @@ -102,7 +118,8 @@ esp32lua::FontIds Gui::fonts() const { ids.body = 2; ids.large = 3; ids.styleNormal = 0; - ids.styleBold = 0; // no bold glyphs, so bold text is normal text rather than a different metric + ids.styleBold = 0; // no bold glyphs, so bold text is normal text rather than + // a different metric return ids; } @@ -113,16 +130,25 @@ int32_t Gui::rotation() const { return tft.getRotation() * 90; } void Gui::setRotation(int32_t degrees) { tft.setRotation((degrees / 90) & 3); host.applyViewport(); - host.refreshStatusBar(); // every slot in the bar just moved + host.refreshStatusBar(); // every slot in the bar just moved } -int32_t Gui::color(int32_t r, int32_t g, int32_t b) const { return tft.color565(r, g, b); } +int32_t Gui::color(int32_t r, int32_t g, int32_t b) const { + return tft.color565(r, g, b); +} void Gui::clear(int32_t color) { tft.fillScreen(color); } -void Gui::fillRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color) { tft.fillRect(x, y, w, h, color); } -void Gui::drawRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color) { tft.drawRect(x, y, w, h, color); } -void Gui::drawPixel(int32_t x, int32_t y, int32_t color) { tft.drawPixel(x, y, color); } +void Gui::fillRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color) { + tft.fillRect(x, y, w, h, color); +} +void Gui::drawRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color) { + tft.drawRect(x, y, w, h, color); +} +void Gui::drawPixel(int32_t x, int32_t y, int32_t color) { + tft.drawPixel(x, y, color); +} -void Gui::drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t color, int32_t width) { +void Gui::drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, + int32_t color, int32_t width) { if (width <= 1) { tft.drawLine(x1, y1, x2, y2, color); return; @@ -130,33 +156,44 @@ void Gui::drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t color tft.drawWideLine(x1, y1, x2, y2, static_cast(width), color, color); } -void Gui::drawCircle(int32_t x, int32_t y, int32_t radius, int32_t color, int32_t width) { +void Gui::drawCircle(int32_t x, int32_t y, int32_t radius, int32_t color, + int32_t width) { for (int32_t ring = 0; ring < (width < 1 ? 1 : width); ring++) { const int32_t r = radius - ring; - if (r > 0) tft.drawCircle(x, y, r, color); + if (r > 0) + tft.drawCircle(x, y, r, color); } } -void Gui::fillCircle(int32_t x, int32_t y, int32_t radius, int32_t color, const int32_t* background) { - tft.fillSmoothCircle(x, y, radius, color, background ? *background : TFT_WHITE); +void Gui::fillCircle(int32_t x, int32_t y, int32_t radius, int32_t color, + const int32_t *background) { + tft.fillSmoothCircle(x, y, radius, color, + background ? *background : TFT_WHITE); } -void Gui::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) { +void Gui::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) { const uint16_t fillTop = top ? static_cast(*top) : 0; const uint16_t fillBottom = bottom ? static_cast(*bottom) : fillTop; - paintRoundRect(tft, x, y, w, h, static_cast(radius), static_cast(background), top != nullptr, - fillTop, fillBottom, border != nullptr, border ? static_cast(*border) : 0); + paintRoundRect(tft, x, y, w, h, static_cast(radius), + static_cast(background), top != nullptr, fillTop, + fillBottom, border != nullptr, + border ? static_cast(*border) : 0); } -// Scanline fill: TFT_eSPI only offers triangles, and fanning a concave shape into triangles -// paints outside it. -void Gui::fillPolygon(const int32_t* xs, const int32_t* ys, size_t count, int32_t color) { - if (count < 3) return; +// Scanline fill: TFT_eSPI only offers triangles, and fanning a concave shape +// into triangles paints outside it. +void Gui::fillPolygon(const int32_t *xs, const int32_t *ys, size_t count, + int32_t color) { + if (count < 3) + return; int32_t top = ys[0], bottom = ys[0]; for (size_t at = 1; at < count; at++) { - if (ys[at] < top) top = ys[at]; - if (ys[at] > bottom) bottom = ys[at]; + if (ys[at] < top) + top = ys[at]; + if (ys[at] > bottom) + bottom = ys[at]; } for (int32_t y = top; y <= bottom; y++) { @@ -166,7 +203,8 @@ void Gui::fillPolygon(const int32_t* xs, const int32_t* ys, size_t count, int32_ const size_t next = (at + 1) % count; const int32_t y0 = ys[at], y1 = ys[next]; if ((y0 <= y && y1 > y) || (y1 <= y && y0 > y)) { - crossings[found++] = xs[at] + (y - y0) * (xs[next] - xs[at]) / (y1 - y0); + crossings[found++] = + xs[at] + (y - y0) * (xs[next] - xs[at]) / (y1 - y0); } } for (size_t a = 0; a + 1 < found; a++) { @@ -179,20 +217,24 @@ void Gui::fillPolygon(const int32_t* xs, const int32_t* ys, size_t count, int32_ } } for (size_t at = 0; at + 1 < found; at += 2) { - tft.drawFastHLine(crossings[at], y, crossings[at + 1] - crossings[at] + 1, color); + tft.drawFastHLine(crossings[at], y, crossings[at + 1] - crossings[at] + 1, + color); } } } -// ponytail: no BMP decoder on this panel yet. Apps that need artwork draw it; wire a decoder -// in when one actually ships a bitmap. -esp32lua::Status Gui::drawBmp(const std::string& path, const int32_t*, const int32_t*, const int32_t*, - const int32_t*) { - if (!SD.exists(path.c_str())) return esp32lua::Status::failure("no such file"); - return esp32lua::Status::failure("BMP drawing is not implemented on this device"); +// ponytail: no BMP decoder on this panel yet. Apps that need artwork draw it; +// wire a decoder in when one actually ships a bitmap. +esp32lua::Status Gui::drawBmp(const std::string &path, const int32_t *, + const int32_t *, const int32_t *, + const int32_t *) { + if (!SD.exists(path.c_str())) + return esp32lua::Status::failure("no such file"); + return esp32lua::Status::failure( + "BMP drawing is not implemented on this device"); } -int32_t Gui::textWidth(int32_t font, const std::string& text, int32_t) const { +int32_t Gui::textWidth(int32_t font, const std::string &text, int32_t) const { tft.setTextSize(scaleFor(font)); return tft.textWidth(text.c_str()); } @@ -202,8 +244,8 @@ int32_t Gui::fontHeight(int32_t font, int32_t) const { return tft.fontHeight(); } -void Gui::drawText(int32_t font, int32_t x, int32_t y, const std::string& text, int32_t color, int32_t, - const int32_t* background) { +void Gui::drawText(int32_t font, int32_t x, int32_t y, const std::string &text, + int32_t color, int32_t, const int32_t *background) { tft.setTextSize(scaleFor(font)); if (background) { tft.setTextColor(color, *background); @@ -215,4 +257,4 @@ void Gui::drawText(int32_t font, int32_t x, int32_t y, const std::string& text, void Gui::setFullscreen(bool on) { host.setFullscreen(on); } -} // namespace slate +} // namespace slate diff --git a/src/host/providers_net.cpp b/src/host/providers_net.cpp index 1ff9cef..ffc3b8b 100644 --- a/src/host/providers_net.cpp +++ b/src/host/providers_net.cpp @@ -8,7 +8,8 @@ #include "../settings.h" #include "providers.h" -extern const uint8_t rootca_crt_bundle_start[] asm("_binary_x509_crt_bundle_start"); +extern const uint8_t + rootca_crt_bundle_start[] asm("_binary_x509_crt_bundle_start"); namespace slate { namespace { @@ -19,20 +20,22 @@ constexpr uint32_t TIMEOUT_MS = 60000; constexpr int REDIRECT_LIMIT = 5; constexpr size_t DOWNLOAD_CHUNK = 2048; -bool isHttps(const std::string& url) { return url.rfind("https://", 0) == 0; } +bool isHttps(const std::string &url) { return url.rfind("https://", 0) == 0; } -// One session owns both clients: the secure one carries the CA bundle, and HTTPClient keeps a -// reference to whichever it was handed, so neither may outlive the request. +// One session owns both clients: the secure one carries the CA bundle, and +// HTTPClient keeps a reference to whichever it was handed, so neither may +// outlive the request. struct Session { WiFiClient plain; WiFiClientSecure secure; HTTPClient http; - bool begin(const std::string& url) { + bool begin(const std::string &url) { if (isHttps(url)) { secure.setCACertBundle(rootca_crt_bundle_start); secure.setTimeout(TIMEOUT_MS / 1000); - if (!http.begin(secure, url.c_str())) return false; + if (!http.begin(secure, url.c_str())) + return false; } else if (!http.begin(plain, url.c_str())) { return false; } @@ -45,16 +48,18 @@ struct Session { } }; -bool hashFile(const char* path, uint8_t digest[32]) { +bool hashFile(const char *path, uint8_t digest[32]) { File file = SD.open(path); - if (!file) return false; + if (!file) + return false; mbedtls_sha256_context sha; mbedtls_sha256_init(&sha); mbedtls_sha256_starts_ret(&sha, 0); static uint8_t chunk[DOWNLOAD_CHUNK]; while (file.available()) { const int read = file.read(chunk, sizeof(chunk)); - if (read <= 0) break; + if (read <= 0) + break; mbedtls_sha256_update_ret(&sha, chunk, read); } file.close(); @@ -63,7 +68,7 @@ bool hashFile(const char* path, uint8_t digest[32]) { return true; } -std::string hex(const uint8_t* digest, size_t length) { +std::string hex(const uint8_t *digest, size_t length) { std::string out; for (size_t at = 0; at < length; at++) { char pair[3]; @@ -73,59 +78,76 @@ std::string hex(const uint8_t* digest, size_t length) { return out; } -bool equalsIgnoreCase(const std::string& a, const std::string& b) { - if (a.size() != b.size()) return false; +bool equalsIgnoreCase(const std::string &a, const std::string &b) { + if (a.size() != b.size()) + return false; for (size_t at = 0; at < a.size(); at++) { - if (tolower(static_cast(a[at])) != tolower(static_cast(b[at]))) return false; + if (tolower(static_cast(a[at])) != + tolower(static_cast(b[at]))) + return false; } return true; } -} // namespace +} // namespace -Status Http::request(const std::string& method, const std::string& url, const std::string& body, - const std::vector& headers, int32_t maxBytes, - esp32lua::HttpResponse& response) { +Status Http::request(const std::string &method, const std::string &url, + const std::string &body, + const std::vector &headers, + int32_t maxBytes, esp32lua::HttpResponse &response) { Session session; - if (!session.begin(url)) return Status::failure("cannot reach " + url); + if (!session.begin(url)) + return Status::failure("cannot reach " + url); for (size_t at = 0; at < headers.size(); at++) { session.http.addHeader(headers[at].name.c_str(), headers[at].value.c_str()); } - const int status = session.http.sendRequest(method.c_str(), reinterpret_cast(const_cast( - body.data())), - body.size()); - if (status <= 0) return Status::failure(HTTPClient::errorToString(status).c_str()); + const int status = session.http.sendRequest( + method.c_str(), + reinterpret_cast(const_cast(body.data())), + body.size()); + if (status <= 0) + return Status::failure(HTTPClient::errorToString(status).c_str()); - // Refused on the declared size where there is one, so an oversized body is never allocated. + // Refused on the declared size where there is one, so an oversized body is + // never allocated. const int declared = session.http.getSize(); - if (declared > maxBytes) return Status::failure("response exceeds maxBytes"); + if (declared > maxBytes) + return Status::failure("response exceeds maxBytes"); const String payload = session.http.getString(); - if (static_cast(payload.length()) > maxBytes) return Status::failure("response exceeds maxBytes"); + if (static_cast(payload.length()) > maxBytes) + return Status::failure("response exceeds maxBytes"); response.status = status; response.body.assign(payload.c_str(), payload.length()); return Status::success(); } -Status Http::download(const std::string& url, const std::string& destination, const esp32lua::HttpDownload& options, - int32_t& bytesWritten) { - if (!isHttps(url)) return Status::failure("download requires an HTTPS URL"); - if (SD.exists(destination.c_str())) return Status::failure("destination exists"); +Status Http::download(const std::string &url, const std::string &destination, + const esp32lua::HttpDownload &options, + int32_t &bytesWritten) { + if (!isHttps(url)) + return Status::failure("download requires an HTTPS URL"); + if (SD.exists(destination.c_str())) + return Status::failure("destination exists"); Session session; - if (!session.begin(url)) return Status::failure("cannot reach " + url); - if (session.http.GET() != HTTP_CODE_OK) return Status::failure("download failed"); + if (!session.begin(url)) + return Status::failure("cannot reach " + url); + if (session.http.GET() != HTTP_CODE_OK) + return Status::failure("download failed"); const int declared = session.http.getSize(); - if (declared > 0 && declared > options.maxBytes) return Status::failure("download exceeds maxBytes"); + if (declared > 0 && declared > options.maxBytes) + return Status::failure("download exceeds maxBytes"); File out = SD.open(destination.c_str(), FILE_WRITE); - if (!out) return Status::failure("cannot open destination"); + if (!out) + return Status::failure("cannot open destination"); const int result = session.http.writeToStream(&out); out.close(); const int32_t written = result > 0 ? result : 0; - const char* failure = nullptr; + const char *failure = nullptr; if (result < 0) { failure = "download failed"; } else if (written == 0) { @@ -137,7 +159,8 @@ Status Http::download(const std::string& url, const std::string& destination, co } if (!failure && !options.sha256.empty()) { uint8_t digest[32]; - if (!hashFile(destination.c_str(), digest) || !equalsIgnoreCase(options.sha256, hex(digest, sizeof(digest)))) { + if (!hashFile(destination.c_str(), digest) || + !equalsIgnoreCase(options.sha256, hex(digest, sizeof(digest)))) { failure = "SHA-256 did not match"; } } @@ -150,28 +173,33 @@ Status Http::download(const std::string& url, const std::string& destination, co return Status::success(); } -Status Wifi::scan(std::vector& networks) { +Status Wifi::scan(std::vector &networks) { const int found = WiFi.scanNetworks(); - if (found < 0) return Status::failure("scan failed"); + if (found < 0) + return Status::failure("scan failed"); for (int at = 0; at < found; at++) { esp32lua::WifiNetwork network; network.ssid = WiFi.SSID(at).c_str(); network.rssi = WiFi.RSSI(at); network.secure = WiFi.encryptionType(at) != WIFI_AUTH_OPEN; - if (!network.ssid.empty()) networks.push_back(network); + if (!network.ssid.empty()) + networks.push_back(network); } WiFi.scanDelete(); return Status::success(); } -Status Wifi::connect(const std::string* ssid, const std::string* password) { +Status Wifi::connect(const std::string *ssid, const std::string *password) { if (ssid) { - if (ssid->empty()) return Status::failure("ssid is empty"); + if (ssid->empty()) + return Status::failure("ssid is empty"); settings.wifiSsid = ssid->c_str(); settings.wifiPassword = password ? password->c_str() : ""; - if (!settings.save()) return Status::failure("cannot save credentials"); + if (!settings.save()) + return Status::failure("cannot save credentials"); } - if (settings.wifiSsid.isEmpty()) return Status::failure("no saved network"); + if (settings.wifiSsid.isEmpty()) + return Status::failure("no saved network"); WiFi.mode(WIFI_STA); WiFi.begin(settings.wifiSsid.c_str(), settings.wifiPassword.c_str()); @@ -184,21 +212,22 @@ esp32lua::WifiStatus Wifi::status() const { status.ip = WiFi.localIP().toString().c_str(); status.rssi = WiFi.RSSI(); switch (WiFi.status()) { - case WL_CONNECTED: - status.state = "connected"; - break; - case WL_IDLE_STATUS: - case WL_DISCONNECTED: - status.state = settings.wifiSsid.isEmpty() ? "disconnected" : "connecting"; - break; - case WL_NO_SSID_AVAIL: - status.state = "not_found"; - break; - default: - status.state = "failed"; - break; + case WL_CONNECTED: + status.state = "connected"; + break; + case WL_IDLE_STATUS: + case WL_DISCONNECTED: + status.state = settings.wifiSsid.isEmpty() ? "disconnected" : "connecting"; + break; + case WL_NO_SSID_AVAIL: + status.state = "not_found"; + break; + default: + status.state = "failed"; + break; } - if (status.state != "connected") status.ip = "0.0.0.0"; + if (status.state != "connected") + status.ip = "0.0.0.0"; return status; } @@ -208,7 +237,8 @@ Status Wifi::forget() { WiFi.disconnect(true); settings.wifiSsid = ""; settings.wifiPassword = ""; - return settings.save() ? Status::success() : Status::failure("cannot save settings"); + return settings.save() ? Status::success() + : Status::failure("cannot save settings"); } -} // namespace slate +} // namespace slate diff --git a/src/host/providers_sys.cpp b/src/host/providers_sys.cpp index d858298..9223533 100644 --- a/src/host/providers_sys.cpp +++ b/src/host/providers_sys.cpp @@ -10,29 +10,36 @@ namespace slate { using esp32lua::Status; -void Log::write(esp32lua::LogLevel level, const std::string& message) { - const char* tag = level == esp32lua::LogLevel::Error ? "error" - : (level == esp32lua::LogLevel::Info ? "info" : "debug"); +void Log::write(esp32lua::LogLevel level, const std::string &message) { + const char *tag = + level == esp32lua::LogLevel::Error + ? "error" + : (level == esp32lua::LogLevel::Info ? "info" : "debug"); Serial.printf("[lua] %s: %s\n", tag, message.c_str()); } int32_t Settings::rotation() const { return settings.rotation; } -// Applies the rotation itself, because a setter that needs a follow-up call is a bug in the -// setter: the frame, the viewport and the bar all move together or not at all. +// Applies the rotation itself, because a setter that needs a follow-up call is +// a bug in the setter: the frame, the viewport and the bar all move together or +// not at all. Status Settings::setRotation(int32_t degrees) { - if (!settings.setRotation(degrees)) return Status::failure("rotation must be 0, 90, 180 or 270"); + if (!settings.setRotation(degrees)) + return Status::failure("rotation must be 0, 90, 180 or 270"); host.applyRotation(); - return settings.save() ? Status::success() : Status::failure("cannot save settings"); + return settings.save() ? Status::success() + : Status::failure("cannot save settings"); } std::string Settings::timezone() const { return settings.timezone.c_str(); } -Status Settings::setTimezone(const std::string& timezone) { - if (timezone.empty() || timezone.size() > 48) return Status::failure("timezone must be 1 to 48 characters"); +Status Settings::setTimezone(const std::string &timezone) { + if (timezone.empty() || timezone.size() > 48) + return Status::failure("timezone must be 1 to 48 characters"); settings.timezone = timezone.c_str(); net::applyTimezone(); - return settings.save() ? Status::success() : Status::failure("cannot save settings"); + return settings.save() ? Status::success() + : Status::failure("cannot save settings"); } int32_t Sys::millis() const { return static_cast(::millis()); } @@ -41,14 +48,16 @@ esp32lua::MemoryInfo Sys::memory() const { esp32lua::MemoryInfo info; info.freeBytes = static_cast(ESP.getFreeHeap()); info.totalBytes = static_cast(ESP.getHeapSize()); - info.largestFreeBlock = static_cast(heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)); + info.largestFreeBlock = static_cast( + heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)); return info; } bool Sys::isClockSynced() const { return net::clockSynced(); } -bool Touch::touch(int32_t& x, int32_t& y) const { - if (!panel.touched()) return false; +bool Touch::touch(int32_t &x, int32_t &y) const { + if (!panel.touched()) + return false; int16_t mappedX = 0, mappedY = 0; host.mapTouch(panel.getPoint(), mappedX, mappedY); x = mappedX; @@ -56,10 +65,12 @@ bool Touch::touch(int32_t& x, int32_t& y) const { return true; } -bool Touch::rawTouch(int32_t& x, int32_t& y) const { - if (!panel.touched()) return false; +bool Touch::rawTouch(int32_t &x, int32_t &y) const { + if (!panel.touched()) + return false; const TS_Point point = panel.getPoint(); - // The controller's axes are swapped relative to the panel, which calibration undoes. + // The controller's axes are swapped relative to the panel, which calibration + // undoes. x = point.y; y = point.x; return true; @@ -72,14 +83,18 @@ Status Touch::setCalibration(int32_t x0, int32_t y0, int32_t x1, int32_t y1) { settings.touchY0 = y0; settings.touchX1 = x1; settings.touchY1 = y1; - return settings.save() ? Status::success() : Status::failure("cannot save settings"); + return settings.save() ? Status::success() + : Status::failure("cannot save settings"); } -// A fixed slot table rather than a heap list: the whole point of the arena elsewhere is that -// a Lua app cannot fragment the heap, and timers are created from Lua. -Status Timer::schedule(esp32lua::TimerId id, int32_t intervalMs, bool repeating) { +// A fixed slot table rather than a heap list: the whole point of the arena +// elsewhere is that a Lua app cannot fragment the heap, and timers are created +// from Lua. +Status Timer::schedule(esp32lua::TimerId id, int32_t intervalMs, + bool repeating) { for (int at = 0; at < SLOTS; at++) { - if (slots[at].id != 0) continue; + if (slots[at].id != 0) + continue; slots[at].id = id; slots[at].dueMs = ::millis() + intervalMs; slots[at].intervalMs = intervalMs; @@ -91,15 +106,19 @@ Status Timer::schedule(esp32lua::TimerId id, int32_t intervalMs, bool repeating) void Timer::cancel(esp32lua::TimerId id) { for (int at = 0; at < SLOTS; at++) { - if (slots[at].id == id) slots[at] = Slot(); + if (slots[at].id == id) + slots[at] = Slot(); } } -void Timer::collectDue(uint32_t nowMs, std::vector& due) { +void Timer::collectDue(uint32_t nowMs, std::vector &due) { for (int at = 0; at < SLOTS; at++) { - if (slots[at].id == 0) continue; - // Signed comparison, so a deadline still pending across the millis() wrap is not "due". - if (static_cast(nowMs - slots[at].dueMs) < 0) continue; + if (slots[at].id == 0) + continue; + // Signed comparison, so a deadline still pending across the millis() wrap + // is not "due". + if (static_cast(nowMs - slots[at].dueMs) < 0) + continue; due.push_back(slots[at].id); if (slots[at].repeating) { slots[at].dueMs = nowMs + slots[at].intervalMs; @@ -109,4 +128,4 @@ void Timer::collectDue(uint32_t nowMs, std::vector& due) { } } -} // namespace slate +} // namespace slate diff --git a/src/main.cpp b/src/main.cpp index e3af618..82d5673 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ -// Board bring-up and the loop. Everything about running a Lua app -- the state, the -// bindings, app loading, navigation history -- lives in the shared runtime behind LuaHost. +// Board bring-up and the loop. Everything about running a Lua app -- the state, +// the bindings, app loading, navigation history -- lives in the shared runtime +// behind LuaHost. #include #include @@ -20,14 +21,14 @@ SET_LOOP_TASK_STACK_SIZE(16 * 1024); TFT_eSPI tft; SPIClass touchSpi(HSPI); -SPIClass& sdSpi = SPI; +SPIClass &sdSpi = SPI; XPT2046_Touchscreen touch(TOUCH_CS); LuaHost host(tft, touch); static bool halted = false; -static void fallbackScreen(const char* message) { - tft.resetViewport(); // no app, no status bar: this message owns the panel +static void fallbackScreen(const char *message) { + tft.resetViewport(); // no app, no status bar: this message owns the panel tft.setRotation(0); tft.fillScreen(TFT_WHITE); tft.setTextSize(2); @@ -53,9 +54,10 @@ void setup() { return; } - settings.load(); // absent file keeps the built-in defaults + settings.load(); // absent file keeps the built-in defaults net::begin(); - if (!host.begin()) fallbackScreen("home failed to start"); + if (!host.begin()) + fallbackScreen("home failed to start"); } void loop() { @@ -66,6 +68,7 @@ void loop() { net::loop(); host.loop(); // The launcher failing to start is the one error no app can recover from. - if (!host.running()) fallbackScreen("home failed to start"); + if (!host.running()) + fallbackScreen("home failed to start"); delay(1); } diff --git a/src/net.cpp b/src/net.cpp index 34e23f4..23d8259 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -11,14 +11,15 @@ namespace { bool synced = false; bool wasConnected = false; -// The device cannot be running before its own firmware was compiled, so the build -// timestamp is a safe floor. Certificate validity checks fail against 1970, and this -// makes them pass before SNTP has answered. mktime reads it as UTC while the build -// machine wrote local time; a few hours of error is irrelevant for a lower bound. +// The device cannot be running before its own firmware was compiled, so the +// build timestamp is a safe floor. Certificate validity checks fail against +// 1970, and this makes them pass before SNTP has answered. mktime reads it as +// UTC while the build machine wrote local time; a few hours of error is +// irrelevant for a lower bound. time_t buildTime() { static const char months[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; char month[4] = {__DATE__[0], __DATE__[1], __DATE__[2], '\0'}; - const char* found = strstr(months, month); + const char *found = strstr(months, month); struct tm parts = {}; parts.tm_mon = found ? (found - months) / 3 : 0; parts.tm_mday = atoi(__DATE__ + 4); @@ -31,12 +32,12 @@ time_t buildTime() { // Latched, because sntp_get_sync_status() reports COMPLETED only briefly before // resetting to wait for the next cycle; polling it would flap. -void onTimeSync(struct timeval*) { +void onTimeSync(struct timeval *) { synced = true; Serial.printf("[net] clock synced: %lu\n", (unsigned long)time(nullptr)); } -} // namespace +} // namespace void net::applyTimezone() { setenv("TZ", settings.timezone.c_str(), 1); @@ -62,14 +63,17 @@ void net::begin() { void net::loop() { bool connected = WiFi.status() == WL_CONNECTED; - if (connected == wasConnected) return; + if (connected == wasConnected) + return; wasConnected = connected; - if (!connected) return; + if (!connected) + return; - // Started per join rather than once at boot: a fresh lease may hand us a different - // NTP server, and the daemon re-syncs on its own hourly from here. + // Started per join rather than once at boot: a fresh lease may hand us a + // different NTP server, and the daemon re-syncs on its own hourly from here. sntp_servermode_dhcp(1); - configTime(0, 0, "pool.ntp.org", "time.nist.gov"); // UTC; the UI decides how to show it + configTime(0, 0, "pool.ntp.org", + "time.nist.gov"); // UTC; the UI decides how to show it Serial.println("[net] wifi up, sntp started"); } diff --git a/src/net.h b/src/net.h index a6d093e..c96ea5b 100644 --- a/src/net.h +++ b/src/net.h @@ -1,7 +1,8 @@ #pragma once -// Background network housekeeping: joining the saved network and keeping the clock -// honest. Nothing here blocks, so the UI keeps running while WiFi and SNTP settle. +// Background network housekeeping: joining the saved network and keeping the +// clock honest. Nothing here blocks, so the UI keeps running while WiFi and +// SNTP settle. namespace net { @@ -11,8 +12,9 @@ void loop(); // Pushes settings.timezone into libc, so os.date() in Lua reports local time. void applyTimezone(); -// True once SNTP has actually answered. Certificate validity checks and any UI that -// shows a wall clock need this; the seeded build-time clock is only a floor. +// True once SNTP has actually answered. Certificate validity checks and any UI +// that shows a wall clock need this; the seeded build-time clock is only a +// floor. bool clockSynced(); -} // namespace net +} // namespace net diff --git a/src/settings.cpp b/src/settings.cpp index 48d76a3..9a8af75 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -9,25 +9,26 @@ extern "C" { Settings settings; -static constexpr const char* PATH = "/settings.lua"; -static constexpr const char* TEMP_PATH = "/settings.tmp"; -static constexpr const char* BACKUP_PATH = "/settings.bak"; +static constexpr const char *PATH = "/settings.lua"; +static constexpr const char *TEMP_PATH = "/settings.tmp"; +static constexpr const char *BACKUP_PATH = "/settings.bak"; -static int16_t fieldOr(lua_State* L, const char* key, int16_t fallback) { +static int16_t fieldOr(lua_State *L, const char *key, int16_t fallback) { lua_getfield(L, -1, key); int16_t value = lua_isinteger(L, -1) ? lua_tointeger(L, -1) : fallback; lua_pop(L, 1); return value; } -static String stringFieldOr(lua_State* L, const char* key, const String& fallback) { +static String stringFieldOr(lua_State *L, const char *key, + const String &fallback) { lua_getfield(L, -1, key); String value = lua_isstring(L, -1) ? lua_tostring(L, -1) : fallback; lua_pop(L, 1); return value; } -static String luaString(const String& value) { +static String luaString(const String &value) { String out = "\""; for (size_t i = 0; i < value.length(); i++) { unsigned char c = value[i]; @@ -47,21 +48,25 @@ static String luaString(const String& value) { } bool Settings::setRotation(int16_t degrees) { - if (degrees % 90 != 0 || degrees < 0 || degrees > 270) return false; + if (degrees % 90 != 0 || degrees < 0 || degrees > 270) + return false; rotation = degrees; return true; } bool Settings::load() { File f = SD.open(PATH); - if (!f) return false; + if (!f) + return false; String source = f.readString(); f.close(); - lua_State* L = luaL_newstate(); - if (!L) return false; - bool ok = luaL_loadbuffer(L, source.c_str(), source.length(), PATH) == LUA_OK && - lua_pcall(L, 0, 1, 0) == LUA_OK && lua_istable(L, -1); + lua_State *L = luaL_newstate(); + if (!L) + return false; + bool ok = + luaL_loadbuffer(L, source.c_str(), source.length(), PATH) == LUA_OK && + lua_pcall(L, 0, 1, 0) == LUA_OK && lua_istable(L, -1); if (ok) { rotation = fieldOr(L, "rotation", rotation); theme = stringFieldOr(L, "theme", theme); @@ -89,27 +94,35 @@ bool Settings::save() const { String source = "return {\n rotation = " + String(rotation) + ",\n"; source += " theme = " + luaString(theme) + ",\n"; source += " timezone = " + luaString(timezone) + ",\n"; - source += " touch = { x0 = " + String(touchX0) + ", y0 = " + String(touchY0) + - ", x1 = " + String(touchX1) + ", y1 = " + String(touchY1) + " },\n"; + source += " touch = { x0 = " + String(touchX0) + + ", y0 = " + String(touchY0) + ", x1 = " + String(touchX1) + + ", y1 = " + String(touchY1) + " },\n"; source += " wifi = { ssid = " + luaString(wifiSsid) + ", password = " + luaString(wifiPassword) + " },\n}\n"; - if (SD.exists(TEMP_PATH)) SD.remove(TEMP_PATH); + if (SD.exists(TEMP_PATH)) + SD.remove(TEMP_PATH); File f = SD.open(TEMP_PATH, FILE_WRITE); - if (!f) return false; - bool ok = f.write(reinterpret_cast(source.c_str()), source.length()) == source.length(); + if (!f) + return false; + bool ok = f.write(reinterpret_cast(source.c_str()), + source.length()) == source.length(); f.close(); if (!ok) { SD.remove(TEMP_PATH); return false; } - if (SD.exists(BACKUP_PATH)) SD.remove(BACKUP_PATH); + if (SD.exists(BACKUP_PATH)) + SD.remove(BACKUP_PATH); bool hadSettings = SD.exists(PATH); - if (hadSettings && !SD.rename(PATH, BACKUP_PATH)) return false; + if (hadSettings && !SD.rename(PATH, BACKUP_PATH)) + return false; if (SD.rename(TEMP_PATH, PATH)) { - if (hadSettings) SD.remove(BACKUP_PATH); + if (hadSettings) + SD.remove(BACKUP_PATH); return true; } - if (hadSettings) SD.rename(BACKUP_PATH, PATH); + if (hadSettings) + SD.rename(BACKUP_PATH, PATH); return false; } diff --git a/src/settings.h b/src/settings.h index 9899b6c..1806215 100644 --- a/src/settings.h +++ b/src/settings.h @@ -11,11 +11,12 @@ struct Settings { int16_t rotation = 0; String wifiSsid; String wifiPassword; - // Name of a palette in /lib/theme.lua; the colors themselves live on the card, - // so improving a theme never has to migrate saved settings. + // Name of a palette in /lib/theme.lua; the colors themselves live on the + // card, so improving a theme never has to migrate saved settings. String theme = "light"; - // POSIX TZ string, applied with setenv("TZ")/tzset(). Storing the rule rather than a - // zone name means newlib handles DST changeovers and an unlisted zone still works. + // POSIX TZ string, applied with setenv("TZ")/tzset(). Storing the rule rather + // than a zone name means newlib handles DST changeovers and an unlisted zone + // still works. String timezone = "UTC0"; uint8_t rotationIndex() const { return (rotation / 90) & 3; }