mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 22:49:59 +00:00
ESP32 light sleep (devdocs/lld/13-power-management.md)
Light sleep on the S3 repeaters never worked as designed, for two
independent reasons:
- No S3 repeater ever slept. Every one carries WiFi OTA, the WiFi
driver starts WiFi at boot (NULL mode), and a started WiFi library
holds its modem PM lock. Measured on the XIAO: zero sleep entries
with WiFi compiled in. helpers/pm_esp32_wake.c now stops WiFi at
boot on PM builds; `start ota` and a station connect start it again
(verified: AP up, sleep blocked for the session, sleeping after
`stop ota`).
- DIO1 was not a wake source. GPIO_INT_WAKEUP means EXT1 in Zephyr's
ESP32 driver: RTC pads only, one trigger polarity for all pins on
the S3, and pads left on the RTC mux after every sleep. Since 1.17.1
pm_esp32.overlay also armed the active-low user button; gpio-keys
configures first, so the driver refused the active-high DIO1.
The new helper arms the radio IRQ line and the user button (sw0) as
per-pin level GPIO wakes (ESP-IDF gpio_wakeup_enable, any pad) for the
length of each sleep, masks their CPU interrupt while in level mode,
and re-raises the interrupt a sleep swallowed through status_w1ts, so
the radio's dio1_isr and gpio-keys run as usual. EXT1 is no longer
used: patch 0003 configures DIO1 as a plain input again and drops the
d7cc4cb pad-restore notifier (preamble section rewritten). Non-RTC
DIO1 boards (XIAO GPIO39, Station G2 GPIO48) can now light-sleep.
Each light sleep is also bounded by the next esp_timer alarm
(pm_policy_next_custom_ticks, CONFIG_PM_CUSTOM_TICKS_HOOK), as ESP-IDF
does; the BLE controller's modem-sleep wake timer lives there.
Light sleep isolates every pad that is not held, RTC or digital, while
the radio keeps receiving. Holds (ESP32_GPIO_SLEEP_HOLD_EN, inert
without PM) now live in each board's own files: NSS and RESET on all
PM-capable boards, VFEM_Ctrl (GPIO7) and Vext on V4/V4.3/V4 R8/WTv2,
CPS on V4, RXEN on the XIAO. The shared overlay keeps only &rtc_timer
and the GPIO wake source; the wrong RTC_PERIPH explanations are gone.
Also:
- `powersaving` (upstream's command and replies) gates light sleep;
light-sleep builds default to on, and a stored 0 without the new
pwr_sav_set marker (the field did nothing before) becomes on once.
`powersaving off` persists and keeps the node (and its USB) awake.
- `get pm`: asleep %, sleeps, wake causes, per-pin wake counts.
- The console window reopens on every user-button press.
- XIAO ESP32-S3: GPIO21 is both the kit's user button and the board's
led0; the heartbeat drove it low and every sleep woke as a button
press. led0 is deleted, the button gets a pull-up.
- Station G2: button active-low with pull-up, as upstream reads it
(was active-high). Untested on hardware.
Verified on the XIAO ESP32-S3 as a light-sleep repeater on the live
mesh (repeat off, 2 dBm) against a T1000-E on the same channel: awake
20 min, ratio of floods heard 0.95; asleep 30.5 min (96.6 % of
uptime), 119 vs 119, ratio 1.00. TX: sent == flood + direct. Heltec
boards: build-only (no unit on the bench).
Companion light sleep stays off by default; the esp_timer bound makes
it possible. Spike on the XIAO (pm_esp32.conf by hand, BLE only): a
held connection survives, 35 min with a poll every minute and no
miss, the SoC asleep ~91 % between connection events, LoRa RX 130 vs
~130 on the T1000-E. Pairing while asleep fails; USB CDC and UI still
need locks before companions can ship with it.
Flash wear (storage)
The companion marked contacts dirty on is_new, but upstream's is_new
means "not in contacts[]": auto-add declined the node (type filter,
hop limit, full table). Every advert from such a node forced a 5 s
full rewrite of an unchanged contacts3, while a contact that really
was just added only got the liveness deadline. onAdvertRecv now
snapshots the stored record first. is_new never dirties. An addition
or a changed name, type or position flushes in 5 s, and a plain
re-advert waits the liveness deadline, now 1 h instead of 10 min.
A new weak hook, zephcore_persist_before_off(), flushes the deferred
writes before every clean reboot and power-off. It is called from
zephcore_power_off(), the ZephyrBoard reboot, bootloader and BLE-OTA
paths, the UI save-and-restart, the joystick restart and wifi_ota.
Off the main thread it posts MESH_EVENT_PERSIST and waits up to 5 s.
Before this, only CMD_REBOOT flushed.
The repeater saved its ACL on every admin login. It now saves only
when the role or secret changed; the secret is the ECDH of two fixed
keys, so re-logins store nothing new. The room server likewise skips
the lazy write unless the role, sync_since or secret changed.
zephcore_prefs_json_save() compares the serialised JSON with the file
and skips an identical rewrite. Not tested on hardware.
Builds and tests: role_builds.sh 10/10; the ESP32 PM set on the
combined tree (five Heltec repeaters, V4 R8, Station G2, XIAO repeater
and companion) 9/9, repeaters at 69-71 % dram0; board_manifest check
clean; host tests quick 114/114. heltec_companion dram0 at 99.80 %
(776 B left, +88 B for both changes together).
206 lines
7.1 KiB
C++
206 lines
7.1 KiB
C++
/*
|
|
* SPDX-License-Identifier: MIT
|
|
* RepeaterDataStore - Filesystem storage for repeater
|
|
*/
|
|
|
|
#include "RepeaterDataStore.h"
|
|
#include "../adapters/datastore/ZephyrFsFormat.h"
|
|
#include "../adapters/datastore/ZephyrFsUtil.h"
|
|
#include "../adapters/datastore/IdentityFile.h"
|
|
#include "../adapters/datastore/PrefsFile.h"
|
|
#include <helpers/PrefsCodec.h>
|
|
#include <helpers/LoRaConfig.h>
|
|
#include <zephyr/fs/fs.h>
|
|
#include <zephyr/sys/util.h>
|
|
#include <zephyr/logging/log.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
LOG_MODULE_REGISTER(zephcore_repeater_store, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL);
|
|
|
|
RepeaterDataStore::RepeaterDataStore() : _initialized(false) {
|
|
}
|
|
|
|
bool RepeaterDataStore::begin() {
|
|
if (_initialized) return true;
|
|
|
|
/* Create repeater directory if it doesn't exist */
|
|
struct fs_dirent entry;
|
|
int ret = fs_stat(BASE_PATH, &entry);
|
|
if (ret < 0) {
|
|
ret = fs_mkdir(BASE_PATH);
|
|
if (ret < 0 && ret != -EEXIST) {
|
|
LOG_ERR("Failed to create %s: %d", BASE_PATH, ret);
|
|
return false;
|
|
}
|
|
LOG_INF("Created %s directory", BASE_PATH);
|
|
}
|
|
|
|
_initialized = true;
|
|
LOG_INF("RepeaterDataStore initialized at %s", BASE_PATH);
|
|
return true;
|
|
}
|
|
|
|
const char* RepeaterDataStore::getBasePath() const { return BASE_PATH; }
|
|
|
|
bool RepeaterDataStore::hasRoleData() const {
|
|
char path[64];
|
|
|
|
/* Only THIS role's files count. A companion volume does not: the roles
|
|
* are deliberately not interchangeable, and a companion's contacts and
|
|
* blob cache would eat into the same 128 KB the repeater needs, so a
|
|
* repeater booting onto a companion volume formats it. The reverse
|
|
* already happens — ZephyrDataStore::hasPrefs() tests /lfs/new_prefs,
|
|
* which a repeater volume never has.
|
|
*
|
|
* Repeater, room server and observer DO share this store and base path;
|
|
* they use the same prefs layout, so switching among them keeps the
|
|
* node's identity, which is what an operator wants.
|
|
*
|
|
* Self-limiting: loadPrefs() persists defaults on boot 1 and main_*.cpp
|
|
* saves a generated identity on the same boot, so after one successful
|
|
* boot at least one of these exists and the check never fires again. */
|
|
static const char* const ours[] = { "prefs.json", "prefs", "_main.id" };
|
|
for (size_t i = 0; i < ARRAY_SIZE(ours); i++) {
|
|
snprintf(path, sizeof(path), "%s/%s", BASE_PATH, ours[i]);
|
|
if (zephcore_fs_exists(path)) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool RepeaterDataStore::loadIdentity(mesh::LocalIdentity& id) {
|
|
char path[48];
|
|
snprintf(path, sizeof(path), "%s/_main.id", BASE_PATH);
|
|
return zephcore_identity_load(path, id);
|
|
}
|
|
|
|
bool RepeaterDataStore::saveIdentity(const mesh::LocalIdentity& id) {
|
|
if (!_initialized) begin();
|
|
|
|
char path[48];
|
|
snprintf(path, sizeof(path), "%s/_main.id", BASE_PATH);
|
|
return zephcore_identity_save(path, id);
|
|
}
|
|
|
|
bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
|
|
char path[48];
|
|
snprintf(path, sizeof(path), "%s/prefs.json", BASE_PATH);
|
|
|
|
if (zephcore_prefs_json_load(path, prefs, serverPrefsFromJson)) {
|
|
LOG_INF("Loaded prefs from %s", path);
|
|
return true;
|
|
}
|
|
bool had_json = zephcore_fs_exists(path);
|
|
|
|
/* No usable prefs.json: the legacy binary file, read once and migrated.
|
|
* It is kept, so firmware from before prefs.json still boots with the
|
|
* settings as they were at the upgrade. With neither, the caller's prefs
|
|
* are the role's defaults (set in its constructor). Either way they are
|
|
* saved, so flash always holds a prefs file from boot 1 and later code
|
|
* (e.g. tempradio revert) can trust it without a "first run" case. */
|
|
char legacy[48];
|
|
snprintf(legacy, sizeof(legacy), "%s/prefs", BASE_PATH);
|
|
if (zephcore_fs_exists(legacy)) {
|
|
if (loadLegacyPrefs(legacy, prefs)) {
|
|
LOG_INF("loadPrefs: %s %s from %s", had_json ? "recovered" : "migrated",
|
|
path, legacy);
|
|
}
|
|
} else {
|
|
LOG_DBG("No prefs file at %s, saving the role defaults", path);
|
|
}
|
|
savePrefs(prefs);
|
|
return true;
|
|
}
|
|
|
|
bool RepeaterDataStore::loadLegacyPrefs(const char* path, NodePrefs& prefs) {
|
|
uint8_t buf[SERVER_PREFS_SIZE + 32];
|
|
size_t len = 0;
|
|
if (!zephcore_fs_read_file(path, buf, sizeof(buf), &len)) {
|
|
LOG_ERR("loadPrefs: read of %s failed, using defaults", path);
|
|
return false;
|
|
}
|
|
LOG_DBG("loadPrefs: file size = %d bytes", (int)len);
|
|
|
|
if (len >= 120) {
|
|
float freq, bw;
|
|
memcpy(&freq, &buf[72], sizeof(freq));
|
|
memcpy(&bw, &buf[116], sizeof(bw));
|
|
if (freq < ZC_RADIO_FREQ_MIN_MHZ || freq > ZC_RADIO_FREQ_MAX_MHZ ||
|
|
buf[112] < 5 || buf[112] > 12 ||
|
|
bw < ZC_RADIO_BW_MIN_KHZ || bw > ZC_RADIO_BW_MAX_KHZ) {
|
|
LOG_WRN("Invalid radio params in prefs, using defaults: freq=%.3f sf=%u bw=%.1f",
|
|
(double)freq, buf[112], (double)bw);
|
|
}
|
|
}
|
|
/* Radio params outside ZC_RADIO_* fall back to the LoRaConfig defaults,
|
|
* everything else is sanitized; fields past the end keep the caller's. */
|
|
serverPrefsDecode(prefs, buf, len);
|
|
|
|
LOG_DBG(" name='%s' freq=%.3f sf=%u bw=%.1f tx_pwr=%d",
|
|
prefs.node_name, (double)prefs.freq, prefs.sf, (double)prefs.bw, prefs.tx_power_dbm);
|
|
|
|
/* Old files (< 294 bytes) never saved the ZephCore extension fields, and
|
|
* stored path_hash_mode/loop_detect as zero padding: the repeater defaults. */
|
|
if (len < 294) {
|
|
prefs.rx_boost = 1;
|
|
prefs.path_hash_mode = 1;
|
|
prefs.loop_detect = LOOP_DETECT_MODERATE;
|
|
LOG_INF("loadPrefs: upgraded an old %d-byte prefs file", (int)len);
|
|
}
|
|
|
|
/* Before the repeater honoured gps_interval it ran a fixed 48 h, so a
|
|
* stored companion default (300) was never a choice: the repeater
|
|
* default instead. Legacy files only; prefs.json is past this. */
|
|
if (prefs.gps_interval == CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC) {
|
|
prefs.gps_interval = CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC;
|
|
}
|
|
/* Likewise gps_enabled: that firmware ran the GPS whatever it said. */
|
|
prefs.gps_enabled = 1;
|
|
prefs.gps_enabled_set = 1;
|
|
/* And powersaving, which it stored but never acted on. */
|
|
prefs.powersaving_set = 0;
|
|
powersaving_upgrade(&prefs);
|
|
return true;
|
|
}
|
|
|
|
bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) {
|
|
if (!_initialized) begin();
|
|
|
|
char path[48];
|
|
snprintf(path, sizeof(path), "%s/prefs.json", BASE_PATH);
|
|
if (!zephcore_prefs_json_save(path, prefs, serverPrefsToJson)) {
|
|
return false;
|
|
}
|
|
LOG_INF("Saved prefs to %s", path);
|
|
return true;
|
|
}
|
|
|
|
bool RepeaterDataStore::formatFileSystem() {
|
|
LOG_WRN("Factory reset: erasing all storage");
|
|
|
|
/* Erase the LittleFS *volume*, not just our files. The old loop walked
|
|
* /lfs/repeater/ with fs_unlink, which left the volume itself untouched:
|
|
* it could not recover a volume another firmware had written into (on
|
|
* nRF52840 the Adafruit core's filesystem overlaps the top of ours), and
|
|
* it left /lfs/settings, stale companion files and all of /ext behind.
|
|
* Shared with the companion so all four roles erase the same regions. */
|
|
bool mounted = zephcore_fs_format_all(nullptr);
|
|
if (!mounted) {
|
|
LOG_ERR("Factory reset: /lfs did not remount");
|
|
return false;
|
|
}
|
|
|
|
/* The format took /lfs/repeater with it. Re-create it now rather than
|
|
* relying on the reboot: the CLI defers the reset so the reply can be
|
|
* transmitted, and anything that saves in that window needs the dir. */
|
|
_initialized = false;
|
|
if (!begin()) {
|
|
LOG_ERR("Factory reset: could not re-create %s", BASE_PATH);
|
|
return false;
|
|
}
|
|
|
|
LOG_INF("Repeater data erased");
|
|
return true;
|
|
}
|