observer lightsleep + companion opt-in

This commit is contained in:
liquidraver
2026-10-09 21:01:26 +02:00
parent 1b3d378c67
commit 5ac3156943
34 changed files with 439 additions and 100 deletions
+18 -3
View File
@@ -838,6 +838,8 @@ Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC
went away is reported as a DTR drop in three cases: DTR low, VBUS removed, and a bus reset. The reset case exists
because the CDC class keeps the old line state across a reset, so nothing else says that the port the host had open
is gone (a hub resetting upstream leaves VBUS up). Bus suspend, resume and reset also reach a second callback.
While a host is on the bus (from reset, configuration or resume until suspend or VBUS loss) the module blocks
ESP32 light sleep: sleep stops the USB clock and the host loses the device without being told.
- **`ZephyrCompanionUSB`** (companion): the wired companion transport, over the CDC port or a plain UART
(`zephcore,companion-uart`). Framing: `<` (to the node) or `>` (from it), a little-endian 16-bit length, the payload;
a first byte that is printable and not `<` starts the text CLI instead. It has its own receive queue and an
@@ -879,8 +881,19 @@ driver. Persisted in `prefs.json` as `zc.leds_brightness`; it is not part of the
### 7.6 WiFi / MQTT / TCP Transports
- **`adapters/wifi/ZephyrWiFiStation.c`**: WiFi STA client (ESP32) used by the observer, the repeater uplink and the WiFi companion
- **`adapters/mqtt/ZephyrMQTTPublisher.c`**: MQTT publisher for observed/uplinked packets
- **`adapters/wifi/ZephyrWiFiStation.c`**: WiFi STA client (ESP32) used by the observer, the repeater uplink and the WiFi companion.
The observer and the uplink run with the driver's WiFi power save (minimum modem sleep: the WiFi radio is off
between the access point's DTIM beacons); the WiFi companion requests power save off, as before. On ESP32
light-sleep builds the station blocks SoC light sleep until the link is ready (DHCP, SNTP) and its client has
reported a session with `zc_wifi_station_session_up()`, and again whenever either is lost. With no such client
(the WiFi companion) the block stays for the life of the boot. While the SoC sleeps the WiFi MAC wakes it for
each beacon it listens to, and `helpers/pm_esp32_wake.c` refuses a sleep while the MAC's beacon timer is active.
On light-sleep companion builds the WiFi companion runs with power save too and reports a session at once, so
the station blocks sleep only while the link is not ready.
- **`adapters/mqtt/ZephyrMQTTPublisher.c`**: MQTT publisher for observed/uplinked packets. It reports its session to
the station: up at CONNACK, down when the session ends for any reason. Keepalive: PINGREQ once a quarter of
the 60 s keepalive is left, so a broker that enforces the keepalive without grace does not close an idle
session.
- **`adapters/ota/wifi_ota.c`**: WiFi SoftAP + HTTP firmware upload to MCUboot slot1 (ESP32, requires `--sysbuild`)
Companion transports, as upstream's `companion_radio`: each is a `BaseSerialInterface`
@@ -888,7 +901,9 @@ Companion transports, as upstream's `companion_radio`: each is a `BaseSerialInte
BLE, USB/UART and TCP can all be connected at once; every connected client gets every
frame (a reply to one app also reaches the other).
- **`adapters/ble/ZephyrBLE.cpp`** (+ `ble_gatt_layout.cpp`, `ble_dfu.cpp`): BLE NUS
- **`adapters/ble/ZephyrBLE.cpp`** (+ `ble_gatt_layout.cpp`, `ble_dfu.cpp`): BLE NUS. Blocks ESP32 light sleep from
connect until the link is encrypted (pairing does not survive sleep); an encrypted link and advertising do not
block it.
- **`adapters/usb/ZephyrCompanionUSB.cpp`**: USB CDC-ACM, or a plain UART (`zephcore,companion-uart` chosen node; the only link on the Bluetooth-less LoRa-E5), with the text CLI
- **`adapters/transport/TcpCompanionTransport.c`**: TCP (port 5000, MeshCore `SerialWifiInterface` framing) on native Linux and on WiFi companions (`app/CompanionWifi.cpp`, `capabilities: wifi: true`)
- **`adapters/transport/frame_txq.c`**: the TX queue BLE and TCP share (congestion, overflow slot, lossless replies)
+2 -2
View File
@@ -45,7 +45,7 @@ The text CLI of every ZephCore role. Commands are sent over USB serial (CDC-ACM)
| `start ota` | ESP32: start WiFi AP + HTTP OTA server. nRF52: reboot into BLE OTA DFU mode |
| `stop ota` | *(ZephCore only)* Stop WiFi OTA server (ESP32 only) |
| `clkreboot` | Set clock to a fixed reference time (15 May 2024 8:50pm UTC) then reboot. On a board with a hardware RTC the reference time is written to the chip too, as upstream, so the reset survives the reboot |
| `powersaving` / `powersaving on` / `powersaving off` | Upstream's command and replies. On ESP32 light-sleep repeaters (boards with `light_sleep: true`) it gates light sleep: `off` (persisted) keeps the SoC awake, which also keeps a USB Serial/JTAG console attached; `on` allows sleep again, replying `on - After N s (console window)` while the console window is open, else `on - Immediate effect`. Default **on** on those builds, and prefs from firmware that stored the field without acting on it are read as on once. nRF52/nRF54L: stored, replies `on - Immediate effect` (they always idle in System ON; `off` changes nothing). Other boards: `Board not supported` |
| `powersaving` / `powersaving on` / `powersaving off` | Upstream's command and replies. On ESP32 light-sleep builds (repeaters of boards with `light_sleep: true`, companions of boards with `light_sleep_companion: true`) it gates light sleep: `off` (persisted) keeps the SoC awake, which also keeps a USB Serial/JTAG console attached; `on` allows sleep again, replying `on - After N s (console window)` while the console window is open, else `on - Immediate effect`. Default **on** on those repeaters, and prefs from firmware that stored the field without acting on it are read as on once. Default **off** on companions: a companion sleeps only after `powersaving on`, and then not while a USB host is attached, the display is on, or a Bluetooth link is not yet encrypted. nRF52/nRF54L: stored, replies `on - Immediate effect` (they always idle in System ON; `off` changes nothing). Other boards: `Board not supported` |
---
@@ -388,7 +388,7 @@ The observer role (ESP32, `boards/common/observer.conf`) has its own CLI and non
| `get extra.sf` | LR2021 side detectors: the extra spreading factors currently received alongside `sf`, comma-separated (bare, no `> ` prefix), or `No extra SF configured`. Reflects the saved prefs, not what the chip accepted — if the set became invalid after an `sf`/`bw` change it is reported here but was refused at boot (a `WRN` line says so). |
| `get adc.multiplier` | Battery voltage ADC calibration multiplier |
| `get bootloader.ver` | Bootloader version string |
| `get pm` | *(ZephCore only)* ESP32 light-sleep builds: `> on asleep P% sleeps S/E wake tT gG oO radioR btnB [win Ns]` (`off` when powersaving is off): powersaving state, share of uptime asleep, sleeps taken out of light-sleep entries, wake causes (RTC timer, GPIO, other), sleeps that ended with the radio IRQ / user button active, and the console window left. Radio wakes should track the packets the node hears. Other builds: `Error: no light sleep on this build` |
| `get pm` | *(ZephCore only)* ESP32 light-sleep builds: `> on asleep P% sleeps S/E wake tT gG wW oO radioR btnB [win Ns]` (`off` when powersaving is off): powersaving state, share of uptime asleep, sleeps taken out of light-sleep entries, wake causes (RTC timer, GPIO, WiFi, other), sleeps that ended with the radio IRQ / user button active, and the console window left. Radio wakes should track the packets the node hears. WiFi wakes are the beacons an associated station (the uplink) wakes for: about ten a second on most access points, none without the uplink. Other builds: `Error: no light sleep on this build` |
| `get pwrmgt.support` | `> supported` on nRF52 (VBUS detection, boot voltage), else `> unsupported` — upstream's reply |
| `get pwrmgt.source` | nRF52: `> external` (VBUS present) or `> battery`; elsewhere `ERROR: Power management not supported` |
| `get pwrmgt.bootreason` | `> Reset: <cause>; Shutdown: <reason>`. The cause is this boot's hardware reset cause as labels (`PIN`, `SOFTWARE`, `BROWNOUT`, `POR`, `WATCHDOG`, `LOWPOWER` for a wake from System OFF, ...), followed by `(crash <K_ERR> in <thread>, pc 0x...)` when the previous run ended in a fatal error and rebooted — resolve the pc with `addr2line` against the same build. The shutdown reason is `User Request`, `Low Voltage` or `None` |
+4
View File
@@ -302,6 +302,10 @@ it polls external state. Repeater and room server share one loop (`server_main_c
- Low-battery policy for every companion (`PowerPolicy`): three low readings 30 s apart, never on external power,
never on a reading under 2000 mV (no battery).
- GPS: duty-cycled; holds the SoC light-sleep lock only while acquiring.
- ESP32 companions on boards that declare it can light-sleep after `powersaving on` (off by default): never
while a USB host is attached, the display is on or a BLE link is not yet encrypted.
- WiFi uplink and observer: the station uses WiFi modem sleep. On ESP32 light-sleep repeaters it holds the SoC
light-sleep lock only until the MQTT session is up, and again whenever the link or the session is lost.
### 8.4 Security
- On-air: Ed25519 identities, X25519 ECDH, AES-128 + truncated HMAC (upstream scheme).
+1 -1
View File
@@ -7,7 +7,7 @@ Status: accepted (recorded 2026-09-25)
**WiFi companion** (`CONFIG_ZEPHCORE_COMPANION_WIFI`, `boards/common/wifi_companion.conf`): auto-added to
companion builds of boards whose `zephcore.yml` declares `capabilities: wifi: true`. On the PSRAM ESP32-S3
boards the WiFi heap, network buffers and the `CompanionMesh` object go to PSRAM. Four no-PSRAM boards also
declare it (Heltec V3 and Wireless Tracker at 140/130 contacts, both C6 boards at 280; the offline queue stays
declare it (Heltec V3 and Wireless Tracker at 140/130 contacts, 120/110 since 2026-10-09 when their companions gained light sleep, both C6 boards at 280; the offline queue stays
256): each sets `ZEPHCORE_MAX_CONTACTS` in `board.conf` to the largest count that links, and an S3 without PSRAM
also gets bounded WiFi buffers and a measured 72 KB heap (Kconfig, `HEAP_MEM_POOL_IGNORE_MIN`). Tracker V2, C3
and the classic ESP32 boards do not fit WiFi + BLE (DRAM). Upstream's
+90 -5
View File
@@ -4,10 +4,14 @@ Faster GPS fixes on three trackers, a clock that survives a reboot, fixes for US
could stop responding right after boot or when the port closed during a contact sync, a contacts
import that no longer stalls every few seconds, more reliable detection of clock chips, repeaters
that power down an external flash chip they do not use, a formatter that erases that chip on every
board that has one, and a few fixes ported from upstream MeshCore.
board that has one, ESP32 repeaters with the WiFi uplink and observers that run much cooler and keep
their broker connection, optional light sleep for ESP32-S3 companions, and a few fixes ported from
upstream MeshCore.
> [!NOTE]
> A normal upgrade keeps your identity, settings, contacts and phone pairing.
> A normal upgrade keeps your identity, settings, contacts and phone pairing. One exception:
> companions on the Heltec V3 and the Heltec Wireless Tracker now hold 120 and 110 contacts
> instead of 140 and 130; see "Companions can light-sleep" below.
---
@@ -142,6 +146,87 @@ This was found while looking into a report of a XIAO nRF52840 repeater drawing a
1.17.6 than on 1.17.4 (thanks to **Kimotu**). Whether it accounts for that difference is not
confirmed yet.
## ESP32 repeaters with the WiFi uplink, and observers, run cooler
**svenlange2** reported in [#107](https://github.com/liquidraver/ZephCore/issues/107) that a Heltec
V4.3 repeater ran about 20 C cooler than stock firmware, and was back at 45 C as soon as the WiFi +
MQTT uplink was built in and connected. Thank you for the report and for the measurements: this
whole section came out of them.
The report was right. A repeater with the uplink kept its WiFi radio fully on all the time, and on
the boards that light-sleep as repeaters it never slept again once the uplink was in the build.
Observers kept the WiFi radio on in the same way. Neither was necessary. This release changes both,
which resolves #107:
- **WiFi power save is on** for the uplink and for observers: the WiFi radio sleeps between the
access point's beacons. The connection to the access point and to the broker is kept.
- **Light sleep works with the uplink connected**, on the boards that light-sleep as repeaters
(Heltec V3, V4, V4.3, Wireless Tracker, Wireless Tracker V2). The node sleeps between beacons once
it is connected to the broker. It stays awake while it is connecting, and for as long as the WiFi
network or the broker cannot be reached.
- **An idle connection to the broker is no longer dropped every two minutes.** Some brokers, and TLS
front ends placed before one, close a connection that has been silent for exactly the keepalive
time (60 seconds). The keepalive message was sent only when that time was already up, so on a
quiet mesh the node lost the connection every two minutes and reconnected. It is now sent 15
seconds earlier.
- **Observers run the CPU at 80 MHz**, like every other role (240 MHz before, 160 MHz on C3 and C6
boards). Connecting to the broker takes a few tenths of a second longer.
On a XIAO ESP32-S3 the chip temperature went from 47 C to about 35 C with WiFi power save alone, and
to about 26 C with light sleep as well, with the uplink connected the whole time. The Heltec V4.3
from the report is one of the boards that now sleeps with the uplink connected. A received LoRa
packet wakes the node as before, and `powersaving off` still keeps it awake.
The MCU temperature that made the comparison possible is also from #107: see "Also in this
release".
`get pm` now counts the wakes for WiFi beacons on their own: its reply has a new `w` field between
`g` and `o`.
A light-sleep repeater with a display now stays awake while the display is on. The display turns
itself off 10 seconds after boot or after the last button press, as before.
## Companions can light-sleep on seven ESP32-S3 boards (off by default)
Companions on these boards can now put the processor into light sleep between events:
- Heltec V3, V4, V4.3, Wireless Tracker and Wireless Tracker V2,
- Seeed XIAO ESP32-S3,
- UnitEng Station G2.
It is **off by default**. Turn it on with `powersaving on` from the app's command line, and off
again with `powersaving off`; the setting is kept across reboots.
This is new, and it is offered for testing: reports of how it behaves on your board are welcome,
the Station G2 most of all. Repeaters on the XIAO ESP32-S3 and the Station G2 do not light-sleep
yet.
With it on, the node keeps advertising, stays connected to the app over Bluetooth or WiFi and
receives LoRa packets as before. It does not sleep:
- while a computer is attached over USB,
- while the display is on,
- while a phone is pairing,
- for 10 minutes after boot and after each press of the user button.
> [!NOTE]
> **USB on a sleeping companion:** a companion that is asleep is not seen by a computer it is
> plugged into. Press the user button after plugging in, or send `powersaving off` from the app
> first.
WiFi companions on these boards now use WiFi power save, with powersaving on or off: the WiFi
radio sleeps between the access point's beacons and the connection is kept.
> [!NOTE]
> **Heltec V3 and Wireless Tracker: fewer contacts.** These two run WiFi and Bluetooth without
> extra memory, and light sleep needs some of it. The contact limit goes from 140 to 120 on the
> Heltec V3 and from 130 to 110 on the Wireless Tracker, whether or not you turn powersaving on.
> A node holding more keeps the first 120 (or 110) after the update; export your contacts from
> the app before updating if you are above that. The offline message queue stays at 256.
On the Heltec V3 a computer talking to the companion over USB also keeps it awake, from its
first message until the next reboot.
## Formatter: the external flash chip is erased on every board that has one
The nRF52 formatters (`SoftDevice_v6_formatter` and `SoftDevice_v7_formatter`, as `.uf2` and as the
@@ -200,9 +285,9 @@ matches the SoftDevice version of your bootloader.
It also brings a fix for boards on WiFi: the starting sequence number of a TCP connection (the
companion port and the update page) was predictable, and is now derived from a secret as intended.
- **Heltec V3 and Wireless Tracker companions** (WiFi and Bluetooth together, no PSRAM) had under
1 KB of memory left; they now have about 2.5 KB. The Bluetooth controller's stack on ESP32-S3 and
ESP32-C3 companions was 4 KB and is now 2 KB; it uses about 1.1 KB with WiFi connected. Nothing
changes in use.
1 KB of memory left; they now have about 2.5 KB, with light sleep included. The Bluetooth
controller's stack on ESP32-S3 and ESP32-C3 companions was 4 KB and is now 2 KB; it uses about
1.1 KB with WiFi connected.
- **Documentation**: the README was reorganised, build and flashing instructions moved to
`docs/BUILDING.md`, and the example board and porting guide were rewritten for the current board
layout.
+21 -18
View File
@@ -172,14 +172,11 @@ if(ZEPHCORE_PLATFORM STREQUAL "esp32")
# first-order term in a battery node's draw — unlike nRF52, which already
# idles at microamps. Every ESP32 build runs at 80 MHz, matching Arduino
# MeshCore; hal_espressif derives ESP_DEFAULT_CPU_FREQ_MHZ from cpu@0, so no
# module patch is needed. Observers keep the SoC maximum (240 MHz S3/classic,
# 160 MHz C3/C6): full WiFi + TLS + MQTT on mains or big-panel solar, so
# throughput matters more than current. "Don't apply the overlay" rather than
# an override, so the per-SoC maximum is inherited, not hardcoded.
if(NOT ZEPHCORE_BUILD_ROLE STREQUAL "observer")
zephcore_add_overlay(${_zc}/esp32_cpu_80mhz.overlay)
message(STATUS " ESP32 CPU: 80 MHz (observers keep the SoC maximum)")
endif()
# module patch is needed. Every role, observers included: 80 MHz is the
# lowest clock WiFi runs at and carries WiFi + TLS + MQTT
# (devdocs/lld/13-power-management.md, section 12).
zephcore_add_overlay(${_zc}/esp32_cpu_80mhz.overlay)
message(STATUS " ESP32 CPU: 80 MHz")
# SoC temperature sensor (MCU temperature in telemetry, image-calibration
# drift on LR11xx/LR2021): every role, every SoC that has the node. The
@@ -188,18 +185,24 @@ if(ZEPHCORE_PLATFORM STREQUAL "esp32")
zephcore_add_overlay(${_zc}/esp32_coretemp.overlay)
endif()
# Light sleep: REPEATERS on boards that declare it. 80 MHz cuts the idle draw;
# light sleep is what removes it (mechanism and requirements:
# boards/common/pm_esp32.conf). Repeaters only: the Espressif HCI driver takes
# no pm_policy locks, so a companion would light-sleep mid-advertising or
# mid-connection; repeater.conf sets CONFIG_BT=n. Which boards qualify is a
# hardware fact (DIO1 on an RTC-wake-capable GPIO, and validated on hardware),
# so the board declares it: `capabilities: light_sleep: true` in its
# zephcore.yml. pm_esp32.conf lists which boards fail the wiring test and why;
# a board that passes it but was never validated (v4_r8) stays undeclared.
# Light sleep: on boards that declare it. 80 MHz cuts the idle draw; light
# sleep is what removes it (mechanism and requirements:
# boards/common/pm_esp32.conf). Which boards qualify is a hardware fact
# (the radio IRQ and button wake the SoC, pins are held, validated on
# hardware), so the board declares it in its zephcore.yml:
# `light_sleep: true` for the repeater, `light_sleep_companion: true` for the
# companion. Two lines because they differ: a repeater sleeps by default, so
# its line waits for validation on hardware (v4_r8, which only passes the
# wiring test, stays undeclared); a companion sleeps only after
# `powersaving on`, and its build costs about 4 KB of DRAM, which a WiFi +
# BLE companion without PSRAM gives up in contacts
# (devdocs/lld/13-power-management.md, section 13).
if(ZEPHCORE_BUILD_ROLE STREQUAL "repeater" AND ZEPHCORE_BOARD_LIGHT_SLEEP)
zephcore_add_conf(${_zc}/pm_esp32.conf)
message(STATUS " ESP32 light sleep: auto-enabled (DIO1 wake-capable repeater board)")
message(STATUS " ESP32 light sleep: auto-enabled (repeater, board declares it)")
elseif(ZEPHCORE_BUILD_ROLE STREQUAL "companion" AND ZEPHCORE_BOARD_LIGHT_SLEEP_COMPANION)
zephcore_add_conf(${_zc}/pm_esp32.conf)
message(STATUS " ESP32 light sleep: auto-enabled (companion, board declares it; opt-in at run time)")
endif()
# WiFi OTA for S3/C-series repeaters: WiFi AP + HTTP server + MCUboot image
+20
View File
@@ -34,6 +34,7 @@ LOG_MODULE_REGISTER(zephcore_ble, CONFIG_ZEPHCORE_BLE_LOG_LEVEL);
#include "ZephyrBLE.h"
#include "ble_internal.h"
#include "frame_txq.h"
#include <helpers/pm_sleep_guard.h>
/* ========== Constants ========== */
@@ -293,6 +294,22 @@ static void build_device_name_and_adv(const char *name_from_prefs)
/* ========== Connection callbacks ========== */
/* No SoC light sleep from connect until the link is encrypted (or gone):
* pairing does not survive it (devdocs/lld/13-power-management.md, section
* 13). An encrypted link may sleep between connection events. */
static atomic_t unencrypted_hold;
static void unencrypted_hold_set(bool hold)
{
if (hold) {
if (atomic_cas(&unencrypted_hold, 0, 1)) {
zc_pm_block_sleep();
}
} else if (atomic_cas(&unencrypted_hold, 1, 0)) {
zc_pm_unblock_sleep();
}
}
static void connected(struct bt_conn *conn, uint8_t err)
{
char addr[BT_ADDR_LE_STR_LEN];
@@ -304,6 +321,7 @@ static void connected(struct bt_conn *conn, uint8_t err)
LOG_INF("connected: %s", addr);
current_conn = bt_conn_ref(conn);
unencrypted_hold_set(true);
/* Zephyr stops advertising internally when the conn slot is consumed
* (BT_MAX_CONN=1 — there's no slot left to advertise from). Sync our
@@ -340,6 +358,7 @@ static void disconnected(struct bt_conn *conn, uint8_t reason)
LOG_INF("disconnected: %s reason 0x%02x", addr, reason);
k_work_cancel_delayable(&sec_timeout_work);
unencrypted_hold_set(false);
if (conn == current_conn) {
bt_conn_unref(current_conn);
@@ -393,6 +412,7 @@ static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_
* so stand the unpaired-connection timeout down. */
if (level >= BT_SECURITY_L2) {
k_work_cancel_delayable(&sec_timeout_work);
unencrypted_hold_set(false);
}
/* Enable TX when we have sufficient security (level 2+ = encrypted).
+23 -6
View File
@@ -9,6 +9,9 @@
* 4. Publish retained "online" status (LWT handles "offline" on disconnect)
* 5. Poll loop: drain publish queue + mqtt_input() keepalive
* 6. On error/disconnect: publish "offline", back off 5s, goto 1
*
* The session (CONNACK until the poll loop ends) is reported to the WiFi
* station, which blocks SoC light sleep outside it.
*/
#include "ZephyrMQTTPublisher.h"
@@ -320,12 +323,20 @@ static void run_poll_loop(void)
}
}
/* Send keepalive ping if needed */
int rc = mqtt_live(&s_client);
if (rc < 0 && rc != -EAGAIN) {
LOG_WRN("mqtt_live error: %d", rc);
s_connected = false;
return;
/* Keepalive ping, a quarter of the keepalive early. mqtt_live()
* pings only once the keepalive has fully elapsed, and a broker
* that enforces it without grace closes the session first
* (devdocs/lld/13-power-management.md, section 12). */
int left_ms = mqtt_keepalive_time_left(&s_client);
if (left_ms >= 0 && left_ms <= (int)s_client.keepalive * 250) {
int rc = mqtt_ping(&s_client);
if (rc < 0 && rc != -EAGAIN) {
LOG_WRN("mqtt_ping error: %d", rc);
s_connected = false;
return;
}
}
if (!s_connected) {
@@ -435,12 +446,18 @@ static void mqtt_thread_fn(void *p1, void *p2, void *p3)
/* Connected — reset the failure backoff */
s_retry_backoff_s = RETRY_BACKOFF_MIN_S;
/* Session up: the station may let the SoC light-sleep until the
* poll loop ends. */
zc_wifi_station_session_up(true);
/* Publish retained "online" status */
publish_status(&s_client, true);
/* Run poll loop until disconnect or reconnect request */
run_poll_loop();
zc_wifi_station_session_up(false);
/* Publish "offline" before closing (best-effort) */
if (s_connected) {
publish_status(&s_client, false);
@@ -22,6 +22,7 @@ LOG_MODULE_REGISTER(zephcore_usb, CONFIG_ZEPHCORE_USB_LOG_LEVEL);
#include "ZephyrCompanionUSB.h"
#include "ZephyrUSBCDC.h"
#include <helpers/pm_sleep_guard.h>
#include "companion_framing.h"
#define USB_RING_BUF_SIZE 512 /* USB RX ring buffer size */
@@ -223,6 +224,11 @@ static void usb_session_begin(uint8_t log_tag, bool is_text)
}
usb_session_active = true;
usb_session_is_text = is_text;
#if !COMPANION_HAS_DTR
/* A plain UART has no DTR: the session never ends, and UART RX cannot
* wake a sleeping SoC. No light sleep from here on. */
zc_pm_block_sleep();
#endif
LOG_INF("usb_rx: first traffic 0x%02x, session started (%s)", log_tag,
is_text ? "text" : "binary");
usb_link_update();
+28
View File
@@ -30,6 +30,7 @@
LOG_MODULE_REGISTER(zephcore_usbd, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL);
#include <adapters/board/ZephyrBoard.h>
#include <helpers/pm_sleep_guard.h>
/* ===== Per-role descriptor strings ====== */
#define ZEPHCORE_USB_VID 0x2fe3 /* Zephyr Project VID */
@@ -88,6 +89,23 @@ static void enter_bootloader(void)
CODE_UNREACHABLE;
}
/* A host is on the bus (it reset, configured or resumed us and has not
* suspended it since). No SoC light sleep while one is: sleep stops the USB
* clock, and the host loses the device without being told
* (devdocs/lld/13-power-management.md, section 13). */
static atomic_t s_host_present;
static void host_present(bool present)
{
if (present) {
if (atomic_cas(&s_host_present, 0, 1)) {
zc_pm_block_sleep();
}
} else if (atomic_cas(&s_host_present, 1, 0)) {
zc_pm_unblock_sleep();
}
}
/* The host went away without a DTR=0: report it as a DTR drop. */
static void host_gone(void)
{
@@ -105,6 +123,15 @@ static void usbd_msg_callback(struct usbd_context *const ctx,
{
ARG_UNUSED(ctx);
if (msg->type == USBD_MSG_SUSPEND || msg->type == USBD_MSG_VBUS_REMOVED) {
host_present(false);
} else if (msg->type == USBD_MSG_RESET || msg->type == USBD_MSG_RESUME ||
msg->type == USBD_MSG_CONFIGURATION ||
msg->type == USBD_MSG_CDC_ACM_LINE_CODING ||
msg->type == USBD_MSG_CDC_ACM_CONTROL_LINE_STATE) {
host_present(true);
}
if (msg->type == USBD_MSG_CDC_ACM_LINE_CODING) {
uint32_t baudrate = 0;
if (uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_BAUD_RATE,
@@ -309,4 +336,5 @@ extern "C" void zephcore_usbd_detach(void)
(void)usbd_disable(&zephcore_usbd);
s_initialized = false;
s_dtr_active = false;
host_present(false);
}
+61 -19
View File
@@ -37,11 +37,42 @@ K_EVENT_DEFINE(g_wifi_events);
static const char *s_ssid;
static const char *s_psk;
static void (*s_time_sync_cb)(uint32_t unix_ts);
static bool s_power_save;
static bool s_started;
/* Protect link-up vs. SNTP state */
static volatile bool s_wifi_link_up; /* WiFi associate event received */
static volatile bool s_wifi_ready; /* DHCP + SNTP done */
/* ========== Light-sleep block ========== */
/* Held unless the link is ready and the client's session is up
* (devdocs/lld/13-power-management.md, section 12). Called from the net_mgmt
* thread and from the client's thread. */
static K_MUTEX_DEFINE(s_sleep_mutex);
static volatile bool s_session_up;
static bool s_sleep_blocked;
static void sleep_block_update(void)
{
if (!s_started) {
return;
}
k_mutex_lock(&s_sleep_mutex, K_FOREVER);
bool block = !(s_wifi_ready && s_session_up);
if (block && !s_sleep_blocked) {
zc_pm_block_sleep();
} else if (!block && s_sleep_blocked) {
zc_pm_unblock_sleep();
}
s_sleep_blocked = block;
k_mutex_unlock(&s_sleep_mutex);
}
/* ========== Reconnect work ========== */
static void connect_work_fn(struct k_work *work);
@@ -68,6 +99,7 @@ static void do_sntp_and_signal(void)
#endif
s_wifi_ready = true;
sleep_block_update();
k_event_post(&g_wifi_events, WIFI_READY_BIT);
LOG_INF("WiFi ready (DHCP+SNTP done)");
}
@@ -104,18 +136,19 @@ static void wifi_event_handler(struct net_mgmt_event_callback *cb,
if (success) {
LOG_INF("WiFi link up (SSID: %s)", s_ssid ? s_ssid : "?");
s_wifi_link_up = true;
/* Disable power save — WIFI_PS_MIN_MODEM (default) sleeps between
* DTIM beacons, which delays ACKs and causes the MQTT broker's TCP
* stack to retransmit and eventually RST the connection (-ECONNRESET
* in the poll loop). WIFI_PS_DISABLED keeps the radio always awake. */
struct wifi_ps_params ps = {
.enabled = WIFI_PS_DISABLED,
.type = WIFI_PS_PARAM_STATE,
};
if (net_mgmt(NET_REQUEST_WIFI_PS, iface, &ps, sizeof(ps)) < 0) {
LOG_WRN("Failed to disable WiFi power save");
} else {
LOG_INF("WiFi power save disabled");
if (!s_power_save) {
/* Power save off: the WiFi radio stays on between
* beacons. Left alone, the driver runs minimum modem
* sleep. */
struct wifi_ps_params ps = {
.enabled = WIFI_PS_DISABLED,
.type = WIFI_PS_PARAM_STATE,
};
if (net_mgmt(NET_REQUEST_WIFI_PS, iface, &ps, sizeof(ps)) < 0) {
LOG_WRN("Failed to disable WiFi power save");
} else {
LOG_INF("WiFi power save disabled");
}
}
/* DHCP will fire ipv4_event_handler when lease is obtained */
} else {
@@ -128,6 +161,7 @@ static void wifi_event_handler(struct net_mgmt_event_callback *cb,
LOG_INF("WiFi disconnected");
s_wifi_link_up = false;
s_wifi_ready = false;
sleep_block_update();
k_event_clear(&g_wifi_events, WIFI_READY_BIT);
/* Reconnect after 5s backoff */
k_work_reschedule(&connect_work, K_SECONDS(5));
@@ -181,20 +215,21 @@ static void connect_work_fn(struct k_work *work)
/* ========== Public API ========== */
void zc_wifi_station_start(const char *ssid, const char *psk,
void (*time_sync_cb)(uint32_t unix_ts))
void (*time_sync_cb)(uint32_t unix_ts), bool power_save)
{
s_ssid = ssid;
s_psk = psk;
s_time_sync_cb = time_sync_cb;
s_power_save = power_save;
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();
/* No light sleep until the link is ready and the client's session is up.
* There is no zc_wifi_station_stop(): a station stays for the life of the
* boot. */
s_started = true;
sleep_block_update();
zc_pm_wifi_station_started();
/* Register WiFi event callback */
net_mgmt_init_event_callback(&wifi_cb, wifi_event_handler,
@@ -222,10 +257,17 @@ void zc_wifi_station_reconnect(void)
/* Clear state and immediately reschedule connect */
s_wifi_link_up = false;
s_wifi_ready = false;
sleep_block_update();
k_event_clear(&g_wifi_events, WIFI_READY_BIT);
k_work_reschedule(&connect_work, K_MSEC(200));
}
void zc_wifi_station_session_up(bool up)
{
s_session_up = up;
sleep_block_update();
}
bool zc_wifi_station_is_connected(void)
{
return s_wifi_ready;
+14 -1
View File
@@ -33,11 +33,24 @@ extern struct k_event g_wifi_events;
* ssid, psk — the network; pointers kept (read at every connect), so they
* must remain valid. psk "" = open network.
* time_sync_cb — called once after SNTP with Unix timestamp; may be NULL.
* power_save — true: run with the driver's WiFi power save (minimum modem
* sleep: the WiFi radio is off between the access point's DTIM
* beacons). false: request power save off at every link-up.
*
* Must be called once from main() before any other wifi_station_* calls.
*
* ESP32 light-sleep builds: the station blocks SoC light sleep until the link
* is ready and a client has reported its session up, and again whenever either
* is lost. With no such client the block stays for the life of the boot.
*/
void zc_wifi_station_start(const char *ssid, const char *psk,
void (*time_sync_cb)(uint32_t unix_ts));
void (*time_sync_cb)(uint32_t unix_ts), bool power_save);
/*
* The client's session over the link (the MQTT publisher's broker session) is
* up, or has ended. Safe to call from any thread.
*/
void zc_wifi_station_session_up(bool up);
/*
* Trigger an immediate reconnect (e.g. after credential change via CLI).
+12 -2
View File
@@ -13,6 +13,7 @@
#include <zephyr/logging/log.h>
#include <helpers/TxtDataHelpers.h>
#include <ZephyrWiFiStation.h>
#include <helpers/pm_sleep_guard.h>
LOG_MODULE_REGISTER(zephcore_wifi_companion, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
@@ -35,8 +36,17 @@ void companion_wifi_start(const NodePrefs &prefs)
memcpy(s_pwd, prefs.wifi_pwd, sizeof(s_pwd));
LOG_INF("WiFi companion: joining %s", s_ssid);
/* No SNTP time: the app sets the companion's clock, and time only moves
* forward (see time_sync.h). */
zc_wifi_station_start(s_ssid, s_pwd, nullptr);
* forward (see time_sync.h).
*
* Light-sleep builds: WiFi power save on (the WiFi library gives its sleep
* lock up only in power save), and no client session to wait for, so the
* station blocks sleep only while the link is not ready. Other builds:
* power save off, as before (devdocs/lld/13-power-management.md,
* sections 12 and 13). */
zc_wifi_station_start(s_ssid, s_pwd, nullptr, ZC_PM_LIGHT_SLEEP != 0);
if (ZC_PM_LIGHT_SLEEP) {
zc_wifi_station_session_up(true);
}
}
bool companion_wifi_cli(const char *command, NodePrefs &prefs, void (*save)(void), char *reply)
+2 -1
View File
@@ -1102,7 +1102,8 @@ void RepeaterMesh::begin(RepeaterDataStore* store) {
if (isUplinkEnabled() && _uplink_creds.wifi_ssid[0] && _uplink_creds.mqtt_host[0]) {
s_uplink_mesh = this;
zc_wifi_station_start(_uplink_creds.wifi_ssid, _uplink_creds.wifi_psk, uplink_time_sync_cb);
zc_wifi_station_start(_uplink_creds.wifi_ssid, _uplink_creds.wifi_psk, uplink_time_sync_cb,
true /* WiFi power save */);
mqtt_publisher_start(&_uplink_creds, _prefs.node_name,
_uplink_status_topic, _uplink_packets_topic);
mqtt_publisher_set_connect_cb([]() {
+2 -1
View File
@@ -403,7 +403,8 @@ int main(void)
print_banner();
/* Start WiFi (non-blocking — MQTT thread waits for WIFI_READY_BIT) */
zc_wifi_station_start(s_creds.wifi_ssid, s_creds.wifi_psk, time_sync_cb);
zc_wifi_station_start(s_creds.wifi_ssid, s_creds.wifi_psk, time_sync_cb,
true /* WiFi power save */);
/* Start MQTT publisher thread */
char client_id[64];
@@ -26,11 +26,10 @@
* classic) and the single-core RISC-V parts (C3, C6) alike. Referencing
* &cpu1 here would be a dtc error on the latter.
*
* Applied automatically to non-observer ESP32 builds — see the ESP32 CPU
* clock block in CMakeLists.txt. Kept out of the per-board DTS files on
* purpose: one place to change, and it makes the observer exception a simple
* "don't include this" rather than an override that would have to know each
* SoC's maximum (240 MHz on S3/classic, 160 MHz on C3/C6).
* Applied automatically to every ESP32 build — see the ESP32 CPU clock block
* in CMakeLists.txt. Kept out of the per-board DTS files on purpose: one place
* to change. Without it a build runs at its SoC's maximum (240 MHz on
* S3/classic, 160 MHz on C3/C6).
*/
&cpu0 {
+20 -11
View File
@@ -1,8 +1,9 @@
# ESP32 system power management — light sleep between events.
#
# AUTO-ENABLED for repeater builds on boards whose zephcore.yml declares
# `light_sleep: true` (cmake/zephcore_board.cmake, CMakeLists.txt). Passing it
# by hand is supported for testing another board:
# `light_sleep: true`, and for companion builds on boards that declare
# `light_sleep_companion: true` (cmake/zephcore_board.cmake, CMakeLists.txt).
# Passing it by hand is supported for testing another board:
#
# west build -b xiao_esp32s3/esp32s3/procpu zephcore --pristine --sysbuild -- # -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/pm_esp32.conf"
#
@@ -37,16 +38,24 @@
# button to reopen the window, or send `powersaving off` (persisted) over LoRa;
# `powersaving on` allows sleep again. Light-sleep builds default to on.
#
# WiFi: the ESP32 WiFi driver starts WiFi at boot (NULL mode), and a started
# WiFi library holds its modem lock, so NO S3 repeater (they all carry WiFi OTA)
# ever light-slept before 2026-09-26. helpers/pm_esp32_wake.c stops WiFi at
# boot; `start ota` (AP) and a station connect start it again. While WiFi is in
# use a pm_policy lock is held — the OTA session for its duration
# (adapters/ota/wifi_ota.c), an uplink or observer station permanently
# (adapters/wifi/ZephyrWiFiStation.c).
# WiFi: the ESP32 WiFi driver starts WiFi at boot (NULL mode), and the WiFi
# library holds its modem lock for as long as the WiFi radio is on, so NO S3
# repeater (they all carry WiFi OTA) ever light-slept before 2026-09-26.
# helpers/pm_esp32_wake.c stops WiFi at boot; `start ota` (AP) and a station
# connect start it again. An OTA session holds a pm_policy lock for its
# duration (adapters/ota/wifi_ota.c). A station (the MQTT uplink) runs in WiFi
# power save: the library gives its lock up between beacons and the WiFi MAC
# wakes the SoC for each one. The station holds a lock of its own until its
# MQTT session is up, and again whenever the link or the session is lost
# (adapters/wifi/ZephyrWiFiStation.c); a node whose access point is away stays
# awake. devdocs/lld/13-power-management.md, section 12.
#
# REPEATERS ONLY for now. Companions keep BLE up; making the controller's
# modem sleep coexist with system light sleep is the open question in LLD 13.
# COMPANIONS sleep only after `powersaving on` (their default is off), and then
# not while a USB host is on the bus, the display is on, or a BLE link is not
# yet encrypted. The BLE controller gives its lock up between events and its
# wake timer bounds each sleep (CONFIG_PM_CUSTOM_TICKS_HOOK below). The build
# costs about 4 KB of DRAM, which is why the companion has its own capability
# line. devdocs/lld/13-power-management.md, section 13.
#
# CHECKING A BOARD: `get pm` shows the share of time asleep, the sleep count and
# wake causes (t = RTC timer, g = GPIO, o = other) and per-pin wake counts.
@@ -49,8 +49,10 @@ CONFIG_POWER_DOMAIN=n
CONFIG_ZEPHCORE_UI_CONFIRM_WINDOW_MS=3000
# WiFi + BLE companion without PSRAM (`capabilities: wifi` in zephcore.yml):
# contacts are what gives. 140 is the largest count that links with ~2 KB DRAM
# to spare (188 B per contact; measured 2026-09-24, see
# devdocs/lld/08-connectivity.md §14). The offline queue stays at 256. An
# existing contacts file holding more is truncated to 140 on first load.
CONFIG_ZEPHCORE_MAX_CONTACTS=140
# contacts are what gives (188 B each; devdocs/lld/08-connectivity.md §14).
# 120 links with ~2 KB DRAM to spare with light sleep in the build
# (`light_sleep_companion`, about 4 KB; 140 before it, 2026-10-09,
# devdocs/lld/13-power-management.md section 13). The offline queue stays
# at 256. An existing contacts file holding more is truncated to 120 on
# first load.
CONFIG_ZEPHCORE_MAX_CONTACTS=120
@@ -3,6 +3,7 @@ target: heltec_wifi_lora32_v3/esp32s3/procpu
capabilities:
wifi: true
light_sleep: true
light_sleep_companion: true
release:
roles: [companion, repeater]
catalog:
@@ -3,6 +3,7 @@ target: heltec_wifi_lora32_v4/esp32s3/procpu
capabilities:
wifi: true
light_sleep: true
light_sleep_companion: true
release:
roles: [companion, repeater]
catalog:
@@ -3,6 +3,7 @@ target: heltec_wifi_lora32_v43/esp32s3/procpu
capabilities:
wifi: true
light_sleep: true
light_sleep_companion: true
release:
roles: [companion, repeater]
catalog:
@@ -32,8 +32,10 @@ CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_ZEPHCORE_UI_CONFIRM_WINDOW_MS=3000
# WiFi + BLE companion without PSRAM (`capabilities: wifi` in zephcore.yml):
# contacts are what gives. 130 is the largest count that links with ~2 KB DRAM
# to spare (188 B per contact; measured 2026-09-24, see
# devdocs/lld/08-connectivity.md §14). The offline queue stays at 256. An
# existing contacts file holding more is truncated to 130 on first load.
CONFIG_ZEPHCORE_MAX_CONTACTS=130
# contacts are what gives (188 B each; devdocs/lld/08-connectivity.md §14).
# 110 links with ~2 KB DRAM to spare with light sleep in the build
# (`light_sleep_companion`, about 4 KB; 130 before it, 2026-10-09,
# devdocs/lld/13-power-management.md section 13). The offline queue stays
# at 256. An existing contacts file holding more is truncated to 110 on
# first load.
CONFIG_ZEPHCORE_MAX_CONTACTS=110
@@ -3,6 +3,7 @@ target: heltec_wireless_tracker/esp32s3/procpu
capabilities:
wifi: true
light_sleep: true
light_sleep_companion: true
release:
roles: [companion, repeater]
catalog:
@@ -2,6 +2,7 @@
target: heltec_wireless_tracker_v2/esp32s3/procpu
capabilities:
light_sleep: true
light_sleep_companion: true
release:
roles: [companion, repeater]
catalog:
@@ -2,6 +2,7 @@
target: station_g2/esp32s3/procpu
capabilities:
wifi: true
light_sleep_companion: true
release:
roles: [companion, repeater]
catalog:
@@ -2,6 +2,7 @@
target: xiao_esp32s3/esp32s3/procpu
capabilities:
wifi: true
light_sleep_companion: true
release:
roles: [companion, repeater]
catalog:
+4 -1
View File
@@ -207,7 +207,10 @@ Only `lora0` is required. Everything else enables its feature by being present.
- **Manifest capabilities:** `wifi: true` gives the companion a WiFi transport and belongs only on boards with
the RAM for WiFi and BLE together ([ADR 0009](../../../docs/adr/0009-wifi-companion-boards.md)).
`light_sleep: true` makes repeaters light-sleep; the requirements are in `boards/common/pm_esp32.conf`, and
it is declared only after it was validated on the hardware. Either way, give NSS, RESET and every MCU-driven
it is declared only after it was validated on the hardware. `light_sleep_companion: true` gives the companion
build light sleep (off until the user sends `powersaving on`); it needs the same pin holds and wake pins and
about 4 KB of free DRAM, which a WiFi + BLE companion without PSRAM pays for in contacts. Being opt-in, it may
be declared before `light_sleep`. Either way, give NSS, RESET and every MCU-driven
RF switch or FEM line `ESP32_GPIO_SLEEP_HOLD_EN` from the start.
- **RAM, not flash, limits the companion.** Boards without PSRAM lower `MAX_CONTACTS`; the classic ESP32 also
trims channels and the offline queue.
@@ -27,6 +27,10 @@ target: my_board
# light_sleep: true # repeater light-sleeps between events. Declare it only
# # after boards/common/pm_esp32.conf's checks pass on
# # hardware; qualifying wiring alone is not enough
# light_sleep_companion: true # the companion can light-sleep (off until
# # `powersaving on`). Needs the same pin holds and
# # wake pins, and about 4 KB of DRAM. It does not need
# # light_sleep: opt-in, so it may come first
# Leave `release:` out while the board is in bring-up: it then builds locally
# and is listed as "source-only", but no release carries firmware for it.
+6
View File
@@ -10,6 +10,7 @@
# ZEPHCORE_BOARD_DIR boards/<platform-dir>/<board>, or "" if none
# ZEPHCORE_PLATFORM nrf52 | nrf54l | esp32 | mg24 | stm32wl | linux | ""
# ZEPHCORE_BOARD_LIGHT_SLEEP TRUE if the board manifest declares it
# ZEPHCORE_BOARD_LIGHT_SLEEP_COMPANION TRUE if the board manifest declares it
# ZEPHCORE_BOARD_WIFI TRUE if the board manifest declares it
# zephcore_board_file(<out-var> <file-name>)
# absolute path of <file-name> inside the board dir, or "".
@@ -83,6 +84,7 @@ endif()
# this runs before Zephyr has set up Python; `board_manifest.py check` enforces
# the exact two-space-indented ` <capability>: true|false` form this relies on.
set(ZEPHCORE_BOARD_LIGHT_SLEEP FALSE)
set(ZEPHCORE_BOARD_LIGHT_SLEEP_COMPANION FALSE)
set(ZEPHCORE_BOARD_WIFI FALSE)
zephcore_board_file(_zb_manifest zephcore.yml)
if(_zb_manifest)
@@ -90,6 +92,10 @@ if(_zb_manifest)
if(_zb_ls)
set(ZEPHCORE_BOARD_LIGHT_SLEEP TRUE)
endif()
file(STRINGS "${_zb_manifest}" _zb_lsc REGEX "^ light_sleep_companion: true[ \t]*$")
if(_zb_lsc)
set(ZEPHCORE_BOARD_LIGHT_SLEEP_COMPANION TRUE)
endif()
file(STRINGS "${_zb_manifest}" _zb_wifi REGEX "^ wifi: true[ \t]*$")
if(_zb_wifi)
set(ZEPHCORE_BOARD_WIFI TRUE)
+6 -4
View File
@@ -238,10 +238,12 @@ static inline void auto_shutdown_upgrade(NodePrefs *p) {
}
/* `powersaving` gates ESP32 light sleep (helpers/pm_esp32_wake.c). On a
* light-sleep build the default is on: those builds slept unconditionally
* before the switch existed, and the stored 0 carried no intent. Elsewhere
* it is upstream's default, off. */
#if defined(CONFIG_PM) && defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
* light-sleep repeater or room server the default is on: those builds slept
* unconditionally before the switch existed, and the stored 0 carried no
* intent. Elsewhere, light-sleep companions included, it is upstream's
* default, off. */
#if defined(CONFIG_PM) && defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32) && \
!defined(CONFIG_ZEPHCORE_ROLE_COMPANION)
#define POWERSAVING_DEFAULT 1
#else
#define POWERSAVING_DEFAULT 0
+39 -4
View File
@@ -46,8 +46,15 @@
#include <soc/gpio_struct.h>
#include <esp_sleep.h>
#include <esp_timer.h>
#include <soc/soc_caps.h>
#if defined(CONFIG_WIFI_ESP32)
#include <esp_wifi.h>
#include <esp_private/wifi.h>
#endif
/* The WiFi MAC keeps its own beacon timer (TSF) and wakes the SoC with it. */
#if defined(CONFIG_WIFI_ESP32) && SOC_WIFI_HW_TSF
#define ZC_PM_WIFI_TSF 1
#endif
#include <stdio.h>
@@ -96,6 +103,7 @@ static struct {
uint32_t slept; /* ... and the SoC actually slept */
uint32_t wake_timer;
uint32_t wake_gpio;
uint32_t wake_wifi; /* the WiFi MAC, for a beacon */
uint32_t wake_other;
uint64_t asleep_us;
} stats;
@@ -146,6 +154,8 @@ static void wake_state_exit(enum pm_state state)
stats.wake_gpio++;
} else if (causes & BIT(ESP_SLEEP_WAKEUP_TIMER)) {
stats.wake_timer++;
} else if (causes & BIT(ESP_SLEEP_WAKEUP_WIFI)) {
stats.wake_wifi++;
} else {
stats.wake_other++;
}
@@ -223,11 +233,11 @@ int zc_pm_format_stats(char *buf, size_t len)
uint32_t pct10 = up_ms ? (uint32_t)(stats.asleep_us / up_ms) : 0; /* 0.1 % units */
uint32_t win_s = zc_pm_console_window_remaining_ms() / 1000U;
int n = snprintf(buf, len,
"> %s asleep %u.%u%% sleeps %u/%u wake t%u g%u o%u",
"> %s asleep %u.%u%% sleeps %u/%u wake t%u g%u w%u o%u",
atomic_get(&powersave_blocked) ? "off" : "on",
pct10 / 10U, pct10 % 10U,
stats.slept, stats.entries,
stats.wake_timer, stats.wake_gpio, stats.wake_other);
stats.wake_timer, stats.wake_gpio, stats.wake_wifi, stats.wake_other);
ARRAY_FOR_EACH_PTR(wake_pins, p) {
if (n > 0 && (size_t)n < len) {
@@ -240,6 +250,19 @@ int zc_pm_format_stats(char *buf, size_t len)
return (n > 0 && (size_t)n < len) ? n : (int)strlen(buf);
}
/* ========== WiFi station ========== */
#if defined(ZC_PM_WIFI_TSF)
static atomic_t wifi_station_started;
#endif
void zc_pm_wifi_station_started(void)
{
#if defined(ZC_PM_WIFI_TSF)
atomic_set(&wifi_station_started, 1);
#endif
}
/* ========== esp_timer deadlines bound the sleep ========== */
#if defined(CONFIG_PM_CUSTOM_TICKS_HOOK)
@@ -248,9 +271,20 @@ int zc_pm_format_stats(char *buf, size_t len)
* would otherwise be slept through — the BLE controller's modem-sleep wake
* timer ("btSlp") is the one that matters: miss it and the controller misses
* its connection event. ESP-IDF's own tickless idle bounds the sleep the same
* way (esp_pm/pm_impl.c). */
* way (esp_pm/pm_impl.c).
*
* A WiFi station in power save gives up the WiFi library's sleep lock between
* beacons a moment before the MAC's beacon timer (TSF) goes idle. No sleep in
* that moment, as in ESP-IDF: 0 ticks means "an event is due now"
* (devdocs/lld/13-power-management.md, section 12). */
int64_t pm_policy_next_custom_ticks(void)
{
#if defined(ZC_PM_WIFI_TSF)
if (atomic_get(&wifi_station_started) && esp_wifi_internal_is_tsf_active()) {
return 0;
}
#endif
int64_t next = esp_timer_get_next_alarm_for_wake_up();
if (next == INT64_MAX) {
@@ -287,7 +321,8 @@ static int pm_wake_init(void)
#if defined(CONFIG_WIFI_ESP32)
/* The ESP32 WiFi driver starts WiFi at boot, in NULL mode, "to enable
* coexistence". Started, the WiFi library holds its modem PM lock
* coexistence". In NULL mode the WiFi radio stays on, the WiFi library
* holds its modem PM lock for as long as it is
* (pm_policy_state_all_lock_get via esp_pm) and the SoC never
* light-sleeps: measured on the XIAO repeater, zero sleep entries with
* WiFi compiled in, sleeps without it. Stop it here. Nothing is lost:
+5
View File
@@ -60,6 +60,10 @@ uint32_t zc_pm_console_window_remaining_ms(void);
/* One `get pm` line into buf; returns its length. */
int zc_pm_format_stats(char *buf, size_t len);
/* A WiFi station is in use from now on: no light sleep while the WiFi MAC's
* beacon timer (TSF) is active. */
void zc_pm_wifi_station_started(void);
#ifdef __cplusplus
}
#endif
@@ -69,6 +73,7 @@ int zc_pm_format_stats(char *buf, size_t len);
#else
static inline void zc_pm_set_powersaving(bool on) { (void)on; }
static inline void zc_pm_wifi_station_started(void) { }
static inline void zc_pm_console_window_open(void) { }
static inline uint32_t zc_pm_console_window_remaining_ms(void) { return 0; }
static inline int zc_pm_format_stats(char *buf, size_t len) { (void)buf; (void)len; return 0; }
+21 -5
View File
@@ -16,6 +16,7 @@
#include "display.h"
#include "doom_game.h"
#include "pm_sleep_guard.h"
#include <zephyr/device.h>
#include <zephyr/display/cfb.h>
@@ -47,6 +48,21 @@ LOG_MODULE_REGISTER(zephcore_display, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL);
static const struct device *disp_dev;
static bool disp_on;
/* The display goes on or off. No SoC light sleep while it is on: someone is
* using the UI (devdocs/lld/13-power-management.md, section 13). */
static void disp_set_on(bool on)
{
if (on == disp_on) {
return;
}
disp_on = on;
if (on) {
zc_pm_block_sleep();
} else {
zc_pm_unblock_sleep();
}
}
static bool disp_initialized;
static bool disp_rotated; /* panel flipped 180 degrees (upside-down mount) */
@@ -375,7 +391,7 @@ static void auto_off_handler(struct k_work *work)
* mc_display_on() → backlight restore. */
if (is_epd) {
backlight_set(false);
disp_on = false;
disp_set_on(false);
return;
}
if (disp_on) {
@@ -546,7 +562,7 @@ int mc_display_init(void)
}
display_blanking_off(disp_dev);
backlight_set(true);
disp_on = true;
disp_set_on(true);
disp_initialized = true;
/* Set up auto-off timer and schedule initial timeout */
@@ -606,7 +622,7 @@ void mc_display_on(void)
if (!is_epd) {
display_blanking_off(disp_dev);
}
disp_on = true;
disp_set_on(true);
}
backlight_set(true);
@@ -622,7 +638,7 @@ void mc_display_off(void)
if (disp_on) {
display_blanking_on(disp_dev);
backlight_set(false);
disp_on = false;
disp_set_on(false);
}
}
@@ -908,7 +924,7 @@ void mc_display_epd_full_reset(void)
/* After splash handoff, keep frontlight off; next user interaction
* wakes it via mc_display_on(). */
backlight_set(false);
disp_on = false;
disp_set_on(false);
}
const struct device *mc_display_get_device(void)
+5 -1
View File
@@ -11,6 +11,10 @@ native-Linux preset boards/linux_native/<preset>.conf carries a sibling
wifi: true # WiFi companion: RAM for WiFi + BLE together
light_sleep: true # DIO1 on an RTC-wake-capable GPIO AND
# validated on hardware (ESP32 repeaters)
light_sleep_companion: true # the companion build has light sleep
# (opt-in with `powersaving on`); needs
# the wake pins and pin holds, and the
# RAM. Independent of light_sleep
release: # absent = not published (bring-up)
roles: [companion, repeater] # REQUIRED in release
variants: # extra published builds of this board
@@ -51,7 +55,7 @@ PLATFORM_DIRS = {"nrf52840": "nrf52", "nrf54l": "nrf54l", "esp32": "esp32",
"mg24": "mg24", "stm32wl": "stm32wl"}
ROLES = ("companion", "repeater", "room_server", "observer")
KEYS = {"target", "capabilities", "release", "catalog", "linux"}
CAPS = {"light_sleep", "wifi"}
CAPS = {"light_sleep", "light_sleep_companion", "wifi"}
CATALOG_KEYS = {"device", "maker", "new", "img", "own_img", "subtitle"}
CAP_LINE = re.compile(r"^ (\w+): (true|false)\s*$", re.M)