Files
HaloKeymind/test
agessaman fcd92e985f feat(display): add R8 observer TFT dashboard, touch toggle and display.timeout
Replace the sparse Heltec V4 R8 observer home screen with a padded dark
analytics dashboard, add manual display control, and make blanking a runtime
setting.

Dashboard (DISPLAY_ACTIVITY_DASHBOARD, the four R8 TFT observer envs):

- RadioActivityWindow: 20 one-minute buckets of valid RX packets, no heap.
  The caller's 32-bit millis() is extended to a monotonic 64-bit clock, so
  nothing downstream has a rollover case; an always-on node passes 2^32 ms
  after ~49.7 days, which would otherwise re-enter warm-up and divide 20
  minutes of traffic by seconds. Rates use 19 whole minutes plus the elapsed
  part of the current one rather than a fixed 1200 s.
- ObserverDashboard: header, radio strip, headline totals, a 20-bar
  packets-per-minute graph and RF/status footers, with separate portrait and
  landscape layouts. A text row is a fixed 16 px, which is 3.2 logical units
  in portrait but 4.27 in landscape, so one shared grid would overlap.
  Text is trimmed by character budget, not measured width: getTextWidth()
  reports an over-long string at the portrait driver's fallback scale, so
  DisplayDriver::drawTextEllipsized() under-trims and the row renders at half
  height.
- Six per-row signatures computed from what is actually drawn, so only the
  rows whose pixels changed repaint. No startFrame(), no whole-screen clear.
  Link state moved out of the full-frame signature, so a DHCP renewal or WiFi
  flap repaints one footer row instead of the panel.
- Dark theme by retuning the UIColor statics at runtime, which needs no
  display-driver edit and carries boot, setup, reboot and power-off with it.

Touch and button (DISPLAY_TOUCH_TOGGLE):

- CHSC6X at I2C 0x2E, polled; TP_INT is unusable (optional R13, and GPIO 43
  is U0TXD). The point-count byte is tested against a valid count, never
  against non-zero: an idle read returns 0xFF, which reads as a finger held
  down forever and latches the tap detector after one event.
- turnOff() no longer parks PIN_TFT_RST low on this board. GPIO 21 is a
  shared LCD_RST/TP_RST net, so doing that held the touch controller in
  reset for as long as the display was off. Verified against Heltec's
  expansion-board and mainboard schematics and the V4-R8 datasheet pinout,
  which also correct the pin comment in HeltecV4R8Board.cpp.
- The USER button click now toggles the display too; it previously did
  nothing whenever the display was already on.

display.timeout:

- `set display.timeout <secs>` / `get display.timeout`, 0 = stay on, 60 s
  default, 3600 max. Read live, so a change applies without a reboot and
  restarts the countdown rather than firing on the old deadline.
- Stored in MQTTPrefs (/mqtt.json), keeping NodePrefs aligned with upstream.
  Runtime-only: LegacyV1MQTTPrefs and the four frozen binary payload sizes
  are unchanged. No JSON format-version bump - the loader skips keys no
  def() claims, so older firmware reads newer files and this firmware reads
  older ones with the default applied. Both directions are covered by tests.
- Joins the observer atomic-setter contract, so a failed save rolls the live
  value back instead of only claiming to.

New periodic work uses a wrap-safe deadline check; `millis() >= deadline`
fires every loop for a whole interval before each rollover.

Adds test_radio_activity_window, test_observer_dashboard (driving the real
renderer against a recording DisplayDriver in both orientation profiles) and
test_touch_tap_detector. 440 native cases pass.
2026-08-28 13:39:16 -07:00
..

Host unit tests

Fast, hardware-free unit tests for the fork's pure logic, run on the host with GoogleTest via PlatformIO's native environment. They cover the extractable observer/WebConfig logic (validation, preset table, topic templates, key parsing) — the parts that don't depend on the ESP32, radio, or network stack. Integration behavior (AsyncTCP transport, WiFi/MQTT, SoftAP) is exercised separately; see "Local testing without hardware" in MQTT_IMPLEMENTATION.md.

Running

pio test -e native                      # all suites
pio test -e native -f test_webconfig_keys   # a single suite

A green [PASSED] per suite means GoogleTest returned 0 (all assertions passed). PlatformIO's "0 test cases" line is just its Unity-style counter and does not reflect the GoogleTest count — run the built binary directly (.pio/build/native/program) to see the per-assertion breakdown.

Suites

