refactor(lua)!: migrate to the per-feature namespaces
Follows lib/esp32-lua-api: gui -> screen, node -> tree, settings and input split into screen/sys/touch/buttons. Settings is one provider lighter, with timezone on Sys and rotation and theme on Gui, which now applies and persists a rotation in one call. The calibration screen stashes the rotation it borrows rather than relying on a transient setter.
This commit is contained in:
@@ -20,24 +20,24 @@ not here; `lua/api/**` there is generated from the C++ that registers it, so run
|
||||
|
||||
## Binding Conventions
|
||||
|
||||
Accessors are `getName` / `setName` / `isName`. Bare names are actions (`gui.fillRect`,
|
||||
`wifi.scan`) or pure conversions (`gui.color`, `http.urlencode`). A missing getter is fine;
|
||||
Accessors are `getName` / `setName` / `isName`. Bare names are actions (`screen.fillRect`,
|
||||
`wifi.scan`) or pure conversions (`screen.color`, `http.urlencode`). A missing getter is fine;
|
||||
an accessor without a prefix is not.
|
||||
|
||||
Namespaces have one job each: `gui` device primitives and the live frame, `node` the widget
|
||||
tree, `ui` widgets and palette, `settings` persisted preferences, `sys` process and runtime,
|
||||
plus `wifi`/`http`/`fs`/`input`/`log`. `ble` uses NimBLE-Arduino because Bluedroid cannot
|
||||
**Every namespace belongs to exactly one feature, or to core.** `screen` panel primitives and
|
||||
its saved rotation and theme, `tree` the widget tree, `touch` the panel's input and its
|
||||
calibration, `buttons` the button roles — all four are the `screen` and `touch`/`buttons`
|
||||
features. Core is `sys` (process, runtime, timezone), `ui` widgets and palette, plus
|
||||
`wifi`/`http`/`fs`/`log`/`timer`. `ble` uses NimBLE-Arduino because Bluedroid cannot
|
||||
initialize beside the Lua runtime; reserve `bt` and its full-ESP32 backend for future Classic Bluetooth.
|
||||
|
||||
**A setter that requires a follow-up call is a bug in the setter.** `settings.setTimezone()`
|
||||
applies the TZ itself; `settings.setRotation()` applies the frame and re-clips. The one
|
||||
unavoidable exception is the theme, because C cannot reload the Lua palette — `ui.setTheme()`
|
||||
is the seam that pairs them: it writes through `settings.setTheme()`, then rebuilds the
|
||||
palette and repaints. Apps call `ui.setTheme()`, never `settings.setTheme()`.
|
||||
|
||||
**Persisted intent is not live state.** `settings.getRotation()` is what the user saved;
|
||||
`gui.getRotation()` is the frame being drawn. They diverge on purpose while an app rotates
|
||||
the panel transiently (touch calibration does). Never resolve one from the other.
|
||||
**A setter that requires a follow-up call is a bug in the setter.** `sys.setTimezone()`
|
||||
applies the TZ itself; `screen.setRotation()` rotates the panel, persists the choice and
|
||||
re-clips, so there is one rotation rather than a live one and a saved one to reconcile. An app
|
||||
that rotates the panel for its own purposes puts the old value back (touch calibration does).
|
||||
The one unavoidable exception is the theme, because C cannot reload the Lua palette —
|
||||
`ui.setTheme()` is the seam that pairs them: it writes through `screen.setTheme()`, then
|
||||
rebuilds the palette and repaints. Apps call `ui.setTheme()`, never `screen.setTheme()`.
|
||||
|
||||
Settings live in C++ (`src/settings.h`) because the firmware reads rotation and calibration
|
||||
before any `lua_State` exists, and calibration again on every touch. Lua reaches them through
|
||||
@@ -54,14 +54,14 @@ a node goes in a Lua table keyed by id, which is what `on_press` itself does.
|
||||
|
||||
The split is by lifetime, and it is the whole design. `Node` holds what hit testing and
|
||||
repainting need forever. `Spec` holds what only `measure`/`place` read -- requested size,
|
||||
pad, gap, alignment -- and is dropped by `node.dropScratch()` when layout ends. **Re-layout
|
||||
pad, gap, alignment -- and is dropped by `tree.dropScratch()` when layout ends. **Re-layout
|
||||
rebuilds from Lua** (~8 ms, which nobody notices on a rotate) rather than retaining ~20 bytes
|
||||
a node against it. `measure` writes the measured size into `w`/`h` and `place` overwrites the
|
||||
same slots, because the two are never needed at once.
|
||||
|
||||
Style is sparse and inherited: a role unset on a node is answered by the nearest ancestor
|
||||
that sets it, so a node naming no colours costs zero bytes. That is what keeps `Node` at 16.
|
||||
Applying a palette is one `node.setStyle()` on a subtree root, which is how a dimmed region
|
||||
Applying a palette is one `tree.setStyle()` on a subtree root, which is how a dimmed region
|
||||
and the lit dialog above it are one call each.
|
||||
|
||||
**What is behind a node is derived, never set.** `bg` is the background a node offers its
|
||||
@@ -78,14 +78,14 @@ build are dead; update a live one with `ui.setText(id, text)`, which is also the
|
||||
between a clock tick and a rebuild.
|
||||
|
||||
Layout has not run while a builder is running, so an app sizing itself to the frame reads
|
||||
`ui.frame()`, not `gui.getHeight()` -- the panel is not the box the app was given. Whatever
|
||||
`ui.frame()`, not `screen.getHeight()` -- the panel is not the box the app was given. Whatever
|
||||
mounts the tree sets the difference with `ui.setInset()`.
|
||||
|
||||
Layout is `-Wall -Wextra` C++ free of Arduino headers, so `test/ui_layout_test.cpp` runs it
|
||||
on the host through `make test-cpp`. Adding a **primitive** (a paint routine, a layout mode)
|
||||
means C++ and a reflash; adding **composition** (`ui.confirm`, a new card) is still Lua on
|
||||
the SD card. Custom painting is the seam between them: a `custom` node paints itself through
|
||||
the `gui` bindings.
|
||||
the `screen` bindings.
|
||||
|
||||
## Apps
|
||||
|
||||
|
||||
+1
-1
Submodule lib/esp32-lua-api updated: 445a9b2b8f...75b3a2c490
@@ -1,6 +1,6 @@
|
||||
local ui = require "ui"
|
||||
|
||||
local FONT = gui.FONT_UI
|
||||
local FONT = screen.FONT_UI
|
||||
|
||||
local M = {}
|
||||
local seconds = 0
|
||||
@@ -15,15 +15,15 @@ function M.init()
|
||||
seconds = seconds + 1
|
||||
ui.setText(uptime, text())
|
||||
end)
|
||||
log.info("hello app started, screen " .. gui.getWidth() .. "x" .. gui.getHeight())
|
||||
log.info("hello app started, screen " .. screen.getWidth() .. "x" .. screen.getHeight())
|
||||
end
|
||||
|
||||
function M.node()
|
||||
-- A fixed slot for the clock, because a node keeps the box it was measured with and the
|
||||
-- text grows a digit at a time.
|
||||
uptime = ui.text(text(), {
|
||||
w = gui.getTextWidth(FONT, "uptime: 00000s"),
|
||||
h = gui.getFontHeight(FONT),
|
||||
w = screen.getTextWidth(FONT, "uptime: 00000s"),
|
||||
h = screen.getFontHeight(FONT),
|
||||
font = FONT,
|
||||
})
|
||||
return ui.box {
|
||||
@@ -41,7 +41,7 @@ end
|
||||
-- tree would need one node each to remember them.
|
||||
function M.on_touch(x, y)
|
||||
log.info("touch at " .. x .. ", " .. y)
|
||||
gui.fillCircle(x, y, 6, ui.theme.accent)
|
||||
screen.fillCircle(x, y, 6, ui.theme.accent)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -14,7 +14,7 @@ local function card(name, side)
|
||||
on_click = function()
|
||||
sys.launch(name)
|
||||
end,
|
||||
ui.label(name, { font = gui.FONT_UI, fit = side - 16 }),
|
||||
ui.label(name, { font = screen.FONT_UI, fit = side - 16 }),
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ function M.on_touch_down(x, y)
|
||||
return
|
||||
end
|
||||
lastX, lastY = x, y
|
||||
gui.fillCircle(x, y, BRUSH, ui.theme.accent)
|
||||
screen.fillCircle(x, y, BRUSH, ui.theme.accent)
|
||||
end
|
||||
|
||||
-- Strokes are joined with a line because the poll rate, not the finger, decides the gap:
|
||||
@@ -60,8 +60,8 @@ function M.on_touch_move(x, y)
|
||||
if not lastX or not inside(x, y) then
|
||||
return
|
||||
end
|
||||
gui.drawLine(lastX, lastY, x, y, ui.theme.accent)
|
||||
gui.fillCircle(x, y, BRUSH, ui.theme.accent)
|
||||
screen.drawLine(lastX, lastY, x, y, ui.theme.accent)
|
||||
screen.fillCircle(x, y, BRUSH, ui.theme.accent)
|
||||
lastX, lastY = x, y
|
||||
end
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ local ui = require "ui"
|
||||
local statusbar = require "statusbar"
|
||||
local zones = require "timezones"
|
||||
|
||||
local FONT = gui.FONT_UI
|
||||
local FONT = screen.FONT_UI
|
||||
local INSET = 30
|
||||
local MENU_PAD, MENU_GAP = 12, 8
|
||||
|
||||
@@ -21,6 +21,7 @@ local bleScanRequested, bleConnectRequested = false, false
|
||||
local selectedNetwork, password = nil, ""
|
||||
local selectedBleDevice
|
||||
local targetIndex = 1
|
||||
local rotationBeforeCalibration = 0
|
||||
local passwordLabel
|
||||
local networkOf, bleDeviceOf = {}, {}
|
||||
|
||||
@@ -51,32 +52,35 @@ end
|
||||
local function paintTarget(_, ox, oy)
|
||||
local x, y = target(targetIndex)
|
||||
local theme = ui.theme
|
||||
gui.drawText(FONT, ox + 10, oy + 10, "tap the cross", theme.color, nil, theme.background)
|
||||
gui.drawLine(x - 12, y, x + 12, y, theme.accent)
|
||||
gui.drawLine(x, y - 12, x, y + 12, theme.accent)
|
||||
screen.drawText(FONT, ox + 10, oy + 10, "tap the cross", theme.color, nil, theme.background)
|
||||
screen.drawLine(x - 12, y, x + 12, y, theme.accent)
|
||||
screen.drawLine(x, y - 12, x, y + 12, theme.accent)
|
||||
end
|
||||
|
||||
local function finishCalibration()
|
||||
local ok = settings.setCalibration(M.computeCalibration(samples[1], samples[2], 320, 480, INSET))
|
||||
local ok = touch.setCalibration(M.computeCalibration(samples[1], samples[2], 320, 480, INSET))
|
||||
message = ok and "calibration saved" or "save failed"
|
||||
gui.setRotation(settings.getRotation())
|
||||
screen.setRotation(rotationBeforeCalibration)
|
||||
mode = "menu"
|
||||
statusbar.setFullscreen(false) -- rebuilds, which is what puts the menu back
|
||||
end
|
||||
|
||||
-- Rotation is saved as it is applied, so calibrating upright is a change the app
|
||||
-- has to put back itself once the samples are in.
|
||||
local function startCalibration()
|
||||
message = nil
|
||||
samples, pending, armed = {}, nil, false
|
||||
targetIndex = 1
|
||||
mode = "calibrate"
|
||||
gui.setRotation(0)
|
||||
rotationBeforeCalibration = screen.getRotation()
|
||||
screen.setRotation(0)
|
||||
-- The targets sit at the physical corners and the samples are read in panel
|
||||
-- coordinates, so the status bar cannot be allowed to shift the frame.
|
||||
statusbar.setFullscreen(true)
|
||||
end
|
||||
|
||||
local function cycleRotation()
|
||||
local ok = settings.setRotation((settings.getRotation() + 90) % 360)
|
||||
local ok = screen.setRotation((screen.getRotation() + 90) % 360)
|
||||
message = ok and "rotation saved" or "save failed"
|
||||
ui.rebuild()
|
||||
end
|
||||
@@ -97,7 +101,7 @@ end
|
||||
-- The stored value is a POSIX rule, so a zone set by hand and missing from the list
|
||||
-- shows its rule rather than pretending to be the first entry.
|
||||
local function zoneLabel()
|
||||
local current = settings.getTimezone()
|
||||
local current = sys.getTimezone()
|
||||
for _, zone in ipairs(zones) do
|
||||
if zone.tz == current then
|
||||
return zone.name
|
||||
@@ -107,14 +111,14 @@ local function zoneLabel()
|
||||
end
|
||||
|
||||
local function cycleTimezone()
|
||||
local current = settings.getTimezone()
|
||||
local current = sys.getTimezone()
|
||||
local next_index = 1
|
||||
for index, zone in ipairs(zones) do
|
||||
if zone.tz == current then
|
||||
next_index = index % #zones + 1
|
||||
end
|
||||
end
|
||||
message = settings.setTimezone(zones[next_index].tz) and nil or "save failed"
|
||||
message = sys.setTimezone(zones[next_index].tz) and nil or "save failed"
|
||||
ui.rebuild()
|
||||
end
|
||||
|
||||
@@ -236,10 +240,10 @@ local function card(side, title, value, on_click)
|
||||
justify = "center",
|
||||
align = "center",
|
||||
on_click = on_click,
|
||||
ui.label(title, { font = gui.FONT_UI, fit = side - 12 }),
|
||||
ui.label(title, { font = screen.FONT_UI, fit = side - 12 }),
|
||||
}
|
||||
if value then
|
||||
spec[#spec + 1] = ui.label(value, { font = gui.FONT_SMALL, fit = side - 12 })
|
||||
spec[#spec + 1] = ui.label(value, { font = screen.FONT_SMALL, fit = side - 12 })
|
||||
end
|
||||
return ui.button(spec)
|
||||
end
|
||||
@@ -250,7 +254,7 @@ local function menuScreen()
|
||||
local side, cols = ui.cardSide(6, MENU_PAD, MENU_GAP, message and 20 or 0)
|
||||
local cards = {
|
||||
card(side, "Calibrate", "touch", startCalibration),
|
||||
card(side, "Rotation", settings.getRotation() .. " deg", cycleRotation),
|
||||
card(side, "Rotation", screen.getRotation() .. " deg", cycleRotation),
|
||||
card(side, "WiFi", wifiValue(wifi.getStatus()), function()
|
||||
show "wifi"
|
||||
end),
|
||||
@@ -356,7 +360,7 @@ local function bleScreen()
|
||||
local initialized = ble.isInitialized()
|
||||
local items = { pad = 12, gap = 7, ui.text "BLE", ui.text(initialized and "on" or "off") }
|
||||
if bleMessage then
|
||||
items[#items + 1] = ui.text(bleMessage, { font = gui.FONT_SMALL })
|
||||
items[#items + 1] = ui.text(bleMessage, { font = screen.FONT_SMALL })
|
||||
end
|
||||
if initialized then
|
||||
items[#items + 1] = ui.button { label = "scan devices", on_click = requestBleScan }
|
||||
@@ -525,7 +529,7 @@ function M.tick()
|
||||
return
|
||||
end
|
||||
|
||||
local rx, ry = input.getRawTouch()
|
||||
local rx, ry = touch.getRawPoint()
|
||||
if not armed then
|
||||
if not rx then
|
||||
armed = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
local ui = require "ui"
|
||||
local FONT = gui.FONT_UI
|
||||
local FONT = screen.FONT_UI
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -106,13 +106,13 @@ end
|
||||
local function drawArrow(x, y, width, color, down)
|
||||
local middle = x + width // 2
|
||||
if down then
|
||||
gui.drawLine(middle, y + 8, middle, y + 20, color)
|
||||
gui.drawLine(middle - 5, y + 15, middle, y + 20, color)
|
||||
gui.drawLine(middle, y + 20, middle + 5, y + 15, color)
|
||||
screen.drawLine(middle, y + 8, middle, y + 20, color)
|
||||
screen.drawLine(middle - 5, y + 15, middle, y + 20, color)
|
||||
screen.drawLine(middle, y + 20, middle + 5, y + 15, color)
|
||||
else
|
||||
gui.drawLine(middle, y + 9, middle, y + 21, color)
|
||||
gui.drawLine(middle - 5, y + 14, middle, y + 9, color)
|
||||
gui.drawLine(middle, y + 9, middle + 5, y + 14, color)
|
||||
screen.drawLine(middle, y + 9, middle, y + 21, color)
|
||||
screen.drawLine(middle - 5, y + 14, middle, y + 9, color)
|
||||
screen.drawLine(middle, y + 9, middle + 5, y + 14, color)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -120,14 +120,14 @@ local function drawKey(id, label, x, y, width, pressed)
|
||||
local theme = ui.theme
|
||||
local face = pressed and theme.pressedFace or theme.face
|
||||
local color = pressed and theme.pressedColor or theme.color
|
||||
gui.roundRect(x, y, width, KEY_H, theme.radius, theme.background, face, nil, color)
|
||||
screen.roundRect(x, y, width, KEY_H, theme.radius, theme.background, face, nil, color)
|
||||
if label == "shift" then
|
||||
drawArrow(x, y, width, color, state[id].page == "upper")
|
||||
else
|
||||
gui.drawText(
|
||||
screen.drawText(
|
||||
FONT,
|
||||
x + (width - gui.getTextWidth(FONT, label)) // 2,
|
||||
y + (KEY_H - gui.getFontHeight(FONT)) // 2,
|
||||
x + (width - screen.getTextWidth(FONT, label)) // 2,
|
||||
y + (KEY_H - screen.getFontHeight(FONT)) // 2,
|
||||
label,
|
||||
color,
|
||||
nil,
|
||||
@@ -172,7 +172,7 @@ function M.eachKey(page, rect, visit)
|
||||
end
|
||||
|
||||
local function rectOf(id)
|
||||
local x, y, w, h = node.getRect(id)
|
||||
local x, y, w, h = tree.getRect(id)
|
||||
return { x = x, y = y, w = w, h = h }
|
||||
end
|
||||
|
||||
@@ -230,13 +230,13 @@ local function press(id, x, y)
|
||||
end
|
||||
elseif action == "shift" then
|
||||
st.page = st.page == "lower" and "upper" or "lower"
|
||||
node.invalidate(id)
|
||||
tree.invalidate(id)
|
||||
elseif action == "symbols" then
|
||||
st.page = st.page == "numbers" and "symbols" or "numbers"
|
||||
node.invalidate(id)
|
||||
tree.invalidate(id)
|
||||
elseif action == "mode" then
|
||||
st.page = (st.page == "lower" or st.page == "upper") and "numbers" or "lower"
|
||||
node.invalidate(id)
|
||||
tree.invalidate(id)
|
||||
elseif action == "backspace" then
|
||||
st.value = st.value:sub(1, -2)
|
||||
changed()
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
local ui = require "ui"
|
||||
|
||||
local FONT = gui.FONT_SMALL
|
||||
local FONT = screen.FONT_SMALL
|
||||
-- The app name reads as a heading, matching a card title; the clock and memory
|
||||
-- stay small so the right-hand stack still fits two rows.
|
||||
local TITLE_FONT = gui.FONT_UI
|
||||
local TITLE_FONT = screen.FONT_UI
|
||||
|
||||
local BAR_H = 44
|
||||
local PAD, GAP = 6, 8
|
||||
@@ -44,10 +44,10 @@ local function paintWifi(_, x, y)
|
||||
local bars, offline = signalBars()
|
||||
local empty = offline and theme.disabled or theme.muted
|
||||
for i, h in ipairs(WIFI_HEIGHTS) do
|
||||
gui.fillRect(x + (i - 1) * 4, y + 8 - h, 3, h, i <= bars and theme.color or empty)
|
||||
screen.fillRect(x + (i - 1) * 4, y + 8 - h, 3, h, i <= bars and theme.color or empty)
|
||||
end
|
||||
if offline then
|
||||
gui.drawLine(x, y, x + WIFI_W - 1, y + 8, theme.muted)
|
||||
screen.drawLine(x, y, x + WIFI_W - 1, y + 8, theme.muted)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,14 +55,14 @@ end
|
||||
-- this size, and a custom node keeps the press feedback to one repaint of one box.
|
||||
local function paintBack(id, x, y, w, h)
|
||||
local theme = ui.theme
|
||||
local pressed = node.isPressed(id)
|
||||
local pressed = tree.isPressed(id)
|
||||
local face = pressed and theme.pressedFace or theme.face
|
||||
local color = pressed and theme.pressedColor or theme.color
|
||||
gui.roundRect(x, y, w, h, 4, theme.background, face)
|
||||
screen.roundRect(x, y, w, h, 4, theme.background, face)
|
||||
local cx, cy = x + w // 2 + 1, y + h // 2
|
||||
for offset = 0, 1 do -- two passes, because a one pixel chevron reads as a speck
|
||||
gui.drawLine(cx + offset, cy - 4, cx + offset - 4, cy, color)
|
||||
gui.drawLine(cx + offset - 4, cy, cx + offset, cy + 4, color)
|
||||
screen.drawLine(cx + offset, cy - 4, cx + offset - 4, cy, color)
|
||||
screen.drawLine(cx + offset - 4, cy, cx + offset, cy + 4, color)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -74,11 +74,11 @@ local function backNode()
|
||||
paint = paintBack,
|
||||
press_style = false, -- paintBack draws its own, so nothing else should repaint
|
||||
on_enter = function(id)
|
||||
node.setPressed(id, true)
|
||||
tree.setPressed(id, true)
|
||||
ui.invalidate(id)
|
||||
end,
|
||||
on_exit = function(id)
|
||||
node.setPressed(id, false)
|
||||
tree.setPressed(id, false)
|
||||
ui.invalidate(id)
|
||||
end,
|
||||
on_click = function()
|
||||
@@ -91,9 +91,9 @@ end
|
||||
function M.node()
|
||||
local theme = ui.theme
|
||||
title = sys.getAppTitle()
|
||||
local clockW = gui.getTextWidth(FONT, "00:00:00")
|
||||
local memW = gui.getTextWidth(FONT, "0000kB")
|
||||
local fontH = gui.getFontHeight(FONT)
|
||||
local clockW = screen.getTextWidth(FONT, "00:00:00")
|
||||
local memW = screen.getTextWidth(FONT, "0000kB")
|
||||
local fontH = screen.getFontHeight(FONT)
|
||||
|
||||
ids = {}
|
||||
ids.clock = ui.text(clock(), { w = clockW, h = fontH, font = FONT, color = theme.muted })
|
||||
@@ -105,7 +105,7 @@ function M.node()
|
||||
-- width, and the three slots have to share it.
|
||||
local statusW = math.max(clockW, memW + GAP + WIFI_W)
|
||||
local backW = sys.canGoBack() and BAR_H - 6 or 0
|
||||
local titleW = gui.getWidth() - 2 * PAD - 2 * GAP - backW - statusW
|
||||
local titleW = screen.getWidth() - 2 * PAD - 2 * GAP - backW - statusW
|
||||
|
||||
local status = ui.box {
|
||||
w = statusW,
|
||||
|
||||
+3
-10
@@ -2,13 +2,9 @@
|
||||
|
||||
#include "../settings.h"
|
||||
|
||||
// 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;
|
||||
providers.settings = &settingsProvider;
|
||||
providers.sys = &sysProvider;
|
||||
providers.fs = &fsProvider;
|
||||
providers.gui = &guiProvider;
|
||||
@@ -17,14 +13,12 @@ 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.
|
||||
return providers;
|
||||
}
|
||||
|
||||
LuaHost::LuaHost(TFT_eSPI& tft, XPT2046_Touchscreen& touch)
|
||||
: tft(tft), touchPanel(touch), settingsProvider(*this),
|
||||
guiProvider(tft, *this), touchProvider(touch, *this), runtime(wire()) {}
|
||||
: tft(tft), touchPanel(touch), guiProvider(tft, *this),
|
||||
touchProvider(touch, *this), runtime(wire()) {}
|
||||
|
||||
void LuaHost::mapTouch(const TS_Point& point, int16_t& x, int16_t& y) const {
|
||||
const int16_t nx =
|
||||
@@ -67,8 +61,7 @@ bool LuaHost::begin() {
|
||||
}
|
||||
|
||||
void LuaHost::prepareForApp() {
|
||||
tft.setRotation(
|
||||
settings.rotationIndex()); // the previous app may have rotated the frame
|
||||
tft.setRotation(settings.rotationIndex());
|
||||
lastTouched = true; // the tap that launched this app may still be down
|
||||
ignoreRelease = true; // and its release is not this app's gesture
|
||||
Serial.printf("[lua] launching free=%u largest=%u\n", ESP.getFreeHeap(),
|
||||
|
||||
@@ -44,7 +44,6 @@ private:
|
||||
XPT2046_Touchscreen& touchPanel;
|
||||
|
||||
slate::Log logProvider;
|
||||
slate::Settings settingsProvider;
|
||||
slate::Sys sysProvider;
|
||||
slate::Fs fsProvider;
|
||||
slate::Gui guiProvider;
|
||||
|
||||
+5
-15
@@ -21,25 +21,13 @@ public:
|
||||
void write(esp32lua::LogLevel level, const std::string& message) override;
|
||||
};
|
||||
|
||||
class Settings : public esp32lua::SettingsProvider {
|
||||
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;
|
||||
std::string theme() const override;
|
||||
esp32lua::Status setTheme(const std::string& theme) override;
|
||||
|
||||
private:
|
||||
LuaHost& host;
|
||||
};
|
||||
|
||||
class Sys : public esp32lua::SysProvider {
|
||||
public:
|
||||
int32_t millis() const override;
|
||||
esp32lua::MemoryInfo memory() const override;
|
||||
bool isClockSynced() const override;
|
||||
std::string timezone() const override;
|
||||
esp32lua::Status setTimezone(const std::string& timezone) override;
|
||||
};
|
||||
|
||||
class Fs : public esp32lua::FsProvider {
|
||||
@@ -77,7 +65,9 @@ public:
|
||||
int32_t width() const override;
|
||||
int32_t height() const override;
|
||||
int32_t rotation() const override;
|
||||
void setRotation(int32_t degrees) override;
|
||||
esp32lua::Status setRotation(int32_t degrees) override;
|
||||
std::string theme() const override;
|
||||
esp32lua::Status setTheme(const std::string& theme) 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,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <SD.h>
|
||||
|
||||
#include "../gfx/round_rect.h"
|
||||
#include "../settings.h"
|
||||
#include "lua_host.h"
|
||||
#include "providers.h"
|
||||
|
||||
@@ -127,7 +128,28 @@ int32_t Gui::width() const { return tft.width(); }
|
||||
int32_t Gui::height() const { return tft.height(); }
|
||||
int32_t Gui::rotation() const { return tft.getRotation() * 90; }
|
||||
|
||||
void Gui::setRotation(int32_t degrees) { tft.setRotation((degrees / 90) & 3); }
|
||||
// Applies the rotation and persists it in one call, because a setter that needs
|
||||
// a follow-up is a bug in the setter: the frame, the viewport and the bar all
|
||||
// move together or not at all.
|
||||
esp32lua::Status Gui::setRotation(int32_t degrees) {
|
||||
if (!settings.setRotation(degrees))
|
||||
return esp32lua::Status::failure("rotation must be 0, 90, 180 or 270");
|
||||
host.applyRotation();
|
||||
return settings.save() ? esp32lua::Status::success()
|
||||
: esp32lua::Status::failure("cannot save settings");
|
||||
}
|
||||
|
||||
std::string Gui::theme() const { return settings.theme.c_str(); }
|
||||
|
||||
// Stores the name only. ui.setTheme() owns applying it, because the palette and
|
||||
// the repaint it drives exist solely in Lua.
|
||||
esp32lua::Status Gui::setTheme(const std::string& theme) {
|
||||
if (theme.empty() || theme.size() > 16)
|
||||
return esp32lua::Status::failure("theme must be 1 to 16 characters");
|
||||
settings.theme = theme.c_str();
|
||||
return settings.save() ? esp32lua::Status::success()
|
||||
: esp32lua::Status::failure("cannot save settings");
|
||||
}
|
||||
|
||||
int32_t Gui::color(int32_t r, int32_t g, int32_t b) const {
|
||||
return tft.color565(r, g, b);
|
||||
|
||||
@@ -18,22 +18,9 @@ void Log::write(esp32lua::LogLevel level, const std::string& message) {
|
||||
Serial.printf("[lua] %s: %s\n", tag, message.c_str());
|
||||
}
|
||||
|
||||
int32_t Settings::rotation() const { return settings.rotation; }
|
||||
std::string Sys::timezone() const { return settings.timezone.c_str(); }
|
||||
|
||||
// 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");
|
||||
host.applyRotation();
|
||||
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) {
|
||||
Status Sys::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();
|
||||
@@ -42,18 +29,6 @@ Status Settings::setTimezone(const std::string& timezone) {
|
||||
: Status::failure("cannot save settings");
|
||||
}
|
||||
|
||||
std::string Settings::theme() const { return settings.theme.c_str(); }
|
||||
|
||||
// Stores the name only. ui.setTheme() owns applying it, because the palette and
|
||||
// the repaint it drives exist solely in Lua.
|
||||
Status Settings::setTheme(const std::string& theme) {
|
||||
if (theme.empty() || theme.size() > 16)
|
||||
return Status::failure("theme must be 1 to 16 characters");
|
||||
settings.theme = theme.c_str();
|
||||
return settings.save() ? Status::success()
|
||||
: Status::failure("cannot save settings");
|
||||
}
|
||||
|
||||
int32_t Sys::millis() const { return static_cast<int32_t>(::millis()); }
|
||||
|
||||
esp32lua::MemoryInfo Sys::memory() const {
|
||||
|
||||
+18
-12
@@ -1,7 +1,3 @@
|
||||
// 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 <SD.h>
|
||||
#include <SPI.h>
|
||||
#include <TFT_eSPI.h>
|
||||
@@ -31,10 +27,10 @@ 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
|
||||
tft.resetViewport();
|
||||
tft.setRotation(0);
|
||||
tft.fillScreen(TFT_WHITE);
|
||||
tft.setTextSize(2);
|
||||
tft.setTextSize(1);
|
||||
tft.setTextColor(TFT_RED, TFT_WHITE);
|
||||
tft.drawString(message, 10, 10);
|
||||
tft.setTextColor(TFT_BLACK, TFT_WHITE);
|
||||
@@ -43,7 +39,7 @@ static void fallbackScreen(const char* message) {
|
||||
halted = true;
|
||||
}
|
||||
|
||||
// ponytail: fixed 10s / 8% dim, no fade. Make it a setting when someone asks.
|
||||
// Auto Dim
|
||||
constexpr uint32_t kDimAfterMs = 10000;
|
||||
constexpr int kBacklightChannel = 0;
|
||||
uint32_t lastTouchMs = 0;
|
||||
@@ -62,8 +58,7 @@ void updateBacklight() {
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
// A failed new otherwise unwinds to terminate() and a bare abort backtrace.
|
||||
// Nothing can be freed at that point, so this only buys a legible cause.
|
||||
// OOM Logging
|
||||
std::set_new_handler([]() {
|
||||
logHeap("oom");
|
||||
Serial.println("[fatal] out of memory");
|
||||
@@ -71,41 +66,52 @@ void setup() {
|
||||
ESP.restart();
|
||||
});
|
||||
|
||||
// Start TFT
|
||||
tft.begin();
|
||||
tft.setRotation(0);
|
||||
tft.fillScreen(TFT_WHITE);
|
||||
touchSpi.begin(14, 12, 13, TOUCH_CS);
|
||||
touch.begin(touchSpi);
|
||||
|
||||
// TFT_eSPI leaves TFT_BL a plain output; LEDC takes it over for dimming.
|
||||
// TFT Dimming
|
||||
ledcSetup(kBacklightChannel, 5000, 8);
|
||||
ledcAttachPin(TFT_BL, kBacklightChannel);
|
||||
ledcWrite(kBacklightChannel, 255);
|
||||
lastTouchMs = millis();
|
||||
|
||||
// SD Card
|
||||
sdSpi.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
|
||||
if (!SD.begin(SD_CS, sdSpi)) {
|
||||
fallbackScreen("SD card mount failed");
|
||||
return;
|
||||
}
|
||||
|
||||
settings.load(); // absent file keeps the built-in defaults
|
||||
// Load Settings & Load Lua
|
||||
settings.load();
|
||||
net::begin();
|
||||
if (!host.begin())
|
||||
fallbackScreen("home failed to start");
|
||||
|
||||
// Start WiFi
|
||||
net::startWifi();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Halted
|
||||
if (halted) {
|
||||
delay(100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto Dim & Net / Lua Loop
|
||||
updateBacklight();
|
||||
net::loop();
|
||||
host.loop();
|
||||
// The launcher failing to start is the one error no app can recover from.
|
||||
|
||||
// Failed Lua Main
|
||||
if (!host.running())
|
||||
fallbackScreen("home failed to start");
|
||||
|
||||
// Yield
|
||||
delay(1);
|
||||
}
|
||||
|
||||
+35
-44
@@ -23,7 +23,7 @@ local device = {
|
||||
bleDevices = {}, -- what ble.scan() returns
|
||||
status = { state = "disconnected", ssid = "", ip = "", rssi = 0 },
|
||||
painted = {}, -- every drawText call, in order
|
||||
calibration = nil, -- last settings.setCalibration()
|
||||
calibration = nil, -- last touch.setCalibration()
|
||||
freeHeap = 200000,
|
||||
totalHeap = 320000,
|
||||
largestBlock = 100000,
|
||||
@@ -56,7 +56,7 @@ function device.install()
|
||||
device.invalidated = {}
|
||||
device.tapTarget = nil
|
||||
|
||||
node = setmetatable({
|
||||
tree = setmetatable({
|
||||
reset = function()
|
||||
for index in ipairs(nodes) do
|
||||
nodes[index] = nil
|
||||
@@ -118,7 +118,7 @@ function device.install()
|
||||
end,
|
||||
})
|
||||
|
||||
gui = {
|
||||
screen = {
|
||||
-- Font roles are scales of the one built-in font, matching the firmware's GuiProvider.
|
||||
FONT_SMALL = 1,
|
||||
FONT_UI = 2,
|
||||
@@ -162,11 +162,22 @@ function device.install()
|
||||
return #text * device.charWidth * (font or 1)
|
||||
end,
|
||||
setRotation = function(degrees)
|
||||
if degrees % 90 ~= 0 or degrees < 0 or degrees > 270 then
|
||||
return false
|
||||
end
|
||||
device.rotation = degrees
|
||||
return saved()
|
||||
end,
|
||||
getRotation = function()
|
||||
return device.rotation
|
||||
end,
|
||||
getTheme = function()
|
||||
return device.theme
|
||||
end,
|
||||
setTheme = function(name)
|
||||
device.theme = name
|
||||
return saved()
|
||||
end,
|
||||
}
|
||||
|
||||
sys = {
|
||||
@@ -195,7 +206,7 @@ function device.install()
|
||||
return 1
|
||||
end,
|
||||
hasFeature = function(name)
|
||||
return name == "touch"
|
||||
return name == "touch" or name == "screen"
|
||||
end,
|
||||
getMemory = function()
|
||||
return device.freeHeap, device.totalHeap, device.largestBlock
|
||||
@@ -203,6 +214,13 @@ function device.install()
|
||||
isClockSynced = function()
|
||||
return device.clockSynced
|
||||
end,
|
||||
getTimezone = function()
|
||||
return device.timezone
|
||||
end,
|
||||
setTimezone = function(tz)
|
||||
device.timezone = tz
|
||||
return saved()
|
||||
end,
|
||||
launch = function(path, arg)
|
||||
device.launched = { path = path, arg = arg, replace = false }
|
||||
end,
|
||||
@@ -211,53 +229,26 @@ function device.install()
|
||||
end,
|
||||
}
|
||||
|
||||
settings = {
|
||||
getRotation = function()
|
||||
return device.rotation
|
||||
touch = {
|
||||
isTouched = function()
|
||||
return device.raw ~= nil
|
||||
end,
|
||||
setRotation = function(degrees)
|
||||
if degrees % 90 ~= 0 or degrees < 0 or degrees > 270 then
|
||||
return false
|
||||
getPoint = function()
|
||||
if not device.raw then
|
||||
return nil
|
||||
end
|
||||
device.rotation = degrees
|
||||
return saved()
|
||||
return device.raw[1], device.raw[2]
|
||||
end,
|
||||
getRawPoint = function()
|
||||
if not device.raw then
|
||||
return nil
|
||||
end
|
||||
return device.raw[1], device.raw[2]
|
||||
end,
|
||||
setCalibration = function(...)
|
||||
device.calibration = { ... }
|
||||
return saved()
|
||||
end,
|
||||
getTimezone = function()
|
||||
return device.timezone
|
||||
end,
|
||||
setTimezone = function(tz)
|
||||
device.timezone = tz
|
||||
return saved()
|
||||
end,
|
||||
getTheme = function()
|
||||
return device.theme
|
||||
end,
|
||||
setTheme = function(name)
|
||||
device.theme = name
|
||||
return saved()
|
||||
end,
|
||||
}
|
||||
|
||||
input = {
|
||||
isTouched = function()
|
||||
return device.raw ~= nil
|
||||
end,
|
||||
getTouch = function()
|
||||
if not device.raw then
|
||||
return nil
|
||||
end
|
||||
return device.raw[1], device.raw[2]
|
||||
end,
|
||||
getRawTouch = function()
|
||||
if not device.raw then
|
||||
return nil
|
||||
end
|
||||
return device.raw[1], device.raw[2]
|
||||
end,
|
||||
}
|
||||
|
||||
fs = {
|
||||
|
||||
@@ -41,20 +41,20 @@ assert(fx0 > fx1, "flipped axis should descend")
|
||||
local zones = require "timezones"
|
||||
restart()
|
||||
tapRow "Timezone"
|
||||
assert(settings.getTimezone() == zones[2].tz, "timezone " .. settings.getTimezone())
|
||||
assert(sys.getTimezone() == zones[2].tz, "timezone " .. sys.getTimezone())
|
||||
-- Cycling rebuilds the screen, which is the only way a screen changes now, so the card
|
||||
-- shows the new zone and the menu is still the same size.
|
||||
local beforeCycle = node.getCount()
|
||||
local beforeCycle = tree.getCount()
|
||||
assert(device.labelled(zones[2].name), "timezone card did not take its new value")
|
||||
assert(node.getCount() == beforeCycle, "the rebuilt menu grew")
|
||||
assert(tree.getCount() == beforeCycle, "the rebuilt menu grew")
|
||||
tapRow "Timezone"
|
||||
assert(settings.getTimezone() == zones[3].tz, "timezone " .. settings.getTimezone())
|
||||
assert(sys.getTimezone() == zones[3].tz, "timezone " .. sys.getTimezone())
|
||||
|
||||
-- Rotation cycles through the four quarter turns and wraps back to 0.
|
||||
restart()
|
||||
for _, expected in ipairs { 90, 180, 270, 0 } do
|
||||
tapRow "Rotation"
|
||||
assert(settings.getRotation() == expected, "rotation " .. settings.getRotation())
|
||||
assert(screen.getRotation() == expected, "rotation " .. screen.getRotation())
|
||||
end
|
||||
|
||||
-- Calibration collects one sample per target and saves on the second release.
|
||||
|
||||
@@ -20,7 +20,7 @@ local function screen()
|
||||
end
|
||||
|
||||
ui.mount(screen)
|
||||
local built = node.getCount()
|
||||
local built = tree.getCount()
|
||||
assert(device.labelled "--:--:--", "the bar built no clock")
|
||||
assert(device.labelled "117kB", "the bar built no memory slot")
|
||||
assert(device.labelled(device.appName), "the bar built no title")
|
||||
@@ -36,7 +36,7 @@ assert(tick() == 0, "an unchanged bar repaints nothing")
|
||||
device.freeHeap = device.freeHeap - 32000
|
||||
assert(tick() == 1, "only the memory slot repaints")
|
||||
assert(device.labelled "148kB", "the memory slot kept its old text")
|
||||
assert(node.getCount() == built, "a field update rebuilt the screen")
|
||||
assert(tree.getCount() == built, "a field update rebuilt the screen")
|
||||
|
||||
device.status = { state = "connected", ssid = "x", ip = "", rssi = -50 }
|
||||
assert(tick() == 1, "the signal strength repaints the icon and nothing else")
|
||||
|
||||
Reference in New Issue
Block a user