Add back-photo button; consolidate reset/manage onto one hold-duration button
Build and push server image / build-and-push (push) Successful in 32s
Build and push server image / build-and-push (push) Successful in 32s
Back button (new GPIO0, POST /frame/back): the server now tracks a bounded history of previously-current photos (photo_queue.py), pushed to on every advance (auto or forced) and popped by back_forced() -- symmetric with advance, so pressing next afterwards returns to right where you were. frame_client.c's force_advance bool becomes a 3-way fetch_action_t (NORMAL/ADVANCE/BACK) threaded through the whole fetch path. Also folds the separate reset and manage buttons onto one pin (combo_button.c, replacing reset_button.c/manage_button.c entirely), disambiguated by hold duration: quick press shows the management menu (unchanged), ~3s hold-then-release soft-resets (esp_restart(), config kept -- new), ~15s hold factory-resets (today's old reset behavior, extended from 10s for clearer tier separation). Driven by a production board (Seeed XIAO ESP32-C6) exposing only 3 of the ESP32-C6's 8 deep-sleep-wakeup-capable GPIOs -- next/back keep their own dedicated pins where instant response matters most, everything else shares the third pin via timing instead of needing its own. Same three-pin layout now works on both the dev board and the production board. Fixed a fast-tap bug in combo_button_check() before shipping: it only did a live gpio_get_level() read to decide whether the button was pressed at all, so a press fast enough to already be released by the time boot reached that check was missed entirely (treated as "never pressed" rather than "quick press"). Added the same latched esp_sleep_get_gpio_wakeup_status() check the other buttons already use for exactly this reason.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c qr_onboarding.c status_screen.c epd_draw.c reset_button.c next_button.c manage_button.c manage_qr_overlay.c
|
||||
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c qr_onboarding.c status_screen.c epd_draw.c next_button.c back_button.c combo_button.c manage_qr_overlay.c
|
||||
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_netif esp_http_server esp_http_client mbedtls dns_server epd7in3e qrcode epaper_fonts esp_driver_gpio
|
||||
EMBED_FILES root.html
|
||||
EMBED_TXTFILES certs/tools_server_ca.pem)
|
||||
|
||||
@@ -83,30 +83,6 @@ menu "ESPresso Frame Configuration"
|
||||
server was unreachable or the fetch/display failed, instead of
|
||||
waiting the full FRAME_SLEEP_INTERVAL_S.
|
||||
|
||||
config FRAME_RESET_BUTTON_GPIO
|
||||
int "Factory-reset button GPIO (-1 to disable)"
|
||||
default 3
|
||||
range -1 7
|
||||
help
|
||||
Button wired between this GPIO and GND (active-low, internal
|
||||
pull-up enabled in firmware -- no external resistor needed).
|
||||
Holding it for FRAME_RESET_BUTTON_HOLD_MS clears the stored
|
||||
WiFi/server config and restarts the device into provisioning
|
||||
mode. Must be GPIO 0-7 -- the only pins the ESP32-C6 can use as
|
||||
an EXT1 deep-sleep wakeup source, which is what lets a press
|
||||
wake the device promptly instead of only being noticed during
|
||||
its brief awake windows. Set to -1 to disable the feature
|
||||
entirely.
|
||||
|
||||
config FRAME_RESET_BUTTON_HOLD_MS
|
||||
int "Factory-reset button hold duration (ms)"
|
||||
default 10000
|
||||
depends on FRAME_RESET_BUTTON_GPIO >= 0
|
||||
help
|
||||
How long the reset button must be held continuously before the
|
||||
device clears its stored config and reboots into provisioning.
|
||||
Long enough that it won't trigger by accident.
|
||||
|
||||
config FRAME_NEXT_BUTTON_GPIO
|
||||
int "Next-photo button GPIO (-1 to disable)"
|
||||
default 2
|
||||
@@ -117,23 +93,66 @@ menu "ESPresso Frame Configuration"
|
||||
Pressing it wakes the device (if asleep), forces the server to
|
||||
advance to the next photo immediately (POST /frame/advance)
|
||||
regardless of the configured refresh interval, and displays
|
||||
it. Must be GPIO 0-7 for the same deep-sleep-wakeup reason as
|
||||
FRAME_RESET_BUTTON_GPIO above; defaults to a different pin
|
||||
than the reset button. Set to -1 to disable the feature.
|
||||
it. Must be GPIO 0-7 -- the only pins the ESP32-C6 can use as
|
||||
a deep-sleep GPIO wakeup source, which is what lets a press
|
||||
wake the device promptly instead of only being noticed during
|
||||
its brief awake windows. Set to -1 to disable the feature.
|
||||
|
||||
config FRAME_MANAGE_BUTTON_GPIO
|
||||
int "Manage button GPIO (-1 to disable)"
|
||||
config FRAME_BACK_BUTTON_GPIO
|
||||
int "Back-photo button GPIO (-1 to disable)"
|
||||
default 0
|
||||
range -1 7
|
||||
help
|
||||
Button wired between this GPIO and GND (active-low, internal
|
||||
pull-up enabled in firmware -- no external resistor needed).
|
||||
Pressing it wakes the device (if asleep), forces the server
|
||||
to return to the previously-current photo immediately
|
||||
(POST /frame/back), and displays it. Pressing next
|
||||
afterwards returns to where you were before pressing back.
|
||||
Must be GPIO 0-7 for the same deep-sleep-wakeup reason as
|
||||
FRAME_NEXT_BUTTON_GPIO above; defaults to a different pin
|
||||
than the other buttons. Set to -1 to disable the feature.
|
||||
|
||||
config FRAME_COMBO_BUTTON_GPIO
|
||||
int "Menu/reset button GPIO (-1 to disable)"
|
||||
default 1
|
||||
range -1 7
|
||||
help
|
||||
Button wired between this GPIO and GND (active-low, internal
|
||||
pull-up enabled in firmware -- no external resistor needed).
|
||||
Pressing it wakes the device (if asleep), displays the
|
||||
current photo with a small "scan to manage" QR code overlaid
|
||||
in the top-right corner (linking to the tools server's config
|
||||
page) for 30 seconds, then reverts to the plain photo. Must
|
||||
be GPIO 0-7 for the same deep-sleep-wakeup reason as
|
||||
FRAME_RESET_BUTTON_GPIO above; defaults to a different pin
|
||||
than the other two buttons. Set to -1 to disable the feature.
|
||||
One pin, three actions depending on how long it's held:
|
||||
a quick press shows the management menu (same as before);
|
||||
holding it FRAME_COMBO_SOFT_RESET_HOLD_MS then releasing
|
||||
soft-resets the device (reboots, keeps the stored WiFi/
|
||||
server config); holding it all the way to
|
||||
FRAME_COMBO_FACTORY_RESET_HOLD_MS clears the stored config
|
||||
and restarts into provisioning, regardless of whether it's
|
||||
released yet. Must be GPIO 0-7 for the same deep-sleep-
|
||||
wakeup reason as FRAME_NEXT_BUTTON_GPIO above; defaults to
|
||||
a different pin than the other buttons. Set to -1 to
|
||||
disable the feature entirely (also disables the management
|
||||
menu, both reset tiers, and factory-reset-via-button --
|
||||
reconfiguring then only works by erasing NVS over USB, see
|
||||
firmware/README.md).
|
||||
|
||||
config FRAME_COMBO_SOFT_RESET_HOLD_MS
|
||||
int "Soft-reset hold duration (ms)"
|
||||
default 3000
|
||||
depends on FRAME_COMBO_BUTTON_GPIO >= 0
|
||||
help
|
||||
How long the menu/reset button must be held before releasing
|
||||
it triggers a soft reset (reboot, config kept). Long enough
|
||||
to be clearly distinct from a quick menu-opening press.
|
||||
|
||||
config FRAME_COMBO_FACTORY_RESET_HOLD_MS
|
||||
int "Factory-reset hold duration (ms)"
|
||||
default 15000
|
||||
depends on FRAME_COMBO_BUTTON_GPIO >= 0
|
||||
help
|
||||
How long the menu/reset button must be held continuously
|
||||
before the device clears its stored config and reboots into
|
||||
provisioning, regardless of release. Comfortably longer than
|
||||
FRAME_COMBO_SOFT_RESET_HOLD_MS so the two tiers can't be
|
||||
confused for each other.
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_sleep.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "back_button.h"
|
||||
|
||||
static const char *TAG = "back_button";
|
||||
|
||||
#if CONFIG_FRAME_BACK_BUTTON_GPIO >= 0
|
||||
|
||||
#define BACK_BUTTON_GPIO ((gpio_num_t)CONFIG_FRAME_BACK_BUTTON_GPIO)
|
||||
#define BACK_BUTTON_DEBOUNCE_MS 20
|
||||
#define BACK_BUTTON_DEBOUNCE_CHECKS 3
|
||||
|
||||
void back_button_init(void)
|
||||
{
|
||||
/* See next_button.c's next_button_init() for why this has to run
|
||||
* before gpio_config() -- a deep sleep with this pin armed as a
|
||||
* wakeup source leaves it "held," and nothing un-holds it on wake
|
||||
* except explicitly asking. */
|
||||
gpio_hold_dis(BACK_BUTTON_GPIO);
|
||||
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = 1ULL << BACK_BUTTON_GPIO,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
|
||||
/* See next_button.c for why this API (not ext1) -- it manages the
|
||||
* pull resistor across the sleep transition itself, so the pin
|
||||
* doesn't float and wake the device spuriously. */
|
||||
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << BACK_BUTTON_GPIO, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
}
|
||||
|
||||
bool back_button_check(void)
|
||||
{
|
||||
/* A quick tap can easily release before this runs (~0.4-0.5s into
|
||||
* boot, confirmed on hardware -- a live gpio_get_level() check here
|
||||
* missed real presses that had already woken the device). The wakeup
|
||||
* status register is latched at the moment of waking and isn't
|
||||
* cleared until the next sleep entry, so it reliably reflects a tap
|
||||
* regardless of how quickly it was released. */
|
||||
if (esp_sleep_get_gpio_wakeup_status() & (1ULL << BACK_BUTTON_GPIO)) {
|
||||
ESP_LOGI(TAG, "Back-photo button caused this wake, going back");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Not a GPIO-wakeup-from-this-pin boot (normal timer wake, or a fresh
|
||||
* power-on/reflash) -- fall back to a live, debounced level check so
|
||||
* holding the button down while powering on also works. */
|
||||
if (gpio_get_level(BACK_BUTTON_GPIO) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < BACK_BUTTON_DEBOUNCE_CHECKS; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(BACK_BUTTON_DEBOUNCE_MS));
|
||||
if (gpio_get_level(BACK_BUTTON_GPIO) != 0) {
|
||||
return false; /* noise, not a real press */
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Back-photo button held during power-on, going back");
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void back_button_init(void) {}
|
||||
bool back_button_check(void) { return false; }
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Configures the back-photo button GPIO (CONFIG_FRAME_BACK_BUTTON_GPIO,
|
||||
* active-low with internal pull-up) and arms it as a deep-sleep wakeup
|
||||
* source, same as next_button_init(). Call once, early in app_main(),
|
||||
* before the device might enter deep sleep.
|
||||
*
|
||||
* A no-op if CONFIG_FRAME_BACK_BUTTON_GPIO is negative (button disabled).
|
||||
*/
|
||||
void back_button_init(void);
|
||||
|
||||
/**
|
||||
* Returns whether the back-photo button is currently held, debounced with
|
||||
* a couple of short re-checks to reject noise. Same immediate-response
|
||||
* reasoning as the next-photo button -- no long hold-to-confirm gate.
|
||||
*/
|
||||
bool back_button_check(void);
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_sleep.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "wifi_provisioning.h"
|
||||
|
||||
#include "combo_button.h"
|
||||
|
||||
static const char *TAG = "combo_button";
|
||||
|
||||
#if CONFIG_FRAME_COMBO_BUTTON_GPIO >= 0
|
||||
|
||||
#define COMBO_BUTTON_GPIO ((gpio_num_t)CONFIG_FRAME_COMBO_BUTTON_GPIO)
|
||||
#define COMBO_BUTTON_POLL_MS 100
|
||||
|
||||
void combo_button_init(void)
|
||||
{
|
||||
/* See next_button.c's next_button_init() for why this has to run
|
||||
* before gpio_config() -- a deep sleep with this pin armed as a
|
||||
* wakeup source leaves it "held," and nothing un-holds it on wake
|
||||
* except explicitly asking. */
|
||||
gpio_hold_dis(COMBO_BUTTON_GPIO);
|
||||
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = 1ULL << COMBO_BUTTON_GPIO,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
|
||||
/* See next_button.c for why this API (not ext1) -- it manages the
|
||||
* pull resistor across the sleep transition itself, so the pin
|
||||
* doesn't float and wake the device spuriously. */
|
||||
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << COMBO_BUTTON_GPIO, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
}
|
||||
|
||||
bool combo_button_check(void)
|
||||
{
|
||||
/* A quick tap can easily release before we get here (~tens to
|
||||
* hundreds of ms into boot, same issue the other buttons already
|
||||
* hit and fixed -- confirmed on hardware: a fast press was being
|
||||
* missed entirely with just a live level check here). The wakeup
|
||||
* status register is latched at the moment of waking and isn't
|
||||
* cleared until the next sleep entry, so it still confirms this pin
|
||||
* caused the wake even if it's since been released -- in which case
|
||||
* the poll loop below simply measures 0ms held, correctly resolving
|
||||
* to a quick press rather than "not pressed at all." */
|
||||
bool caused_wake = esp_sleep_get_gpio_wakeup_status() & (1ULL << COMBO_BUTTON_GPIO);
|
||||
if (!caused_wake && gpio_get_level(COMBO_BUTTON_GPIO) != 0) {
|
||||
return false; /* not pressed, and didn't cause this wake either */
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Combo button held -- quick press for menu, %dms for soft reset, %dms for factory reset",
|
||||
CONFIG_FRAME_COMBO_SOFT_RESET_HOLD_MS, CONFIG_FRAME_COMBO_FACTORY_RESET_HOLD_MS);
|
||||
|
||||
int elapsed_ms = 0;
|
||||
while (gpio_get_level(COMBO_BUTTON_GPIO) == 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(COMBO_BUTTON_POLL_MS));
|
||||
elapsed_ms += COMBO_BUTTON_POLL_MS;
|
||||
if (elapsed_ms >= CONFIG_FRAME_COMBO_FACTORY_RESET_HOLD_MS) {
|
||||
/* Fires immediately, doesn't wait for release -- same
|
||||
* convention as the old dedicated reset button. */
|
||||
ESP_LOGW(TAG, "Held %dms, clearing config and restarting into provisioning",
|
||||
CONFIG_FRAME_COMBO_FACTORY_RESET_HOLD_MS);
|
||||
frame_config_clear();
|
||||
esp_restart();
|
||||
}
|
||||
}
|
||||
|
||||
if (elapsed_ms >= CONFIG_FRAME_COMBO_SOFT_RESET_HOLD_MS) {
|
||||
ESP_LOGW(TAG, "Held %dms and released, soft-restarting (config kept)", elapsed_ms);
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Quick press (%dms), showing management menu", elapsed_ms);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool combo_button_is_pressed(void)
|
||||
{
|
||||
return gpio_get_level(COMBO_BUTTON_GPIO) == 0;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void combo_button_init(void) {}
|
||||
bool combo_button_check(void) { return false; }
|
||||
bool combo_button_is_pressed(void) { return false; }
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Configures the combined menu/reset button GPIO
|
||||
* (CONFIG_FRAME_COMBO_BUTTON_GPIO, active-low with internal pull-up) and
|
||||
* arms it as a deep-sleep wakeup source, same as the other buttons. Call
|
||||
* once, early in app_main(), before the device might enter deep sleep.
|
||||
*
|
||||
* A no-op if CONFIG_FRAME_COMBO_BUTTON_GPIO is negative (button disabled).
|
||||
*/
|
||||
void combo_button_init(void);
|
||||
|
||||
/**
|
||||
* Checks the combined menu/reset button and acts on how long it was
|
||||
* held, evaluated once per wake:
|
||||
* - Not pressed: returns false immediately.
|
||||
* - Released before CONFIG_FRAME_COMBO_SOFT_RESET_HOLD_MS (a quick
|
||||
* press): returns true -- caller should show the management menu.
|
||||
* - Released between the soft-reset and factory-reset thresholds: a
|
||||
* soft reset (esp_restart(), stored WiFi/server config kept) --
|
||||
* never returns.
|
||||
* - Held through CONFIG_FRAME_COMBO_FACTORY_RESET_HOLD_MS: a factory
|
||||
* reset (frame_config_clear() + esp_restart(), fires immediately
|
||||
* without waiting for release) -- never returns.
|
||||
*/
|
||||
bool combo_button_check(void);
|
||||
|
||||
/**
|
||||
* Returns whether the combined button is currently held, with no
|
||||
* debounce and no hold-duration interpretation -- a bare live level
|
||||
* check. Used only by the already-shipped escalating-menu polling in
|
||||
* frame_client.c (wait_for_button_press()) to detect a further press
|
||||
* while the menu is already showing; deliberately never reused for
|
||||
* reset detection, so holding the button too long while escalating the
|
||||
* menu can't be misread as a reset request.
|
||||
*/
|
||||
bool combo_button_is_pressed(void);
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "epd7in3e.h"
|
||||
#include "status_screen.h"
|
||||
#include "manage_qr_overlay.h"
|
||||
#include "manage_button.h"
|
||||
#include "combo_button.h"
|
||||
|
||||
#include "frame_client.h"
|
||||
|
||||
@@ -487,22 +487,31 @@ static size_t http_read_fn(uint8_t *chunk, size_t chunk_size, void *ctx_)
|
||||
return (size_t)n;
|
||||
}
|
||||
|
||||
/* GETs /frame/image (or, if force_advance, POSTs /frame/advance to skip
|
||||
* ahead immediately) and streams the response directly into the panel,
|
||||
* splicing in overlay's pixels (if non-NULL) as it streams. Returning
|
||||
* non-ESP_OK means the panel was never actually refreshed --
|
||||
* epd_display_stream() (see epd7in3e.c) refuses to trigger a physical
|
||||
* refresh on a short/wrong-size stream, so a failure here always leaves
|
||||
* the visible screen exactly as it was. */
|
||||
static esp_err_t fetch_and_display(const frame_config_t *cfg, bool force_advance,
|
||||
/* GETs /frame/image (FETCH_NORMAL), or POSTs /frame/advance or
|
||||
* /frame/back to force a move in either direction (FETCH_ADVANCE /
|
||||
* FETCH_BACK -- the next-photo / back-photo buttons), and streams the
|
||||
* response directly into the panel, splicing in overlay's pixels (if
|
||||
* non-NULL) as it streams. Returning non-ESP_OK means the panel was
|
||||
* never actually refreshed -- epd_display_stream() (see epd7in3e.c)
|
||||
* refuses to trigger a physical refresh on a short/wrong-size stream,
|
||||
* so a failure here always leaves the visible screen exactly as it
|
||||
* was. */
|
||||
static esp_err_t fetch_and_display(const frame_config_t *cfg, fetch_action_t action,
|
||||
const manage_overlay_set_t *overlay)
|
||||
{
|
||||
const char *path = "frame/image";
|
||||
if (action == FETCH_ADVANCE) {
|
||||
path = "frame/advance";
|
||||
} else if (action == FETCH_BACK) {
|
||||
path = "frame/back";
|
||||
}
|
||||
|
||||
char url[256];
|
||||
build_url(url, sizeof(url), cfg, force_advance ? "frame/advance" : "frame/image");
|
||||
build_url(url, sizeof(url), cfg, path);
|
||||
|
||||
esp_http_client_config_t config = {
|
||||
.url = url,
|
||||
.method = force_advance ? HTTP_METHOD_POST : HTTP_METHOD_GET,
|
||||
.method = action == FETCH_NORMAL ? HTTP_METHOD_GET : HTTP_METHOD_POST,
|
||||
.timeout_ms = CONFIG_FRAME_FETCH_TIMEOUT_MS,
|
||||
.cert_pem = tools_server_ca_pem_start,
|
||||
};
|
||||
@@ -571,14 +580,14 @@ static bool wait_for_button_press(uint32_t timeout_ms)
|
||||
vTaskDelay(pdMS_TO_TICKS(MANAGE_MENU_POLL_MS));
|
||||
elapsed_ms += MANAGE_MENU_POLL_MS;
|
||||
|
||||
if (!manage_button_is_pressed()) {
|
||||
if (!combo_button_is_pressed()) {
|
||||
continue;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(MANAGE_MENU_DEBOUNCE_MS));
|
||||
if (!manage_button_is_pressed()) {
|
||||
if (!combo_button_is_pressed()) {
|
||||
continue; /* noise, not a real press */
|
||||
}
|
||||
while (manage_button_is_pressed()) {
|
||||
while (combo_button_is_pressed()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(MANAGE_MENU_POLL_MS));
|
||||
}
|
||||
return true;
|
||||
@@ -588,10 +597,10 @@ static bool wait_for_button_press(uint32_t timeout_ms)
|
||||
|
||||
/* Builds and shows one level of the manage menu: level 1 is the base
|
||||
* overlay (management QR + location/date/share-QR wherever the server
|
||||
* had that data); level 2 adds named-face labels on top. force_advance
|
||||
* only applies at level 1 -- escalating to level 2 redisplays the same
|
||||
* photo, so it never re-advances. */
|
||||
static esp_err_t show_menu_level(const frame_config_t *cfg, bool force_advance, int level)
|
||||
* had that data); level 2 adds named-face labels on top. action only
|
||||
* applies at level 1 -- escalating to level 2 redisplays the same
|
||||
* photo, so it never re-advances/-backs. */
|
||||
static esp_err_t show_menu_level(const frame_config_t *cfg, fetch_action_t action, int level)
|
||||
{
|
||||
char management_url[256];
|
||||
build_url(management_url, sizeof(management_url), cfg, "");
|
||||
@@ -626,7 +635,7 @@ static esp_err_t show_menu_level(const frame_config_t *cfg, bool force_advance,
|
||||
return err;
|
||||
}
|
||||
|
||||
err = fetch_and_display(cfg, force_advance, &overlay);
|
||||
err = fetch_and_display(cfg, action, &overlay);
|
||||
manage_overlay_free(&overlay);
|
||||
return err;
|
||||
}
|
||||
@@ -641,13 +650,13 @@ static esp_err_t show_menu_level(const frame_config_t *cfg, bool force_advance,
|
||||
* that (escalating, or the final revert) are logged but don't count as
|
||||
* an overall failure -- something was already shown successfully, which
|
||||
* was the point of the button. */
|
||||
static esp_err_t run_management_menu(const frame_config_t *cfg, bool force_advance)
|
||||
static esp_err_t run_management_menu(const frame_config_t *cfg, fetch_action_t action)
|
||||
{
|
||||
int level = 1;
|
||||
esp_err_t err = show_menu_level(cfg, force_advance, level);
|
||||
esp_err_t err = show_menu_level(cfg, action, level);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Could not render management overlay (%s), showing photo normally", esp_err_to_name(err));
|
||||
return fetch_and_display(cfg, force_advance, NULL);
|
||||
return fetch_and_display(cfg, action, NULL);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
@@ -657,14 +666,14 @@ static esp_err_t run_management_menu(const frame_config_t *cfg, bool force_advan
|
||||
break; /* timeout at any level, or a press while already maxed out -- exit */
|
||||
}
|
||||
level++;
|
||||
esp_err_t level_err = show_menu_level(cfg, false, level);
|
||||
esp_err_t level_err = show_menu_level(cfg, FETCH_NORMAL, level);
|
||||
if (level_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Could not render menu level %d (%s), reverting", level, esp_err_to_name(level_err));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t revert_err = fetch_and_display(cfg, false, NULL);
|
||||
esp_err_t revert_err = fetch_and_display(cfg, FETCH_NORMAL, NULL);
|
||||
if (revert_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to revert management overlay (%s)", esp_err_to_name(revert_err));
|
||||
}
|
||||
@@ -674,15 +683,15 @@ static esp_err_t run_management_menu(const frame_config_t *cfg, bool force_advan
|
||||
/* Runs the appropriate fetch for this cycle: a plain fetch, or -- if
|
||||
* show_management_qr -- the escalating manage menu (see
|
||||
* run_management_menu()). */
|
||||
static esp_err_t run_fetch_cycle(const frame_config_t *cfg, bool force_advance, bool show_management_qr)
|
||||
static esp_err_t run_fetch_cycle(const frame_config_t *cfg, fetch_action_t action, bool show_management_qr)
|
||||
{
|
||||
if (!show_management_qr) {
|
||||
return fetch_and_display(cfg, force_advance, NULL);
|
||||
return fetch_and_display(cfg, action, NULL);
|
||||
}
|
||||
return run_management_menu(cfg, force_advance);
|
||||
return run_management_menu(cfg, action);
|
||||
}
|
||||
|
||||
void frame_client_run(const frame_config_t *cfg, bool force_advance, bool show_management_qr)
|
||||
void frame_client_run(const frame_config_t *cfg, fetch_action_t action, bool show_management_qr)
|
||||
{
|
||||
esp_err_t epd_err = epd_init();
|
||||
bool have_display = (epd_err == ESP_OK);
|
||||
@@ -716,7 +725,7 @@ void frame_client_run(const frame_config_t *cfg, bool force_advance, bool show_m
|
||||
* worth it to stop false-failing on the common case. */
|
||||
bool image_ok = true;
|
||||
if (have_display) {
|
||||
esp_err_t fetch_err = run_fetch_cycle(cfg, force_advance, show_management_qr);
|
||||
esp_err_t fetch_err = run_fetch_cycle(cfg, action, show_management_qr);
|
||||
image_ok = (fetch_err == ESP_OK);
|
||||
if (!image_ok) {
|
||||
/* epd_display_stream() never triggers a physical refresh on a
|
||||
|
||||
@@ -5,6 +5,20 @@
|
||||
#include "esp_err.h"
|
||||
#include "wifi_provisioning.h"
|
||||
|
||||
/**
|
||||
* Which photo-fetch behavior this wake cycle should use -- normally the
|
||||
* idempotent GET /frame/image (the server decides on its own whether to
|
||||
* advance, based on its configured refresh interval, so a plain
|
||||
* wake/reboot never skips a photo just by asking), or POST
|
||||
* /frame/advance / POST /frame/back to force a move in either direction
|
||||
* (the next-photo / back-photo buttons).
|
||||
*/
|
||||
typedef enum {
|
||||
FETCH_NORMAL,
|
||||
FETCH_ADVANCE,
|
||||
FETCH_BACK,
|
||||
} fetch_action_t;
|
||||
|
||||
/**
|
||||
* Connects to the home WiFi network described by cfg, retrying up to
|
||||
* CONFIG_FRAME_STA_CONNECT_MAX_RETRIES times with a per-attempt timeout of
|
||||
@@ -15,14 +29,8 @@ esp_err_t frame_wifi_connect_sta(const frame_config_t *cfg);
|
||||
|
||||
/**
|
||||
* Runs the frame's normal-operation cycle: fetch the current image from
|
||||
* cfg->toolsserver, display it, and deep-sleep until the next refresh.
|
||||
*
|
||||
* If force_advance is true (the next-photo button was held), the fetch
|
||||
* forces the server to skip ahead to the next photo immediately
|
||||
* (POST /frame/advance) instead of the normal idempotent GET /frame/image
|
||||
* -- the server decides on its own whether to advance in the normal case,
|
||||
* based on its configured refresh interval, so a plain wake/reboot never
|
||||
* skips a photo just by asking.
|
||||
* cfg->toolsserver per `action` (see fetch_action_t), display it, and
|
||||
* deep-sleep until the next refresh.
|
||||
*
|
||||
* If show_management_qr is true (the manage button was held), the
|
||||
* displayed photo gets a small "scan to manage" QR overlay in the
|
||||
@@ -30,4 +38,4 @@ esp_err_t frame_wifi_connect_sta(const frame_config_t *cfg);
|
||||
* seconds (the device stays awake), then reverted back to the plain
|
||||
* photo before proceeding to the normal sleep-interval logic.
|
||||
*/
|
||||
void frame_client_run(const frame_config_t *cfg, bool force_advance, bool show_management_qr);
|
||||
void frame_client_run(const frame_config_t *cfg, fetch_action_t action, bool show_management_qr);
|
||||
|
||||
+15
-8
@@ -7,9 +7,9 @@
|
||||
|
||||
#include "wifi_provisioning.h"
|
||||
#include "frame_client.h"
|
||||
#include "reset_button.h"
|
||||
#include "next_button.h"
|
||||
#include "manage_button.h"
|
||||
#include "back_button.h"
|
||||
#include "combo_button.h"
|
||||
|
||||
static const char *TAG = "main";
|
||||
|
||||
@@ -36,20 +36,27 @@ void app_main(void)
|
||||
* windows), then checks them -- covers both "held while asleep, just
|
||||
* woke us up" and "held while powering on" the same way, since both
|
||||
* look identical from here: the pin is just low right now. */
|
||||
reset_button_init();
|
||||
next_button_init();
|
||||
manage_button_init();
|
||||
reset_button_check(); /* clears config + restarts if held the full duration; never returns in that case */
|
||||
back_button_init();
|
||||
combo_button_init();
|
||||
|
||||
bool force_advance = next_button_check();
|
||||
bool show_management_qr = manage_button_check();
|
||||
bool next_pressed = next_button_check();
|
||||
bool back_pressed = back_button_check();
|
||||
/* Next takes priority over back if somehow both read pressed at once
|
||||
* (e.g. both held through a power-on) -- an arbitrary but
|
||||
* deterministic tie-break, not expected to matter in practice. */
|
||||
fetch_action_t action = next_pressed ? FETCH_ADVANCE : back_pressed ? FETCH_BACK : FETCH_NORMAL;
|
||||
/* Soft-resets or clears config + restarts internally for a medium/
|
||||
* long hold and never returns in those cases -- only returns here
|
||||
* for "not pressed" (false) or "quick press" (true, show the menu). */
|
||||
bool show_management_qr = combo_button_check();
|
||||
|
||||
frame_config_t cfg;
|
||||
esp_err_t cfg_err = frame_config_load(&cfg);
|
||||
if (cfg_err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Found stored config for '%s', connecting to home WiFi", cfg.sta_ssid);
|
||||
if (frame_wifi_connect_sta(&cfg) == ESP_OK) {
|
||||
frame_client_run(&cfg, force_advance, show_management_qr);
|
||||
frame_client_run(&cfg, action, show_management_qr);
|
||||
return; /* frame_client_run currently never returns */
|
||||
}
|
||||
ESP_LOGW(TAG, "Could not connect to stored WiFi after %d attempts, falling back to provisioning",
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_sleep.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "manage_button.h"
|
||||
|
||||
static const char *TAG = "manage_button";
|
||||
|
||||
#if CONFIG_FRAME_MANAGE_BUTTON_GPIO >= 0
|
||||
|
||||
#define MANAGE_BUTTON_GPIO ((gpio_num_t)CONFIG_FRAME_MANAGE_BUTTON_GPIO)
|
||||
#define MANAGE_BUTTON_DEBOUNCE_MS 20
|
||||
#define MANAGE_BUTTON_DEBOUNCE_CHECKS 3
|
||||
|
||||
void manage_button_init(void)
|
||||
{
|
||||
/* Every deep sleep with this pin armed as a wakeup source leaves it
|
||||
* "held" (ESP-IDF locks the pin's pull/config across the sleep
|
||||
* transition -- see esp_sleep_gpio_wakeup_prepare_on_hp_periph_powerdown()
|
||||
* in sleep_modes.c) and, confirmed on hardware, never un-holds it on
|
||||
* wake -- the application has to. Left held, live gpio_get_level()
|
||||
* reads stay frozen at whatever level the pin had the moment sleep
|
||||
* began (idle/unpressed, almost always), which looks identical to
|
||||
* "not pressed" and silently breaks any live poll for a NEW press
|
||||
* later in the same session -- exactly what the manage menu's
|
||||
* escalation needs. Must run before gpio_config() below. */
|
||||
gpio_hold_dis(MANAGE_BUTTON_GPIO);
|
||||
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = 1ULL << MANAGE_BUTTON_GPIO,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
|
||||
/* See reset_button.c for why this API (not ext1) -- it manages the
|
||||
* pull resistor across the sleep transition itself, so the pin
|
||||
* doesn't float and wake the device spuriously. */
|
||||
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << MANAGE_BUTTON_GPIO, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
}
|
||||
|
||||
bool manage_button_check(void)
|
||||
{
|
||||
/* See next_button.c for why the latched wakeup status is checked
|
||||
* first: a quick tap can release before a live gpio_get_level() call
|
||||
* this far into boot would still see it held, even though it's what
|
||||
* woke the device. */
|
||||
if (esp_sleep_get_gpio_wakeup_status() & (1ULL << MANAGE_BUTTON_GPIO)) {
|
||||
ESP_LOGI(TAG, "Manage button caused this wake, showing management QR");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (gpio_get_level(MANAGE_BUTTON_GPIO) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < MANAGE_BUTTON_DEBOUNCE_CHECKS; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(MANAGE_BUTTON_DEBOUNCE_MS));
|
||||
if (gpio_get_level(MANAGE_BUTTON_GPIO) != 0) {
|
||||
return false; /* noise, not a real press */
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Manage button held during power-on, showing management QR");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool manage_button_is_pressed(void)
|
||||
{
|
||||
return gpio_get_level(MANAGE_BUTTON_GPIO) == 0;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void manage_button_init(void) {}
|
||||
bool manage_button_check(void) { return false; }
|
||||
bool manage_button_is_pressed(void) { return false; }
|
||||
|
||||
#endif
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Configures the "manage" button GPIO (CONFIG_FRAME_MANAGE_BUTTON_GPIO,
|
||||
* active-low with internal pull-up) and arms it as a deep-sleep wakeup
|
||||
* source, same as reset_button_init()/next_button_init(). Call once,
|
||||
* early in app_main(), before the device might enter deep sleep.
|
||||
*
|
||||
* A no-op if CONFIG_FRAME_MANAGE_BUTTON_GPIO is negative (button disabled).
|
||||
*/
|
||||
void manage_button_init(void);
|
||||
|
||||
/**
|
||||
* Returns whether the manage button is currently held, debounced with a
|
||||
* couple of short re-checks to reject noise. Like the next-photo button,
|
||||
* there's no long hold-to-confirm gate -- showing the management QR is
|
||||
* low-stakes and should feel immediate.
|
||||
*/
|
||||
bool manage_button_check(void);
|
||||
|
||||
/**
|
||||
* Bare, undebounced "is the pin low right now" read -- unlike
|
||||
* manage_button_check(), this doesn't consult the latched deep-sleep
|
||||
* wakeup status (only meaningful once, immediately after waking from
|
||||
* sleep) and isn't meant to detect what woke the device. Used for
|
||||
* polling for a subsequent press while already awake and the manage
|
||||
* overlay is up (see frame_client.c's wait_for_button_press()), which
|
||||
* does its own debounce/release-wait around repeated calls to this.
|
||||
*/
|
||||
bool manage_button_is_pressed(void);
|
||||
@@ -17,10 +17,11 @@ static const char *TAG = "next_button";
|
||||
|
||||
void next_button_init(void)
|
||||
{
|
||||
/* See manage_button.c's manage_button_init() for why this has to run
|
||||
* before gpio_config() -- a deep sleep with this pin armed as a
|
||||
* wakeup source leaves it "held," and nothing un-holds it on wake
|
||||
* except explicitly asking. */
|
||||
/* Every deep sleep with this pin armed as a wakeup source leaves it
|
||||
* "held" (ESP-IDF locks the pin's pull/config across the sleep
|
||||
* transition -- see esp_sleep_gpio_wakeup_prepare_on_hp_periph_powerdown()
|
||||
* in sleep_modes.c) and, confirmed on hardware, never un-holds it on
|
||||
* wake -- the application has to. Must run before gpio_config() below. */
|
||||
gpio_hold_dis(NEXT_BUTTON_GPIO);
|
||||
|
||||
gpio_config_t io_conf = {
|
||||
@@ -30,9 +31,12 @@ void next_button_init(void)
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
|
||||
/* See reset_button.c for why this API (not ext1) -- it manages the
|
||||
* pull resistor across the sleep transition itself, so the pin
|
||||
* doesn't float and wake the device spuriously. */
|
||||
/* Not esp_sleep_enable_ext1_wakeup_io(): its internal pull resistors
|
||||
* don't hold once the RTC_PERIPH domain powers down for deep sleep, so
|
||||
* the pin floats and reads spuriously low, waking the device instantly
|
||||
* on every sleep entry (confirmed on hardware). This GPIO-wakeup
|
||||
* variant manages the pull resistor itself across the sleep
|
||||
* transition. */
|
||||
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << NEXT_BUTTON_GPIO, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
/**
|
||||
* Configures the next-photo button GPIO (CONFIG_FRAME_NEXT_BUTTON_GPIO,
|
||||
* active-low with internal pull-up) and arms it as a deep-sleep wakeup
|
||||
* source, same as reset_button_init(). Call once, early in app_main(),
|
||||
* before the device might enter deep sleep.
|
||||
* source. Call once, early in app_main(), before the device might enter
|
||||
* deep sleep.
|
||||
*
|
||||
* A no-op if CONFIG_FRAME_NEXT_BUTTON_GPIO is negative (button disabled).
|
||||
*/
|
||||
@@ -14,8 +14,8 @@ void next_button_init(void);
|
||||
|
||||
/**
|
||||
* Returns whether the next-photo button is currently held, debounced with
|
||||
* a couple of short re-checks to reject noise. Unlike the reset button,
|
||||
* there's no long hold-to-confirm gate -- advancing a photo is low-stakes
|
||||
* and should feel immediate, so this returns right away either way.
|
||||
* a couple of short re-checks to reject noise. No long hold-to-confirm
|
||||
* gate -- advancing a photo is low-stakes and should feel immediate, so
|
||||
* this returns right away either way.
|
||||
*/
|
||||
bool next_button_check(void);
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_sleep.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "wifi_provisioning.h"
|
||||
|
||||
#include "reset_button.h"
|
||||
|
||||
static const char *TAG = "reset_button";
|
||||
|
||||
#if CONFIG_FRAME_RESET_BUTTON_GPIO >= 0
|
||||
|
||||
#define RESET_BUTTON_GPIO ((gpio_num_t)CONFIG_FRAME_RESET_BUTTON_GPIO)
|
||||
#define RESET_BUTTON_POLL_MS 100
|
||||
|
||||
void reset_button_init(void)
|
||||
{
|
||||
/* See manage_button.c's manage_button_init() for why this has to run
|
||||
* before gpio_config() -- a deep sleep with this pin armed as a
|
||||
* wakeup source leaves it "held," and nothing un-holds it on wake
|
||||
* except explicitly asking. */
|
||||
gpio_hold_dis(RESET_BUTTON_GPIO);
|
||||
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = 1ULL << RESET_BUTTON_GPIO,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
|
||||
/* Not esp_sleep_enable_ext1_wakeup_io(): its internal pull resistors
|
||||
* don't hold once the RTC_PERIPH domain powers down for deep sleep, so
|
||||
* the pin floats and reads spuriously low, waking the device instantly
|
||||
* on every sleep entry (confirmed on hardware -- boot-looped every
|
||||
* ~27s, the length of one fetch/display cycle, instead of sleeping for
|
||||
* the configured interval). This GPIO-wakeup variant manages the pull
|
||||
* resistor itself across the sleep transition. */
|
||||
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << RESET_BUTTON_GPIO, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
}
|
||||
|
||||
void reset_button_check(void)
|
||||
{
|
||||
if (gpio_get_level(RESET_BUTTON_GPIO) != 0) {
|
||||
return; /* not pressed */
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Reset button held -- hold for %dms to clear config and reprovision",
|
||||
CONFIG_FRAME_RESET_BUTTON_HOLD_MS);
|
||||
|
||||
int elapsed_ms = 0;
|
||||
while (elapsed_ms < CONFIG_FRAME_RESET_BUTTON_HOLD_MS) {
|
||||
vTaskDelay(pdMS_TO_TICKS(RESET_BUTTON_POLL_MS));
|
||||
elapsed_ms += RESET_BUTTON_POLL_MS;
|
||||
if (gpio_get_level(RESET_BUTTON_GPIO) != 0) {
|
||||
ESP_LOGI(TAG, "Reset button released early, continuing normal boot");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Reset button held for %dms, clearing config and restarting into provisioning",
|
||||
CONFIG_FRAME_RESET_BUTTON_HOLD_MS);
|
||||
frame_config_clear();
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void reset_button_init(void) {}
|
||||
void reset_button_check(void) {}
|
||||
|
||||
#endif
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Configures the factory-reset button GPIO (CONFIG_FRAME_RESET_BUTTON_GPIO,
|
||||
* active-low with internal pull-up) and arms it as a deep-sleep wakeup
|
||||
* source, so holding it wakes the device promptly even while it's asleep
|
||||
* rather than only being noticed during its brief awake windows. Call once,
|
||||
* early in app_main(), before the device might enter deep sleep.
|
||||
*
|
||||
* A no-op if CONFIG_FRAME_RESET_BUTTON_GPIO is negative (button disabled).
|
||||
*/
|
||||
void reset_button_init(void);
|
||||
|
||||
/**
|
||||
* Checks whether the reset button is currently held. If it's held
|
||||
* continuously for CONFIG_FRAME_RESET_BUTTON_HOLD_MS, clears the stored
|
||||
* WiFi/server config and restarts (does not return in that case) so the
|
||||
* device comes back up in provisioning mode. Returns immediately if the
|
||||
* button isn't pressed, or as soon as it's released before the hold
|
||||
* duration elapses -- normal boot continues either way.
|
||||
*/
|
||||
void reset_button_check(void);
|
||||
Reference in New Issue
Block a user