Add WiFi setup QR to ESP32 displays

This commit is contained in:
mikecarper committed 2026-08-31 21:49:12 -07:00
1 parent 8ef309a299
commit 1264f62e9d
11 files changed
+165 -3

No files matched your search

+10
View File
@@ -70,6 +70,7 @@ MultiSerialInterface interface_manager;
#include <helpers/CompanionWiFiNtpPolicy.h>
#include <helpers/ui/DisplayTextLayout.h>
#include <helpers/ui/IndicatorRenderProfile.h>
#include <helpers/ui/WiFiSetupQrDisplay.h>
#include <helpers/esp32/SntpOperationCoordinator.h>
#include <helpers/esp32/TlsClockValidity.h>
#include <helpers/esp32/WiFiRadioPolicy.h>
@@ -1286,6 +1287,11 @@ void halt() {
char setup_ip[16] = {0};
if (WebConfigServer::getSetupInfo(setup_ssid, sizeof(setup_ssid),
setup_ip, sizeof(setup_ip))) {
if (mesh::ui::drawWiFiSetupQr(
*companion_setup_display, setup_ssid, setup_ip)) {
companion_setup_display->endFrame();
return;
}
if (hasLargeCompanionSetupDisplay()) {
renderLargeCompanionSetupDisplay(
"WebUI setup", "Join open WiFi:", setup_ssid, setup_ip, false);
@@ -1749,7 +1755,11 @@ void setup() {
}
#if defined(ESP32) && defined(WIFI_SSID)
companion_display_available = disp != NULL;
#if defined(SENSECAP_INDICATOR_LORA)
companion_display_begin_status = display.beginStatus();
#else
companion_display_begin_status = companion_display_available ? 1 : 0;
#endif
#endif
#endif
+4
View File
@@ -2,6 +2,7 @@
#include "target.h"
#include <Arduino.h>
#include <helpers/CommonCLI.h>
#include <helpers/ui/WiFiSetupQrDisplay.h>
#ifdef DISPLAY_REDRAW_ON_CHANGE
#include <helpers/ui/DisplayFrameSignature.h>
@@ -175,6 +176,9 @@ void UITask::renderCurrScreen() {
char wc_ssid[33], wc_ip[16];
if (WebConfigServer::getSetupInfo(wc_ssid, sizeof(wc_ssid), wc_ip, sizeof(wc_ip))) {
// setup portal active: show join instructions instead of the home screen
if (mesh::ui::drawWiFiSetupQr(*_display, wc_ssid, wc_ip)) {
return;
}
_display->setTextSize(1);
_display->setColor(UIColor::corp_blue);
_display->setCursor(0, 0);
+4
View File
@@ -4,6 +4,7 @@
#endif
#include <Arduino.h>
#include <helpers/CommonCLI.h>
#include <helpers/ui/WiFiSetupQrDisplay.h>
#ifdef DISPLAY_REDRAW_ON_CHANGE
#include <helpers/ui/DisplayFrameSignature.h>
@@ -175,6 +176,9 @@ void UITask::renderCurrScreen() {
char wc_ssid[33], wc_ip[16];
if (WebConfigServer::getSetupInfo(wc_ssid, sizeof(wc_ssid), wc_ip, sizeof(wc_ip))) {
// setup portal active: show join instructions instead of the home screen
if (mesh::ui::drawWiFiSetupQr(*_display, wc_ssid, wc_ip)) {
return;
}
_display->setTextSize(1);
_display->setColor(UIColor::corp_blue);
_display->setCursor(0, 0);
+3
View File
@@ -89,7 +89,10 @@ lib_deps = ${arduino_base.lib_deps}
ESP32Async/AsyncTCP @ 3.5.0
ESP32Async/ESPAsyncWebServer @ 3.12.0
bblanchon/ArduinoJson @ 7.4.3
; Generic WiFi-join QR rendering is linked only by ESP32 display builds.
ricmoo/QRCode @ 0.0.1
build_src_filter = ${arduino_base.build_src_filter}
+<helpers/ui/DisplayDriver.cpp>
+<helpers/esp32/WebConfigServer.cpp>
+<helpers/esp32/PortableErrorStrings.cpp>
+<helpers/esp32/WiFiOtaSeeder.cpp>
+7 -1
View File
@@ -30,6 +30,7 @@
#include <helpers/WebConfigBatch.h>
#include <helpers/WiFiPowerSave.h>
#include <helpers/WiFiReconnectPolicy.h>
#include <helpers/ui/DisplayBuildFlags.h>
class AsyncWebServer;
class AsyncWebServerRequest;
@@ -53,7 +54,12 @@ class DNSServer;
#define WEBCONFIG_SESSION_TTL_MS (20UL * 60UL * 1000UL)
#endif
#ifndef WEBCONFIG_AP_PREFIX
#define WEBCONFIG_AP_PREFIX "MeshCore-Setup"
#if defined(MESHCORE_HAS_REAL_DISPLAY)
// Keeps an open-network WiFi QR in Version 1 on 64-pixel-high screens.
#define WEBCONFIG_AP_PREFIX "MC"
#else
#define WEBCONFIG_AP_PREFIX "MeshCore-Setup"
#endif
#endif
class WebConfigServer {
+18
View File
@@ -0,0 +1,18 @@
#pragma once
// DISPLAY_CLASS is supplied as a build token (for example SSD1306Display).
// Convert it to a boolean that shared ESP32 code can use without including a
// concrete driver. Undefined identifiers evaluate to zero in a #if expression.
#define MESHCORE_NULL_DISPLAY_CLASS_NullDisplayDriver 1
#define MESHCORE_DISPLAY_TOKEN_JOIN_INNER(prefix, name) prefix##name
#define MESHCORE_DISPLAY_TOKEN_JOIN(prefix, name) \
MESHCORE_DISPLAY_TOKEN_JOIN_INNER(prefix, name)
#if defined(DISPLAY_CLASS) \
&& !MESHCORE_DISPLAY_TOKEN_JOIN(MESHCORE_NULL_DISPLAY_CLASS_, DISPLAY_CLASS)
#define MESHCORE_HAS_REAL_DISPLAY 1
#endif
#undef MESHCORE_DISPLAY_TOKEN_JOIN
#undef MESHCORE_DISPLAY_TOKEN_JOIN_INNER
#undef MESHCORE_NULL_DISPLAY_CLASS_NullDisplayDriver
+59
View File
@@ -0,0 +1,59 @@
#include "DisplayDriver.h"
#if defined(ESP32_PLATFORM) && defined(MESHCORE_HAS_REAL_DISPLAY)
#include <qrcode.h>
bool DisplayDriver::drawQrCode(const char* text, int x, int y, int size) {
if (text == nullptr || text[0] == 0 || x < 0 || y < 0 || size <= 0
|| size > width() || size > height()
|| x > width() - size || y > height() - size) {
return false;
}
// A 64-pixel square can hold at most Version 9 with the QR-standard
// four-module quiet zone. Setup AP payloads are deliberately kept at
// Version 1 on small screens so each module is 2x2 physical pixels.
static constexpr uint8_t max_version = 9;
static constexpr uint8_t max_modules = 4 * max_version + 17;
uint8_t modules[(max_modules * max_modules + 7) / 8];
QRCode qr = {};
bool initialized = false;
for (uint8_t version = 1; version <= max_version; ++version) {
if (qrcode_initText(&qr, modules, version, ECC_LOW, text) == 0) {
initialized = true;
break;
}
}
if (!initialized) return false;
static constexpr int quiet_zone = 4;
const int module_size = size / (qr.size + quiet_zone * 2);
if (module_size <= 0) return false;
const int used_size = module_size * (qr.size + quiet_zone * 2);
const int left = x + (size - used_size) / 2;
const int top = y + (size - used_size) / 2;
const int matrix_left = left + quiet_zone * module_size;
const int matrix_top = top + quiet_zone * module_size;
// Dark-theme OLED/TFT drivers use a dark window and light popup text;
// e-paper reverses that convention. Always emit the conventional dark
// matrix on a light background instead of relying on scanner inversion.
const ColorVal light = isEink() ? UIColor::window_bkg : UIColor::popup_txt;
const ColorVal dark = isEink() ? UIColor::primary_txt : UIColor::window_bkg;
setColor(light);
fillRect(x, y, size, size);
setColor(dark);
for (uint8_t row = 0; row < qr.size; ++row) {
for (uint8_t column = 0; column < qr.size; ++column) {
if (qrcode_getModule(&qr, column, row)) {
fillRect(matrix_left + column * module_size,
matrix_top + row * module_size,
module_size, module_size);
}
}
}
return true;
}
#endif
+6
View File
@@ -1,5 +1,7 @@
#pragma once
#include "DisplayBuildFlags.h"
#include <stdint.h>
#include <string.h>
@@ -70,6 +72,9 @@ public:
// Optional QR renderer. Coordinates and size use the same logical units as
// the rest of the UI. Drivers without QR support leave the caller's
// fallback layout in place.
#if defined(ESP32_PLATFORM) && defined(MESHCORE_HAS_REAL_DISPLAY)
virtual bool drawQrCode(const char* text, int x, int y, int size);
#else
virtual bool drawQrCode(const char* text, int x, int y, int size) {
(void)text;
(void)x;
@@ -77,6 +82,7 @@ public:
(void)size;
return false;
}
#endif
virtual uint16_t getTextWidth(const char* str) = 0;
virtual bool getTouch(int* x, int* y) {
(void)x;
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "DisplayDriver.h"
#include "WiFiSetupQrPayload.h"
namespace mesh {
namespace ui {
// Render a setup-AP join QR at the largest size that still leaves the right
// half of compact displays available for a short textual fallback/reference.
// Returns false when the payload or panel geometry cannot be represented.
inline bool drawWiFiSetupQr(DisplayDriver& display, const char* ssid,
const char* address) {
char payload[256];
const char* password = nullptr;
#ifdef WEBCONFIG_AP_PASSWORD
password = WEBCONFIG_AP_PASSWORD;
#endif
if (!buildWiFiSetupQrPayload(
payload, sizeof(payload), ssid, password)) {
return false;
}
const int qr_size = display.height() < display.width() / 2
? display.height() : display.width() / 2;
if (!display.drawQrCode(payload, 0, 0, qr_size)) return false;
const int text_x = qr_size + 4;
display.setTextSize(1);
display.setColor(UIColor::primary_txt);
display.setCursor(text_x, 2);
display.print("SCAN TO");
display.setCursor(text_x, 12);
display.print("JOIN:");
display.drawTextEllipsized(
text_x, 22, display.width() - text_x, ssid);
display.setCursor(text_x, 37);
display.print("THEN OPEN");
display.drawTextEllipsized(
text_x, 48, display.width() - text_x, address);
return true;
}
} // namespace ui
} // namespace mesh
+1 -1
View File
@@ -44,7 +44,7 @@ inline bool buildWiFiSetupQrPayload(char* payload, size_t payload_size,
if (!appendWiFiQrText(output, remaining,
protected_network ? "WIFI:T:WPA;S:"
: "WIFI:T:nopass;S:",
: "WIFI:S:",
false)
|| !appendWiFiQrText(output, remaining, ssid, true)) {
return false;
@@ -109,7 +109,14 @@ TEST(WiFiSetupQrPayload, EncodesOpenSetupNetwork) {
char payload[64];
EXPECT_TRUE(mesh::ui::buildWiFiSetupQrPayload(
payload, sizeof(payload), "MC-Set-90DF"));
EXPECT_STREQ("WIFI:T:nopass;S:MC-Set-90DF;;", payload);
EXPECT_STREQ("WIFI:S:MC-Set-90DF;;", payload);
char small_payload[32];
EXPECT_TRUE(mesh::ui::buildWiFiSetupQrPayload(
small_payload, sizeof(small_payload), "MC-90DF"));
EXPECT_STREQ("WIFI:S:MC-90DF;;", small_payload);
// MC-XXXX uses 16 payload bytes, within QR Version 1-L's 17-byte capacity.
EXPECT_LE(strlen(small_payload), 17U);
}
TEST(WiFiSetupQrPayload, EscapesProtectedNetworkFields) {