Fix "Show next" staleness bug; add location/date/share-QR to manage overlay
Build and push server image / build-and-push (push) Successful in 32s
Build and push server image / build-and-push (push) Successful in 32s
Two changes, bundled since they landed in the same session and touch
overlapping files:
1. Fix: "Show next" sent the browser's full queue snapshot to
POST /api/queue/reorder, which hard-rejected if the server's queue
had shifted since the last fetch (e.g. right after a queue-length
trim). New POST /api/queue/promote moves one photo to the front
authoritatively, with no dependency on client staleness. /reorder
itself is now tolerant too -- unrecognized IDs are dropped and
missing ones appended, instead of rejecting the whole request.
2. Feature: the manage button's overlay now also shows the photo's
location (top-left, only if Immich reverse-geocoded it from GPS
EXIF), the date it was taken (bottom-right), and a QR code (bottom-
left) linking to a 30-minute public Immich share link -- created
lazily when someone actually scans it, not when the button's
pressed. New server endpoints GET /frame/photo-info and
GET /frame/share/{asset_id} (scoped to the frame's current/queued
photos, not any arbitrary Immich asset). Firmware-side, the overlay
mechanism generalizes from one spliced region to up to four
(manage_qr_overlay.c), each its own small buffer, still never
holding the full frame in RAM.
This commit is contained in:
@@ -14,18 +14,47 @@ static const char *TAG = "manage_qr_overlay";
|
||||
|
||||
#define QR_MAX_VERSION 10
|
||||
#define QR_BUFFER_LEN qrcodegen_BUFFER_LEN_FOR_VERSION(QR_MAX_VERSION)
|
||||
/* Smaller than qr_onboarding.c's QR_MODULE_PX (8) -- this is a compact
|
||||
* corner popup, not a full-screen setup step. */
|
||||
/* Smaller than qr_onboarding.c's QR_MODULE_PX (8) -- these are compact
|
||||
* corner popups, not a full-screen setup step. */
|
||||
#define QR_MODULE_PX 4
|
||||
#define PADDING 16
|
||||
#define QR_TEXT_GAP 8
|
||||
#define LINE_GAP 4
|
||||
/* Distance from the panel's top/right edges to the overlay box. Combined
|
||||
* with EPD_WIDTH and the forced-even box width below, this guarantees
|
||||
* x0 is always even -- required so the overlay's columns land on frame
|
||||
* byte boundaries (2px/byte) when spliced into the fetch stream. */
|
||||
/* Distance from the panel's edges to each overlay box. Combined with
|
||||
* EPD_WIDTH/EPD_HEIGHT and each region's forced-even width below, this
|
||||
* guarantees x0 is always even -- required so a region's columns land on
|
||||
* frame byte boundaries (2px/byte) when spliced into the fetch stream. */
|
||||
#define PANEL_MARGIN 20
|
||||
|
||||
typedef enum {
|
||||
CORNER_TOP_LEFT,
|
||||
CORNER_TOP_RIGHT,
|
||||
CORNER_BOTTOM_LEFT,
|
||||
CORNER_BOTTOM_RIGHT,
|
||||
} overlay_corner_t;
|
||||
|
||||
static void position_region(manage_overlay_region_t *region, overlay_corner_t corner)
|
||||
{
|
||||
switch (corner) {
|
||||
case CORNER_TOP_LEFT:
|
||||
region->x0 = PANEL_MARGIN;
|
||||
region->y0 = PANEL_MARGIN;
|
||||
break;
|
||||
case CORNER_TOP_RIGHT:
|
||||
region->x0 = EPD_WIDTH - PANEL_MARGIN - region->w;
|
||||
region->y0 = PANEL_MARGIN;
|
||||
break;
|
||||
case CORNER_BOTTOM_LEFT:
|
||||
region->x0 = PANEL_MARGIN;
|
||||
region->y0 = EPD_HEIGHT - PANEL_MARGIN - region->h;
|
||||
break;
|
||||
case CORNER_BOTTOM_RIGHT:
|
||||
region->x0 = EPD_WIDTH - PANEL_MARGIN - region->w;
|
||||
region->y0 = EPD_HEIGHT - PANEL_MARGIN - region->h;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_qr(uint8_t *buf, int stride, int width, int height, const uint8_t *qrcode, int origin_x,
|
||||
int origin_y)
|
||||
{
|
||||
@@ -43,13 +72,18 @@ static void draw_qr(uint8_t *buf, int stride, int width, int height, const uint8
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t manage_qr_overlay_render(const char *management_url, manage_qr_overlay_t *out)
|
||||
/* White-padded box with a QR code encoding payload, plus zero, one, or
|
||||
* two centered caption lines beneath it (either may be NULL). Used for
|
||||
* both the top-right "scan to manage" box (two lines) and the
|
||||
* bottom-left share-link box (no lines). */
|
||||
static esp_err_t render_qr_region(const char *payload, const char *line1, const char *line2, overlay_corner_t corner,
|
||||
manage_overlay_region_t *out)
|
||||
{
|
||||
uint8_t temp_buffer[QR_BUFFER_LEN];
|
||||
uint8_t qrcode[QR_BUFFER_LEN];
|
||||
bool ok = qrcodegen_encodeText(management_url, temp_buffer, qrcode, qrcodegen_Ecc_MEDIUM, qrcodegen_VERSION_MIN,
|
||||
bool ok = qrcodegen_encodeText(payload, temp_buffer, qrcode, qrcodegen_Ecc_MEDIUM, qrcodegen_VERSION_MIN,
|
||||
QR_MAX_VERSION, qrcodegen_Mask_AUTO, true);
|
||||
ESP_RETURN_ON_FALSE(ok, ESP_FAIL, TAG, "QR encoding failed for '%s' (too long for max version)", management_url);
|
||||
ESP_RETURN_ON_FALSE(ok, ESP_FAIL, TAG, "QR encoding failed for '%s' (too long for max version)", payload);
|
||||
|
||||
int qr_size = qrcodegen_getSize(qrcode);
|
||||
int qr_px = qr_size * QR_MODULE_PX;
|
||||
@@ -57,15 +91,18 @@ esp_err_t manage_qr_overlay_render(const char *management_url, manage_qr_overlay
|
||||
/* Font24 (32x41px uppercase glyphs) is the only font vendored into
|
||||
* this project -- see components/epaper_fonts. "SCAN TO MANAGE" on
|
||||
* one line would be 448px wide, too wide for a compact corner box,
|
||||
* so it's wrapped across two lines here instead. */
|
||||
static const char *line1 = "SCAN TO";
|
||||
static const char *line2 = "MANAGE";
|
||||
int line1_w = (int)strlen(line1) * Font24.Width;
|
||||
int line2_w = (int)strlen(line2) * Font24.Width;
|
||||
int text_w = line1_w > line2_w ? line1_w : line2_w;
|
||||
* so it's passed in pre-wrapped across two lines instead. */
|
||||
int text_w = 0;
|
||||
int text_h = 0;
|
||||
if (line1 != NULL) {
|
||||
int w1 = (int)strlen(line1) * Font24.Width;
|
||||
int w2 = line2 != NULL ? (int)strlen(line2) * Font24.Width : 0;
|
||||
text_w = w1 > w2 ? w1 : w2;
|
||||
text_h = QR_TEXT_GAP + Font24.Height + (line2 != NULL ? LINE_GAP + Font24.Height : 0);
|
||||
}
|
||||
|
||||
int content_w = qr_px > text_w ? qr_px : text_w;
|
||||
int content_h = qr_px + QR_TEXT_GAP + Font24.Height + LINE_GAP + Font24.Height;
|
||||
int content_h = qr_px + text_h;
|
||||
|
||||
int w = content_w + PADDING * 2;
|
||||
int h = content_h + PADDING * 2;
|
||||
@@ -73,28 +110,91 @@ esp_err_t manage_qr_overlay_render(const char *management_url, manage_qr_overlay
|
||||
|
||||
int stride = w / 2;
|
||||
uint8_t *buf = malloc((size_t)stride * h);
|
||||
ESP_RETURN_ON_FALSE(buf != NULL, ESP_ERR_NO_MEM, TAG, "Failed to allocate overlay buffer");
|
||||
ESP_RETURN_ON_FALSE(buf != NULL, ESP_ERR_NO_MEM, TAG, "Failed to allocate overlay region");
|
||||
memset(buf, (EPD_COLOR_WHITE << 4) | EPD_COLOR_WHITE, (size_t)stride * h);
|
||||
|
||||
int center_x = w / 2;
|
||||
int y = PADDING;
|
||||
draw_qr(buf, stride, w, h, qrcode, center_x - qr_px / 2, y);
|
||||
y += qr_px + QR_TEXT_GAP;
|
||||
epd_draw_text_centered_ex(buf, stride, w, h, &Font24, line1, center_x, y);
|
||||
y += Font24.Height + LINE_GAP;
|
||||
epd_draw_text_centered_ex(buf, stride, w, h, &Font24, line2, center_x, y);
|
||||
y += qr_px;
|
||||
if (line1 != NULL) {
|
||||
y += QR_TEXT_GAP;
|
||||
epd_draw_text_centered_ex(buf, stride, w, h, &Font24, line1, center_x, y);
|
||||
y += Font24.Height;
|
||||
}
|
||||
if (line2 != NULL) {
|
||||
y += LINE_GAP;
|
||||
epd_draw_text_centered_ex(buf, stride, w, h, &Font24, line2, center_x, y);
|
||||
}
|
||||
|
||||
out->buf = buf;
|
||||
out->w = w;
|
||||
out->h = h;
|
||||
out->x0 = EPD_WIDTH - PANEL_MARGIN - w;
|
||||
out->y0 = PANEL_MARGIN;
|
||||
position_region(out, corner);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* White-padded box with a single centered line of text -- used for the
|
||||
* top-left location and bottom-right date-taken labels. */
|
||||
static esp_err_t render_text_region(const char *text, overlay_corner_t corner, manage_overlay_region_t *out)
|
||||
{
|
||||
int text_w = (int)strlen(text) * Font24.Width;
|
||||
int w = text_w + PADDING * 2;
|
||||
int h = Font24.Height + PADDING * 2;
|
||||
w += w % 2;
|
||||
|
||||
int stride = w / 2;
|
||||
uint8_t *buf = malloc((size_t)stride * h);
|
||||
ESP_RETURN_ON_FALSE(buf != NULL, ESP_ERR_NO_MEM, TAG, "Failed to allocate overlay region");
|
||||
memset(buf, (EPD_COLOR_WHITE << 4) | EPD_COLOR_WHITE, (size_t)stride * h);
|
||||
|
||||
epd_draw_text_centered_ex(buf, stride, w, h, &Font24, text, w / 2, PADDING);
|
||||
|
||||
out->buf = buf;
|
||||
out->w = w;
|
||||
out->h = h;
|
||||
position_region(out, corner);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t manage_overlay_render(const manage_overlay_content_t *content, manage_overlay_set_t *out)
|
||||
{
|
||||
out->count = 0;
|
||||
|
||||
esp_err_t err = render_qr_region(content->management_url, "SCAN TO", "MANAGE", CORNER_TOP_RIGHT,
|
||||
&out->regions[out->count]);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
out->count++;
|
||||
|
||||
if (content->location != NULL && content->location[0] != '\0') {
|
||||
if (render_text_region(content->location, CORNER_TOP_LEFT, &out->regions[out->count]) == ESP_OK) {
|
||||
out->count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (content->taken_at != NULL && content->taken_at[0] != '\0') {
|
||||
if (render_text_region(content->taken_at, CORNER_BOTTOM_RIGHT, &out->regions[out->count]) == ESP_OK) {
|
||||
out->count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (content->share_url != NULL && content->share_url[0] != '\0') {
|
||||
if (render_qr_region(content->share_url, NULL, NULL, CORNER_BOTTOM_LEFT, &out->regions[out->count]) ==
|
||||
ESP_OK) {
|
||||
out->count++;
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void manage_qr_overlay_free(manage_qr_overlay_t *overlay)
|
||||
void manage_overlay_free(manage_overlay_set_t *overlay)
|
||||
{
|
||||
free(overlay->buf);
|
||||
overlay->buf = NULL;
|
||||
for (int i = 0; i < overlay->count; i++) {
|
||||
free(overlay->regions[i].buf);
|
||||
overlay->regions[i].buf = NULL;
|
||||
}
|
||||
overlay->count = 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user