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:
+16
-6
@@ -99,12 +99,22 @@ redisplays whatever was already showing instead of skipping ahead.
|
||||
## Scanning to manage the queue
|
||||
|
||||
Wire a momentary push button between GPIO1 and GND (same wiring style as
|
||||
the other two buttons). A press wakes the device and overlays a small QR
|
||||
code -- "SCAN TO MANAGE" -- in the top-right corner of whatever photo is
|
||||
currently showing, linking to the server's config page. The rest of the
|
||||
photo stays visible and unchanged. After 30 seconds it automatically
|
||||
reverts to the plain photo. See `FRAME_MANAGE_BUTTON_GPIO` above to
|
||||
change the pin or disable the feature.
|
||||
the other two buttons). A press wakes the device and overlays several
|
||||
corners of whatever photo is currently showing, leaving the middle of
|
||||
the photo visible and unchanged:
|
||||
|
||||
- **Top-right**: a QR code -- "SCAN TO MANAGE" -- linking to the
|
||||
server's config page.
|
||||
- **Top-left**: the photo's location, if Immich reverse-geocoded it from
|
||||
GPS EXIF (skipped entirely if not).
|
||||
- **Bottom-right**: the date the photo was taken, if known.
|
||||
- **Bottom-left**: a QR code linking to a public, view-only Immich share
|
||||
link for that exact photo. The link is only created once someone
|
||||
actually scans it, and expires 30 minutes after that.
|
||||
|
||||
After 30 seconds it automatically reverts to the plain photo. See
|
||||
`FRAME_MANAGE_BUTTON_GPIO` above to change the pin or disable the
|
||||
feature.
|
||||
|
||||
The device stays awake for the full 30 seconds (two physical refreshes,
|
||||
one for the overlay and one to revert), so this costs meaningfully more
|
||||
|
||||
+157
-27
@@ -146,6 +146,48 @@ static bool json_extract_uint(const char *json, const char *key, uint32_t *out)
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Finds the string value associated with "key" in a small, flat JSON
|
||||
* blob, e.g. "San Francisco, CA" in {"location": "San Francisco, CA"}.
|
||||
* Same rationale as json_extract_uint() -- not a general parser. Returns
|
||||
* false if the key is missing or its value is JSON null. Only unescapes
|
||||
* \" -- values from this server need nothing fancier. */
|
||||
static bool json_extract_string(const char *json, const char *key, char *out, size_t out_size)
|
||||
{
|
||||
char needle[48];
|
||||
snprintf(needle, sizeof(needle), "\"%s\"", key);
|
||||
const char *pos = strstr(json, needle);
|
||||
if (pos == NULL) {
|
||||
return false;
|
||||
}
|
||||
pos = strchr(pos, ':');
|
||||
if (pos == NULL) {
|
||||
return false;
|
||||
}
|
||||
pos++;
|
||||
while (*pos == ' ') {
|
||||
pos++;
|
||||
}
|
||||
if (strncmp(pos, "null", 4) == 0) {
|
||||
return false;
|
||||
}
|
||||
if (*pos != '"') {
|
||||
return false;
|
||||
}
|
||||
pos++;
|
||||
|
||||
size_t i = 0;
|
||||
while (*pos != '\0' && *pos != '"' && i + 1 < out_size) {
|
||||
if (pos[0] == '\\' && pos[1] == '"') {
|
||||
out[i++] = '"';
|
||||
pos += 2;
|
||||
} else {
|
||||
out[i++] = *pos++;
|
||||
}
|
||||
}
|
||||
out[i] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
/* GETs the server's /frame/config -- doubles as both the reachability
|
||||
* check (any completed HTTP response means the socket-level connection
|
||||
* succeeded) and the source of the server-configurable refresh interval. */
|
||||
@@ -199,24 +241,87 @@ static frame_server_config_t fetch_frame_config(const char *toolsserver)
|
||||
return result;
|
||||
}
|
||||
|
||||
/* GETs the server's /frame/photo-info for the manage-button overlay:
|
||||
* location/taken_at text (left empty if the server didn't have them --
|
||||
* e.g. no GPS EXIF to geocode, or no capture date) and a share_url built
|
||||
* from the returned asset_id, same construction pattern as
|
||||
* run_fetch_cycle()'s management_url. Any failure (unreachable, no
|
||||
* current photo, etc.) just leaves all three outputs empty -- the caller
|
||||
* treats that as "skip these optional overlay regions", not a hard
|
||||
* error, since the base "scan to manage" QR should still show. */
|
||||
static void fetch_photo_info(const char *toolsserver, char *location, size_t location_size, char *taken_at,
|
||||
size_t taken_at_size, char *share_url, size_t share_url_size)
|
||||
{
|
||||
location[0] = '\0';
|
||||
taken_at[0] = '\0';
|
||||
share_url[0] = '\0';
|
||||
|
||||
char url[160];
|
||||
snprintf(url, sizeof(url), "http://%s/frame/photo-info", toolsserver);
|
||||
|
||||
esp_http_client_config_t config = {
|
||||
.url = url,
|
||||
.method = HTTP_METHOD_GET,
|
||||
.timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS,
|
||||
};
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
|
||||
esp_err_t err = esp_http_client_open(client, 0);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "'%s' not reachable: %s", url, esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
|
||||
int status = esp_http_client_fetch_headers(client) >= 0 ? esp_http_client_get_status_code(client) : -1;
|
||||
if (status != 200) {
|
||||
ESP_LOGW(TAG, "'%s' returned HTTP %d", url, status);
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
|
||||
char body[384];
|
||||
int total = 0;
|
||||
int n;
|
||||
while (total < (int)sizeof(body) - 1 &&
|
||||
(n = esp_http_client_read(client, body + total, sizeof(body) - 1 - total)) > 0) {
|
||||
total += n;
|
||||
}
|
||||
body[total] = '\0';
|
||||
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
|
||||
json_extract_string(body, "location", location, location_size);
|
||||
json_extract_string(body, "taken_at", taken_at, taken_at_size);
|
||||
|
||||
char asset_id[48];
|
||||
if (json_extract_string(body, "asset_id", asset_id, sizeof(asset_id))) {
|
||||
snprintf(share_url, share_url_size, "http://%s/frame/share/%s", toolsserver, asset_id);
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
esp_http_client_handle_t client;
|
||||
size_t stream_pos; /* running absolute offset into the frame, for overlay splicing */
|
||||
const manage_qr_overlay_t *overlay; /* NULL = no overlay this fetch */
|
||||
size_t stream_pos; /* running absolute offset into the frame, for overlay splicing */
|
||||
const manage_overlay_set_t *overlay; /* NULL = no overlay this fetch */
|
||||
} http_read_ctx_t;
|
||||
|
||||
/* Splices overlay pixels over the real photo bytes in chunk wherever
|
||||
* chunk's absolute byte range [chunk_start, chunk_start+chunk_len) within
|
||||
* the full frame intersects the overlay's rectangle. Rows/chunks outside
|
||||
* the overlay's footprint are left completely untouched. overlay->x0 is
|
||||
* always even (see manage_qr_overlay.h), so byte_x0 below is exact. */
|
||||
static void splice_overlay(uint8_t *chunk, size_t chunk_len, size_t chunk_start, const manage_qr_overlay_t *overlay)
|
||||
/* Splices one overlay region's pixels over the real photo bytes in chunk
|
||||
* wherever chunk's absolute byte range [chunk_start, chunk_start+chunk_len)
|
||||
* within the full frame intersects that region's rectangle. Rows/chunks
|
||||
* outside the region's footprint are left completely untouched.
|
||||
* region->x0 is always even (see manage_qr_overlay.h), so byte_x0 below
|
||||
* is exact. */
|
||||
static void splice_overlay_region(uint8_t *chunk, size_t chunk_len, size_t chunk_start,
|
||||
const manage_overlay_region_t *region)
|
||||
{
|
||||
int byte_x0 = overlay->x0 / 2;
|
||||
int byte_w = overlay->w / 2;
|
||||
int byte_x0 = region->x0 / 2;
|
||||
int byte_w = region->w / 2;
|
||||
size_t chunk_end = chunk_start + chunk_len;
|
||||
|
||||
for (int row = overlay->y0; row < overlay->y0 + overlay->h; row++) {
|
||||
for (int row = region->y0; row < region->y0 + region->h; row++) {
|
||||
size_t row_start = (size_t)row * EPD_BYTES_PER_ROW + (size_t)byte_x0;
|
||||
size_t row_end = row_start + (size_t)byte_w;
|
||||
|
||||
@@ -226,14 +331,21 @@ static void splice_overlay(uint8_t *chunk, size_t chunk_len, size_t chunk_start,
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t overlay_row_offset = (size_t)(row - overlay->y0) * (size_t)byte_w + (lo - row_start);
|
||||
memcpy(chunk + (lo - chunk_start), overlay->buf + overlay_row_offset, hi - lo);
|
||||
size_t region_row_offset = (size_t)(row - region->y0) * (size_t)byte_w + (lo - row_start);
|
||||
memcpy(chunk + (lo - chunk_start), region->buf + region_row_offset, hi - lo);
|
||||
}
|
||||
}
|
||||
|
||||
static void splice_overlay(uint8_t *chunk, size_t chunk_len, size_t chunk_start, const manage_overlay_set_t *overlay)
|
||||
{
|
||||
for (int i = 0; i < overlay->count; i++) {
|
||||
splice_overlay_region(chunk, chunk_len, chunk_start, &overlay->regions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pulls the next chunk straight out of the in-progress HTTP response --
|
||||
* epd_write_frame() calls this to feed the panel without ever holding
|
||||
* the full ~192KB frame in RAM. Splices in ctx->overlay's pixels (if
|
||||
* the full ~192KB frame in RAM. Splices in ctx->overlay's regions (if
|
||||
* set) as chunks pass through, so the panel driver never needs to know
|
||||
* an overlay exists at all. */
|
||||
static size_t http_read_fn(uint8_t *chunk, size_t chunk_size, void *ctx_)
|
||||
@@ -259,7 +371,8 @@ static size_t http_read_fn(uint8_t *chunk, size_t chunk_size, void *ctx_)
|
||||
* epd_display_stream() (see epd7in3e.c) refuses to trigger a physical
|
||||
* refresh on a short/wrong-size stream, so a failure here always leaves
|
||||
* the visible screen exactly as it was. */
|
||||
static esp_err_t fetch_and_display(const frame_config_t *cfg, bool force_advance, const manage_qr_overlay_t *overlay)
|
||||
static esp_err_t fetch_and_display(const frame_config_t *cfg, bool force_advance,
|
||||
const manage_overlay_set_t *overlay)
|
||||
{
|
||||
char url[160];
|
||||
snprintf(url, sizeof(url), "http://%s/%s", cfg->toolsserver, force_advance ? "frame/advance" : "frame/image");
|
||||
@@ -317,12 +430,15 @@ static esp_err_t fetch_and_display(const frame_config_t *cfg, bool force_advance
|
||||
}
|
||||
|
||||
/* Runs the appropriate fetch for this cycle: a plain fetch, or -- if
|
||||
* show_management_qr -- a fetch with the "scan to manage" QR overlay
|
||||
* spliced in, held on screen for 30s (device stays awake, doesn't sleep
|
||||
* the panel or the chip), then reverted with a second plain fetch.
|
||||
* Returns non-ESP_OK only if the FIRST fetch failed; a revert failure
|
||||
* afterward is logged but doesn't count as an overall failure -- the QR
|
||||
* itself displayed fine, which was the point of the button. */
|
||||
* show_management_qr -- a fetch with the manage overlay spliced in (the
|
||||
* top-right "scan to manage" QR always, plus location/date-taken/
|
||||
* share-QR corners wherever the server had that data -- see
|
||||
* fetch_photo_info()), held on screen for 30s (device stays awake,
|
||||
* doesn't sleep the panel or the chip), then reverted with a second
|
||||
* plain fetch. Returns non-ESP_OK only if the FIRST fetch failed; a
|
||||
* revert failure afterward is logged but doesn't count as an overall
|
||||
* failure -- the overlay itself displayed fine, which was the point of
|
||||
* the button. */
|
||||
static esp_err_t run_fetch_cycle(const frame_config_t *cfg, bool force_advance, bool show_management_qr)
|
||||
{
|
||||
if (!show_management_qr) {
|
||||
@@ -332,28 +448,42 @@ static esp_err_t run_fetch_cycle(const frame_config_t *cfg, bool force_advance,
|
||||
char management_url[160];
|
||||
snprintf(management_url, sizeof(management_url), "http://%s/", cfg->toolsserver);
|
||||
|
||||
manage_qr_overlay_t overlay;
|
||||
esp_err_t overlay_err = manage_qr_overlay_render(management_url, &overlay);
|
||||
char location[64];
|
||||
char taken_at[32];
|
||||
char share_url[160];
|
||||
fetch_photo_info(cfg->toolsserver, location, sizeof(location), taken_at, sizeof(taken_at), share_url,
|
||||
sizeof(share_url));
|
||||
|
||||
manage_overlay_content_t content = {
|
||||
.management_url = management_url,
|
||||
.location = location[0] != '\0' ? location : NULL,
|
||||
.taken_at = taken_at[0] != '\0' ? taken_at : NULL,
|
||||
.share_url = share_url[0] != '\0' ? share_url : NULL,
|
||||
};
|
||||
|
||||
manage_overlay_set_t overlay;
|
||||
esp_err_t overlay_err = manage_overlay_render(&content, &overlay);
|
||||
if (overlay_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Could not render management QR overlay (%s), showing photo normally",
|
||||
manage_overlay_free(&overlay);
|
||||
ESP_LOGW(TAG, "Could not render management overlay (%s), showing photo normally",
|
||||
esp_err_to_name(overlay_err));
|
||||
return fetch_and_display(cfg, force_advance, NULL);
|
||||
}
|
||||
|
||||
esp_err_t err = fetch_and_display(cfg, force_advance, &overlay);
|
||||
manage_qr_overlay_free(&overlay);
|
||||
manage_overlay_free(&overlay);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Showing management QR for 30s");
|
||||
ESP_LOGI(TAG, "Showing management overlay for 30s");
|
||||
vTaskDelay(pdMS_TO_TICKS(30000));
|
||||
|
||||
/* force_advance is always false here -- reverting shouldn't skip
|
||||
* ahead a second time. */
|
||||
esp_err_t revert_err = fetch_and_display(cfg, false, NULL);
|
||||
if (revert_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to revert management QR overlay (%s)", esp_err_to_name(revert_err));
|
||||
ESP_LOGW(TAG, "Failed to revert management overlay (%s)", esp_err_to_name(revert_err));
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,19 +4,38 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#define MANAGE_OVERLAY_MAX_REGIONS 4
|
||||
|
||||
typedef struct {
|
||||
uint8_t *buf; /* malloc'd (w/2)*h bytes, packed 2px/byte; caller must free */
|
||||
uint8_t *buf; /* malloc'd (w/2)*h bytes, packed 2px/byte; owned by the region */
|
||||
int x0, y0; /* top-left corner, panel pixel coordinates (x0 is always even) */
|
||||
int w, h; /* pixel dimensions (w is always even) */
|
||||
} manage_qr_overlay_t;
|
||||
} manage_overlay_region_t;
|
||||
|
||||
typedef struct {
|
||||
manage_overlay_region_t regions[MANAGE_OVERLAY_MAX_REGIONS];
|
||||
int count;
|
||||
} manage_overlay_set_t;
|
||||
|
||||
typedef struct {
|
||||
const char *management_url; /* top-right QR + "SCAN TO"/"MANAGE" caption -- always shown */
|
||||
const char *location; /* top-left text; NULL/empty skips this region */
|
||||
const char *taken_at; /* bottom-right text; NULL/empty skips this region */
|
||||
const char *share_url; /* bottom-left QR (no caption); NULL/empty skips this region */
|
||||
} manage_overlay_content_t;
|
||||
|
||||
/**
|
||||
* Renders a small "scan to manage" overlay -- a QR code encoding
|
||||
* management_url plus a "SCAN TO" / "MANAGE" caption, on a white
|
||||
* padded background -- into a freshly allocated buffer sized just for
|
||||
* the overlay itself (not a full EPD_FRAME_BYTES frame), positioned for
|
||||
* the panel's top-right corner. Caller must call manage_qr_overlay_free().
|
||||
* Renders the manage-button overlay: always a "scan to manage" QR in the
|
||||
* top-right corner, plus whichever of location/taken_at/share_url are
|
||||
* non-NULL/non-empty in their own corners (top-left, bottom-right,
|
||||
* bottom-left respectively). Each region is its own separately malloc'd
|
||||
* small buffer (not a full EPD_FRAME_BYTES frame). A failure rendering
|
||||
* the top-right region fails the whole call; a failure rendering one of
|
||||
* the optional regions just skips that region and keeps going. Caller
|
||||
* must call manage_overlay_free() on out regardless of the return value
|
||||
* (out->count reflects however many regions were actually populated).
|
||||
*/
|
||||
esp_err_t manage_qr_overlay_render(const char *management_url, manage_qr_overlay_t *out);
|
||||
esp_err_t manage_overlay_render(const manage_overlay_content_t *content, manage_overlay_set_t *out);
|
||||
|
||||
void manage_qr_overlay_free(manage_qr_overlay_t *overlay);
|
||||
/** Frees every populated region's buffer in overlay. */
|
||||
void manage_overlay_free(manage_overlay_set_t *overlay);
|
||||
|
||||
Reference in New Issue
Block a user