Ported from meshcore-dev/MeshCore#2140 (Quency-D <hj_zzns@163.com>).
The upstream v1.16.0 merge changed the Heltec FEM LNA to disabled by
default (upstream commit 696aae6e). This patch restores the enabled
default for repeater via the new "radio.fem.rxgain" preference
(defaulted to 1), while also making the setting user-configurable
and persisted across reboots.
Before this commit, there was no way to set a different max hop count
for unscoped messages.
Now with this change, by defaul it tracks the flood.max setting, until
a user provides a flood.max.unscoped value, which tax precidence for
packets if ROUTE_TYPE_FLOOD is true.
Implement getResetReason() and getResetReasonString() for ESP32Board,
covering all esp_reset_reason_t values: power-on, external reset,
software reset, panic/exception, interrupt watchdog, task watchdog,
generic watchdog, deep sleep wake, brownout, and SDIO reset.
Extend the pwrmgt.bootreason CLI command to support ESP32 alongside
NRF52. On ESP32 only the reset reason is reported (no shutdown reason).
Pass the reset reason as the value field when recording the
HISTORY_EVENT_BOOT stats event so the reason is stored in the in-memory
ring buffer and persisted to the archive events log.
Add bootReasonLabel() to the web panel and update renderEventsSection()
to annotate boot events with the human-readable reset reason string,
e.g. "boot (panic/exception)", when a non-null value is present.
AUTO_OFF_MILLIS is a power-save feature aimed at battery use. When the
board reports isExternalPowered() == true (USB or other DC source),
blanking the screen serves no purpose — there's nothing to conserve.
But OLEDs are vulnerable to burn-in with static content, so this
behaviour is gated behind a new build flag KEEP_DISPLAY_ON_USB. Default
is unchanged from upstream — the display blanks after AUTO_OFF_MILLIS
on USB or battery. Variants that ship with an LCD instead of an OLED
(e.g. heltec_t096) can opt in by adding -D KEEP_DISPLAY_ON_USB to
their env, gaining always-on-while-powered without exposing OLED users
to burn-in risk.
When the flag is enabled, the implementation refreshes _auto_off every
loop iteration while externally powered, so the timer naturally counts
a fresh AUTO_OFF_MILLIS window from the moment power is removed —
no instantaneous-blank-on-unplug.
Applied to all three companion_radio UI flavours (ui-new, ui-tiny,
ui-orig). Boards without an isExternalPowered() override use the
base-class default in MeshCore.h (returns false), so battery-powered
behaviour is unchanged everywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Framework for upcoming variant-specific PRs that add LED feedback during boot. The hook gives users visual cues that the device is busy and
shouldn't be interacted with until startup completes.
The error_rate graph showed all zeros after ~65535 receive errors had
accumulated. Root cause was a two-part bug:
1. In MyMesh::updateStatsHistory(), recv_errors was clamped to 0xFFFF
via min<uint32_t>(..., 0xFFFF). Once the counter reached 65535,
every subsequent sample stored the same constant value, making the
per-interval delta always zero and therefore error_rate always zero.
2. HistorySample::recv_errors was declared as uint16_t while the
underlying counter (n_recv_errors) is uint32_t. parseSummaryLine()
also truncated the restored value back to uint16_t.
Fix:
- Remove the clamp in MyMesh::updateStatsHistory(); assign
getPacketsRecvErrors() (uint32_t) directly.
- Change HistorySample::recv_errors from uint16_t to uint32_t,
moving it next to the other uint32_t fields.
- Update both parseSummaryLine() code paths to cast recv_errors to
uint32_t instead of uint16_t.
The archive format is unchanged: recv_errors is written as %u and
read back into an unsigned local, which is correct for a 32-bit
value on ESP32.
The previous implementation maintained three exponential moving averages
(1/5/15-minute) of CPU busy-fraction, sampled every 5 seconds. This had
two problems:
1. Wrong semantics: the metric was labelled "load_avg" (a Unix concept
measuring run-queue depth), but the tracker actually measures CPU
busy-time as a fraction of FreeRTOS ticks — i.e. utilization, not
load. The name was misleading.
2. Poor responsiveness: the 1-minute EMA (DECAY = exp(-5/60) ≈ 0.920)
has a ~60-second time constant. On a live web panel, this made the
metric appear frozen, especially during short bursts of activity
(e.g. an HTTP request from the panel itself).
This commit replaces the EMA with a 64-sample simple moving average
(SMA) over a compact uint8_t circular buffer. The sample interval is
60 s / 64 = 937,500 µs, so the window covers exactly 60 seconds:
Algorithm:
- Every 937,500 µs (= 60 s / 64), the FreeRTOS tick delta (busy vs
idle ticks on core 0) is computed and stored as a uint8_t
(0–255 = 0–100% utilization).
- The circular buffer holds 64 samples, spanning exactly 60 seconds
(64 × 937,500 µs = 60,000,000 µs). The timer interval is derived
directly from the window size: 60 000 000 / SMA_WINDOW µs.
- The index wraps with a bitwise AND (& 63) instead of modulo, since
64 is a power of 2.
- The running sum is a uint16_t (max 64 × 255 = 16 320, fits easily).
- The result _sma_avg is a volatile float written atomically by the
esp_timer task (core 0) and read from the main loop (core 1).
No spinlock is needed: on Xtensa LX7, a 32-bit aligned float store
is a single instruction.
The 60-second window smooths out short bursts (e.g. WiFi/HTTP spikes)
while reacting to sustained load changes within ~10–15 seconds.
Naming:
- The public API is now getCore0Util() returning a float in [0.0, 1.0].
The name explicitly identifies which core is measured (core 0, which
runs WiFi, MQTT, HTTP, and LwIP — see task_pinning.c).
- JSON keys: "load_avg" (array) → "core0_util" (scalar float, percent)
- HistorySample field: load_avg1_pct → core0_util_pct
- StatsHistory series key: "cpu_load" → "core0_util"
- Web panel label: "Load Avg" → "Core0 Util"
The history snapshot (once per minute) reads the same _sma_avg value,
which at that point represents the rolling average of the last 60
seconds — exactly one history interval.
Wire the 1-minute CPU load average into the stats history pipeline:
- Rename HistorySample::reserved to load_avg1_pct (uint8_t, 0-100);
struct size unchanged.
- Populate load_avg1_pct in updateStatsHistory on ESP32 from
_cpu_tracker.getLoadAvg1() * 100.
- Register "cpu_load" series in buildPointValue / seriesTitle /
seriesUnit so buildSeriesJson serves it via
/api/stats?series=cpu_load.
- Persist load_avg1_pct as a 29th CSV column in flushSummaryLog.
parseSummaryLine accepts both 28-column (old, load_avg1_pct=0)
and 29-column (new) formats for backward compatibility.
- Add "cpu_load" to the web panel trend card order with orange
sparkline color (#e07b39), "N %" hover formatting, and a Y-axis
floored at 0 with a 10% minimum ceiling.
Track CPU utilisation on core 0 using a FreeRTOS tick hook that
increments per-tick idle/busy counters, sampled every 5s by an
esp_timer callback. Exponential moving averages with Linux-style
time constants (1 / 5 / 15 min) are maintained in software:
DECAY1 = exp(-5/60) ≈ 0.9200 (1-minute window)
DECAY5 = exp(-5/300) ≈ 0.9835 (5-minute window)
DECAY15 = exp(-5/900) ≈ 0.9945 (15-minute window)
Core 1 is excluded intentionally: it runs the Arduino loopTask at
100% load for LoRa packet processing, so its figure is always 1.0
and carries no diagnostic value. All other tasks are pinned to
core 0 by task_pinning.c, so core-0 load reflects the true system
utilisation.
New files:
arch/esp32/CPUUsageTracker.h – class declaration
arch/esp32/CPUUsageTracker.cpp – tick hook + esp_timer sampling
Integration:
MyMesh::begin() calls _cpu_tracker.begin() on ESP32 builds.
formatStatsReply() emits "load_avg":[<1m>,<5m>,<15m>] in the
compact stats JSON payload.
formatWebStatsSummaryJson() adds the same field to the web-panel
stats endpoint under core.load_avg.
The web-panel HUD gains a "Load Avg" metric tile showing all
three values side-by-side; the core-metrics grid is widened from
4 to 5 columns. The tile is rendered conditionally so older
firmware (or non-ESP32 builds) that omit the field degrade
gracefully.
CPUUsageTracker.cpp is added to the esp32_base build_src_filter
in platformio.ini.