Radek (Sixi)

Faro 0.12.1: CRC-checked flash state, hardened phone Wi-Fi setup, Polish UI, and a correctness pass across five LoRa boards

Radek (Sixi) · 12 November 2026 · sixi-tdeck-plus (Faro) v0.12.1

After shipping the initial Faro handheld UI and desktop console for MeshCore LoRa radios and expanding it to five ESP32-S3 targets, I ran the same kind of line-by-line correctness review over Faro's firmware, desktop companion client, and build pipeline. v0.12.1 is the result.

Embedded bugs rarely announce themselves on a clean bench. They wait for a battery brownout mid-save on SPIFFS, a 3-byte repeater path preset from another region, a 63-character WPA2 passphrase with punctuation, or a CMake release flag that quietly compiles your test assertions into no-ops. Every class of bug below now has a host regression test in CTest, and the firmware was verified on a live LilyGo T-Deck Plus (162 mesh contacts on EU 868) and a USB-attached Heltec V3 before tagging.

1. A truncated SPIFFS file used to wipe in-memory state

Alongside upstream MeshCore's own NVS/SPIFFS identity store, Faro writes UI preferences, contact notes, and chat history to /faro_ui.bin (FUI1) and /faro_msgs.bin (FMS1) at most once a minute via write-to-.tmp and rename(). Reviewing the loader and writer turned up three failure modes under brownout or flash wear:

In v0.12.1, both files append a magic + IEEE 802.3 CRC32 trailer, fsync before rename(), and load in two passes: the entire stream and optional CRC trailer are validated first without touching live state, legacy pre-0.12.1 files without the trailer still upgrade cleanly on first boot, an intact .tmp file is recovered automatically if power died mid-rename, and clear history unlinks both /faro_msgs.bin and /faro_msgs.bin.tmp immediately.

Takeaway: atomic rename is only half of crash-safe persistence. Validate the whole record stream before mutating live state, and test what happens when power drops between write and rename.

2. Multi-byte repeater paths and bounded exports

MeshCore supports 1-, 2-, and 3-byte repeater path hashes (such as the Canada 3-byte preset), packing the stride into the top two bits of path_len (hash_size = 1 << (path_len >> 6)) and the hop count into the low six bits (path_len & 0x3F).

AreaBug before 0.12.1Fix in 0.12.1
Advert & RX hop counts Treated pkt->path_len as a raw byte count in discovered-contact callbacks, displaying 0x83 (131 hops) instead of 3 hops of 3 bytes, and dropped last_snr / last_rssi on discovery. Decodes pathHashSize() and pathHopCount() consistently across adverts, chat headers, and contact cards, preserving link SNR/RSSI.
AppStore::buildTracePath Indexed out_path[64] without checking h * hash_size + sz <= sizeof(out_path) on malformed inputs. Strict bounds check per hop for 1-, 2-, and 3-byte hashes before reading.
SD JSON & GPX exports fwrite(buf, 1, n, f) used snprintf's return value (which can exceed sizeof(buf) on truncation), and node names with quotes or <& broke JSON/XML. Clamps write lengths to buffer capacity and escapes JSON strings in settings backups and XML entities in GPX tracks.

3. Hardening phone Wi-Fi provisioning

Typing a long random router password on a thumb keyboard is painful, so 0.11.1 added a temporary 5-minute WPA2 SoftAP (Faro-XXXX) serving a tiny HTML form at 192.168.4.1. Reviewing it as an adversarial surface exposed four sharp edges:

  1. GET /save?ssid=...&pwd=... truncated encoded passwords. A 63-character WPA2 passphrase containing punctuation expands up to 3× when URL-encoded (%XX), overflowing a small query-line read buffer, and placed credentials in the request URI.
  2. No CSRF token and active companion port. Any page open in the phone's browser while joined to Faro-XXXX could forge a cross-origin request to 192.168.4.1, and TCP port 5000 remained reachable during setup.
  3. Background STA channel hopping. If an older unreachable SSID was stored, ESP32 STA auto-reconnect kept scanning across channels, knocking the SoftAP off its channel while the user was submitting the form.
  4. Modulo bias in the setup AP password. Mapping random bytes onto the character alphabet with plain % slightly skewed character selection.

0.12.1 moves submission to POST /save with a single-use random CSRF token verified in constant time, parses a body large enough for a worst-case URL-encoded 32-byte SSID + 63-character WPA2 passphrase, generates the temporary AP password via rejection sampling, pauses both STA reconnect scanning and TCP :5000 until setup finishes or times out, and zeroes credential buffers on the stack before returning.

4. Desktop console resilience and the CMake -DNDEBUG trap

faro-console runs the exact same LVGL 9.3 UI on Linux over SDL2, talking to a radio via USB serial or Wi-Fi TCP :5000. Four transport and build fixes landed here:

# Always keep assertions armed in CTest targets, even in Release / RelWithDebInfo builds
target_compile_options(test_core PRIVATE -UNDEBUG)
target_compile_options(test_companion_client PRIVATE -UNDEBUG)
target_compile_options(test_ui_sim PRIVATE -UNDEBUG)

Takeaway: if your test suite uses <cassert> and CI builds with -DCMAKE_BUILD_TYPE=Release, your tests are not running their assertions unless you pass -UNDEBUG.

5. Five boards, Polish (PL) at zero flash cost, and 522 KB more PSRAM

The rest of 0.12.1 focuses on hardware ergonomics across all five supported ESP32-S3 boards (LilyGo T-Deck / T-Deck Plus, T-Deck MAX, Elecrow ThinkNode M9, T-Beam S3 Supreme, and Heltec WiFi LoRa 32 V3):

Getting v0.12.1

Firmware images (-merged.bin for first install at 0x0, -app.bin at 0x10000 to update while keeping your MeshCore keys, contacts, and channels), the Ubuntu amd64 desktop package (faro-desktop_0.12.1_amd64.deb), and SHA256SUMS are on the v0.12.1 release page, or flash directly from Chrome/Edge on the 1-click Web Flasher:

# Back up full 16 MB flash first, then update app partition (keeps keys, contacts & channels)
esptool --chip esp32s3 --port /dev/ttyACM0 --baud 921600 read-flash 0 ALL backup-$(date +%F).bin
esptool --chip esp32s3 --port /dev/ttyACM0 --baud 921600 write-flash 0x10000 faro-tdeck-0.12.1-app.bin

Faro is Apache-2.0 at github.com/radeksixi/sixi-tdeck-plus; the browser flasher and Mesh America catalog are at radeksixi.github.io/sixi-tdeck-plus.