- A stop that timed out into StopUnproven refused begin(), and only a
later begin() honoured the task's late acknowledgement. Nothing called
it, so restartBridge() (any `set mqtt...` restart) or an aborted OTA left
the observer offline for the rest of the boot. MyMesh now records that a
start was refused and restarts the bridge once stopAcknowledgedLate()
reports the ack. The OTA-abort alert no longer claims "bridge resumed"
when the restart was refused.
- refreshNTP() now runs the validated probe with one attempt per server.
With two attempts and a 1 s pause it blocked the MQTT task ~18 s every
hour on networks that drop UDP/123, where the old async SNTP cost
nothing. Loop comment and docs updated to match.
Today if a user builds firmware with an incorrect WiFi SSID or password,
they get a generic `WiFi disconnected.` error message.
Extend logging to descirbe the reason that WiFi was disconnected.
I gathered the updated logs for the 3 scenarios below.
```
WiFi: Attempting manual WiFi reconnect...
WiFi: WiFi disconnected (reason=ASSOC_LEAVE). Flagging for reconnect...
WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect...
WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect...
WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect...
```
```
WiFi: WiFi disconnected (reason=NO_AP_FOUND). Flagging for reconnect...
```
```
WiFi: WiFi connected successfully!
WiFi: Got connection
```
Payloads of 166 or 167 bytes previously passed the frame-sized bound
(MAX_CHANNEL_DATA_LENGTH = 167) but were rejected by sendGroupData
against MAX_GROUP_DATA_LENGTH (165), causing ERR_CODE_TABLE_FULL (retry
later) to be returned instead of ERR_CODE_ILLEGAL_ARG.
Fixes#3345
- scope the serial config CLI to RP2040; it was exposing the rescue CLI
(cat/rm/erase) on every ESP32 WiFi build, which gates it behind a
physical long-press
- bound and space out RP2040 rejoins: the core's join busy-waits, so cap
it at 5s and retry every 30s instead of every 10s
- stamp the reconnect timer in setup(), so the first loop() doesn't tear
down an association that is still finishing DHCP
- treat stored credentials as a pair, and pass NULL (not "") for an open
network
- teach build_as_lib.py where SerialWifiInterface moved
arduino-pico's WiFi.begin() blocks for up to 2x its 15s timeout, which
stalled the mesh loop on every reconnect attempt; use beginNoBlock().
Log the IP when the link comes up, and the status code when retrying.
Store ssid/pwd in NodePrefs and set them with 'set wifi.ssid' /
'set wifi.pwd' over USB serial; build-time WIFI_SSID/WIFI_PWD stay as
the fallback. Headless WiFi builds get the config CLI on Serial, which
is otherwise unused there.
SerialWifiInterface has no ESP32-specific code, so move it to
helpers/wifi and reuse it on RP2040. Guard the ESP32-only WiFi
event/auto-reconnect calls and poll link state on RP2040 instead.
The serial command buffers must stay NUL-terminated within their
bounds: if they ever aren't, strlen() can return >= sizeof(command)
and the read loop would index past the buffer. Additionally, a full
buffer now becomes a completed line (end-of-line marker placed
inside the buffer, NUL terminator kept) instead of overwriting the
terminator and silently corrupting the buffer for the next pass.
Applies to the serial CLI readers of the repeater, room server,
sensor and secure chat examples, and to the CLI rescue reader of
the companion example.
* Add tz_offset companion prefs and get/set commands for companion cli
* Use tz_offset for clock on display
Signed-off-by: Aleksei Mamlin <mamlinav@gmail.com>
display.flip lives in /mqtt.json, so it survives a reflash and is invisible
while someone is chasing a wrong orientation - a node still carrying flip=1
from testing looks exactly like a firmware that was never fixed. Boot now
reports "Display: flip off" or "flip on (rotated 180)".
Power-off now needs a 3 second hold; any shorter press toggles the display.
MomentaryButton reports a CLICK for any release short of its threshold, so
that single value defines both.
The button also felt unreliable - "a brief press doesn't wake it, more often
than not". MomentaryButton's multi-click detection withholds a CLICK for
MULTI_CLICK_WINDOW_MS (280 ms) after release, and folds a second press
arriving inside that window into a DOUBLE_CLICK. Since the handler only acts
on CLICK, an impatient second press produced nothing at all: press, see
nothing, press again, still nothing. Multi-click is now off for these targets,
so CLICK fires on release.
Both settings are build flags defaulted in variants/heltec_v4_r8/target.cpp
and overridden only on the two TFT observer bases, because the companion
builds share this user_btn and do use double/triple click.
DISPLAY_TOUCH_DEBUG additionally logs which input caused a toggle
("Display: button -> on"), so any remaining flake can be attributed to the
button or to a spurious touch read rather than guessed at.
`set display.flip 0|1` (also off/on) turns the panel 180 degrees from its
compiled DISPLAY_ROTATION, persisted in MQTTPrefs alongside display.timeout
and applied live without a reboot.
Adding 2 to the compiled rotation rather than setting an absolute value keeps
portrait portrait and landscape landscape, so the DisplayViewport geometry
never changes with it and the setting cannot produce a nonsensical mix.
DisplayDriver gains a defaulted no-op setFlipped(), so no other display
driver is affected.
The compiled rotation was verified identical across `pio run` and `build.sh`
on two machines (movi a11, 2 at the setRotation call site), yet the panel read
upside down for one tester and upright for another - which is what a board
mounted either way up looks like. No single compiled constant satisfies both,
so orientation becomes a setting rather than another rebuild.
Runtime-only, like display_timeout_secs: LegacyV1MQTTPrefs and the frozen
binary payload sizes are untouched, and the JSON group is an append that older
firmware skips.
Four defects found by hardware testing of the Expansion Kit V2.
Touch never registered. The panel's controller does not use the point-count
encoding the reference CHSC6X drivers document: byte 0 reads 0x00 idle and
0x1F while a finger is down, so testing for a count of 1 never fired. A
partly-failed read leaves 0xFF, which must not count as a press either, so
the test is now != 0x00 && != 0xFF.
Touch polling could stall the UI loop for ~1 s at a time. The controller
NACKs its address whenever it has nothing to report, and calling requestFrom()
unconditionally logged a bus error on every 50 ms poll and, once the bus
wedged, burned a full ESP_ERR_TIMEOUT inside loop(). Probe the address first,
which reports the same NACK quietly, and bound the read with setTimeOut().
The display could not be woken once it blanked; only RST brought it back.
turnOn() re-ran the whole display.init(), which re-enters SPI setup, spends
~500 ms in Adafruit's reset delays and pulses GPIO 21 - the line shared with
TP_RST, so it reset the touch controller on every wake. Since turnOff() no
longer parks that line low, the panel stays configured while dark and waking
is just the backlight. Toggling also clears the refresh deadline so the
current frame is drawn immediately instead of the stale one.
Power-off rebooted instead of staying off. powerOff() went through
enterDeepSleep(), which always arms an ext1 wake on P_LORA_DIO_1; a deep-sleep
wake is a full reboot, so a node in live traffic restarted within seconds of
showing "Turning OFF". It now disables every wake source, so the node stays
down until RST or a power cycle.
Note that the display off/on cycle had never been exercised on this board
before: observer builds pinned AUTO_OFF_MILLIS=0, so the panel never blanked
until display.timeout made it a runtime setting.
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.