eec9b692c6
Adds a wifi binding over the Arduino API and a settings flow that scans, picks the strongest AP per SSID, takes a password from an on-screen keyboard, and reports connection state. Credentials join /settings.lua and reconnect at boot. Settings are now written through a temp file and rename, and strings are Lua-escaped, so a password cannot corrupt the file the firmware parses at boot.
257 lines
7.6 KiB
Lua
257 lines
7.6 KiB
Lua
local ui = require("ui")
|
|
|
|
TICK_MS = 50
|
|
|
|
local INSET = 30
|
|
local BLACK, WHITE, RED = gui.color(0, 0, 0), gui.color(255, 255, 255), gui.color(255, 0, 0)
|
|
local ACCENT, MUTED = gui.color(0, 120, 255), gui.color(100, 100, 100)
|
|
|
|
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)
|
|
items.color, items.bg = BLACK, WHITE
|
|
items.press_bg, items.press_color = ACCENT, WHITE
|
|
screen = ui.screen(ui.box(items))
|
|
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)
|
|
gui.clear(WHITE)
|
|
gui.drawText("tap the cross", 10, 10, BLACK, WHITE)
|
|
gui.drawLine(x - 12, y, x + 12, y, RED)
|
|
gui.drawLine(x, y - 12, x, y + 12, RED)
|
|
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 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 = "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 = 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 setup()
|
|
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
|