Per-frame palette calibration + sidebar battery indicator
Build and push server image / build-and-push (push) Successful in 40s

Advanced configuration (Configuration tab, collapsed <details> section):
a color picker per ink color (black/white/yellow/red/blue/green),
overriding image_pipeline.DEFAULT_PALETTE_RGB for that frame's actual
panel -- different units can vary enough from the documented
approximations to be worth calibrating once you can compare a rendered
photo against the real hardware. Stored as Frame.palette_rgb (NULL =
default, schema migration v4), threaded through render_frame/
render_placeholder/_quantize_and_pack (which now builds the PIL palette
image per call instead of once at import) so both photos and the
unclaimed/unconfigured placeholder screen respect it. "Reset to
defaults" clears back to NULL. Config-save validates exactly 6 #rrggbb
values, rejecting anything else with a 400.

Also: each frame's sidebar entry now shows its last-reported battery
percent (🔋NN%) next to the name, using the frame_dot's existing
recently-seen indicator conventions -- silent when never reported
(mains-only frames, or before the first report), matching how battery
is hidden everywhere else it's not applicable.

Verified against the same live-shaped database as the SMTP work: the
v3->v4 migration, save/reload/reset round trip through the real HTTP
route, an actual rendered image using a custom palette (confirmed via
its packed panel-code bytes), input validation, and the sidebar badge
against real battery data -- plus the standing legacy-device curl suite.
This commit is contained in:
2026-07-22 01:19:06 -04:00
parent c1c803b497
commit 5b11f2accb
12 changed files with 185 additions and 38 deletions
+12
View File
@@ -28,6 +28,7 @@ from sqlalchemy.orm import Session
from .. import gitea_releases, photo_queue, quiet_hours
from ..auth import require_frame_control, require_frame_view, require_user_api
from ..db import frame_locked, get_db
from ..image_pipeline import PALETTE_LABELS, hex_to_rgb
from ..firmware import firmware_path, parse_app_version
from ..models import BatteryLog, Frame
from .common import (
@@ -79,6 +80,8 @@ def api_config_save(
firmware_update_repo_url: str | None = Form(None),
firmware_auto_update: bool | None = Form(None),
battery_alert_threshold_pct: int | None = Form(None),
palette: list[str] | None = Form(None),
palette_reset: bool | None = Form(None),
frame: Frame = Depends(require_frame_control),
db: Session = Depends(get_db),
):
@@ -124,6 +127,15 @@ def api_config_save(
# A changed threshold should be able to fire again immediately,
# not stay suppressed by a flag set under the old value.
cfg.battery_alert_sent = False
if palette_reset:
cfg.palette_rgb = None
elif palette is not None:
if len(palette) != len(PALETTE_LABELS):
raise HTTPException(400, f"Expected {len(PALETTE_LABELS)} palette colors, got {len(palette)}")
parsed = [hex_to_rgb(h) for h in palette]
if any(rgb is None for rgb in parsed):
raise HTTPException(400, "Palette colors must be #rrggbb hex values")
cfg.palette_rgb = [list(rgb) for rgb in parsed]
cfg.stats_config_saves += 1
return {"status": "saved"}