Add QR-code WiFi onboarding screen
Vendors two small MIT/BSD-3-Clause libraries rather than hand-rolling either: Nayuki's qrcodegen (QR matrix generation) and Waveshare's Font24 bitmap table from their e-Paper repo (same repo the epd7in3e driver came from) for rendering readable text on the panel. qr_onboarding_show() builds a standard WIFI:T:WPA;S:...;P:...;; payload, rasterizes the QR module matrix plus the SSID and password as plaintext underneath (for anyone provisioning from a device that can't scan a QR) onto a malloc'd frame buffer, and pushes it to the panel via epd_display_buffer(). The buffer is heap-allocated on demand rather than statically reserved, since 192KB held permanently in BSS would eat into the RAM budget the HTTP fetch path (task 6) is specifically trying to keep free. Wired into wifi_provisioning_start() before the softAP comes up, so the join instructions are already on-screen by the time the network is joinable.
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c
|
||||
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_netif esp_http_server dns_server epd7in3e
|
||||
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c qr_onboarding.c
|
||||
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_netif esp_http_server dns_server epd7in3e qrcode epaper_fonts
|
||||
EMBED_FILES root.html)
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#include <ctype.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "epd7in3e.h"
|
||||
#include "fonts.h"
|
||||
#include "qrcodegen.h"
|
||||
|
||||
#include "qr_onboarding.h"
|
||||
|
||||
static const char *TAG = "qr_onboarding";
|
||||
|
||||
#define QR_MAX_VERSION 10
|
||||
#define QR_BUFFER_LEN qrcodegen_BUFFER_LEN_FOR_VERSION(QR_MAX_VERSION)
|
||||
#define QR_MODULE_PX 8
|
||||
#define TEXT_MARGIN_X 40
|
||||
#define TEXT_LINE_GAP 16
|
||||
|
||||
static void set_pixel(uint8_t *frame, int x, int y, epd_color_t color)
|
||||
{
|
||||
if (x < 0 || x >= EPD_WIDTH || y < 0 || y >= EPD_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
size_t byte_index = (size_t)y * EPD_BYTES_PER_ROW + (size_t)x / 2;
|
||||
uint8_t nibble = (uint8_t)color & 0x0F;
|
||||
if (x % 2 == 0) {
|
||||
frame[byte_index] = (frame[byte_index] & 0x0F) | (nibble << 4);
|
||||
} else {
|
||||
frame[byte_index] = (frame[byte_index] & 0xF0) | nibble;
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_qr(uint8_t *frame, 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++) {
|
||||
set_pixel(frame, origin_x + x * QR_MODULE_PX + dx, origin_y + y * QR_MODULE_PX + dy, 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). */
|
||||
static bool font_pixel_set(const sFONT *font, char c, int row, int col)
|
||||
{
|
||||
if (c < ' ' || (unsigned char)c > 0x7E) {
|
||||
return false;
|
||||
}
|
||||
int bytes_per_row = (font->Width + 7) / 8;
|
||||
size_t glyph_index = (size_t)(c - ' ');
|
||||
const uint8_t *glyph = font->table + glyph_index * font->Height * bytes_per_row + (size_t)row * bytes_per_row;
|
||||
int byte_idx = col / 8;
|
||||
int bit_in_byte = 7 - (col % 8);
|
||||
return (glyph[byte_idx] >> bit_in_byte) & 0x1;
|
||||
}
|
||||
|
||||
static int draw_text(uint8_t *frame, 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++) {
|
||||
char c = (char)toupper((unsigned char)*p);
|
||||
for (int row = 0; row < font->Height; row++) {
|
||||
for (int col = 0; col < font->Width; col++) {
|
||||
if (font_pixel_set(font, c, row, col)) {
|
||||
set_pixel(frame, cursor_x + col, origin_y + row, EPD_COLOR_BLACK);
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor_x += font->Width;
|
||||
}
|
||||
return cursor_x;
|
||||
}
|
||||
|
||||
esp_err_t qr_onboarding_show(const char *ssid, const char *password)
|
||||
{
|
||||
char payload[96];
|
||||
int written = snprintf(payload, sizeof(payload), "WIFI:T:WPA;S:%s;P:%s;;", ssid, password);
|
||||
ESP_RETURN_ON_FALSE(written > 0 && (size_t)written < sizeof(payload), ESP_ERR_INVALID_SIZE, TAG,
|
||||
"QR payload too long");
|
||||
|
||||
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 (payload too long for max version)");
|
||||
|
||||
/* Allocated on demand rather than statically reserved: this runs once,
|
||||
* before WiFi/HTTP buffers exist, so the heap has plenty of headroom,
|
||||
* and the space is freed again as soon as the screen is drawn. */
|
||||
uint8_t *frame = malloc(EPD_FRAME_BYTES);
|
||||
ESP_RETURN_ON_FALSE(frame != NULL, ESP_ERR_NO_MEM, TAG, "Failed to allocate frame buffer");
|
||||
memset(frame, (EPD_COLOR_WHITE << 4) | EPD_COLOR_WHITE, EPD_FRAME_BYTES);
|
||||
|
||||
int qr_size = qrcodegen_getSize(qrcode);
|
||||
int qr_px = qr_size * QR_MODULE_PX;
|
||||
int qr_x = (EPD_WIDTH - qr_px) / 2;
|
||||
int qr_y = 24;
|
||||
draw_qr(frame, qrcode, qr_x, qr_y);
|
||||
|
||||
int text_y = qr_y + qr_px + 32;
|
||||
|
||||
char ssid_line[64];
|
||||
snprintf(ssid_line, sizeof(ssid_line), "SSID: %s", ssid);
|
||||
draw_text(frame, &Font24, ssid_line, TEXT_MARGIN_X, text_y);
|
||||
|
||||
char pass_line[64];
|
||||
snprintf(pass_line, sizeof(pass_line), "PASSWORD: %s", password);
|
||||
draw_text(frame, &Font24, pass_line, TEXT_MARGIN_X, text_y + Font24.Height + TEXT_LINE_GAP);
|
||||
|
||||
esp_err_t err = epd_display_buffer(frame, EPD_FRAME_BYTES);
|
||||
free(frame);
|
||||
|
||||
return err;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/**
|
||||
* Renders a WiFi-join QR code for the given AP SSID/password onto the
|
||||
* e-ink panel, with the password (and SSID) also printed as plaintext
|
||||
* underneath for anyone provisioning from a device that can't scan a QR
|
||||
* code. Call after epd_init() and before the softAP goes up, so the join
|
||||
* instructions are already on-screen by the time the network is joinable.
|
||||
*/
|
||||
esp_err_t qr_onboarding_show(const char *ssid, const char *password);
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "epd7in3e.h"
|
||||
#include "qr_onboarding.h"
|
||||
#include "wifi_provisioning.h"
|
||||
|
||||
#define NVS_NAMESPACE "frame_cfg"
|
||||
@@ -373,13 +374,15 @@ void wifi_provisioning_start(void)
|
||||
|
||||
ESP_LOGI(TAG, "Provisioning AP: SSID='%s' password='%s'", ap_ssid, ap_password);
|
||||
|
||||
/* Bring up the display before the AP so join instructions are on-screen
|
||||
* by the time the network is joinable.
|
||||
* TODO(step 5): replace this clear with the WiFi-join QR code +
|
||||
* plaintext password. */
|
||||
/* Bring up the display and show the join QR/password before the AP
|
||||
* goes up, so the instructions are already on-screen by the time the
|
||||
* network is joinable. */
|
||||
esp_err_t epd_err = epd_init();
|
||||
if (epd_err == ESP_OK) {
|
||||
epd_clear(EPD_COLOR_WHITE);
|
||||
esp_err_t qr_err = qr_onboarding_show(ap_ssid, ap_password);
|
||||
if (qr_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "QR onboarding render failed (%s)", esp_err_to_name(qr_err));
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "EPD init failed (%s), continuing without display", esp_err_to_name(epd_err));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user