mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 21:49:55 +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).
360 lines
8.5 KiB
C++
360 lines
8.5 KiB
C++
/*
|
|
* SPDX-License-Identifier: MIT
|
|
* PrefsJson - prefs.json for both roles. See PrefsJson.h.
|
|
*/
|
|
|
|
#include "PrefsJson.h"
|
|
|
|
namespace {
|
|
|
|
/* radio{} and gps{}: the same keys in both of upstream's trees. Not written:
|
|
* "cad" (ZephCore always runs CAD), "agc_int" (periodic AGC reset removed),
|
|
* "fem_txgain" (no such setting here). */
|
|
class RadioJson : public ConfigSerializer {
|
|
NodePrefs *_p;
|
|
|
|
protected:
|
|
void structure() override
|
|
{
|
|
def("freq", _p->freq);
|
|
def("bw", _p->bw);
|
|
def("sf", _p->sf);
|
|
def("cr", _p->cr);
|
|
def("int_thr", _p->interference_threshold);
|
|
def("rxgain", _p->rx_boost);
|
|
def("fem_rxgain", _p->fem_rxgain);
|
|
def("tx", _p->tx_power_dbm);
|
|
def("af", _p->airtime_factor);
|
|
def("rxdelay", _p->rx_delay_base);
|
|
def("f_txdelay", _p->tx_delay_factor);
|
|
def("d_txdelay", _p->direct_tx_delay_factor);
|
|
def("hash_mode", _p->path_hash_mode);
|
|
def("multi_ack", _p->multi_acks);
|
|
}
|
|
|
|
public:
|
|
explicit RadioJson(NodePrefs *p) : _p(p) {}
|
|
};
|
|
|
|
class GpsJson : public ConfigSerializer {
|
|
NodePrefs *_p;
|
|
|
|
protected:
|
|
void structure() override
|
|
{
|
|
def("en", _p->gps_enabled);
|
|
def("int", _p->gps_interval);
|
|
def("adv_loc", _p->advert_loc_policy);
|
|
}
|
|
|
|
public:
|
|
explicit GpsJson(NodePrefs *p) : _p(p) {}
|
|
};
|
|
|
|
/* zc{}: ZephCore's own settings, common to both roles */
|
|
class ZcCommonJson : public ConfigSerializer {
|
|
protected:
|
|
NodePrefs *_p;
|
|
|
|
void structure() override
|
|
{
|
|
def("leds_off", _p->leds_disabled);
|
|
def("leds_radio", _p->leds_radio_mode);
|
|
def("leds_hb", _p->leds_hb_mode);
|
|
def("rx_dc", _p->rx_duty_cycle);
|
|
def("mts", _p->meshtimesync);
|
|
def("cad_auto", _p->cad_auto);
|
|
def("cad_off", _p->cad_offset);
|
|
def("cad_base", _p->cad_base);
|
|
def("probe_int", _p->probe_interval);
|
|
def("busycap", _p->cad_busycap);
|
|
def("xsf", (void *)_p->extra_sf, EXTRA_SF_MAX);
|
|
def("disp_rot", _p->display_rotate);
|
|
def("in_rot", _p->input_rotate);
|
|
def("pwr_sav_set", _p->powersaving_set);
|
|
}
|
|
|
|
public:
|
|
explicit ZcCommonJson(NodePrefs *p) : _p(p) {}
|
|
};
|
|
|
|
/* ── Companion: upstream examples/companion_radio/NodePrefs.h ──────── */
|
|
|
|
class RepeatDisableJson : public ConfigSerializer {
|
|
protected:
|
|
void structure() override { def("disable", disable); }
|
|
|
|
public:
|
|
uint8_t disable = 1;
|
|
};
|
|
|
|
class CompJson : public ConfigSerializer {
|
|
NodePrefs *_p;
|
|
|
|
protected:
|
|
void structure() override
|
|
{
|
|
def("auto_max", _p->autoadd_max_hops);
|
|
def("defs_nm", _p->default_scope_name, sizeof(_p->default_scope_name));
|
|
def("defs_key", (void *)_p->default_scope_key, sizeof(_p->default_scope_key));
|
|
def("pin", _p->ble_pin);
|
|
def("buzz_q", _p->buzzer_quiet);
|
|
def("auto_add", _p->autoadd_config);
|
|
def("man_add", _p->manual_add_contacts);
|
|
def("tel_base", _p->telemetry_mode_base);
|
|
def("tel_loc", _p->telemetry_mode_loc);
|
|
def("tel_env", _p->telemetry_mode_env);
|
|
def("tz_offset", _p->tz_offset);
|
|
}
|
|
|
|
public:
|
|
explicit CompJson(NodePrefs *p) : _p(p) {}
|
|
};
|
|
|
|
class WifiJson : public ConfigSerializer {
|
|
NodePrefs *_p;
|
|
|
|
protected:
|
|
void structure() override
|
|
{
|
|
def("ssid", _p->wifi_ssid, sizeof(_p->wifi_ssid));
|
|
def("pwd", _p->wifi_pwd, sizeof(_p->wifi_pwd));
|
|
def("enabled", _p->wifi_enabled);
|
|
}
|
|
|
|
public:
|
|
explicit WifiJson(NodePrefs *p) : _p(p) {}
|
|
};
|
|
|
|
class ZcCompanionJson : public ZcCommonJson {
|
|
protected:
|
|
void structure() override
|
|
{
|
|
ZcCommonJson::structure();
|
|
def("ble_off", _p->ble_disabled);
|
|
def("bright", _p->display_brightness);
|
|
def("wake", _p->wake_on_msg);
|
|
def("scr_off", _p->screen_off_secs);
|
|
def("auto_off_mv", _p->auto_shutdown_mv);
|
|
def("aoff_set", _p->auto_shutdown_set);
|
|
def("vc_en", _p->v_contact_enabled);
|
|
def("vc_flags", _p->v_contact_flags);
|
|
def("vbat_mv", _p->v_battery_alert_mv);
|
|
def("adc_mult", _p->adc_multiplier);
|
|
def("pwr_sav_en", _p->powersaving_enabled);
|
|
}
|
|
|
|
public:
|
|
explicit ZcCompanionJson(NodePrefs *p) : ZcCommonJson(p) {}
|
|
};
|
|
|
|
class CompanionJson : public ConfigSerializer {
|
|
NodePrefs *_p;
|
|
RadioJson _radio;
|
|
GpsJson _gps;
|
|
RepeatDisableJson _repeat;
|
|
CompJson _comp;
|
|
WifiJson _wifi;
|
|
ZcCompanionJson _zc;
|
|
|
|
protected:
|
|
void structure() override
|
|
{
|
|
def("name", _p->node_name, sizeof(_p->node_name));
|
|
def("lat", _p->node_lat);
|
|
def("lon", _p->node_lon);
|
|
def("radio", _radio);
|
|
def("gps", _gps);
|
|
def("repeat", _repeat);
|
|
def("comp", _comp);
|
|
def("wifi", _wifi);
|
|
def("zc", _zc);
|
|
}
|
|
|
|
public:
|
|
explicit CompanionJson(NodePrefs *p)
|
|
: _p(p), _radio(p), _gps(p), _comp(p), _wifi(p), _zc(p)
|
|
{
|
|
/* upstream: repeat.disable; ours: client_repeat (1 = forward) */
|
|
_repeat.disable = p->client_repeat ? 0 : 1;
|
|
}
|
|
|
|
bool save(Stream &s) { return saveSerial(s); }
|
|
bool load(Stream &s)
|
|
{
|
|
bool ok = loadSerial(s);
|
|
|
|
_p->client_repeat = _repeat.disable ? 0 : 1;
|
|
return ok;
|
|
}
|
|
};
|
|
|
|
/* ── Servers: upstream helpers/CommonCLI.h ──────────────────────────── */
|
|
|
|
class RepeatJson : public ConfigSerializer {
|
|
NodePrefs *_p;
|
|
|
|
protected:
|
|
void structure() override
|
|
{
|
|
def("disable", _p->disable_fwd);
|
|
def("f_max", _p->flood_max);
|
|
def("f_max_uns", _p->flood_max_unscoped);
|
|
def("f_max_adv", _p->flood_max_advert);
|
|
def("loop", _p->loop_detect);
|
|
}
|
|
|
|
public:
|
|
explicit RepeatJson(NodePrefs *p) : _p(p) {}
|
|
};
|
|
|
|
class RoomJson : public ConfigSerializer {
|
|
NodePrefs *_p;
|
|
|
|
protected:
|
|
void structure() override { def("rd_only", _p->allow_read_only); }
|
|
|
|
public:
|
|
explicit RoomJson(NodePrefs *p) : _p(p) {}
|
|
};
|
|
|
|
class PowerJson : public ConfigSerializer {
|
|
NodePrefs *_p;
|
|
|
|
protected:
|
|
void structure() override
|
|
{
|
|
def("adc_mult", _p->adc_multiplier);
|
|
def("pwr_sav_en", _p->powersaving_enabled);
|
|
}
|
|
|
|
public:
|
|
explicit PowerJson(NodePrefs *p) : _p(p) {}
|
|
};
|
|
|
|
class ZcServerJson : public ZcCommonJson {
|
|
protected:
|
|
void structure() override
|
|
{
|
|
ZcCommonJson::structure();
|
|
def("tz_offset", _p->tz_offset);
|
|
def("backoff", _p->backoff_multiplier);
|
|
def("disc_ts", _p->discovery_mod_timestamp);
|
|
def("gps_set", _p->gps_enabled_set);
|
|
}
|
|
|
|
public:
|
|
explicit ZcServerJson(NodePrefs *p) : ZcCommonJson(p) {}
|
|
};
|
|
|
|
class ServerJson : public ConfigSerializer {
|
|
NodePrefs *_p;
|
|
RadioJson _radio;
|
|
GpsJson _gps;
|
|
RepeatJson _repeat;
|
|
RoomJson _room;
|
|
PowerJson _power;
|
|
ZcServerJson _zc;
|
|
|
|
protected:
|
|
void structure() override
|
|
{
|
|
def("name", _p->node_name, sizeof(_p->node_name));
|
|
def("pass", _p->password, sizeof(_p->password));
|
|
def("guest", _p->guest_password, sizeof(_p->guest_password));
|
|
def("owner", _p->owner_info, sizeof(_p->owner_info));
|
|
def("adv_int", _p->advert_interval);
|
|
def("f_adv_int", _p->flood_advert_interval);
|
|
def("lat", _p->node_lat);
|
|
def("lon", _p->node_lon);
|
|
def("radio", _radio);
|
|
def("gps", _gps);
|
|
def("repeat", _repeat);
|
|
def("room", _room);
|
|
def("power", _power);
|
|
def("zc", _zc);
|
|
}
|
|
|
|
public:
|
|
explicit ServerJson(NodePrefs *p)
|
|
: _p(p), _radio(p), _gps(p), _repeat(p), _room(p), _power(p), _zc(p) {}
|
|
|
|
bool save(Stream &s) { return saveSerial(s); }
|
|
bool load(Stream &s) { return loadSerial(s); }
|
|
};
|
|
|
|
void saneRadio(NodePrefs &p)
|
|
{
|
|
if (p.freq < ZC_RADIO_FREQ_MIN_MHZ || p.freq > ZC_RADIO_FREQ_MAX_MHZ ||
|
|
p.sf < 5 || p.sf > 12 ||
|
|
p.bw < ZC_RADIO_BW_MIN_KHZ || p.bw > ZC_RADIO_BW_MAX_KHZ) {
|
|
p.freq = mesh::LoRaConfig::FREQ_MHZ;
|
|
p.bw = mesh::LoRaConfig::BANDWIDTH_KHZ;
|
|
p.sf = mesh::LoRaConfig::SPREADING_FACTOR;
|
|
p.cr = mesh::LoRaConfig::CODING_RATE;
|
|
p.tx_power_dbm = mesh::LoRaConfig::TX_POWER_DBM;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
/* Writing only reads the struct; the serializer API takes references. */
|
|
|
|
bool companionPrefsToJson(const NodePrefs &p, Stream &out)
|
|
{
|
|
CompanionJson j(const_cast<NodePrefs *>(&p));
|
|
|
|
return j.save(out);
|
|
}
|
|
|
|
bool serverPrefsToJson(const NodePrefs &p, Stream &out)
|
|
{
|
|
ServerJson j(const_cast<NodePrefs *>(&p));
|
|
|
|
return j.save(out);
|
|
}
|
|
|
|
bool companionPrefsFromJson(NodePrefs &p, Stream &in)
|
|
{
|
|
NodePrefs t = p;
|
|
CompanionJson j(&t);
|
|
|
|
/* Absent keys keep the caller's value: clear the marker first, so a file
|
|
* from before it existed is recognised (NodePrefs.h). */
|
|
t.auto_shutdown_set = 0;
|
|
t.powersaving_set = 0;
|
|
if (!j.load(in)) {
|
|
return false;
|
|
}
|
|
auto_shutdown_upgrade(&t);
|
|
powersaving_upgrade(&t);
|
|
saneRadio(t);
|
|
sanitizeNodePrefs(&t);
|
|
p = t;
|
|
return true;
|
|
}
|
|
|
|
bool serverPrefsFromJson(NodePrefs &p, Stream &in)
|
|
{
|
|
NodePrefs t = p;
|
|
ServerJson j(&t);
|
|
|
|
/* Absent keys keep the caller's value, so clear the marker first: a
|
|
* file from before it existed was written by firmware that ran the GPS
|
|
* regardless of gps.en, and keeps doing so (NodePrefs.h). */
|
|
t.gps_enabled_set = 0;
|
|
t.powersaving_set = 0;
|
|
if (!j.load(in)) {
|
|
return false;
|
|
}
|
|
if (!t.gps_enabled_set) {
|
|
t.gps_enabled = 1;
|
|
t.gps_enabled_set = 1;
|
|
}
|
|
powersaving_upgrade(&t);
|
|
saneRadio(t);
|
|
sanitizeNodePrefs(&t);
|
|
p = t;
|
|
return true;
|
|
}
|