Add two physical buttons: factory-reset and next-photo
Build and push server image / build-and-push (push) Successful in 35s
Build and push server image / build-and-push (push) Successful in 35s
Factory-reset (GPIO3, hold 10s): clears stored WiFi/server config and restarts into provisioning -- the deliberate, USB-free replacement for the earlier reverted RST-based auto-reprovisioning idea. Next-photo (GPIO2, tap): wakes the device and forces the server to advance immediately via a new POST /frame/advance, instead of waiting for the refresh interval. Both buttons arm themselves as deep-sleep GPIO wakeup sources so a press is noticed promptly even while asleep. Also makes GET /frame/image side-effect-free: it now only advances once refresh_interval_s has elapsed since the current photo was set (tracked server-side), so a device reboot for any reason just redisplays the current photo instead of silently skipping ahead. The server maintains a small reorderable upcoming-photos queue, viewable and rearrangeable from the web UI.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""Tracks which photo is currently displayed and what's queued up next.
|
||||
|
||||
`current_asset_id` only ever changes two ways: the configured refresh
|
||||
interval elapsing (`get_current`, called on every `GET /frame/image` --
|
||||
a no-op otherwise, so an unplanned device reboot just redisplays the same
|
||||
photo instead of silently skipping ahead) or an explicit forced advance
|
||||
(`advance_forced`, called from `POST /frame/advance` -- the next-photo
|
||||
button -- ignoring elapsed time).
|
||||
|
||||
`queue` is a small reorderable lookahead the web UI can preview and
|
||||
rearrange, topped up automatically from the album 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import time
|
||||
|
||||
from .config import FrameConfig
|
||||
|
||||
QUEUE_TARGET_LEN = 10
|
||||
|
||||
|
||||
def _top_up(cfg: FrameConfig, assets: list[dict]) -> None:
|
||||
valid_ids = {a["id"] for a in assets}
|
||||
cfg.queue = [asset_id for asset_id in cfg.queue if asset_id in valid_ids]
|
||||
|
||||
needed = QUEUE_TARGET_LEN - len(cfg.queue)
|
||||
if needed <= 0 or not assets:
|
||||
return
|
||||
|
||||
excluded = set(cfg.queue)
|
||||
if cfg.current_asset_id:
|
||||
excluded.add(cfg.current_asset_id)
|
||||
|
||||
if cfg.order == "shuffle":
|
||||
candidates = [a["id"] for a in assets if a["id"] not in excluded]
|
||||
cfg.queue.extend(random.sample(candidates, min(needed, len(candidates))))
|
||||
return
|
||||
|
||||
# Sequential: walk the album starting at queue_cursor, at most one full
|
||||
# pass, wrapping around. queue_cursor resumes right after wherever this
|
||||
# pass stopped, whether or not it filled the queue (e.g. a small album
|
||||
# where everything's already queued/current -- next call is then a
|
||||
# cheap no-op scan until something's consumed).
|
||||
n = len(assets)
|
||||
cfg.queue_cursor %= n
|
||||
added = 0
|
||||
i = 0
|
||||
for i in range(n):
|
||||
if added >= needed:
|
||||
break
|
||||
asset_id = assets[(cfg.queue_cursor + i) % n]["id"]
|
||||
if asset_id not in excluded:
|
||||
cfg.queue.append(asset_id)
|
||||
excluded.add(asset_id)
|
||||
added += 1
|
||||
cfg.queue_cursor = (cfg.queue_cursor + i + 1) % n
|
||||
|
||||
|
||||
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."""
|
||||
_top_up(cfg, assets)
|
||||
if cfg.queue:
|
||||
cfg.current_asset_id = cfg.queue.pop(0)
|
||||
elif assets:
|
||||
# Queue still empty after top-up (e.g. a single-photo album whose
|
||||
# only asset is already current) -- keep showing what we have.
|
||||
cfg.current_asset_id = assets[0]["id"]
|
||||
cfg.current_asset_set_at = time.time()
|
||||
# Refill back up to QUEUE_TARGET_LEN now that current_asset_id has
|
||||
# changed -- otherwise the queue is left one short until the *next*
|
||||
# advance, since the pop above consumes one of the items _top_up just
|
||||
# added.
|
||||
_top_up(cfg, assets)
|
||||
|
||||
|
||||
def get_current(cfg: FrameConfig, assets: list[dict]) -> bool:
|
||||
"""Time-based, idempotent path used by GET /frame/image. Advances only
|
||||
if the current photo is unset/invalid or refresh_interval_s has
|
||||
elapsed since it was set. Returns whether it changed anything, so the
|
||||
caller knows whether to persist. Calling this repeatedly well within
|
||||
the interval is a no-op both times -- what makes an unplanned device
|
||||
reboot safe: it just re-reads the current photo instead of skipping
|
||||
ahead, while a wake that lands after the interval has elapsed still
|
||||
advances exactly once, even after a long time offline."""
|
||||
valid_ids = {a["id"] for a in assets}
|
||||
stale = (
|
||||
not cfg.current_asset_id
|
||||
or cfg.current_asset_id not in valid_ids
|
||||
or (time.time() - cfg.current_asset_set_at) >= cfg.refresh_interval_s
|
||||
)
|
||||
if not stale:
|
||||
return False
|
||||
advance_forced(cfg, assets)
|
||||
return True
|
||||
Reference in New Issue
Block a user