Suite Source under test Covers
test_mqtt_presets src/helpers/MQTTPresets.h preset lookup; table integrity (unique names, non-empty URLs, JWT-audience invariant, names fit the slot buffer); mqttPresetNeedsSlotCredentials; slot-count constants
test_observer_validation src/helpers/MQTTObserverValidation.h IATA (exactly 3 alphanumerics), owner key (64 hex), NTP hostname, and the buffer-fit check behind the #17 length validation — including boundaries and nulls
test_webconfig_keys src/helpers/WebConfigKeys.h POST-key allowlist, secret detection, admin-password classification/validation, slot-index bounds, and the short-key out-of-bounds guard (attacker-supplied keys)
test_topic_template src/helpers/MQTTTopicTemplate.h {iata}/{device}/{token}/{type} expansion, overflow/NUL-termination, and a buffer-size fuzz
test_mqtt_topic_router src/helpers/MQTTTopicRouter.h complete preset/custom topic-routing contract; MeshRank all types except raw; required identifiers; invalid inputs/slots; exact buffer boundaries
test_mqtt_connection_policy src/helpers/MQTTConnectionPolicy.h reconnect guard/backoff/stagger and breaker transitions; stable reset; JWT lifetime/renewal policy; exact timing boundaries and 32-bit millis() rollover; WiFi current-outage start sticky across STA reconnect attempts
test_alert_fault_policy src/helpers/AlertFaultPolicy.h WiFi/MQTT fault edge detector; OutageSnapshot (down / started_ms / initiating reason) fed to tick and formatWifiAlert; reason-8 reconnects change neither duration nor initiating reason; flap between status polls; down at millis()==0; packed 64-bit cross-task word; rate-limit floor and first-fire; 5 s poll cadence and millis() rollover
test_display_viewport src/helpers/ui/DisplayViewport.h, src/helpers/ui/DisplayFrameSignature.h logical-to-physical portrait mapping; fractional span coverage; fitted-width conversion; preferred/fallback text scaling; stable visible-frame change detection
test_mqtt_packet_queue_policy src/helpers/MQTTPacketQueuePolicy.h queue-full eviction; stale-disconnect flush; adaptive drain limits; bounded QoS0 retries; exact timing boundaries and 32-bit millis() rollover
test_mqtt_packet_filter src/helpers/MQTTPacketFilter.h per-slot 0-15 allowlist parsing/formatting, numeric and named spellings; exact bounds; membership; candidate/eligible split and retry-completion policy; pre-queue union gate; default-mask detection
test_mqtt_runtime_buffer_lifecycle src/helpers/MQTTRuntimeBufferLifecycle.h idempotent allocation/release; partial-allocation degradation; retry of only missing buffers
test_mqtt_prefs_codec src/helpers/MQTTPrefsStorage.h, src/helpers/MQTTPrefsCodec.h binary pre-slot/3-slot/6-slot migration fixtures; v1 header integrity; downgrade preservation; shortest-payload write policy (default filters stay downgrade-readable)
test_mqtt_prefs_serializer src/helpers/MQTTPrefsSerializer.h, src/helpers/ConfigSerializer.* semantic nested /mqtt.json round trips; numeric slot keys; required/future version handling; strict length/overflow/duplicate rejection; safe semantic repair; scratch-before-live loading
test_mqtt_prefs_atomic_store src/helpers/MQTTPrefsAtomicStore.h, src/helpers/MQTTPrefsRecovery.h production JSON begin/write/checksum-finish/schema-verify/commit orchestration; first-migration and rename-boundary recovery; legacy /node_prefs handoff; failure cleanup and original-file preservation
test_mqtt_payload_builder src/helpers/MQTTPayloadBuilder.cpp status/packet/raw JSON contracts; optional fields; escaping; RX metrics and path; score handling; exact buffer bounds; maximum representative payloads
test_radio_activity_window src/helpers/RadioActivityWindow.h 20-minute minute-bucketed RX window: totals and derived rates; bucket rotation and oldest-to-newest ordering; expiry at the boundary; ring clear after 20 minutes of silence; warm-up versus steady-state denominators; peak minute; last-packet age and staleness; counter saturation; millis() rollover, including the minute boundary a now_ms / 60000 quotient would corrupt
test_observer_dashboard src/helpers/ui/ObserverDashboard.h R8 TFT observer dashboard against a recording DisplayDriver in both orientation profiles: compact number/byte/age formatting and the 5 s age quantisation; per-row character budgets; on-panel and inside-the-margin bounds; no silent portrait scale fallback; non-overlapping row rectangles and each row's repaint covering everything it draws; 20-bar graph scaling, ordering and empty/spike cases; per-row signatures and the partial-repaint policy
test_touch_tap_detector src/helpers/ui/TouchTapDetector.h debounced rising-edge detection for the polled Expansion Kit touch panel: idle quiet; one tap per touch; long presses do not repeat; sub-debounce blips ignored; contact bounce still counts once; minimum gap between accepted taps; millis() rollover; reset semantics
test_utils src/Utils.cpp Utils::toHex (upstream)

Conventions (and how to add a suite)

  • Each test/test_<name>/ directory builds into its own GoogleTest program and must define its own main() (::testing::InitGoogleTest + RUN_ALL_TESTS).
  • Tests are host-only: include only pure headers. Arduino/crypto stubs live in test/mocks/ (on the include path via -I test/mocks).
  • Firmware headers are included from src (via -I src, e.g. #include "helpers/MQTTPresets.h"). Some are guarded or ESP-flavored, so a suite may need shims before the include — e.g. test_mqtt_presets does #define WITH_MQTT_BRIDGE 1 (the preset table is behind that flag) and #define PROGMEM (the embedded CA-cert strings are PROGMEM-qualified).
  • To add a suite: create test/test_<name>/test_<name>.cpp with a main(), and add any host-only source it links to the native env's build_src_filter in platformio.ini (header-only code needs no source entry). No other wiring.
  • Keep logic testable by extracting pure functions into headers (as MQTTObserverValidation.h / WebConfigKeys.h / MQTTTopicTemplate.h do) and having the firmware call the same functions.