diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index be9f6ecbc..239af3b9b 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -70,6 +70,7 @@ MultiSerialInterface interface_manager; #include #include #include + #include #include #include #include @@ -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 diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 90a8bdd82..66e3b0110 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -2,6 +2,7 @@ #include "target.h" #include #include +#include #ifdef DISPLAY_REDRAW_ON_CHANGE #include @@ -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); diff --git a/examples/simple_room_server/UITask.cpp b/examples/simple_room_server/UITask.cpp index 40e22840c..e157a92cc 100644 --- a/examples/simple_room_server/UITask.cpp +++ b/examples/simple_room_server/UITask.cpp @@ -4,6 +4,7 @@ #endif #include #include +#include #ifdef DISPLAY_REDRAW_ON_CHANGE #include @@ -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); diff --git a/platformio.ini b/platformio.ini index cc7135ef0..3bdab1623 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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} + + + + + diff --git a/src/helpers/esp32/WebConfigServer.h b/src/helpers/esp32/WebConfigServer.h index 50f1da88b..55fc650cc 100644 --- a/src/helpers/esp32/WebConfigServer.h +++ b/src/helpers/esp32/WebConfigServer.h @@ -30,6 +30,7 @@ #include #include #include +#include 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 { diff --git a/src/helpers/ui/DisplayBuildFlags.h b/src/helpers/ui/DisplayBuildFlags.h new file mode 100644 index 000000000..6b6128c65 --- /dev/null +++ b/src/helpers/ui/DisplayBuildFlags.h @@ -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 diff --git a/src/helpers/ui/DisplayDriver.cpp b/src/helpers/ui/DisplayDriver.cpp new file mode 100644 index 000000000..e338ad0e7 --- /dev/null +++ b/src/helpers/ui/DisplayDriver.cpp @@ -0,0 +1,59 @@ +#include "DisplayDriver.h" + +#if defined(ESP32_PLATFORM) && defined(MESHCORE_HAS_REAL_DISPLAY) + +#include + +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 diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index e05f4f763..d4e1e7b03 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -1,5 +1,7 @@ #pragma once +#include "DisplayBuildFlags.h" + #include #include @@ -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; diff --git a/src/helpers/ui/WiFiSetupQrDisplay.h b/src/helpers/ui/WiFiSetupQrDisplay.h new file mode 100644 index 000000000..e25f975f5 --- /dev/null +++ b/src/helpers/ui/WiFiSetupQrDisplay.h @@ -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 diff --git a/src/helpers/ui/WiFiSetupQrPayload.h b/src/helpers/ui/WiFiSetupQrPayload.h index 004fe3878..f6e77cd70 100644 --- a/src/helpers/ui/WiFiSetupQrPayload.h +++ b/src/helpers/ui/WiFiSetupQrPayload.h @@ -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; diff --git a/test/test_display_driver/test_display_driver.cpp b/test/test_display_driver/test_display_driver.cpp index 9e95a2e2a..4628b8d5c 100644 --- a/test/test_display_driver/test_display_driver.cpp +++ b/test/test_display_driver/test_display_driver.cpp @@ -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) {