d338dfe3e8
crosspoint-reader calls it init() and requires it; this called it setup() and treated it as optional. Same concept, two spellings, so an app could not move between the two firmwares for no reason worth defending. init() wins because it is also the stricter contract: a misspelled entry point is now an error instead of an app that starts, draws nothing, and explains nothing. Requiring it exposed that error screens were unreadable. fail() painted the message and the host relaunched the launcher over it on the very next frame, so every Lua error was serial-only -- which would have made "Missing init()" useless to anyone holding the device rather than a console.
292 lines
8.6 KiB
Lua
292 lines
8.6 KiB
Lua
local ui = require("ui")
|
|
|
|
TICK_MS = 50
|
|
|
|
local INSET = 30
|
|
|
|
local screen, message
|
|
local mode = "menu"
|
|
local samples, pending, armed = {}, nil, false
|
|
local scanRequested, selectedNetwork, password = false, nil, ""
|
|
local keyboardPage = "lower"
|
|
|
|
local buildMenu, buildWifi, buildNetworks, buildKeyboard, startCalibration, cycleRotation
|
|
|
|
-- Two inset targets give a raw-per-pixel slope; extrapolate it to the screen edges.
|
|
-- Exposed as a global so test/settings_calibration.lua can exercise it.
|
|
function computeCalibration(s1, s2, w, h, inset)
|
|
local sx = (s2.x - s1.x) / (w - 2 * inset)
|
|
local sy = (s2.y - s1.y) / (h - 2 * inset)
|
|
return math.floor(s1.x - sx * inset), math.floor(s1.y - sy * inset),
|
|
math.floor(s2.x + sx * inset), math.floor(s2.y + sy * inset)
|
|
end
|
|
|
|
local function themed(items)
|
|
screen = ui.screen(ui.box(items)) -- colors come from the theme by inheritance
|
|
end
|
|
|
|
local function target(n)
|
|
if n == 1 then return INSET, INSET end
|
|
return 320 - INSET, 480 - INSET
|
|
end
|
|
|
|
local function drawTarget(n)
|
|
local x, y = target(n)
|
|
-- Drawn outside the component tree, so this is the case that reads the theme directly.
|
|
local theme = ui.theme
|
|
gui.clear(theme.bg)
|
|
gui.drawText("tap the cross", 10, 10, theme.fg, theme.bg)
|
|
gui.drawLine(x - 12, y, x + 12, y, theme.accent)
|
|
gui.drawLine(x, y - 12, x, y + 12, theme.accent)
|
|
end
|
|
|
|
local function finishCalibration()
|
|
local ok = sys.setCalibration(computeCalibration(samples[1], samples[2], 320, 480, INSET))
|
|
message = ok and "calibration saved" or "save failed"
|
|
mode = "menu"
|
|
gui.setRotation(sys.getRotation() / 90)
|
|
buildMenu()
|
|
end
|
|
|
|
function startCalibration()
|
|
message = nil
|
|
mode = "calibrate"
|
|
samples, pending, armed = {}, nil, false
|
|
gui.setRotation(0)
|
|
drawTarget(1)
|
|
end
|
|
|
|
function cycleRotation()
|
|
local ok = sys.setRotation((sys.getRotation() + 90) % 360)
|
|
message = ok and "rotation saved" or "save failed"
|
|
buildMenu()
|
|
end
|
|
|
|
local function cycleTheme()
|
|
local names = ui.themeNames()
|
|
local next_index = 1
|
|
for index, name in ipairs(names) do
|
|
if name == ui.themeName then next_index = index % #names + 1 end
|
|
end
|
|
local ok = sys.setTheme(names[next_index])
|
|
ui.reloadTheme()
|
|
message = ok and "theme saved" or "save failed"
|
|
buildMenu()
|
|
end
|
|
|
|
local zones = require("timezones")
|
|
|
|
-- 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 = sys.getTimezone()
|
|
for _, zone in ipairs(zones) do
|
|
if zone.tz == current then return zone.name end
|
|
end
|
|
return current
|
|
end
|
|
|
|
local function cycleTimezone()
|
|
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
|
|
local ok = sys.setTimezone(zones[next_index].tz)
|
|
message = ok and "timezone saved" or "save failed"
|
|
buildMenu()
|
|
end
|
|
|
|
local function statusLabel(status)
|
|
if status.state == "connected" then return "wifi: " .. status.ssid end
|
|
if status.ssid ~= "" then return "wifi: " .. status.state end
|
|
return "wifi: not configured"
|
|
end
|
|
|
|
function buildMenu()
|
|
mode = "menu"
|
|
local items = {
|
|
pad = 12,
|
|
gap = 8,
|
|
ui.text("settings"),
|
|
ui.button{label = "calibrate touch", on_press = startCalibration},
|
|
ui.button{label = "rotation: " .. sys.getRotation() .. " deg", on_press = cycleRotation},
|
|
ui.button{label = statusLabel(wifi.status()), on_press = buildWifi},
|
|
ui.button{label = "theme: " .. ui.themeName, on_press = cycleTheme},
|
|
ui.button{label = "timezone: " .. zoneLabel(), on_press = cycleTimezone},
|
|
ui.button{label = "exit", on_press = sys.exit},
|
|
}
|
|
if message then items[#items + 1] = ui.text(message) end
|
|
themed(items)
|
|
end
|
|
|
|
local function requestScan()
|
|
mode = "scanning"
|
|
scanRequested = true
|
|
themed{pad = 12, gap = 8, ui.text("wifi"), ui.text("scanning...")}
|
|
end
|
|
|
|
local function forgetNetwork()
|
|
message = wifi.forget() and "wifi forgotten" or "save failed"
|
|
buildWifi()
|
|
end
|
|
|
|
function buildWifi()
|
|
mode = "wifi"
|
|
local status = wifi.status()
|
|
local items = {pad = 12, gap = 8, ui.text("wifi"), ui.text(statusLabel(status))}
|
|
if status.state == "connected" then
|
|
items[#items + 1] = ui.text("ip: " .. status.ip)
|
|
end
|
|
items[#items + 1] = ui.button{label = "scan networks", on_press = requestScan}
|
|
if status.ssid ~= "" then
|
|
items[#items + 1] = ui.button{label = "forget network", on_press = forgetNetwork}
|
|
end
|
|
items[#items + 1] = ui.button{label = "back", on_press = buildMenu}
|
|
themed(items)
|
|
end
|
|
|
|
local function connectSelected()
|
|
if not wifi.connect(selectedNetwork.ssid, password) then
|
|
message = "could not save wifi"
|
|
buildWifi()
|
|
return
|
|
end
|
|
mode = "connecting"
|
|
themed{pad = 12, gap = 8,
|
|
ui.text("wifi"),
|
|
ui.text("connecting to " .. selectedNetwork.ssid .. "..."),
|
|
ui.button{label = "back", on_press = buildWifi},
|
|
}
|
|
end
|
|
|
|
local function chooseNetwork(network)
|
|
selectedNetwork, password = network, ""
|
|
if network.secure then buildKeyboard() else connectSelected() end
|
|
end
|
|
|
|
function buildNetworks(networks)
|
|
mode = "networks"
|
|
local byName = {}
|
|
for _, network in ipairs(networks) do
|
|
local current = byName[network.ssid]
|
|
if network.ssid ~= "" and (not current or network.rssi > current.rssi) then
|
|
byName[network.ssid] = network
|
|
end
|
|
end
|
|
local list = {}
|
|
for _, network in pairs(byName) do list[#list + 1] = network end
|
|
table.sort(list, function(a, b) return a.rssi > b.rssi end)
|
|
|
|
local items = {pad = 12, gap = 6, ui.text("wifi networks")}
|
|
for i = 1, math.min(#list, 6) do
|
|
local network = list[i]
|
|
local lock = network.secure and " *" or ""
|
|
items[#items + 1] = ui.button{
|
|
label = network.ssid .. lock .. " " .. network.rssi,
|
|
on_press = function() chooseNetwork(network) end,
|
|
}
|
|
end
|
|
if #list == 0 then items[#items + 1] = ui.text("no networks found", {color = ui.theme.muted}) end
|
|
items[#items + 1] = ui.button{label = "rescan", on_press = requestScan}
|
|
items[#items + 1] = ui.button{label = "back", on_press = buildWifi}
|
|
themed(items)
|
|
end
|
|
|
|
local function keyButton(label, value, width)
|
|
return ui.button{label = label, w = width or 24, h = 28, pad = 4,
|
|
on_press = function()
|
|
if #password < 64 then password = password .. value end
|
|
buildKeyboard()
|
|
end,
|
|
}
|
|
end
|
|
|
|
local function keyRow(chars)
|
|
local row = {row = true, h = 28, gap = 4}
|
|
for char in chars:gmatch(".") do row[#row + 1] = keyButton(char, char) end
|
|
return ui.box(row)
|
|
end
|
|
|
|
function buildKeyboard()
|
|
mode = "password"
|
|
local shown = string.rep("*", #password)
|
|
local rows
|
|
if keyboardPage == "symbols" then
|
|
rows = {"!@#$%^&*()", "-_=+[]{}", "`~;:'\",.?", "/\\|<>"}
|
|
else
|
|
rows = {"1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm,.?"}
|
|
if keyboardPage == "upper" then
|
|
for i, row in ipairs(rows) do rows[i] = row:upper() end
|
|
end
|
|
end
|
|
local controls = {row = true, h = 28, gap = 4,
|
|
ui.button{label = keyboardPage, w = 48, h = 28, pad = 4,
|
|
on_press = function()
|
|
keyboardPage = keyboardPage == "lower" and "upper" or
|
|
(keyboardPage == "upper" and "symbols" or "lower")
|
|
buildKeyboard()
|
|
end},
|
|
ui.button{label = "backspace", w = 68, h = 28, pad = 4,
|
|
on_press = function() password = password:sub(1, -2); buildKeyboard() end},
|
|
keyButton("space", " ", 40),
|
|
ui.button{label = "connect", w = 60, h = 28, pad = 4, on_press = connectSelected},
|
|
ui.button{label = "cancel", w = 52, h = 28, pad = 4, on_press = buildWifi},
|
|
}
|
|
themed{pad = 8, gap = 5,
|
|
ui.text(selectedNetwork.ssid),
|
|
ui.text("password: " .. shown),
|
|
keyRow(rows[1]), keyRow(rows[2]), keyRow(rows[3]), keyRow(rows[4]),
|
|
ui.box(controls),
|
|
}
|
|
end
|
|
|
|
function init()
|
|
buildMenu()
|
|
end
|
|
|
|
function draw()
|
|
if mode ~= "calibrate" then screen:draw() end
|
|
end
|
|
|
|
function on_touch_down(x, y)
|
|
if mode ~= "calibrate" then screen:down(x, y) end
|
|
end
|
|
|
|
function on_touch_up(x, y)
|
|
if mode ~= "calibrate" then screen:up(x, y) end
|
|
end
|
|
|
|
function on_tick()
|
|
if scanRequested then
|
|
scanRequested = false
|
|
buildNetworks(wifi.scan())
|
|
return
|
|
end
|
|
if mode == "connecting" then
|
|
local status = wifi.status()
|
|
if status.state == "connected" then
|
|
message = "connected: " .. status.ip
|
|
buildWifi()
|
|
elseif status.state == "failed" or status.state == "not_found" then
|
|
message = "connection " .. status.state
|
|
buildWifi()
|
|
end
|
|
return
|
|
end
|
|
if mode ~= "calibrate" then return end
|
|
|
|
local rx, ry = input.getRawTouch()
|
|
if not armed then
|
|
if not rx then armed = true end
|
|
return
|
|
end
|
|
if rx then
|
|
pending = {x = rx, y = ry}
|
|
elseif pending then
|
|
samples[#samples + 1] = pending
|
|
pending = nil
|
|
if #samples == 1 then drawTarget(2) else finishCalibration() end
|
|
end
|
|
end
|