feat(wifi): scan, connect and persist networks from the settings app

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.
This commit is contained in:
2026-08-01 06:29:38 -04:00
parent 4e5796fd51
commit eec9b692c6
8 changed files with 357 additions and 25 deletions
+56 -8
View File
@@ -10,6 +10,8 @@ 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 int16_t fieldOr(lua_State* L, const char* key, int16_t fallback) {
lua_getfield(L, -1, key);
@@ -18,6 +20,32 @@ static int16_t fieldOr(lua_State* L, const char* key, int16_t fallback) {
return value;
}
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) {
String out = "\"";
for (size_t i = 0; i < value.length(); i++) {
unsigned char c = value[i];
if (c == '\\' || c == '\"') {
out += '\\';
out += (char)c;
} else if (c >= 32 && c < 127) {
out += (char)c;
} else {
char escaped[5];
snprintf(escaped, sizeof(escaped), "\\%03u", c);
out += escaped;
}
}
out += '\"';
return out;
}
bool Settings::setRotation(int16_t degrees) {
if (degrees % 90 != 0 || degrees < 0 || degrees > 270) return false;
rotation = degrees;
@@ -44,20 +72,40 @@ bool Settings::load() {
touchY1 = fieldOr(L, "y1", touchY1);
}
lua_pop(L, 1);
lua_getfield(L, -1, "wifi");
if (lua_istable(L, -1)) {
wifiSsid = stringFieldOr(L, "ssid", wifiSsid);
wifiPassword = stringFieldOr(L, "password", wifiPassword);
}
lua_pop(L, 1);
}
lua_close(L);
return ok;
}
bool Settings::save() const {
File f = SD.open(PATH, FILE_WRITE);
String source = "return {\n rotation = " + String(rotation) + ",\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);
File f = SD.open(TEMP_PATH, FILE_WRITE);
if (!f) return false;
char buf[200];
int len = snprintf(buf, sizeof(buf),
"return {\n rotation = %d,\n"
" touch = { x0 = %d, y0 = %d, x1 = %d, y1 = %d },\n}\n",
rotation, touchX0, touchY0, touchX1, touchY1);
bool ok = f.write(reinterpret_cast<const uint8_t*>(buf), len) == (size_t)len;
bool ok = f.write(reinterpret_cast<const uint8_t*>(source.c_str()), source.length()) == source.length();
f.close();
return ok;
if (!ok) {
SD.remove(TEMP_PATH);
return false;
}
if (SD.exists(BACKUP_PATH)) SD.remove(BACKUP_PATH);
bool hadSettings = SD.exists(PATH);
if (hadSettings && !SD.rename(PATH, BACKUP_PATH)) return false;
if (SD.rename(TEMP_PATH, PATH)) {
if (hadSettings) SD.remove(BACKUP_PATH);
return true;
}
if (hadSettings) SD.rename(BACKUP_PATH, PATH);
return false;
}