mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 03:49:55 +00:00
observer lightsleep + companion opt-in
This commit is contained in:
+18
-3
@@ -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)
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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([]() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user