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.
66 lines
2.2 KiB
Python
66 lines
2.2 KiB
Python
"""JSON-file-backed config: Immich connection, selected album, and cursor
|
|
state (which photo /frame/image serves next)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from threading import Lock
|
|
|
|
from pydantic import BaseModel
|
|
|
|
CONFIG_PATH = Path(os.environ.get("CONFIG_PATH", "/data/config.json"))
|
|
|
|
_lock = Lock()
|
|
|
|
|
|
class FrameConfig(BaseModel):
|
|
immich_url: str = ""
|
|
immich_api_key: str = ""
|
|
management_token: str = "" # gates the web UI (see main.py); empty = no gate, open on trusted LAN
|
|
album_id: str = ""
|
|
order: str = "sequential" # or "shuffle"
|
|
refresh_interval_s: int = 3600
|
|
smart_crop_faces: bool = True
|
|
|
|
# Current photo + upcoming queue (see app/photo_queue.py). current_asset_set_at
|
|
# is what lets the server decide "has it been long enough to advance" on its
|
|
# own clock, independent of how/why the device asked for a photo.
|
|
current_asset_id: str = ""
|
|
current_asset_set_at: float = 0.0
|
|
queue: list[str] = []
|
|
queue_cursor: int = 0 # internal bookkeeping for sequential queue top-up; not user-facing
|
|
queue_target_len: int = 20 # how many upcoming photos to keep queued/shown in the web UI
|
|
history: list[str] = [] # bounded stack of previously-current asset ids, most recent last
|
|
|
|
|
|
def load() -> FrameConfig:
|
|
with _lock:
|
|
if not CONFIG_PATH.exists():
|
|
cfg = FrameConfig()
|
|
else:
|
|
cfg = FrameConfig(**json.loads(CONFIG_PATH.read_text()))
|
|
|
|
# IMMICH_URL/IMMICH_API_KEY/MANAGEMENT_TOKEN set in the environment
|
|
# (e.g. docker-compose.yml, see docker-compose.yml.example) take
|
|
# precedence over whatever's saved in CONFIG_PATH, so credentials never
|
|
# need to go through the web UI.
|
|
env_url = os.environ.get("IMMICH_URL")
|
|
env_key = os.environ.get("IMMICH_API_KEY")
|
|
env_token = os.environ.get("MANAGEMENT_TOKEN")
|
|
if env_url:
|
|
cfg.immich_url = env_url
|
|
if env_key:
|
|
cfg.immich_api_key = env_key
|
|
if env_token:
|
|
cfg.management_token = env_token
|
|
|
|
return cfg
|
|
|
|
|
|
def save(cfg: FrameConfig) -> None:
|
|
with _lock:
|
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
CONFIG_PATH.write_text(cfg.model_dump_json(indent=2))
|