Compare commits

..

2 Commits

Author SHA1 Message Date
football2801 d900083398 chore(13e6): TEMP power-monitor telemetry headers
To validate the PIN_PWR rail-cut change (e2c9d8f) without a bench
multimeter, have the device report its previous cycle's awake time
and panel-init time on each poll:

  X-Prev-Awake-Ms       — millis() at the moment esp_deep_sleep_start
                          armed, last cycle. Total awake duration
                          since reset, ~5–10 s steady-state.
  X-Prev-Panel-Init-Ms  — duration of epd_init() last cycle. Spikes
                          here would suggest the rail isn't coming
                          back up cleanly after the GPIO-hold release.

Headers are sent only when the cached NVS values are non-zero (skips
the first boot under this firmware). All call sites marked `// TEMP:
power-monitor` for clean removal once the change is validated. Two
new NVS keys (tm_awk, tm_pin) sit alongside the existing ones; mock
Preferences extended with getUInt/putUInt to match.

Server side logs the headers via `device.poll.power_telemetry`
(separate commit in pictureFrame-webApp).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 14:15:37 -04:00
football2801 e2c9d8f1e4 feat(13e6): cut panel power rail in deep sleep via PIN_PWR + GPIO hold
The Waveshare board exposes PIN_PWR (GPIO 1) specifically so battery
designs can gate the panel rail between refreshes. Before this commit
PIN_PWR was driven HIGH at boot and never released, so the panel's
boost converter kept its quiescent draw (50–500 µA) through every
deep sleep. The e-ink particles are bistable so the displayed image
persists without VDD; dropping the rail is a free win.

Three pieces:
  • epd_sleep() drives PIN_PWR LOW after issuing the panel-internal
    DEEP_SLEEP command, then gpio_hold_en() latches the level so it
    survives the chip's RTC transition.
  • normal_operation_impl() calls gpio_deep_sleep_hold_en() just
    before esp_deep_sleep_start() so the per-pin hold extends through
    the deep sleep period itself (without this the holds release on
    the transition and the rail comes back up).
  • epd_setup_pins() calls gpio_hold_dis() at the very top to free
    PIN_PWR on wake before re-driving it HIGH; no-op on cold boot.

