#include #include #include "esp_check.h" #include "epd7in3e.h" #include "epd_draw.h" #include "fonts.h" #include "qrcodegen.h" #include "manage_qr_overlay.h" 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) -- 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 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) { int size = qrcodegen_getSize(qrcode); for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { epd_color_t color = qrcodegen_getModule(qrcode, x, y) ? EPD_COLOR_BLACK : EPD_COLOR_WHITE; for (int dy = 0; dy < QR_MODULE_PX; dy++) { for (int dx = 0; dx < QR_MODULE_PX; dx++) { epd_draw_pixel_ex(buf, stride, width, height, origin_x + x * QR_MODULE_PX + dx, origin_y + y * QR_MODULE_PX + dy, color); } } } } } /* 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(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)", payload); int qr_size = qrcodegen_getSize(qrcode); int qr_px = qr_size * QR_MODULE_PX; /* 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 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 + text_h; int w = content_w + PADDING * 2; int h = content_h + PADDING * 2; w += w % 2; /* keep byte-aligned (2px/byte) */ 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); int center_x = w / 2; int y = PADDING; draw_qr(buf, stride, w, h, qrcode, center_x - qr_px / 2, 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; position_region(out, corner); return ESP_OK; } /* White-padded box with one or two centered lines of text (line2 may be * NULL) -- used for the top-left location (city + state/country, two * lines rather than cramming both onto one to keep the box from * threatening to overlap the top-right QR box) and the bottom-right * date-taken label (one line). */ static esp_err_t render_text_region(const char *line1, const char *line2, overlay_corner_t corner, manage_overlay_region_t *out) { int w1 = (int)strlen(line1) * Font24.Width; int w2 = line2 != NULL ? (int)strlen(line2) * Font24.Width : 0; int text_w = w1 > w2 ? w1 : w2; int text_h = Font24.Height + (line2 != NULL ? LINE_GAP + Font24.Height : 0); int w = text_w + PADDING * 2; int h = text_h + 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); int center_x = w / 2; int y = PADDING; epd_draw_text_centered_ex(buf, stride, w, h, &Font24, line1, center_x, y); if (line2 != NULL) { y += Font24.Height + 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; position_region(out, corner); return ESP_OK; } /* Deliberately tighter than PADDING (used for the fixed QR/text corner * boxes) -- these labels sit right next to a face rather than needing * generous QR-scanning margin, and there can be several of them * simultaneously (see MANAGE_FACE_LABELS_MAX's memory-budget note in * the header). */ #define FACE_LABEL_PADDING 8 #define FACE_LABEL_GAP 4 /* distance from the face's anchor point to the label box */ /* White-padded single-line name label positioned near an arbitrary * (anchor_x, anchor_y) face position, rather than a fixed corner -- * unlike the four corner regions (always in-bounds by construction), * this needs real clamping since a face can be anywhere, including near * an edge. Centered horizontally on the face, placed just below it by * default, flipped above if there's no room below. */ static esp_err_t render_face_label_region(const char *name, int anchor_x, int anchor_y, manage_overlay_region_t *out) { int text_w = (int)strlen(name) * Font24.Width; int w = text_w + FACE_LABEL_PADDING * 2; int h = Font24.Height + FACE_LABEL_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, name, w / 2, FACE_LABEL_PADDING); int x0 = anchor_x - w / 2; int y0 = anchor_y + FACE_LABEL_GAP; if (y0 + h > EPD_HEIGHT) { y0 = anchor_y - FACE_LABEL_GAP - h; /* no room below -- place above the face instead */ } if (x0 < 0) { x0 = 0; } else if (x0 + w > EPD_WIDTH) { x0 = EPD_WIDTH - w; } if (y0 < 0) { y0 = 0; } else if (y0 + h > EPD_HEIGHT) { y0 = EPD_HEIGHT - h; } x0 -= x0 % 2; /* keep byte-aligned (2px/byte) */ out->buf = buf; out->w = w; out->h = h; out->x0 = x0; out->y0 = y0; return ESP_OK; } /* Battery glyph dimensions -- a static outline (body rectangle + small * terminal nub on the right), deliberately NOT a fill-level graphic. */ #define BATTERY_ICON_W 44 #define BATTERY_ICON_H 24 #define BATTERY_ICON_STROKE 2 #define BATTERY_NUB_W 6 #define BATTERY_NUB_H 12 #define BATTERY_ICON_TEXT_GAP 8 #define BATTERY_REGION_GAP 8 /* vertical gap below the manage QR box */ static void draw_battery_icon(uint8_t *buf, int stride, int width, int height, int x0, int y0) { for (int y = 0; y < BATTERY_ICON_H; y++) { for (int x = 0; x < BATTERY_ICON_W; x++) { bool edge = x < BATTERY_ICON_STROKE || x >= BATTERY_ICON_W - BATTERY_ICON_STROKE || y < BATTERY_ICON_STROKE || y >= BATTERY_ICON_H - BATTERY_ICON_STROKE; if (edge) { epd_draw_pixel_ex(buf, stride, width, height, x0 + x, y0 + y, EPD_COLOR_BLACK); } } } int nub_y = y0 + (BATTERY_ICON_H - BATTERY_NUB_H) / 2; for (int y = 0; y < BATTERY_NUB_H; y++) { for (int x = 0; x < BATTERY_NUB_W; x++) { epd_draw_pixel_ex(buf, stride, width, height, x0 + BATTERY_ICON_W + x, nub_y + y, EPD_COLOR_BLACK); } } } /* White-padded box with the battery glyph and "NN%" beside it, placed * directly below an already-positioned anchor region (the top-right * manage QR box), right-aligned to the anchor's right edge. */ static esp_err_t render_battery_region(int percent, const manage_overlay_region_t *anchor, manage_overlay_region_t *out) { char text[8]; snprintf(text, sizeof(text), "%d%%", percent); int icon_total_w = BATTERY_ICON_W + BATTERY_NUB_W; int text_w = (int)strlen(text) * Font24.Width; int content_w = icon_total_w + BATTERY_ICON_TEXT_GAP + text_w; int content_h = Font24.Height > BATTERY_ICON_H ? Font24.Height : BATTERY_ICON_H; int w = content_w + PADDING * 2; int h = content_h + 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); draw_battery_icon(buf, stride, w, h, PADDING, PADDING + (content_h - BATTERY_ICON_H) / 2); epd_draw_text_ex(buf, stride, w, h, &Font24, text, PADDING + icon_total_w + BATTERY_ICON_TEXT_GAP, PADDING + (content_h - Font24.Height) / 2); out->buf = buf; out->w = w; out->h = h; out->x0 = anchor->x0 + anchor->w - w; out->x0 -= out->x0 % 2; /* keep byte-aligned (2px/byte) */ out->y0 = anchor->y0 + anchor->h + BATTERY_REGION_GAP; 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->battery_percent >= 0 && content->battery_percent <= 100) { /* Anchored below the manage QR box just rendered (regions[0]). */ if (render_battery_region(content->battery_percent, &out->regions[0], &out->regions[out->count]) == ESP_OK) { out->count++; } } if (content->location_line1 != NULL && content->location_line1[0] != '\0') { const char *line2 = (content->location_line2 != NULL && content->location_line2[0] != '\0') ? content->location_line2 : NULL; if (render_text_region(content->location_line1, line2, 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, NULL, 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, "SCAN TO", "DOWNLOAD", CORNER_BOTTOM_LEFT, &out->regions[out->count]) == ESP_OK) { out->count++; } } int face_count = content->face_label_count; if (face_count > MANAGE_FACE_LABELS_MAX) { face_count = MANAGE_FACE_LABELS_MAX; } for (int i = 0; content->face_labels != NULL && i < face_count; i++) { const manage_face_label_t *label = &content->face_labels[i]; if (label->name[0] == '\0') { continue; } if (render_face_label_region(label->name, label->x, label->y, &out->regions[out->count]) == ESP_OK) { out->count++; } } return ESP_OK; } void manage_overlay_free(manage_overlay_set_t *overlay) { for (int i = 0; i < overlay->count; i++) { free(overlay->regions[i].buf); overlay->regions[i].buf = NULL; } overlay->count = 0; }