fc65b19cf29a394684db1f9a308e4f4f515ed97e
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e1bca5a81a |
Fix false recharge-cycle detection from a single noisy battery reading
Build and push server image / build-and-push (push) Successful in 38s
A report was flagged as "the battery got recharged" (resetting battery_history and stats_recharge_cycles, and re-arming the low-battery alert) whenever it came in >= RECHARGE_JUMP_PCT above the single immediately-previous report. That's exactly what a real recharge looks like, but it's also exactly what a normal reading looks like right after one noisy low report: e.g. 60, 59, 58, then a stray 53, then back to a perfectly normal 58 -- 58 >= 53+5 falsely read as a recharge. Now compared against the max of the last RECHARGE_LOOKBACK (3) reports instead of just the one before it, so a lone stray reading doesn't get to set the bar a normal reading then trips. A real recharge still needs to clear all of them, so genuine recharges are still caught immediately (verified: 18% -> 90% still triggers, history still resets). Paired with the firmware-side battery.c change (trimmed-mean ADC sampling) that reduces how often a stray reading like the 53 above happens in the first place. |
||
|
|
e48ac50ea1 |
Color/contrast/dithering sliders + before/after render preview
Advanced configuration gains three sliders (PIL ImageEnhance factors for color/contrast, 0-2, 1=unchanged; a 0-1 dithering strength) applied to every photo this frame renders. Confirmed the parameter conventions against a similar project (jwchen119/EPF: ImageEnhance.Color/Contrast, 1.0 baseline) before implementing; dithering strength isn't natively exposed by PIL's quantize(), so it's implemented by blending the source toward its own flat/undithered quantization before running Floyd- Steinberg on the blend -- at 0 there's no quantization error left to diffuse (exactly the flat result), at 1 it's the original unmodified behavior, with a smooth continuum between rather than dithering being an on/off toggle. image_pipeline.py split into composition (_compose), enhancement (_enhance), quantization (_quantize), and transpose+pack stages so render_frame (device bytes) and the new render_preview_png (a normal viewable PNG, upright logical orientation) share the same pipeline instead of duplicating it. Named-face overlay label math (face_labels.py) was already routed through the shared _placement_transform, so it needed no changes for the new params. Also added the requested before/after comparison: the Configuration tab's new Preview card shows the current photo's untouched Immich preview next to that same photo run through the frame's actual saved rendering pipeline (two new GET endpoints, /preview/original and /preview/rendered) -- immediate visual feedback for tuning the palette and these new sliders. "Refresh preview" re-fetches after saving. Schema migration v6 adds color_boost/contrast_boost/dither_strength, defaulting to 1.0/1.0/1.0 -- reproduces the exact previous rendering until a frame's Configuration tab changes one. Verified against the live-shaped test database: the migration, sliders persisting and clamping out-of-range input, both preview endpoints (real JPEG passthrough / real PNG at correct logical size+orientation), confirmed dither_strength=0 actually changes the rendered bytes vs. default, and the standing legacy-device curl suite. |
||
|
|
49bc9f9ec9 |
Display mode: crop to fill, crop to faces, stretch to fill, shrink to fit
Build and push server image / build-and-push (push) Successful in 40s
Replaces the smart_crop_faces boolean with a 4-way display_mode select on each frame's Configuration tab (image_pipeline.DISPLAY_MODES): - Crop to fill / Crop to faces: the previous False/True behavior, unchanged (center-crop trimming excess, optionally shifted to keep faces on screen). - Stretch to fill (new): fills the panel exactly, aspect ratio not preserved -- a plain resize, no crop. - Shrink to fit (new): the whole photo visible, letterboxed with white where it doesn't fill the panel. Named-face overlay label positioning (face_labels.py, the manage menu's "who's in this photo") now goes through a shared _placement_transform() in image_pipeline.py instead of duplicating crop-box math, so label placement stays correct (and in-bounds) under all four modes, not just the two crop ones -- letterbox/stretch never crop a face out, so labels just use straight scale+offset math there. Schema migration v5 adds display_mode, backfills it from the old boolean (True/False -> crop_faces/crop_fill), and drops the boolean. Verified against the live-shaped test database: the migration (existing frames correctly preserved as crop_faces), the config page's new 4-option select, actual renders under letterbox (confirmed real white letterbox padding in the packed panel-code bytes) and stretch_fill, invalid-input fallback, and the standing legacy-device curl suite. |
||
|
|
5b11f2accb |
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.
|
||
|
|
8ac3fc0de3 |
Redesign phase D: sidebar app shell, per-frame tabs, namespaced API
Build and push server image / build-and-push (push) Successful in 43s
The web UI grows into the multi-frame world: a left sidebar lists the
user's frames (with an online dot driven by the same overdue math as
the Device panel; collapsible off-canvas with a hamburger on mobile),
and each frame gets three tabs -- Photos (album picker, now displaying,
the drag-to-reorder upcoming grid), Configuration (name/order/
orientation/refresh/quiet hours/timezone/smart crop + the firmware
card), and Stats (device telemetry, lifetime counters, battery chart).
Settings and Admin adopt the same shell. / becomes a routing hub:
first frame, empty-state onboarding page, setup/login, or the
manage-QR redirect.
The JSON API moves to /api/frames/{id}/... behind require_frame_view /
require_frame_control: any linked user (admins see all) can view; 404
for frames outside your view so ids aren't confirmed; mutations 409
with the holder's name unless you hold the soft control lock, and
POST take-control always flips it to you. Config saves are now partial
updates -- each tab posts only its own fields (checkboxes always sent
explicitly), so the split forms can't clobber each other.
All CSS moves to static/theme.css and the old 680-line inline script
block splits into static/*.js -- the Pointer Events drag-drop state
machine and the canvas battery chart ported intact, not rewritten. The
CSRF fetch wrapper now reads a <meta> tag. No build step, still vanilla.
Verified end-to-end: page/static/API suites, control-lock handoff in
both directions, partial-save field preservation, non-admin frame
isolation, and the legacy-device curl suite (still byte-identical
responses for the deployed frame).
|
||
|
|
9fbbb8ed2b |
Redesign phase A: SQLite storage, per-frame data model, device identity
Replaces the single global config.json (whole-file pydantic model under one RLock) with SQLite via SQLAlchemy 2.0: users/sessions/frames/links/ pending-claims/battery_log tables (models.py), a per-frame lock registry (db.frame_locked) succeeding config.locked(), and hand-rolled schema versioning (migration.py). A pre-database deployment's config.json is imported verbatim as frame #1 on first boot and left untouched as the rollback path; the old single firmware.bin slot becomes per-frame firmware/<id>.bin. Routes split out of the 900-line main.py into routers/device.py (the frozen /frame/* protocol) and routers/api.py (web UI, still on the old single-frame paths for now). Device auth moves to require_device, which already speaks the full multi-frame protocol: per-frame device tokens pushed via /frame/config and acknowledged on first use, self- registration of unknown device ids as unclaimed frames, pending-claim attachment, and the legacy-token migration window that keeps the currently-deployed firmware (no id, shared MANAGEMENT_TOKEN) resolving to frame #1 -- including the one-time binding of its device id when it first reports one after a future OTA. Externally identical for existing deployments: same paths, same token semantics, same response shapes -- verified with a migration fixture, the legacy-device curl suite, a 20-way concurrent-advance smoke test, and a mutate-restart-assert persistence check against a fake Immich. photo_queue.py ports nearly verbatim onto the Frame ORM row (MutableList JSON columns make its in-place list mutations dirty-track); quiet-hours math extracted unchanged into quiet_hours.py. |