Tests: 47/47 pass. Added test/mocks/driver/gpio.h with no-op stubs so
the native test build links cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 14:05:24 -04:00
5 changed files with 82 additions and 1 deletions
+7
View File
@@ -117,6 +117,13 @@
// would silently keep displaying the prior owner's photos until the new
// owner happens to navigate to /setup/{mac}.
#define NVS_KEY_JUST_PROVISIONED "just_prov"
// TEMP: power-monitor — stores previous cycle's awake duration and
// epd_init() duration so the next boot can report them server-side as
// X-Prev-Awake-Ms / X-Prev-Panel-Init-Ms. Lets us verify PIN_PWR rail
// cut doesn't slow panel re-init or extend the awake window. Remove
// these keys + their reads/writes once the change is validated.
#define NVS_KEY_PREV_AWAKE_MS "tm_awk"
#define NVS_KEY_PREV_PANEL_INIT_MS "tm_pin"
// Bump when introducing a schema migration. Each new value can force a one-shot
// recovery action on first boot of the new firmware.
+39
View File
@@ -9,9 +9,11 @@
// The test build adds test/mocks to the include path via -iquote.
#include "epd_mock.h"
#include "esp_sleep.h"
#include "driver/gpio.h"
#else
#include "epd.h"
#include <esp_sleep.h>
#include <driver/gpio.h>
#include <mbedtls/sha256.h>
#endif
@@ -114,6 +116,12 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre
bool errBorder = prefs.getInt(NVS_KEY_ERR_BORDER, 0) != 0;
int schemaV = prefs.getInt(NVS_KEY_SCHEMA_V, 0);
bool justProvisioned = prefs.getInt(NVS_KEY_JUST_PROVISIONED, 0) != 0;
// TEMP: power-monitor — previous cycle's awake + panel-init durations.
// Reported as X-Prev-Awake-Ms / X-Prev-Panel-Init-Ms below. Remove
// along with the corresponding writes near deep_sleep_start + the
// epd_init() timing block once the PIN_PWR cut is validated.
uint32_t prevAwakeMs = prefs.getUInt(NVS_KEY_PREV_AWAKE_MS, 0);
uint32_t prevPanelInitMs = prefs.getUInt(NVS_KEY_PREV_PANEL_INIT_MS, 0);
prefs.end();
// Schema migration: on first boot under err-border-aware firmware, the
@@ -160,6 +168,17 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre
http.addHeader("X-Just-Provisioned", "1");
}
// TEMP: power-monitor — last cycle's awake + panel-init times.
// Lets us see, server-side, whether the PIN_PWR rail cut affects
// either. Send only if non-zero (skips the first boot after a
// firmware that didn't store them).
if (prevAwakeMs > 0) {
http.addHeader("X-Prev-Awake-Ms", String((unsigned long)prevAwakeMs));
}
if (prevPanelInitMs > 0) {
http.addHeader("X-Prev-Panel-Init-Ms", String((unsigned long)prevPanelInitMs));
}
const char* collectHeaders[] = { "X-Interval-Ms", "X-Image-Id", "X-Image-Sha256", "X-Claimed" };
http.collectHeaders(collectHeaders, 4);
int code = http.GET();
@@ -242,7 +261,12 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre
}
displayInitialized = true;
// TEMP: power-monitor — time the panel init, stored in NVS at
// end of cycle for next boot to report. Remove the timing
// wrapper when the PIN_PWR cut is validated.
uint32_t panelInitStart = millis();
epd_init();
uint32_t panelInitMs = millis() - panelInitStart;
File r = LittleFS.open(IMAGE_PATH, "r");
if (r) { epd_draw_image_from_file(r); r.close(); }
@@ -251,6 +275,7 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre
prefs.begin(NVS_NAMESPACE, false);
prefs.putInt(NVS_KEY_DRAW_NEEDED, 0);
prefs.putInt(NVS_KEY_ERR_BORDER, 0);
prefs.putUInt(NVS_KEY_PREV_PANEL_INIT_MS, panelInitMs); // TEMP
prefs.end();
}
@@ -354,6 +379,20 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre
// false → normal_operation_impl runs → the next poll fetches a fresh
// image, which doubles as a "force refresh" gesture.
esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_BTN_RESET, 0);
// Latch any gpio_hold_en pins through the deep sleep period.
// epd_sleep() cuts PIN_PWR LOW + holds it; without this call the
// hold releases on the RTC transition and the panel rail comes back
// up, losing the saving. Released per-pin in epd_setup_pins() on
// wake via gpio_hold_dis().
gpio_deep_sleep_hold_en();
// TEMP: power-monitor — millis() at this point is the total awake
// duration since boot. Next boot reads it back and reports as
// X-Prev-Awake-Ms. Remove when PIN_PWR cut is validated.
prefs.begin(NVS_NAMESPACE, false);
prefs.putUInt(NVS_KEY_PREV_AWAKE_MS, millis());
prefs.end();
esp_deep_sleep_start();
}
@@ -24,6 +24,7 @@
#include <qrcode.h>
#include <esp_task_wdt.h>
#include <esp_heap_caps.h>
#include <driver/gpio.h>
static constexpr uint16_t W = 1200;
static constexpr uint16_t H = 1600;
@@ -103,6 +104,12 @@ static void panel_reset() {
}
void epd_setup_pins() {
// Release the PIN_PWR hold latched from the previous deep sleep so we
// can drive it again. No-op on cold boot (nothing was held). Paired
// with gpio_hold_en() in epd_sleep() and gpio_deep_sleep_hold_en() in
// operation.h.
gpio_hold_dis((gpio_num_t)PIN_PWR);
pinMode(PIN_PWR, OUTPUT);
pinMode(PIN_RST, OUTPUT);
pinMode(PIN_DC, OUTPUT);
@@ -191,6 +198,16 @@ void epd_sleep() {
SPI.transfer(0xA5); // sentinel
cs_both(HIGH);
s_initialized = false;
// Cut the panel power rail. The Waveshare board exposes PIN_PWR
// specifically for battery operation — the e-ink image persists
// without VDD (particles are bistable), and dropping the rail kills
// the boost converter's quiescent draw (~50500 µA depending on the
// load on the rail). Latch LOW so it survives deep sleep; paired with
// gpio_deep_sleep_hold_en() in operation.h just before the chip
// enters sleep.
digitalWrite(PIN_PWR, LOW);
gpio_hold_en((gpio_num_t)PIN_PWR);
}
// ── Draw helpers ───────────────────────────────────────────────────────────────
+8 -1
View File
@@ -8,6 +8,7 @@ extern int g_prefs_putint_seq; // sequence position of last putInt call
struct Preferences {
std::map<std::string, int32_t> ints;
std::map<std::string, uint32_t> uints;
std::map<std::string, std::string> strings;
bool _open = false;
@@ -26,10 +27,16 @@ struct Preferences {
g_call_seq++;
}
uint32_t getUInt(const char* key, uint32_t def = 0) {
auto it = uints.find(key);
return it != uints.end() ? it->second : def;
}
void putUInt(const char* key, uint32_t val) { uints[key] = val; }
String getString(const char* key, const char* def = "") {
auto it = strings.find(key);
return it != strings.end() ? String(it->second) : String(def);
}
void putString(const char* key, const String& val) { strings[key] = val._s; }
void clear() { ints.clear(); strings.clear(); }
void clear() { ints.clear(); uints.clear(); strings.clear(); }
};
+11
View File
@@ -0,0 +1,11 @@
#pragma once
// Native-test stubs for gpio_hold_* — operation.h calls
// gpio_deep_sleep_hold_en() before esp_deep_sleep_start() to latch
// PIN_PWR LOW through deep sleep. epd_driver.cpp (not built natively)
// also uses gpio_hold_en / gpio_hold_dis. Stubs let tests link.
#include "esp_sleep.h" // for gpio_num_t
inline void gpio_hold_en(gpio_num_t) {}
inline void gpio_hold_dis(gpio_num_t) {}
inline void gpio_deep_sleep_hold_en() {}
inline void gpio_deep_sleep_hold_dis() {}