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,5 @@
|
||||
"""Tracks which photo is currently displayed and what's queued up next.
|
||||
"""Tracks which photo is currently displayed, what's queued up next, and
|
||||
what's already been shown.
|
||||
|
||||
`current_asset_id` only ever changes two ways: the configured refresh
|
||||
interval elapsing (`get_current`, called on every `GET /frame/image` --
|
||||
@@ -12,6 +13,13 @@ rearrange, topped up (or trimmed) automatically to match
|
||||
`cfg.queue_target_len` (user-configurable from the web UI) as it's
|
||||
consumed. `queue_cursor` is separate, internal-only bookkeeping for where
|
||||
sequential top-up resumes in the album -- not shown or reordered in the UI.
|
||||
|
||||
`history` is the mirror image of `queue`: every time `advance_forced`
|
||||
actually changes `current_asset_id`, the old one is pushed onto
|
||||
`history`. `back_forced` (the back-photo button) is the exact reverse of
|
||||
`advance_forced` -- it pops `history` back into `current_asset_id` and
|
||||
pushes the photo it's replacing onto the *front* of `queue`, so pressing
|
||||
next afterwards lands you right back where you were.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -21,6 +29,8 @@ import time
|
||||
|
||||
from .config import FrameConfig
|
||||
|
||||
HISTORY_MAX_LEN = 20
|
||||
|
||||
|
||||
def _top_up(cfg: FrameConfig, assets: list[dict]) -> None:
|
||||
valid_ids = {a["id"] for a in assets}
|
||||
@@ -69,8 +79,16 @@ def _top_up(cfg: FrameConfig, assets: list[dict]) -> None:
|
||||
|
||||
def advance_forced(cfg: FrameConfig, assets: list[dict]) -> None:
|
||||
"""Unconditionally moves to the next photo, ignoring elapsed time, and
|
||||
resets the interval clock from now. Used only by the explicit
|
||||
next-photo action (POST /frame/advance) -- always mutates cfg."""
|
||||
resets the interval clock from now. Used by the explicit next-photo
|
||||
action (POST /frame/advance) and by get_current() once the refresh
|
||||
interval has elapsed -- always mutates cfg."""
|
||||
if cfg.current_asset_id:
|
||||
# Recorded regardless of *why* this advance happened (a manual
|
||||
# next-press or the timer just elapsing) -- back should be able
|
||||
# to undo either kind.
|
||||
cfg.history.append(cfg.current_asset_id)
|
||||
cfg.history = cfg.history[-HISTORY_MAX_LEN:]
|
||||
|
||||
_top_up(cfg, assets)
|
||||
if cfg.queue:
|
||||
cfg.current_asset_id = cfg.queue.pop(0)
|
||||
@@ -86,6 +104,29 @@ def advance_forced(cfg: FrameConfig, assets: list[dict]) -> None:
|
||||
_top_up(cfg, assets)
|
||||
|
||||
|
||||
def back_forced(cfg: FrameConfig, assets: list[dict]) -> bool:
|
||||
"""Unconditionally moves to the previously-current photo, the mirror
|
||||
image of advance_forced() -- pops the most recent entry off history,
|
||||
pushes the photo it's replacing onto the front of queue (so pressing
|
||||
next afterwards returns to it), and resets the interval clock from
|
||||
now. Skips over any history entries no longer in the album (deleted
|
||||
since). Returns whether it actually moved -- False (history empty or
|
||||
entirely stale) is a no-op, callers should still just display
|
||||
whatever's current rather than treating it as an error. Used by the
|
||||
back-photo button (POST /frame/back)."""
|
||||
valid_ids = {a["id"] for a in assets}
|
||||
while cfg.history:
|
||||
previous_id = cfg.history.pop()
|
||||
if previous_id not in valid_ids:
|
||||
continue
|
||||
if cfg.current_asset_id:
|
||||
cfg.queue.insert(0, cfg.current_asset_id)
|
||||
cfg.current_asset_id = previous_id
|
||||
cfg.current_asset_set_at = time.time()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def sync_queue_length(cfg: FrameConfig, assets: list[dict]) -> None:
|
||||
"""Tops up or trims cfg.queue to match cfg.queue_target_len without
|
||||
otherwise touching current_asset_id. Used by GET /api/queue so a
|
||||
|
||||
Reference in New Issue
Block a user