Two races between the new GPS duty-cycle scheduler and code that changes
GPS state independently of it:
- gpsDutyCycleLoop() capped every "GPS on" phase at a fixed 60s and would
stop_gps() as soon as a fix went valid, with no awareness of an
in-flight "!gps fix" bot request -- so a fix's own up-to-300s acquire
window (and its 10s averaging phase) could get cut short by the
scheduler shutting GPS off mid-request. MyMesh::isGpsFixPending() now
feeds into UITask's existing "is anything live using GPS right now"
hold, alongside trail/live-share/locator/nearby.
- setSettingValue("gps", ...) (Settings toggle, bot !gps on/off, CLI)
starts/stops GPS directly without resetting the scheduler's own phase
timer, so a manual toggle could land on a stale, already-expired
deadline left over from before -- immediately re-stopping GPS a tick
after turning it on. The phase timer now resets on every external
change, so the next duty-cycle tick re-arms fresh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adopts hardware Channel Activity Detection (wired into
RadioLibWrapper::isChannelActive() alongside our RSSI-threshold check
and RX duty-cycle power-save), MCU temperature telemetry, LR2021
standby workaround, DISPLAY_SCALE/FLIP overrides, NRF52Board
shutdownPeripherals() refactor, and misc upstream fixes.
Declines upstream's ConfigSerializer-based NodePrefs rewrite,
MultiSerialInterface/interface_manager, and UIColor palette system —
each would have broken large parts of the Solo-specific feature set
(NodePrefs fields, per-variant single serial_interface, enum-based
DisplayDriver::Color). Flagged as candidate follow-up migrations, not
permanent no's.
Also fixes several pre-existing bugs surfaced while chasing silent
merge breaks (stale newMsg() override signature in ui-tiny/ui-orig,
dead UIEventType::newContactMessage case, missing ContactsIterator
init), bumps FIRMWARE_VERSION/MESHCORE_VERSION to 1.17, and fixes a
missing <cstdlib> include that broke the native ConfigSerializer unit
tests.
Verified via 13+ pio run builds across ESP32/nRF52, all 3 companion UI
variants, and 7 display drivers, plus the full native unit test suite
(33/33 passing).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New M5Stack Cardputer ADV variant (ESP32-S3, ST7789 TFT, built-in TCA8418
QWERTY keyboard, PI4IOE5V6408 LoRa-cap IO-expander autodetect), and a
KeyShield accessory variant for the existing LilyGO T-Echo Lite (external
TCA8418 T9 keypad + AW21009 backlight driver). Both keyboards share one
ENV_USE_TCA8418 polling block in UITask.cpp::loop(), coexisting with the
unrelated CardKB support (different chip/address/flag).
Fixes carried in from the contributed T-Echo Lite code: swapped GPS RX/TX
pins, TX-LED hooks, TCXO voltage, missing GxEPD2_122_T61 panel include.
Fixed during integration: I2C bus was probed for an RTC before Wire.begin()
configured its pins on Cardputer ADV (silent RTC autodetect failure).
Added dedicated *_solo_dual release envs for both boards (auto-picked up by
the solo-firmware release workflow). Gave the T-Echo Lite KeyShield solo
build -Os/-Ofast-unflag like every other nRF52 solo build (was missing,
cut flash usage from 90.7% to 61.4%).
Ported the shared misc-fixed 6x9 font (full Latin/Greek/Cyrillic, opt-in via
OLED_MISC_FIXED_FONT) to ST7789Display for the Cardputer's on-screen
keyboard. ST7789Spi isn't Adafruit_GFX-based like the other single-font
drivers, and this panel's logical->physical scale is non-integer, so glyphs
are re-packed to XBM and blitted through the existing drawXbm(), which
already does correct fractional-scale boundary math, rather than
duplicating that logic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three power-saving additions, prompted by comparing this fork's existing
RX duty-cycle support against IoTThinks/EasySkyMesh:
- RX duty-cycle watchdog: the SX126x's hardware RX<->sleep sequencer runs
with no MCU polling, so a desync (a known failure mode) previously had
nothing watching for it. A new watchdog samples the BUSY pin every tick;
no transition for too long triggers a soft re-arm, then a full chip
reset (with cached radio params reapplied, since std_init() resets to
compiled firmware defaults) if that doesn't clear it. Soft/hard recovery
counts surface on Tools > Diagnostics > Live as "RXPS wd s/h".
- Noise-floor recalibration during power-save: sampling was previously
skipped entirely while duty-cycling, freezing int.thresh interference
detection at whatever the floor was when power-save turned on. Now
borrows a brief continuous-RX window once a minute to take a fresh
reading before re-arming duty-cycle.
- GPS duty-cycling (Settings > System > "GPS pwr"): cycles GPS off between
fixes instead of running it continuously. Each wake waits for a fix
(capped at 60s) before sleeping again for the configured interval.
Repurposes the long-dead NodePrefs::gps_interval byte rather than adding
a new persisted field. A "is anything live using GPS right now" hold in
UITask keeps GPS continuously on whenever trail recording, live-share,
an armed Locator, or the Compass/Nearby-navigate view actually need a
live fix, so none of those features degrade. Locator crossing-state is
reset on each wake so a still-settling first fix can't read as a false
geofence crossing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Port the Wio Tracker L1 solo firmware (full on-device UI, dual BLE/USB
companion transport) to Heltec V3 and V4 OLED boards. Neither board has
a joystick or CardKB on-board, so each new env wires up both as optional
peripherals with default pins from what the board leaves free, gated
behind the existing UI_HAS_JOYSTICK/ENV_PIN_SDA+SCL flags.
DUAL_SERIAL was nRF52-only; added an ESP32 helpers/esp32/DualSerialInterface.h
counterpart so the flag isn't silently ignored on these boards. On V4's
native USB CDC, isClientConnected() also honours (bool)Serial (real DTR),
same as the nRF52 version; V3 has no native CDC so it stays BLE-only there.
Screen (SDA 17/SCL 18) and CardKB (SDA 3/SCL 4) confirmed working on real
V4 hardware.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CustomLR1110::startReceive() passed RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED
(1<<4 = 16) as RadioLib's first argument, which is the RX *timeout*, not an
IRQ mask. At the LR11x0's 30.52us tick that armed the receiver for ~488us, so
it dropped out of RX before any packet could arrive and the node received
nothing at all -- while transmitting normally.
Symptoms on a SenseCAP T1000-E: tx_air_secs rising, rx_air_secs stuck at 0,
recv_errors 0, and the noise floor pinned at the -120 clamp because
getCurrentRSSI() never sampled a live receiver.
Pass RADIOLIB_LR11X0_RX_TIMEOUT_INF (continuous RX), which is what
LR11x0::startReceive() itself uses, keeping the PREAMBLE_DETECTED flag in the
reported IRQ flags as intended.
Introduced in ea5d7c8b ("LR1110: add PREAMBLE_DETECTED to reported irq flags").
Verified on two T1000-E units: with only the repeater fixed it began receiving
(last_rssi -29, SNR 17.0) while the unfixed companion stayed deaf; fixing both
brought up the link in each direction.
The misc-fixed font (full Latin/Greek/Cyrillic) had only ever reached
SH1106Display and the e-ink driver. GAT562 30S -- like 24 other variants
-- uses SSD1306Display, which stayed on the built-in 5x7 font, so every
keyboard alphabet beyond ASCII and every accented contact name was
transliterated (Łódź -> Lodz) or drawn as a filled block.
Extract the renderer into MiscFixedRenderer.h rather than copying it:
Adafruit_SH110X and Adafruit_SSD1306 both derive from Adafruit_GFX and
glyph drawing is pure pixel plotting, so one implementation serves both
drivers (-79 lines from SH1106Display.cpp, same flash usage as before).
The header is .cpp-only by contract -- the font tables are static const,
so including it from a driver header would land a copy in every
translation unit.
On SSD1306 the path is opt-in behind OLED_MISC_FIXED_FONT, set only in
the two GAT562 30S solo envs: the font costs ~14 KB of flash and the
other 24 variants' repeater/companion builds have no keyboard to type
those alphabets on. Without the flag that driver is unchanged, verified
by building GAT562_30S_Mesh_Kit_repeater.
Drive-by consistency fix from the extraction: a newline in print() now
advances the cursor by yAdvance * text_size instead of a single yAdvance,
which was half a row short at setTextSize(2). The e-ink driver always
scaled it. No effect at size 1, which is everywhere the UI prints '\n'.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The screen faded as the device was used: text went grey a few updates
after it was drawn, while whatever had just changed stayed crisp. Only a
full refresh brought it back, and the next few updates ate it again.
A partial update is differential -- the controller drives only the pixels
that differ between its "current" (0x24) and "previous" (0x26) RAM and
leaves the rest to hold their own charge, which this panel doesn't do
well. Priming the previous-image RAM with the inverse of the incoming
frame makes every pixel a difference, so all of them get driven to their
target, in both directions.
The inverse matters: priming with flat white makes only white->black a
difference, so ink is re-driven but never erased and every screen ever
shown accumulates as a ghost. Confirmed on hardware, both ways round.
Costs one extra full-screen RAM write (a few ms of SPI). The refresh
itself is unchanged -- the waveform clocks the whole panel regardless of
how many pixels it drives -- so nothing got slower. Clearing ghosts is
still the periodic full refresh's job (Settings > Full rfsh), which can
now stay off.
The helper needs GxEPD2_BW's private _buffer/_page_height, so it goes in
the patched copy of the header, which every e-ink build now includes
unconditionally rather than only screenshot-enabled ones. All e-ink
variants pin GxEPD2 1.6.2, the version that copy tracks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A message containing a line break drew two words on top of each other in
the fullscreen reader. wrapLines() treated '\n' as an ordinary character:
it measured it via getCodepointWidth() -- which reports a full 6px cell
for it, since 0x0A sits below the font's first glyph -- and copied it into
the wrapped line. Both display drivers' print() then acts on '\n' by
resetting the cursor to x=0 and stepping down one row, so the tail of that
line was drawn straight over the following one.
wrapLines() now ends the line at '\n'/'\r' (CRLF counts as one break) and
consumes the byte rather than emitting it, preserving blank lines the
sender typed while still skipping degenerate empty wrap segments so the
loop can't stall. This covers the fullscreen view and the history list's
portrait bubbles, which share the function.
drawTextEllipsized() folds newlines into spaces for the same reason: it
draws one line clipped to max_width, and the compact one-line message
previews in the landscape list feed it raw message bodies. A space keeps
the words apart and measures the same, so the ellipsis maths is unchanged;
for names and labels it's a no-op.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
If Bluefruit.begin fails, BLE OTA mode won't actually start and the board might require a reboot to reattempt.
Fixes:
- Bluefruit.begin returns false in NRF52Board.startOTAUpdate if OTA mode fails to start. User is notified of the fault through existing error message in CommonCLI and can reattempt "start ota" command.
Satellite count alone is a poor proxy for fix quality -- few satellites
in good geometry can beat many in poor geometry. LocationProvider now
exposes getHDOP() (default -1 = unsupported); MicroNMEA implements it.
isLocFixReady() prefers HDOP <= 2.0 when available, falling back to the
old >=8 satellite threshold for providers that don't report it (e.g.
RAK12500/u-blox).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auto-detected at boot on Wire1/Grove (addr 0x5F) -- no setting to flip, and a
no-op on boards without that bus or with nothing attached. This UI's key
codes (KEY_LEFT/UP/DOWN/RIGHT/ENTER/CANCEL) already match CardKB's own byte
protocol, so most input needs zero translation and flows through the same
key queue as physical buttons.
Two bytes get remapped in UITask::pollCardKB():
- Enter, only when the on-screen keyboard's plain grid state is active (no
placeholder/accent popup, not in cursor-mode), becomes a new KEY_KB_ENTER
sentinel meaning "submit the field" -- reusing plain KEY_ENTER there would
insert a stray character, since a CardKB typist's row/col never reflect an
intentional grid selection. Everywhere else Enter is untouched, so
selecting a placeholder or committing an accent still works normally.
- Tab (otherwise unused) becomes KEY_CONTEXT_MENU, standing in for the
"Hold-Enter" long-press gesture CardKB has no way to produce -- without it,
~30 context menus across the UI (message reply/navigate, Bot/Admin/
Repeater, ...) would be unreachable from the keyboard alone.
KeyboardWidget gains a direct-typing path: printable ASCII inserts straight
at the cursor bypassing the grid, Backspace deletes, KEY_KB_ENTER submits.
Build-verified: WioTrackerL1_companion_solo_dual and
WioTrackerL1Eink_companion_solo_dual both compile and link clean; also
smoke-tested Heltec_mesh_solar_companion_radio_ble (no ENV_PIN_SDA/SCL) to
confirm zero regression on boards without the feature.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>