Restore the V4 WiFi setup QR with a one-pixel border

This commit is contained in:
mikecarper committed 2026-09-08 09:30:31 -07:00
1 parent 626ead5433
commit 7ff11a2d64
6 files changed
+94 -17

No files matched your search

+12 -3
View File
@@ -14,6 +14,7 @@
#if UI_WIFI_SETUP_HOME_PAGE == 1
#include <helpers/esp32/WebConfigServer.h>
#include <helpers/ui/WiFiSetupQrPayload.h>
#include <helpers/ui/WiFiSetupQrDisplay.h>
#endif
#if defined(ESP32)
#include <esp_timer.h>
@@ -124,9 +125,17 @@ static bool drawCompactCompanionWiFiSetupPage(
bool wifi_configured) {
if (display.width() > 128 || display.height() > 64) return false;
// Four small rows fit below the shared title bar and page dots. The OLED
// deliberately uses text instead of a QR: at this scale the SSID, portal IP,
// and physical-button action are more useful and remain readable.
// Use the whole compact screen for an active setup QR. Clear the shared
// title/page dots first so they cannot overlap its one-pixel white border.
if (setup_active) {
display.setCompactText(false);
display.setColor(UIColor::window_bkg);
display.fillRect(0, 0, display.width(), display.height());
if (mesh::ui::drawWiFiSetupQr(
display, setup_ssid, setup_ip, true, "HOLD STOP")) return true;
}
// Keep the readable text fallback for unsupported panels or QR payloads.
display.setCompactText(true);
display.setTextSize(1);
display.setColor(UIColor::primary_txt);
+19 -6
View File
@@ -5,9 +5,15 @@
#include <qrcode.h>
bool DisplayDriver::drawQrCode(const char* text, int x, int y, int size) {
return drawQrCodeWithBorder(text, x, y, size, -1);
}
bool DisplayDriver::drawQrCodeWithBorder(const char* text, int x, int y,
int size, int border_pixels) {
if (text == nullptr || text[0] == 0 || x < 0 || y < 0 || size <= 0
|| size > width() || size > height()
|| x > width() - size || y > height() - size) {
|| x > width() - size || y > height() - size
|| border_pixels < -1 || border_pixels > (size - 1) / 2) {
return false;
}
@@ -28,13 +34,17 @@ bool DisplayDriver::drawQrCode(const char* text, int x, int y, int size) {
if (!initialized) return false;
static constexpr int quiet_zone = 4;
const int module_size = size / (qr.size + quiet_zone * 2);
const int module_size = border_pixels < 0
? size / (qr.size + quiet_zone * 2)
: (size - border_pixels * 2) / qr.size;
if (module_size <= 0) return false;
const int used_size = module_size * (qr.size + quiet_zone * 2);
const int border = border_pixels < 0
? quiet_zone * module_size : border_pixels;
const int used_size = module_size * qr.size + border * 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;
const int matrix_left = left + border;
const int matrix_top = top + border;
// Dark-theme OLED/TFT drivers use a dark window and light popup text;
// e-paper reverses that convention. Always emit the conventional dark
@@ -42,7 +52,10 @@ bool DisplayDriver::drawQrCode(const char* text, int x, int y, int size) {
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);
// An explicit border is exact, even if a larger QR version leaves unused
// space in the requested square. Do not add that space to the white border.
if (border_pixels < 0) fillRect(x, y, size, size);
else fillRect(left, top, used_size, used_size);
setColor(dark);
for (uint8_t row = 0; row < qr.size; ++row) {
for (uint8_t column = 0; column < qr.size; ++column) {
+13
View File
@@ -74,6 +74,10 @@ public:
// 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);
// Explicit pixel border for compact setup screens; -1 keeps the normal
// four-module quiet zone. The matrix always uses whole pixels.
virtual bool drawQrCodeWithBorder(const char* text, int x, int y, int size,
int border_pixels);
#else
virtual bool drawQrCode(const char* text, int x, int y, int size) {
(void)text;
@@ -82,6 +86,15 @@ public:
(void)size;
return false;
}
virtual bool drawQrCodeWithBorder(const char* text, int x, int y, int size,
int border_pixels) {
(void)text;
(void)x;
(void)y;
(void)size;
(void)border_pixels;
return false;
}
#endif
virtual uint16_t getTextWidth(const char* str) = 0;
virtual bool getTouch(int* x, int* y) {
+12 -5
View File
@@ -13,7 +13,8 @@ namespace ui {
// panels so a normal setup IP has the full 66-pixel text row beside it.
// Returns false when the payload or panel geometry cannot be represented.
inline bool drawWiFiSetupQr(DisplayDriver& display, const char* ssid,
const char* address) {
const char* address, bool compact_border = false,
const char* action = nullptr) {
char payload[256];
const char* password = nullptr;
#ifdef WEBCONFIG_AP_PASSWORD
@@ -27,18 +28,24 @@ inline bool drawWiFiSetupQr(DisplayDriver& display, const char* ssid,
int qr_size = display.height() < display.width() / 2
? display.height() : display.width() / 2;
static constexpr int compact_qr_size = (21 + 4 * 2) * 2;
if (display.height() == 64 && qr_size >= compact_qr_size) {
if (compact_border && qr_size >= 44) {
// Version 1: 21 modules at 2 pixels, plus an exact one-pixel border.
qr_size = 44;
} else if (display.height() == 64 && qr_size >= compact_qr_size) {
qr_size = compact_qr_size;
}
if (!display.drawQrCode(payload, 0, 0, qr_size)) return false;
const bool rendered = compact_border
? display.drawQrCodeWithBorder(payload, 0, 0, qr_size, 1)
: display.drawQrCode(payload, 0, 0, qr_size);
if (!rendered) 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.print(action ? "SCAN JOIN" : "SCAN TO");
display.setCursor(text_x, 12);
display.print("JOIN:");
display.print(action ? action : "JOIN:");
display.drawTextEllipsized(
text_x, 22, display.width() - text_x, ssid);
display.setCursor(text_x, 37);
@@ -106,6 +106,7 @@ public:
int qr_x = -1;
int qr_y = -1;
int qr_size = -1;
int qr_border = -1;
QrTestDisplay(int width, int height, int glyph_width)
: TestDisplay(width, height, glyph_width) {}
@@ -116,6 +117,11 @@ public:
qr_size = size;
return true;
}
bool drawQrCodeWithBorder(const char* text, int x, int y, int size,
int border) override {
qr_border = border;
return drawQrCode(text, x, y, size);
}
};
} // namespace
@@ -163,6 +169,31 @@ TEST(WiFiSetupQrPayload, EncodesOpenSetupNetwork) {
EXPECT_LE(strlen(small_payload), 17U);
}
TEST(DisplayDriver, V4QrUsesOnePixelBorderAndKeepsActionsBesideTheCode) {
QrTestDisplay display(128, 64, 6);
ASSERT_TRUE(mesh::ui::drawWiFiSetupQr(
display, "MC-F7F0", "192.168.4.1", true, "HOLD STOP"));
EXPECT_EQ(44, display.qr_size);
EXPECT_EQ(1, display.qr_border);
EXPECT_EQ(21 * 2 + 2 * display.qr_border, display.qr_size);
ASSERT_EQ(5U, display.rows.size());
EXPECT_EQ("SCAN JOIN", display.rows[0].text);
EXPECT_EQ("HOLD STOP", display.rows[1].text);
EXPECT_EQ("192.168.4.1", display.rows.back().text);
for (const auto& row : display.rows) {
EXPECT_GE(row.x, display.qr_size + 4);
EXPECT_LE(row.x + display.getTextWidth(row.text.c_str()), display.width());
EXPECT_LE(row.y + 8, display.height());
}
}
TEST(DisplayDriver, V4QrKeepsFallbackWhenRendererIsUnavailable) {
TestDisplay display(128, 64, 6);
EXPECT_FALSE(mesh::ui::drawWiFiSetupQr(
display, "MC-F7F0", "192.168.4.1", true, "HOLD STOP"));
EXPECT_TRUE(display.rows.empty());
}
TEST(WiFiSetupQrPayload, EscapesProtectedNetworkFields) {
char payload[96];
EXPECT_TRUE(mesh::ui::buildWiFiSetupQrPayload(
+7 -3
View File
@@ -41,7 +41,7 @@ class HeltecV4WiFiSetupPageTest(unittest.TestCase):
)
self.assertNotIn("UI_WIFI_SETUP_HOME_PAGE", R8_PROFILE.read_text())
def test_compact_page_fits_128_by_64_without_qr_or_cli_copy(self):
def test_compact_page_restores_qr_with_a_readable_text_fallback(self):
ui = UI.read_text(encoding="utf-8")
start = ui.index("static bool drawCompactCompanionWiFiSetupPage")
end = ui.index("\nstatic void drawCompanionWiFiSetupPage", start)
@@ -53,8 +53,12 @@ class HeltecV4WiFiSetupPageTest(unittest.TestCase):
self.assertIn('"HOLD STOP AP" : "HOLD START AP"', compact)
self.assertIn('snprintf(open_ip, sizeof(open_ip), "OPEN %s", setup_ip)', compact)
self.assertIn("display.drawTextEllipsized", compact)
self.assertNotIn("drawQrCode", compact)
self.assertNotIn("buildWiFiSetupQrPayload", compact)
self.assertIn("mesh::ui::drawWiFiSetupQr(", compact)
self.assertIn('display, setup_ssid, setup_ip, true, "HOLD STOP"', compact)
self.assertLess(compact.index("display.fillRect(0, 0,"),
compact.index("mesh::ui::drawWiFiSetupQr("))
self.assertLess(compact.index("mesh::ui::drawWiFiSetupQr("),
compact.index('"SETUP AP ACTIVE"'))
self.assertNotIn("start webconfig", compact.lower())
# Default size-one glyphs are six by eight logical pixels.