Add third button: "scan to manage" QR overlay on the current photo

Pressing the manage button (GPIO1) overlays a small QR code -- "SCAN TO
MANAGE" -- in the top-right corner of whatever photo is currently on
screen, linking to the server's config page, then reverts to the plain
photo after 30 seconds.

The overlay is spliced into the existing streaming fetch as chunks pass
through (frame_client.c's http_read_fn), rather than buffering the full
192,000-byte frame in RAM: only the small overlay rectangle itself
(~30KB) is ever held in memory, generated via new stride-parameterized
drawing helpers (epd_draw_*_ex in epd_draw.c) that let the existing
QR/text drawing code target an arbitrarily-sized buffer instead of a
full-frame one. epd7in3e.c is untouched -- it has no idea an overlay
exists.
This commit is contained in:
2026-07-19 00:50:07 -04:00
parent c4cd9b73e8
commit f74085cedf
13 changed files with 395 additions and 27 deletions
+5 -1
View File
@@ -28,7 +28,7 @@ Configuration** if your wiring differs.
| VCC | 3.3V | -- |
| GND | GND | -- |
Optionally, two buttons, both wired the same way -- momentary push button
Optionally, three buttons, all wired the same way -- momentary push button
between the GPIO and GND, no external resistor needed (the firmware
enables each pin's internal pull-up, so it idles high and reads low when
pressed):
@@ -39,6 +39,10 @@ pressed):
- **Next photo (GPIO2)**: a normal press skips immediately to the next
photo; see
[`firmware/README.md`](../firmware/README.md#skipping-to-the-next-photo).
- **Manage (GPIO1)**: a normal press overlays a "scan to manage" QR code
on the current photo for 30 seconds, linking to the server's config
page; see
[`firmware/README.md`](../firmware/README.md#scanning-to-manage-the-queue).
Both pins were picked because they're within GPIO 0-7 -- the only pins
the ESP32-C6 can wake from deep sleep on -- aren't strapping pins, and
+16
View File
@@ -47,6 +47,7 @@ Under **ESPresso Frame Configuration**:
| `FRAME_RESET_BUTTON_GPIO` | 3 | Factory-reset button GPIO (-1 to disable). Must be 0-7 (ESP32-C6's deep-sleep-wakeup-capable pins) |
| `FRAME_RESET_BUTTON_HOLD_MS` | 10000 | How long the button must be held to trigger a reset |
| `FRAME_NEXT_BUTTON_GPIO` | 2 | Next-photo button GPIO (-1 to disable). Must be 0-7 |
| `FRAME_MANAGE_BUTTON_GPIO` | 1 | "Scan to manage" button GPIO (-1 to disable). Must be 0-7 |
Under **E-Paper Display (epd7in3e) Configuration**: SPI/GPIO pin
assignments and SPI clock speed -- see
@@ -95,6 +96,21 @@ server decides when to advance based on its own clock (see
[`server/README.md`](../server/README.md)), so an unplanned reboot just
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 device stays awake for the full 30 seconds (two physical refreshes,
one for the overlay and one to revert), so this costs meaningfully more
power than a normal wake -- expected for a deliberate, occasional action,
same tradeoff as the other two buttons.
## Resetting to provisioning mode
Wire a momentary push button between GPIO3 and GND (internal pull-up,
+1 -1
View File
@@ -1,3 +1,3 @@
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c qr_onboarding.c status_screen.c epd_draw.c reset_button.c next_button.c
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c qr_onboarding.c status_screen.c epd_draw.c reset_button.c next_button.c manage_button.c manage_qr_overlay.c
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_netif esp_http_server esp_http_client dns_server epd7in3e qrcode epaper_fonts esp_driver_gpio
EMBED_FILES root.html)
+15
View File
@@ -121,4 +121,19 @@ menu "ESPresso Frame Configuration"
FRAME_RESET_BUTTON_GPIO above; defaults to a different pin
than the reset button. Set to -1 to disable the feature.
config FRAME_MANAGE_BUTTON_GPIO
int "Manage button GPIO (-1 to disable)"
default 1
range -1 7
help
Button wired between this GPIO and GND (active-low, internal
pull-up enabled in firmware -- no external resistor needed).
Pressing it wakes the device (if asleep), displays the
current photo with a small "scan to manage" QR code overlaid
in the top-right corner (linking to the tools server's config
page) for 30 seconds, then reverts to the plain photo. Must
be GPIO 0-7 for the same deep-sleep-wakeup reason as
FRAME_RESET_BUTTON_GPIO above; defaults to a different pin
than the other two buttons. Set to -1 to disable the feature.
endmenu
+26 -9
View File
@@ -4,20 +4,25 @@
#include "epd_draw.h"
void epd_draw_pixel(uint8_t *frame, int x, int y, epd_color_t color)
void epd_draw_pixel_ex(uint8_t *buf, int stride, int width, int height, int x, int y, epd_color_t color)
{
if (x < 0 || x >= EPD_WIDTH || y < 0 || y >= EPD_HEIGHT) {
if (x < 0 || x >= width || y < 0 || y >= height) {
return;
}
size_t byte_index = (size_t)y * EPD_BYTES_PER_ROW + (size_t)x / 2;
size_t byte_index = (size_t)y * stride + (size_t)x / 2;
uint8_t nibble = (uint8_t)color & 0x0F;
if (x % 2 == 0) {
frame[byte_index] = (frame[byte_index] & 0x0F) | (nibble << 4);
buf[byte_index] = (buf[byte_index] & 0x0F) | (nibble << 4);
} else {
frame[byte_index] = (frame[byte_index] & 0xF0) | nibble;
buf[byte_index] = (buf[byte_index] & 0xF0) | nibble;
}
}
void epd_draw_pixel(uint8_t *frame, int x, int y, epd_color_t color)
{
epd_draw_pixel_ex(frame, EPD_BYTES_PER_ROW, EPD_WIDTH, EPD_HEIGHT, x, y, color);
}
/* fonts.h's sFONT tables cover the printable ASCII range starting at ' '
* (0x20), one glyph per character, MSB-first within each row (see
* components/epaper_fonts). */
@@ -34,7 +39,8 @@ static bool font_pixel_set(const sFONT *font, char c, int row, int col)
return (glyph[byte_idx] >> bit_in_byte) & 0x1;
}
void epd_draw_text(uint8_t *frame, const sFONT *font, const char *text, int origin_x, int origin_y)
void epd_draw_text_ex(uint8_t *buf, int stride, int width, int height, const sFONT *font, const char *text,
int origin_x, int origin_y)
{
int cursor_x = origin_x;
for (const char *p = text; *p != '\0'; p++) {
@@ -42,7 +48,7 @@ void epd_draw_text(uint8_t *frame, const sFONT *font, const char *text, int orig
for (int row = 0; row < font->Height; row++) {
for (int col = 0; col < font->Width; col++) {
if (font_pixel_set(font, c, row, col)) {
epd_draw_pixel(frame, cursor_x + col, origin_y + row, EPD_COLOR_BLACK);
epd_draw_pixel_ex(buf, stride, width, height, cursor_x + col, origin_y + row, EPD_COLOR_BLACK);
}
}
}
@@ -50,10 +56,21 @@ void epd_draw_text(uint8_t *frame, const sFONT *font, const char *text, int orig
}
}
void epd_draw_text(uint8_t *frame, const sFONT *font, const char *text, int origin_x, int origin_y)
{
epd_draw_text_ex(frame, EPD_BYTES_PER_ROW, EPD_WIDTH, EPD_HEIGHT, font, text, origin_x, origin_y);
}
void epd_draw_text_centered_ex(uint8_t *buf, int stride, int width, int height, const sFONT *font, const char *text,
int center_x, int y)
{
int text_width = (int)strlen(text) * font->Width;
epd_draw_text_ex(buf, stride, width, height, font, text, center_x - text_width / 2, y);
}
void epd_draw_text_centered(uint8_t *frame, const sFONT *font, const char *text, int center_x, int y)
{
int width = (int)strlen(text) * font->Width;
epd_draw_text(frame, font, text, center_x - width / 2, y);
epd_draw_text_centered_ex(frame, EPD_BYTES_PER_ROW, EPD_WIDTH, EPD_HEIGHT, font, text, center_x, y);
}
void epd_draw_line(uint8_t *frame, int x0, int y0, int x1, int y1, int thickness)
+16
View File
@@ -18,6 +18,22 @@ void epd_draw_text(uint8_t *frame, const sFONT *font, const char *text, int orig
/** Same as epd_draw_text(), but horizontally centered on center_x. */
void epd_draw_text_centered(uint8_t *frame, const sFONT *font, const char *text, int center_x, int y);
/**
* Same as epd_draw_pixel()/epd_draw_text()/epd_draw_text_centered(), but
* against an arbitrarily-sized buffer instead of a full EPD_FRAME_BYTES
* one -- stride is bytes per row (packed 2px/byte, so width/2), width and
* height are that buffer's pixel dimensions, used for bounds-checking
* instead of the panel's fixed EPD_WIDTH/EPD_HEIGHT. The non-_ex
* functions above are thin wrappers around these, passing
* EPD_BYTES_PER_ROW/EPD_WIDTH/EPD_HEIGHT -- existing callers are
* unaffected.
*/
void epd_draw_pixel_ex(uint8_t *buf, int stride, int width, int height, int x, int y, epd_color_t color);
void epd_draw_text_ex(uint8_t *buf, int stride, int width, int height, const sFONT *font, const char *text,
int origin_x, int origin_y);
void epd_draw_text_centered_ex(uint8_t *buf, int stride, int width, int height, const sFONT *font, const char *text,
int center_x, int y);
/** Draws a line of the given thickness (in pixels) between two points. */
void epd_draw_line(uint8_t *frame, int x0, int y0, int x1, int y1, int thickness);
+93 -9
View File
@@ -13,6 +13,7 @@
#include "epd7in3e.h"
#include "status_screen.h"
#include "manage_qr_overlay.h"
#include "frame_client.h"
@@ -200,25 +201,65 @@ static frame_server_config_t fetch_frame_config(const char *toolsserver)
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 */
} 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)
{
int byte_x0 = overlay->x0 / 2;
int byte_w = overlay->w / 2;
size_t chunk_end = chunk_start + chunk_len;
for (int row = overlay->y0; row < overlay->y0 + overlay->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;
size_t lo = row_start > chunk_start ? row_start : chunk_start;
size_t hi = row_end < chunk_end ? row_end : chunk_end;
if (lo >= hi) {
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);
}
}
/* Pulls the next chunk straight out of the in-progress HTTP response --
* epd_display_stream() calls this to feed the panel without ever holding
* the full ~192KB frame in RAM. */
* 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
* 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_)
{
http_read_ctx_t *ctx = (http_read_ctx_t *)ctx_;
int n = esp_http_client_read(ctx->client, (char *)chunk, (int)chunk_size);
return n > 0 ? (size_t)n : 0;
if (n <= 0) {
return 0;
}
if (ctx->overlay != NULL) {
splice_overlay(chunk, (size_t)n, ctx->stream_pos, ctx->overlay);
}
ctx->stream_pos += (size_t)n;
return (size_t)n;
}
/* GETs /frame/image (or, if force_advance, POSTs /frame/advance to skip
* ahead immediately) and streams the response directly into the panel.
* Returning non-ESP_OK means the panel was never actually refreshed --
* ahead immediately) and streams the response directly into the panel,
* splicing in overlay's pixels (if non-NULL) as it streams. Returning
* non-ESP_OK means the panel was never actually refreshed --
* 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)
static esp_err_t fetch_and_display(const frame_config_t *cfg, bool force_advance, const manage_qr_overlay_t *overlay)
{
char url[160];
snprintf(url, sizeof(url), "http://%s/%s", cfg->toolsserver, force_advance ? "frame/advance" : "frame/image");
@@ -247,7 +288,7 @@ static esp_err_t fetch_and_display(const frame_config_t *cfg, bool force_advance
}
ESP_LOGI(TAG, "Fetching frame (%d bytes) from '%s'", content_length, url);
http_read_ctx_t ctx = { .client = client };
http_read_ctx_t ctx = { .client = client, .overlay = overlay };
uint32_t crc = 0;
err = epd_write_frame(http_read_fn, &ctx, &crc);
@@ -275,7 +316,50 @@ static esp_err_t fetch_and_display(const frame_config_t *cfg, bool force_advance
return err;
}
void frame_client_run(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. */
static esp_err_t run_fetch_cycle(const frame_config_t *cfg, bool force_advance, bool show_management_qr)
{
if (!show_management_qr) {
return fetch_and_display(cfg, force_advance, NULL);
}
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);
if (overlay_err != ESP_OK) {
ESP_LOGW(TAG, "Could not render management QR 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);
if (err != ESP_OK) {
return err;
}
ESP_LOGI(TAG, "Showing management QR 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));
}
return ESP_OK;
}
void frame_client_run(const frame_config_t *cfg, bool force_advance, bool show_management_qr)
{
esp_err_t epd_err = epd_init();
bool have_display = (epd_err == ESP_OK);
@@ -309,7 +393,7 @@ void frame_client_run(const frame_config_t *cfg, bool force_advance)
* worth it to stop false-failing on the common case. */
bool image_ok = true;
if (have_display) {
esp_err_t fetch_err = fetch_and_display(cfg, force_advance);
esp_err_t fetch_err = run_fetch_cycle(cfg, force_advance, show_management_qr);
image_ok = (fetch_err == ESP_OK);
if (!image_ok) {
/* epd_display_stream() never triggers a physical refresh on a
+7 -1
View File
@@ -23,5 +23,11 @@ esp_err_t frame_wifi_connect_sta(const frame_config_t *cfg);
* -- the server decides on its own whether to advance in the normal case,
* based on its configured refresh interval, so a plain wake/reboot never
* skips a photo just by asking.
*
* If show_management_qr is true (the manage button was held), the
* displayed photo gets a small "scan to manage" QR overlay in the
* top-right corner linking to the server's config page, held for 30
* seconds (the device stays awake), then reverted back to the plain
* photo before proceeding to the normal sleep-interval logic.
*/
void frame_client_run(const frame_config_t *cfg, bool force_advance);
void frame_client_run(const frame_config_t *cfg, bool force_advance, bool show_management_qr);
+9 -6
View File
@@ -9,6 +9,7 @@
#include "frame_client.h"
#include "reset_button.h"
#include "next_button.h"
#include "manage_button.h"
static const char *TAG = "main";
@@ -30,23 +31,25 @@ void app_main(void)
}
ESP_ERROR_CHECK(nvs_err);
/* Arms both buttons as deep-sleep wakeup sources (so holding one wakes
* the device promptly, not just during its brief awake windows), then
* checks them -- covers both "held while asleep, just woke us up" and
* "held while powering on" the same way, since both look identical
* from here: the pin is just low right now. */
/* Arms all three buttons as deep-sleep wakeup sources (so holding one
* wakes the device promptly, not just during its brief awake
* windows), then checks them -- covers both "held while asleep, just
* woke us up" and "held while powering on" the same way, since both
* look identical from here: the pin is just low right now. */
reset_button_init();
next_button_init();
manage_button_init();
reset_button_check(); /* clears config + restarts if held the full duration; never returns in that case */
bool force_advance = next_button_check();
bool show_management_qr = manage_button_check();
frame_config_t cfg;
esp_err_t cfg_err = frame_config_load(&cfg);
if (cfg_err == ESP_OK) {
ESP_LOGI(TAG, "Found stored config for '%s', connecting to home WiFi", cfg.sta_ssid);
if (frame_wifi_connect_sta(&cfg) == ESP_OK) {
frame_client_run(&cfg, force_advance);
frame_client_run(&cfg, force_advance, show_management_qr);
return; /* frame_client_run currently never returns */
}
ESP_LOGW(TAG, "Could not connect to stored WiFi after %d attempts, falling back to provisioning",
+64
View File
@@ -0,0 +1,64 @@
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_sleep.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "manage_button.h"
static const char *TAG = "manage_button";
#if CONFIG_FRAME_MANAGE_BUTTON_GPIO >= 0
#define MANAGE_BUTTON_GPIO ((gpio_num_t)CONFIG_FRAME_MANAGE_BUTTON_GPIO)
#define MANAGE_BUTTON_DEBOUNCE_MS 20
#define MANAGE_BUTTON_DEBOUNCE_CHECKS 3
void manage_button_init(void)
{
gpio_config_t io_conf = {
.pin_bit_mask = 1ULL << MANAGE_BUTTON_GPIO,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
};
gpio_config(&io_conf);
/* See reset_button.c for why this API (not ext1) -- it manages the
* pull resistor across the sleep transition itself, so the pin
* doesn't float and wake the device spuriously. */
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << MANAGE_BUTTON_GPIO, ESP_GPIO_WAKEUP_GPIO_LOW);
}
bool manage_button_check(void)
{
/* See next_button.c for why the latched wakeup status is checked
* first: a quick tap can release before a live gpio_get_level() call
* this far into boot would still see it held, even though it's what
* woke the device. */
if (esp_sleep_get_gpio_wakeup_status() & (1ULL << MANAGE_BUTTON_GPIO)) {
ESP_LOGI(TAG, "Manage button caused this wake, showing management QR");
return true;
}
if (gpio_get_level(MANAGE_BUTTON_GPIO) != 0) {
return false;
}
for (int i = 0; i < MANAGE_BUTTON_DEBOUNCE_CHECKS; i++) {
vTaskDelay(pdMS_TO_TICKS(MANAGE_BUTTON_DEBOUNCE_MS));
if (gpio_get_level(MANAGE_BUTTON_GPIO) != 0) {
return false; /* noise, not a real press */
}
}
ESP_LOGI(TAG, "Manage button held during power-on, showing management QR");
return true;
}
#else
void manage_button_init(void) {}
bool manage_button_check(void) { return false; }
#endif
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <stdbool.h>
/**
* Configures the "manage" button GPIO (CONFIG_FRAME_MANAGE_BUTTON_GPIO,
* active-low with internal pull-up) and arms it as a deep-sleep wakeup
* source, same as reset_button_init()/next_button_init(). Call once,
* early in app_main(), before the device might enter deep sleep.
*
* A no-op if CONFIG_FRAME_MANAGE_BUTTON_GPIO is negative (button disabled).
*/
void manage_button_init(void);
/**
* Returns whether the manage button is currently held, debounced with a
* couple of short re-checks to reject noise. Like the next-photo button,
* there's no long hold-to-confirm gate -- showing the management QR is
* low-stakes and should feel immediate.
*/
bool manage_button_check(void);
+100
View File
@@ -0,0 +1,100 @@
#include <stdlib.h>
#include <string.h>
#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) -- this is a compact
* corner popup, 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. */
#define PANEL_MARGIN 20
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);
}
}
}
}
}
esp_err_t manage_qr_overlay_render(const char *management_url, manage_qr_overlay_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,
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);
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 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;
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 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 buffer");
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);
out->buf = buf;
out->w = w;
out->h = h;
out->x0 = EPD_WIDTH - PANEL_MARGIN - w;
out->y0 = PANEL_MARGIN;
return ESP_OK;
}
void manage_qr_overlay_free(manage_qr_overlay_t *overlay)
{
free(overlay->buf);
overlay->buf = NULL;
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <stdint.h>
#include "esp_err.h"
typedef struct {
uint8_t *buf; /* malloc'd (w/2)*h bytes, packed 2px/byte; caller must free */
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;
/**
* 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().
*/
esp_err_t manage_qr_overlay_render(const char *management_url, manage_qr_overlay_t *out);
void manage_qr_overlay_free(manage_qr_overlay_t *overlay);