enable PM on ESP repeaters that support it

This commit is contained in:
liquidraver
2026-07-30 17:14:05 +02:00
parent 92f171a3a2
commit aa14944a93
11 changed files with 536 additions and 7 deletions
+29
View File
@@ -9,6 +9,24 @@ env:
python_version: 3.12
toolchains: arm-zephyr-eabi:xtensa-espressif_esp32s3_zephyr-elf:xtensa-espressif_esp32_zephyr-elf:riscv64-zephyr-elf
# Mark the GitHub release as a pre-release AND skip the Mesh America catalog
# publish. Set to "true" for a release that should not reach ordinary users
# through apps.meshamerica.com's browser flasher; set back to "false" for a
# normal release.
#
# Two separate effects, both driven from here so they cannot drift apart:
# - the release is created with prerelease: true, so GitHub does not mark it
# "Latest" and it is excluded from the /releases/latest API.
# - publish-catalog is skipped entirely, so the firmware-dist branch and its
# catalog.json are left ALONE — the configurator keeps offering whatever
# the last non-prerelease build published. Nothing is unpublished or
# rolled back; that branch is simply not force-pushed this time.
# Release artifacts are still built and attached, so a manual download and
# flash works exactly as usual.
#
# 1.16.8: pre-release while ESP32 light sleep is unvalidated on hardware.
prerelease: "true"
jobs:
setup-zephyr:
runs-on: ubuntu-latest
@@ -228,6 +246,10 @@ jobs:
contents: write
outputs:
release_tag: ${{ steps.tag.outputs.release_tag }}
# Re-exported as a job output because the `env` context is NOT available
# in a job-level `if:` — publish-catalog cannot read env.prerelease
# directly and has to gate on this instead.
prerelease: ${{ env.prerelease }}
steps:
- uses: actions/checkout@v6
@@ -326,6 +348,7 @@ jobs:
body_path: ${{ steps.notes.outputs.notes_path }}
target_commitish: ${{ github.sha }}
draft: false
prerelease: ${{ env.prerelease }}
files: firmware/*
# Publish firmware + provider catalog for the Mesh America configurator
@@ -335,8 +358,14 @@ jobs:
# CORS-clean hosts (firmware via jsDelivr, catalog via raw.githubusercontent).
# The stable catalog URL registered with Mesh America is:
# https://raw.githubusercontent.com/<owner>/<repo>/firmware-dist/catalog.json
#
# Skipped entirely on a pre-release (see the `prerelease` env at the top).
# This branch is force-pushed, so skipping is the whole mechanism: the
# previous release's firmware and catalog.json stay exactly as they are and
# the configurator keeps offering that version.
publish-catalog:
needs: create-release
if: needs.create-release.outputs.prerelease != 'true'
runs-on: ubuntu-latest
permissions:
contents: write
@@ -0,0 +1,222 @@
# ZephCore 1.16.8-zephcore
> [!CAUTION]
> **This is a pre-release.** ESP32 light sleep is enabled by default on Heltec repeaters and has not
> been validated on hardware yet — see the light-sleep section for what to check and how to build it
> out. It is deliberately **not published to the apps.meshamerica.com configurator**, which keeps
> offering v1.16.7; flash this one manually from the assets below if you want to help test it.
> The nRF based boards should be fine.
> [!IMPORTANT]
> ## Before you upgrade
>
> **From v1.16.5 / v1.16.6 / v1.16.7** — clean flash, no re-bond, bonds and data survive.
>
> **From v1.16.2 / v1.16.3 / v1.16.4** — clean flash, bonds and data survive. If you are on an
> **ESP32-S3 / ESP32-C board** and have *not* yet taken the v1.16.5 update, you still owe the one-time
> serial reflash: the app moved to flash offset `0x10000` in v1.16.5, and a board on the old layout
> cannot cross that update over WiFi-OTA or the browser flasher — **flash the `-merged.bin` once over
> USB/serial.** Affected boards: *Heltec V3 / V4 / V4.3, Station G2, Wireless Tracker / V2, XIAO
> ESP32-S3 / C3 / C6, LilyGo T-Lora C6, ThinkNode M9.* Identity, contacts, channels, prefs, and BLE
> bonds are preserved. nRF52, classic ESP32 (T-Beam / PICO-D4 / TTGO LoRa32), STM32WL, and native
> Linux are unaffected.
>
> **From v1.16.1 or older** — flash it; on first boot it clears BLE bonds automatically (identity,
> contacts, channels, prefs preserved). Re-bond your phone/desktop once.
>
> **Coming from Arduino MeshCore** — flash it; auto-formats on first boot (new identity, clean storage).
>
> **Heltec ESP32 repeaters now light-sleep**, and their USB CLI stops answering 10 minutes after boot
> as a result. Reconnecting your terminal normally resets the node and gives you another 10 minutes;
> if it doesn't, power-cycle. Details in the light-sleep section below.
>
> **One CLI rename:** `set/get cad.probe.interval` is now `set/get probe.interval` — the same setting,
> renamed because one measurement now feeds both the noise floor and the CAD probe. Your stored value
> carries over; only the command name changed. `set/get agc.reset.interval` is gone (see below).
>
> Take this with a grain of salt — try the formatters if anything anomalous happens with your node.
---
A **power and radio-measurement release**. Repeaters no longer wake on a fixed 5 s tick — they sleep
until their next real deadline. Every ESP32 build drops to 80 MHz, and Heltec repeaters now light-sleep
between packets. The noise-floor sampler and CAD probe were unified into one measurement, the
SX126x RSSI/AGC calibration is now band-correct, and the observer role got a batch of fixes for settings
that silently never reached the hardware.
## Highlights
### Repeaters are now deadline-driven — the periodic housekeeping tick is gone
Every repeater woke five times a second-and-a-bit forever: a fixed 5 s "housekeeping" timer that ran the
noise-floor sample, the CAD probe, advert timers and the tempradio revert. None of that actually needs a
poll — each item is either a deadline or a sampler with its own interval.
The new `mesh/Maintenance.h` contract lets the event loop ask *"when does maintenance next have work?"*
and arm **one one-shot wake for exactly that moment**. An idle repeater now sleeps until its soonest real
deadline instead of on a fixed cadence. A hard ceiling (`CONFIG_ZEPHCORE_MAINTENANCE_BACKSTOP_MS`,
default **60 s**) clamps the wake as a safety net against a missed deadline — it should never bind in
normal operation.
Companions and room servers keep their periodic tick deliberately: the companion block contains genuine
pollers (contact-dump progress, BLE advertising watchdog) with no deadline to arm, and the room server is
paced by its own 500 ms push timer regardless.
### ESP32 power: 80 MHz everywhere, and light sleep on Heltec repeaters
Espressif parts have no equivalent of the nRF52's "idle at microamps in WFI" — `WAITI` gates the core
clock but leaves PLL, peripherals and RAM powered, so an idle ESP32 sits in the **tens of mA**. CPU
frequency is therefore a first-order term in a battery node's draw, and light sleep (sub-mA) is what
removes the rest.
- **Every ESP32 build is now pulled to 80 MHz**, matching what Arduino MeshCore ships (new
hal-espressif patch `0002` — upstream omits 80 from its DT map). **Observers are the exception** and
keep the SoC maximum (240 MHz on S3/classic, 160 MHz on C3/C6): they run a full WiFi + TLS + MQTT
stack and are mains- or big-panel powered, so throughput matters more than current.
- **Light sleep is now enabled automatically** for **repeater** builds on **Heltec V3, V4, V4.3,
Wireless Tracker and Wireless Tracker V2**. Patch `0012` marks the LoRa DIO1 line as a wakeup source,
so a received packet wakes the SoC — the node sleeps between packets and deadlines and wakes for real
work. Combined with the deadline-driven change above, an idle repeater now spends most of its life
asleep instead of waking five times a second at 240 MHz.
> [!IMPORTANT]
> **The USB CLI stops answering 10 minutes after boot on these boards.** Nothing arms a UART wake
> source on ESP32, so characters typed at a sleeping node are dropped. To make configuration reliable,
> light sleep is **blocked for the first 10 minutes after every boot**
> (`CONFIG_ZEPHCORE_PM_BOOT_AWAKE_MS`) — and because these boards' USB bridge drives EN from DTR,
> **connecting a terminal normally resets the node and re-arms that window.** If your terminal doesn't
> toggle DTR, power-cycle the node to get a console. Remote admin over LoRa is unaffected.
Console output itself is safe: a new `pm_notifier` flushes the console UART to idle before every sleep,
which Zephyr's PM path does not do — without it, lines would be cut mid-character. Packet-logging and
debug builds are therefore fine to run with sleep enabled.
Anything that can't tolerate sleeping holds a `pm_policy` lock instead of being excluded by hand: a
**WiFi OTA session** holds one for its duration, an **uplink/observer WiFi station** holds one
permanently (the association would drop), and **GPS holds one while the module is powered**, since the
GNSS UART is not a wake source and inbound NMEA would be lost. Under a GPS duty cycle the lock is only
held during the awake phase.
> [!WARNING]
> **Not yet validated on hardware**, and now on by default for the boards above — so this is the first
> thing to suspect if an allowlisted board misbehaves. The failure mode to rule out is a **deaf
> repeater**: check the boot log for `Pin N is not wakeup capable`, confirm the node still receives
> packets, and only then measure current. Rebuild with `-DCONFIG_PM=n` to take it back out.
>
> Boards **deliberately excluded** because DIO1 can't wake the SoC: XIAO ESP32-S3 (GPIO39), Station G2
> (GPIO48), ThinkNode M9 (GPIO42), LilyGo T-Lora C6 (GPIO23, outside the C6's LP range), and
> TTGO LoRa32 (SX127x — patch 0012 only arms the sx126x driver). **Companion builds never get it** on
> any board: the Espressif HCI driver takes no `pm_policy` locks, so the SoC could sleep mid-advertising
> or mid-connection. Passing `pm_esp32.conf` by hand still works for testing another board.
### One measurement now serves both the noise floor and CAD — `set probe.interval`
The noise-floor sampler and the CAD calibration probe used to be two separate things riding the same
5 s tick. They are now a **single periodic radio measurement**: one median-of-8 RSSI reading that the
CAD probe consumes rather than measuring again.
- `set cad.probe.interval <sec>`**`set probe.interval <sec>`** (default still **15**, `0` still
disables CAD probing). It is now also the noise-floor sampling rate, and it sets how often an idle
repeater wakes.
- New `CONFIG_ZEPHCORE_NOISE_FLOOR_INTERVAL_MS` is the build-time fallback used when probing is off —
15 s on repeaters, 5 s on companions/room servers (which wake on their tick anyway).
- A sample blocked by TX, mid-RX or a duty-cycle sleep window retries on a 5 s deadline (max 2 retries)
instead of losing a full interval — deliberately reproducing the old retry grid, which matters under
RX duty cycle where blocked attempts are the norm.
**RSSI reads are now correctly spaced.** A median only rejects outliers if the reads are independent,
and the chip refreshes RSSI once per averaging window (~16 µs at BW 62.5, ~134 µs at BW 7.8) — reads
issued faster return the *same* underlying sample and the median collapses to a single read. The spacing
and the RX-entry settle delay are now derived from the SX1261/2 datasheet (rev 2.2, Table 13-82).
`get cad` reports the result so the assumption stays falsifiable on real hardware: the old `iv:` field is
replaced by **`sp:<mean-spread>/<zero-spread %>(<count>)`**. A **non-zero mean proves the reads are
independent**, however high the zero-spread share climbs — only mean `0.0` with a high share indicts the
sampler. Measured on air at BW 62.5: `0.6/90%` quiet, `0.9/84%` with the floor at 103 dBm.
### Fixed: SX126x RSSI/AGC calibration used the wrong band on sub-GHz builds
Datasheet rev 2.2 (§6.1.6, new in Dec 2024) documents that the SX126x RSSI/AGC calibration registers are
**band-specific**. ZephCore applied the 868915 MHz set unconditionally, so every 433 MHz build was
running the wrong AGC gain selection — that is **packet loss, not just wrong telemetry**. New patch
`0011` adds the per-band table plus the shared-register `0x08AC` / `AgcSensiAdjust` fix.
### RAK3401 1-watt: external-PA RX register patch enabled
The undocumented register `0x8B5` RX improvement (`CONFIG_ZEPHCORE_SX126X_HELTEC_REG_PATCH`) is not
Heltec-specific despite its name — upstream MeshCore also enables it for the RAK3401's SKY66122 FEM. It
is now on for `rak3401_1watt`, and the Kconfig help was corrected to say "boards with an external PA/FEM
in the RX path."
### The AGC-reset workaround is gone
`set/get agc.reset.interval` has been **removed** — it now replies `use rxduty instead`, and `set` is
accepted-and-ignored for prefs compatibility. It was a periodic blind AGC kick that only existed because
of the fixed tick; the in-driver RX restore and duty-cycle re-arm paths handle the real cases.
### Observer role fixes
Several observer settings looked accepted and then never reached the hardware:
- **`set freq/sf/bw/cr` never reached the radio.** The radio was bound to a static placeholder
`NodePrefs` that is never loaded from flash, so it stayed on compiled-in defaults forever — the value
was written to flash and echoed back by the CLI and MQTT, then "reverted" on reboot. The radio is now
bound to the mesh's live prefs before `begin()`, matching repeater and room server.
- **First boot discarded the observer defaults.** The store is shared with the repeater, and its no-file
branch re-applies *repeater* defaults over whatever the caller passed in. The observer now detects
first boot itself and re-applies (and persists) its own `cr` / `tx_power` once.
- **`set name Observer` reverted on every reboot** — "Observer" was in the regenerate-if-unset list. It
is a name a user can legitimately set, so it is no longer treated as unset.
- **`set freq` / `set bw` now refuse values the loader will reject** (3001000 MHz, 7500 kHz). Out-of-
range values used to save fine, then get silently reset to defaults on the next boot — taking
`bw`/`sf`/`cr`/`tx_power` with them.
- **USB-console routing fixed** on Station G2 and XIAO ESP32-S3, so non-companion roles keep their
console and serial CLI.
- The observer `help` banner was reorganised (network / node / radio / query) and now lists the query
commands that already existed.
## Upstream sync
Synced with Arduino MeshCore `dev`:
- **Room server: `room.post <message>`** — post to the shared room as the server itself. Pushed to
clients like any other post.
- **Fixed: an empty guest password granted read+write to anyone.** An unset `guest.password` (the
default) now *disables* guest login instead of matching an empty submitted password. To run an open
room server use `set allow.read.only on`, which grants read-only. Both password compares still run
unconditionally, so timing behaviour is unchanged.
- **Companion telemetry: MCU die temperature is now reported under base permission**, matching Arduino
MeshCore and ZephCore's own repeater/room-server telemetry. It is still suppressed when an external
sensor already supplied a temperature, so a node never emits two.
- **Advert names are truncated on a UTF-8 code-point boundary** — a name whose multi-byte character
straddled the advert size limit used to go out mangled.
- **ThinkNode M3: GPS reset pin left floating.** Driving the REINIT pin (P0.25) stops the module
producing NMEA on this hardware; upstream fixed it by never driving it. The `gps-reset` alias is gone,
which compiles out every reset path — `GPS_EN` alone handles wake/standby.
- **New patch `0013` bounds the SX126x RX-busy latch with a payload deadline.** Continuous RX has no
symbol timer, so a `HEADER_VALID` whose packet never completes would pin the TX gate open forever and
silently mute the node. The bound is max-length airtime at the current SF/BW +25% +100 ms —
deliberately generous, a stuck-state safety net rather than a timing mechanism.
## Other changes
- **west manifest bumped** (routine pinned-tree advance).
- `ARCHITECTURE.md`, `ADAPTIVE_CAD.md` and `Repeater_CLI_commands.md` updated for the probe unification
and the removed commands.
## Recommended upgrade checklist
1. **Heltec V3 / V4 / V4.3 / Wireless Tracker repeater?** You get 80 MHz *and* light sleep
automatically. Do your configuration in the first 10 minutes after boot, or reconnect your terminal
(which resets the node) to get the window back.
2. **Running a 433 MHz SX126x node?** Update — the RSSI/AGC calibration band fix affects reception, not
just readings.
3. **Running an observer?** Update, then re-apply your radio settings once — they may never have reached
the hardware before.
4. **Scripts or notes using `cad.probe.interval` / `agc.reset.interval`?** Update them to
`probe.interval`; the AGC command is gone.
5. **ESP32-S3 / ESP32-C boards that skipped v1.16.5:** flash `-merged.bin` once over USB/serial. Data survives.
6. **Everything else, from v1.16.2 onward:** just flash — bonds and data survive.
7. **From v1.16.1 or older:** just flash — self-migrates on first boot; re-bond once.
8. **From Official MeshCore:** just flash — auto-formats on first boot.
9. **Anything odd?** Format first.
+41 -1
View File
@@ -369,6 +369,40 @@ if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND NOT EXTRA_CONF_FILE MATCHES
endif()
endif()
# ========== ESP32 light sleep (allowlisted repeater boards) ==========
# 80 MHz above cuts the idle draw; light sleep is what removes it. See
# boards/common/pm_esp32.conf for the mechanism and its requirements — this
# block is only the policy that decides who gets it automatically.
#
# REPEATERS ONLY, and the gate is deliberately the same one WiFi OTA uses
# below: the Espressif HCI driver takes no pm_policy locks, so a companion
# build would light-sleep mid-advertising or mid-connection. repeater.conf
# sets CONFIG_BT=n, which removes the problem instead of racing it.
#
# BOARD ALLOWLIST, not a platform check. Light sleep is only safe where DIO1
# can wake the SoC, i.e. an RTC-capable pin (GPIO 0-21 on the S3). Every board
# below wires DIO1 to GPIO14 and consoles on uart0 through a USB-serial bridge,
# so both the packet wake and the console flush land correctly. Deliberately
# ABSENT and must stay absent unless their wiring changes:
# xiao_esp32s3 (DIO1=GPIO39), station_g2 (GPIO48), thinknode_m9 (GPIO42),
# lilygo_tlora_c6 (GPIO23, outside the C6's LP range 0-7)
# -> cannot wake on a received packet; the node would go deaf while
# looking excellent on a current meter.
# ttgo_lora32 -> SX127x; patches/zephyr/0012 arms DIO1 in the sx126x driver
# only, so nothing marks the pin as a wake source.
# Boards with a native USB Serial/JTAG console are also unsuitable: the USB
# peripheral drops off the bus in light sleep and there is no USJ wake source
# in the HAL (only UART0/1/2 and LP-UART).
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND EXTRA_CONF_FILE MATCHES "repeater"
AND BOARD MATCHES "heltec_wifi_lora32_v3|heltec_wifi_lora32_v4|heltec_wireless_tracker")
set(ZEPHCORE_PM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/pm_esp32.conf")
if(EXISTS ${ZEPHCORE_PM_CONF})
list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_PM_CONF})
zephcore_auto_pair_overlay("${ZEPHCORE_PM_CONF}")
message(STATUS " ESP32 light sleep: auto-enabled (DIO1 wake-capable repeater board)")
endif()
endif()
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND EXTRA_CONF_FILE MATCHES "repeater"
AND NOT BOARD MATCHES "/esp32/")
set(ZEPHCORE_WIFI_OTA_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/wifi_ota.conf")
@@ -491,7 +525,7 @@ add_definitions(-DFIRMWARE_BUILD_EPOCH=${ZEPHCORE_BUILD_EPOCH}u)
# release is tagged/named, and what the Mesh America catalog uses as its version
# key — so the configurator can match a running device against the catalog. Keep
# all four identical; the release workflow reads this value directly.
set(ZEPHCORE_FIRMWARE_VERSION "1.16.7-zephcore")
set(ZEPHCORE_FIRMWARE_VERSION "1.16.8-zephcore")
add_definitions(-DFIRMWARE_VERSION="${ZEPHCORE_FIRMWARE_VERSION}")
add_subdirectory(lib/monocypher)
@@ -607,6 +641,12 @@ if(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
endif()
endif()
# ESP32 light-sleep console guards — TX flush before sleep + post-boot awake
# window. Only meaningful when CONFIG_PM is on (boards/common/pm_esp32.conf).
if(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32 AND CONFIG_PM)
target_sources(app PRIVATE helpers/pm_esp32_console.c)
endif()
# Native-Linux (native_sim) runtime setup: force real-time clock mode so the
# simulated clock tracks wall time (required for the real SX126x radio's
# BUSY/DIO1 timing). Bakes in the equivalent of the --rt command-line flag.
+39
View File
@@ -436,6 +436,45 @@ config ZEPHCORE_NOISE_FLOOR_INTERVAL_MS
30000, ~4 min and ~8 min. Past 15000 the CAD probe becomes the binding
deadline instead, so raise both or neither.
config ZEPHCORE_PM_BOOT_AWAKE_MS
int "Block light sleep for this long after boot (ms)"
depends on PM && SOC_FAMILY_ESPRESSIF_ESP32
default 600000
range 0 3600000
help
How long after boot the SoC refuses to light-sleep, so the USB console
is guaranteed reachable for a configuration session.
This exists because nothing arms a UART wake source on ESP32 — see
helpers/pm_esp32_console.c. Once the window closes, characters typed at
a sleeping node are DROPPED and the CLI stops answering over USB until
the board resets. In practice connecting a terminal usually does reset
it (the USB bridge drives EN from DTR), which re-arms the window.
0 disables the window entirely — the node may sleep immediately after
boot, and the USB console becomes unusable except in the instants it
happens to be awake. Do not set 0 on a board you still need to
configure.
Raising it costs only the extra awake time at the start of each boot.
config ZEPHCORE_PM_CONSOLE_UART_NUM
int "Console UART index flushed before light sleep"
depends on PM && SOC_FAMILY_ESPRESSIF_ESP32
default 0
range 0 2
help
Hardware UART index (not a DT node) passed to
esp_rom_uart_tx_wait_idle() before entering light sleep, so a line in
the TX FIFO is not cut off mid-character by the clock stopping.
0 is correct for every board this is currently enabled on: their
console is uart0, whether via a USB-serial bridge (Heltec V3/V4/V4.3)
or the ROM-default pads. Change it only if a board's console is moved
to another hardware UART — a wrong index does not fail the build, it
just silently flushes the wrong peripheral and lets the console
truncate again.
menu "Radio Configuration"
choice ZEPHCORE_RADIO_TYPE
@@ -17,6 +17,7 @@
*/
#include "ZephyrGPSManager.h"
#include "../../helpers/pm_sleep_guard.h"
#include <zephyr/kernel.h>
#include <zephyr/device.h>
@@ -1513,6 +1514,15 @@ void gps_enable(bool enable)
if (enable) {
LOG_INF("GPS enabled - starting acquisition");
/* Block SoC light sleep for as long as the module is powered.
* The GNSS UART is not a wake source, so a sleeping SoC drops
* inbound NMEA outright sentences would be lost mid-stream and
* a fix would never converge. Balanced by the put in the disable
* branch; the early return above keeps the pair 1:1, and under a
* GPS duty cycle the lock is only held during the awake phase.
* Compiles to nothing without CONFIG_PM. */
zc_pm_block_sleep();
/* Start acquiring immediately (no delay for first wake) */
gps_current_state = GPS_STATE_ACQUIRING;
consecutive_good_fixes = 0;
@@ -1543,6 +1553,9 @@ void gps_enable(bool enable)
} else {
LOG_INF("GPS disabled - canceling timers and powering off");
/* Matches the block taken in the enable branch. */
zc_pm_unblock_sleep();
/* Cancel any pending work */
k_work_cancel_delayable(&gps_wake_work);
k_work_cancel_delayable(&gps_timeout_work);
+15
View File
@@ -13,6 +13,7 @@
#include "wifi_ota.h"
#include "ota_page.h"
#include "../../helpers/pm_sleep_guard.h"
#include <zephyr/kernel.h>
#include <zephyr/net/wifi_mgmt.h>
@@ -459,10 +460,18 @@ int wifi_ota_start(const char *node_name, const char *board_name)
node_name ? node_name : "Unknown",
board_name ? board_name : "Unknown");
/* Hold off light sleep for the whole session. The WiFi stack and the
* HTTP upload have no PM coordination here, so a node that slept
* mid-transfer would drop the client's connection. Released in
* wifi_ota_stop(); the post-upload reboot path also passes through
* there, and a reboot would clear the lock regardless. */
zc_pm_block_sleep();
/* Start WiFi AP */
int ret = wifi_ap_start();
if (ret) {
zc_pm_unblock_sleep();
return ret;
}
@@ -472,6 +481,7 @@ int wifi_ota_start(const char *node_name, const char *board_name)
if (ret) {
LOG_ERR("HTTP server start failed: %d", ret);
wifi_ap_stop();
zc_pm_unblock_sleep();
return ret;
}
@@ -505,6 +515,11 @@ int wifi_ota_stop(void)
flash_ctx_initialized = false;
total_bytes_received = 0;
/* Matches the lock taken in wifi_ota_start(). The early return above
* (!ota_active) is why this sits after the flag check an unbalanced
* put would drop someone else's lock. */
zc_pm_unblock_sleep();
LOG_INF("OTA server stopped");
return 0;
}
@@ -11,6 +11,7 @@
#include "ZephyrWiFiStation.h"
#include "observer_creds.h"
#include "../../helpers/pm_sleep_guard.h"
#include <zephyr/kernel.h>
#include <zephyr/net/net_if.h>
@@ -182,6 +183,13 @@ void zc_wifi_station_start(const struct ObserverCreds *creds,
s_wifi_link_up = false;
s_wifi_ready = false;
/* Permanent, and intentionally never released: there is no
* zc_wifi_station_stop() once an uplink or observer node associates it
* stays associated for the life of the boot. Light sleep would drop the
* association (nothing here coordinates with the WiFi modem's own sleep),
* so a node built with WiFi simply does not light-sleep. */
zc_pm_block_sleep();
/* Register WiFi event callback */
net_mgmt_init_event_callback(&wifi_cb, wifi_event_handler,
NET_EVENT_WIFI_CONNECT_RESULT |
+27 -5
View File
@@ -1,9 +1,25 @@
# ESP32 system power management — light sleep between events.
#
# AUTO-ENABLED for repeater builds on the DIO1-wake-capable Heltec boards; see
# the allowlist block in CMakeLists.txt for who qualifies and why the others
# must not. Passing it by hand is still supported for testing another board:
#
# west build -b heltec_wifi_lora32_v3/esp32s3/procpu zephcore --pristine -- \
# -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/pm_esp32.conf"
#
# OPT-IN, and deliberately not enabled by default. Read this before shipping it.
# CONSOLE BEHAVIOUR — helpers/pm_esp32_console.c does two things without which
# light sleep makes a node unusable: it flushes the console UART before every
# sleep (Zephyr's PM path does not, so lines would be cut mid-character), and
# it blocks sleep for ZEPHCORE_PM_BOOT_AWAKE_MS (default 10 min) after boot so
# there is a guaranteed window to configure the node. AFTER THAT WINDOW THE USB
# CLI STOPS ANSWERING: nothing arms a UART wake source, so typed characters are
# dropped. Connecting a terminal usually resets the board (the USB bridge
# drives EN from DTR) which re-arms the window — but a terminal that does not
# toggle DTR needs a manual power cycle. Remote admin over LoRa is unaffected.
#
# WiFi builds hold a pm_policy lock and never light-sleep at all — the OTA
# session takes one for its duration (adapters/ota/wifi_ota.c) and an uplink or
# observer station takes one permanently (adapters/wifi/ZephyrWiFiStation.c).
#
# What it buys: Espressif parts have no equivalent of the nRF52's "idle at
# microamps in WFI" — WAITI gates the core clock but leaves PLL, peripherals
@@ -23,7 +39,10 @@
#
# OK heltec_wifi_lora32_v3 / v4 / v43, heltec_wireless_tracker(_v2)
# -> DIO1 = GPIO14
# NOT OK xiao_esp32s3 (GPIO39), station_g2 (GPIO48), thinknode_m9 (GPIO42)
# NOT OK xiao_esp32s3 (GPIO39), station_g2 (GPIO48), thinknode_m9 (GPIO42),
# lilygo_tlora_c6 (GPIO23, outside the C6's LP range 0-7)
# N/A ttgo_lora32 -> SX127x; patch 0012 arms DIO1 in the sx126x driver
# only, so nothing marks the pin as a wake source at all
#
# On a NOT-OK board the driver logs "Pin N is not wakeup capable" at boot and
# the node will light-sleep straight through inbound packets while looking
@@ -43,9 +62,12 @@
# and announce elapsed ticks, so k_uptime_get() and every maintenance deadline
# stay correct.
#
# UNVALIDATED ON HARDWARE. Verify in this order: (1) no "not wakeup capable"
# line at boot, (2) the node still receives packets, (3) only then measure
# current. A deaf repeater is the expected failure and it is not obvious.
# NOT YET VALIDATED ON HARDWARE — and it is now auto-enabled, so this is the
# first thing to check if an allowlisted board misbehaves. Verify in this
# order: (1) no "not wakeup capable" line at boot, (2) the node still receives
# packets, (3) only then measure current. A deaf repeater is the expected
# failure and it is not obvious from the outside. To rule it out on a suspect
# unit, rebuild with -DCONFIG_PM=n.
CONFIG_PM=y
+1 -1
View File
@@ -156,7 +156,7 @@ CONFIG_BT_DIS_FW_REV=y
# of truth for the C side, injected as -DFIRMWARE_VERSION). This Kconfig value can't
# read a C macro, so it must be bumped here too. Phones that read DIS and phones
# that query CMD_DEVICE_QUERY should see the same version.
CONFIG_BT_DIS_FW_REV_STR="1.16.7-zephcore"
CONFIG_BT_DIS_FW_REV_STR="1.16.8-zephcore"
CONFIG_BT_DIS_SW_REV=y
CONFIG_BT_DIS_SW_REV_STR="Zephyr"
CONFIG_BT_DIS_PNP=n
+106
View File
@@ -0,0 +1,106 @@
/*
* SPDX-License-Identifier: MIT
* ESP32 light-sleep console guards.
*
* Two independent problems sit between ESP32 light sleep and a usable serial
* console. This file solves both; neither fix substitutes for the other.
*
* 1. TRUNCATED OUTPUT. soc/espressif/common/power.c calls
* esp_light_sleep_start() with no wait for the console UART to drain the
* TX-idle flush that ESP-IDF performs lives behind its own console Kconfig,
* which is not part of a Zephyr build. Sleeping with bytes still in the
* FIFO cuts the line mid-character. A pm_notifier's state_entry callback
* runs immediately before pm_state_set() (subsys/pm/pm.c), which is exactly
* the right moment to poll the UART to idle.
*
* 2. UNREACHABLE INPUT. Nothing arms a UART wake source Zephyr's PM path
* never calls esp_sleep_enable_uart_wakeup(), and on the boards this runs on
* the console RX pad (GPIO44 on an S3 uart0) is outside the RTC range that
* could carry a GPIO wake instead. Characters typed at a sleeping node are
* therefore dropped, and no amount of TX handling changes that.
*
* The answer here is a boot window rather than a wake source: sleep is
* blocked outright for ZEPHCORE_PM_BOOT_AWAKE_MS after boot, so a node is
* always reachable for that long. This works better in practice than it
* sounds, because the USB bridge on these boards drives EN from DTR a
* terminal that asserts DTR on open resets the board, which re-arms the
* window at the moment someone connects. A terminal that does not toggle
* DTR gets no window, and the node has to be power-cycled to answer over
* USB; that is the known limitation of this approach. Arming
* esp_sleep_enable_uart_wakeup() and re-taking the lock on console activity
* would remove it, at the cost of the first keystroke (the hardware
* consumes it as the wake trigger).
*
* Remote admin over LoRa is unaffected by any of this: DIO1 is armed as a wake
* source by patches/zephyr/0012 and wakes the SoC on a received packet.
*/
#include <zephyr/kernel.h>
#include <zephyr/init.h>
#include <zephyr/pm/pm.h>
#include <esp_rom_uart.h>
#include "pm_sleep_guard.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_pm, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
/* ========== 1. Flush the console before every sleep ========== */
static void pm_console_flush(enum pm_state state)
{
if (state != PM_STATE_STANDBY) {
return;
}
/* Polls a status register until the shifter empties — no locking, so it
* is safe in this context (called with the scheduler locked). Bounded
* by the FIFO depth at the configured baud: ~11 ms worst case for a full
* 128-byte FIFO at 115200, and normally microseconds. */
esp_rom_uart_tx_wait_idle(CONFIG_ZEPHCORE_PM_CONSOLE_UART_NUM);
}
static struct pm_notifier console_notifier = {
.state_entry = pm_console_flush,
};
/* ========== 2. Keep the node awake for a window after boot ========== */
#if CONFIG_ZEPHCORE_PM_BOOT_AWAKE_MS > 0
static void boot_window_expired(struct k_work *work)
{
ARG_UNUSED(work);
zc_pm_unblock_sleep();
LOG_INF("boot window elapsed — light sleep enabled "
"(USB console answers again after a reset)");
}
static K_WORK_DELAYABLE_DEFINE(boot_window_work, boot_window_expired);
#endif /* CONFIG_ZEPHCORE_PM_BOOT_AWAKE_MS > 0 */
static int pm_console_init(void)
{
pm_notifier_register(&console_notifier);
#if CONFIG_ZEPHCORE_PM_BOOT_AWAKE_MS > 0
/* Taken here rather than released here: the lock is held from init and
* dropped by the work item, so there is no window at startup in which
* the node could sleep before the guard is in place. */
zc_pm_block_sleep();
k_work_schedule(&boot_window_work,
K_MSEC(CONFIG_ZEPHCORE_PM_BOOT_AWAKE_MS));
LOG_INF("light sleep deferred %d ms (console configuration window)",
CONFIG_ZEPHCORE_PM_BOOT_AWAKE_MS);
#endif
return 0;
}
/* POST_KERNEL: needs the kernel work queue, and must be in place before the
* application can idle long enough to sleep. */
SYS_INIT(pm_console_init, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
+35
View File
@@ -0,0 +1,35 @@
/*
* SPDX-License-Identifier: MIT
* Sleep guard block SoC light sleep across a critical stretch.
*
* Zephyr's policy locks are reference-counted, so every zc_pm_block_sleep()
* needs exactly one matching zc_pm_unblock_sleep(); nested holders compose.
* While at least one lock is held the idle path skips PM_STATE_STANDBY and the
* SoC idles normally instead device power management is untouched either way.
*
* Both calls compile to nothing without CONFIG_PM, so callers do not need to
* guard their own use sites.
*/
#pragma once
#if defined(CONFIG_PM)
#include <zephyr/pm/policy.h>
static inline void zc_pm_block_sleep(void)
{
pm_policy_state_lock_get(PM_STATE_STANDBY, PM_ALL_SUBSTATES);
}
static inline void zc_pm_unblock_sleep(void)
{
pm_policy_state_lock_put(PM_STATE_STANDBY, PM_ALL_SUBSTATES);
}
#else /* !CONFIG_PM */
static inline void zc_pm_block_sleep(void) { }
static inline void zc_pm_unblock_sleep(void) { }
#endif /* CONFIG_PM */