Add Indicator WiFi setup as eighth home page
This commit is contained in:
1 parent
a38e0370f9
commit
42888e6e3a
20 files changed
+1660
-1311
No files matched your search
@@ -3429,7 +3429,7 @@ apply_companion_radio_full_profile() {
|
||||
case "${env_name,,}" in
|
||||
sensecapindicator-espnow_companion_radio_full|\
|
||||
sensecapindicator-lora_companion_radio_full)
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DCOMPANION_EXCLUSIVE_WIFI_BLE=1 -DINDICATOR_TRANSPORT_RENDER_PROFILE=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DCOMPANION_EXCLUSIVE_WIFI_BLE=1 -DINDICATOR_TRANSPORT_RENDER_PROFILE=1 -DUI_WIFI_SETUP_HOME_PAGE=1 -DWEBCONFIG_AP_PREFIX='\"MC-Set\"'"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef UI_WIFI_SETUP_HOME_PAGE
|
||||
#define UI_WIFI_SETUP_HOME_PAGE 0
|
||||
#endif
|
||||
|
||||
#if UI_WIFI_SETUP_HOME_PAGE == 1 \
|
||||
&& !(defined(ESP32) && defined(WIFI_SSID))
|
||||
#error "UI_WIFI_SETUP_HOME_PAGE requires ESP32 infrastructure WiFi"
|
||||
#endif
|
||||
|
||||
#if defined(COMPANION_EXCLUSIVE_WIFI_BLE) \
|
||||
&& !(defined(ESP32) && defined(WIFI_SSID) && defined(BLE_PIN_CODE))
|
||||
#error "COMPANION_EXCLUSIVE_WIFI_BLE requires ESP32, WIFI_SSID, and BLE_PIN_CODE"
|
||||
|
||||
@@ -1173,7 +1173,8 @@ void halt() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(WITH_WEBCONFIG) && defined(DISPLAY_CLASS)
|
||||
#if defined(WITH_WEBCONFIG) && defined(DISPLAY_CLASS) \
|
||||
&& UI_WIFI_SETUP_HOME_PAGE != 1
|
||||
static DisplayDriver* companion_setup_display = nullptr;
|
||||
static unsigned long companion_setup_display_refresh = 0;
|
||||
|
||||
@@ -1182,91 +1183,6 @@ void halt() {
|
||||
&& companion_setup_display->height() >= 128;
|
||||
}
|
||||
|
||||
static bool hasIndicatorSetupTypography() {
|
||||
#if defined(INDICATOR_TRANSPORT_RENDER_PROFILE)
|
||||
// This flag is emitted only for the two SenseCAP Indicator Full builds.
|
||||
// Runtime canvas dimensions may differ after allocation fallback (and
|
||||
// across historical UI macro ordering), but the physical panel remains
|
||||
// 480x480. Never send this board through the generic size-1 setup page.
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int drawIndicatorSetupValue(int y, const char* value) {
|
||||
companion_setup_display->setTextSize(2);
|
||||
// The setup AP has a stable prefix. Split on its semantic boundary so
|
||||
// the two large centered rows read cleanly instead of wrapping midway
|
||||
// through "Setup".
|
||||
if (value != nullptr
|
||||
&& strncmp(value, "MeshCore-Setup-", 15) == 0) {
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, y, "MeshCore-");
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, y + 17, value + 9);
|
||||
return 2;
|
||||
}
|
||||
const int max_width = companion_setup_display->width() - 12;
|
||||
int line_height = 17;
|
||||
// A configured SSID can contain 32 bytes. Keep ordinary names at the
|
||||
// large size, but let the longest legal value use two complete size-1
|
||||
// rows instead of silently losing its tail.
|
||||
if (value != nullptr
|
||||
&& companion_setup_display->getTextWidth(value) > max_width * 2) {
|
||||
companion_setup_display->setTextSize(1);
|
||||
line_height = 9;
|
||||
}
|
||||
return mesh::ui::drawTextWrapped(
|
||||
*companion_setup_display, 6, y,
|
||||
max_width, line_height, 2,
|
||||
value != nullptr && value[0] != 0 ? value : "(not set)");
|
||||
}
|
||||
|
||||
static void drawIndicatorSetupAddress(int y, const char* address) {
|
||||
const char* value = address != nullptr && address[0] != 0
|
||||
? address : "(not set)";
|
||||
companion_setup_display->setTextSize(2);
|
||||
if (companion_setup_display->getTextWidth(value)
|
||||
> companion_setup_display->width()) {
|
||||
companion_setup_display->setTextSize(1);
|
||||
}
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, y, value);
|
||||
}
|
||||
|
||||
static void renderIndicatorSetupDisplay(const char* title,
|
||||
const char* wifi_name,
|
||||
const char* address,
|
||||
bool connecting) {
|
||||
// Keep the requested sizes physically identical on a native canvas and
|
||||
// the 320-to-480 emergency canvas. This page is sparse enough to use
|
||||
// genuinely large type instead of relying on the global 480 boost.
|
||||
companion_setup_display->setCompactText(true);
|
||||
companion_setup_display->setTextSize(4);
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, 0, title);
|
||||
|
||||
companion_setup_display->setTextSize(4);
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, 35, "JOIN");
|
||||
drawIndicatorSetupValue(70, wifi_name);
|
||||
|
||||
if (connecting) {
|
||||
companion_setup_display->setTextSize(4);
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, 108, "WAIT");
|
||||
return;
|
||||
}
|
||||
|
||||
companion_setup_display->setTextSize(4);
|
||||
companion_setup_display->drawTextCentered(
|
||||
companion_setup_display->width() / 2, 105, "OPEN");
|
||||
// The scheme is implicit here so the address itself can remain nearly
|
||||
// panel-wide at size 2. Long DHCP addresses fall back to size 1.
|
||||
drawIndicatorSetupAddress(140, address);
|
||||
}
|
||||
|
||||
static int drawCompanionSetupValue(int y, int max_lines,
|
||||
const char* value) {
|
||||
static constexpr int margin = 6;
|
||||
@@ -1318,10 +1234,7 @@ void halt() {
|
||||
char setup_ip[16] = {0};
|
||||
if (WebConfigServer::getSetupInfo(setup_ssid, sizeof(setup_ssid),
|
||||
setup_ip, sizeof(setup_ip))) {
|
||||
if (hasIndicatorSetupTypography()) {
|
||||
renderIndicatorSetupDisplay(
|
||||
"SETUP", setup_ssid, setup_ip, false);
|
||||
} else if (hasLargeCompanionSetupDisplay()) {
|
||||
if (hasLargeCompanionSetupDisplay()) {
|
||||
renderLargeCompanionSetupDisplay(
|
||||
"WebUI setup", "Join open WiFi:", setup_ssid, setup_ip, false);
|
||||
} else {
|
||||
@@ -1338,10 +1251,7 @@ void halt() {
|
||||
}
|
||||
} else if (WiFi.status() == WL_CONNECTED) {
|
||||
const String ip = WiFi.localIP().toString();
|
||||
if (hasIndicatorSetupTypography()) {
|
||||
renderIndicatorSetupDisplay(
|
||||
"READY", configured_wifi_ssid, ip.c_str(), false);
|
||||
} else if (hasLargeCompanionSetupDisplay()) {
|
||||
if (hasLargeCompanionSetupDisplay()) {
|
||||
renderLargeCompanionSetupDisplay(
|
||||
"WebUI ready", "Join WiFi:", configured_wifi_ssid, ip.c_str(),
|
||||
false);
|
||||
@@ -1358,10 +1268,7 @@ void halt() {
|
||||
companion_setup_display->width() / 2, 51, ip.c_str());
|
||||
}
|
||||
} else {
|
||||
if (hasIndicatorSetupTypography()) {
|
||||
renderIndicatorSetupDisplay(
|
||||
"WIFI", configured_wifi_ssid, nullptr, true);
|
||||
} else if (hasLargeCompanionSetupDisplay()) {
|
||||
if (hasLargeCompanionSetupDisplay()) {
|
||||
renderLargeCompanionSetupDisplay(
|
||||
"WiFi connecting", "WiFi name:", configured_wifi_ssid, nullptr,
|
||||
true);
|
||||
@@ -1376,8 +1283,6 @@ void halt() {
|
||||
companion_setup_display->width() / 2, 52, "Please wait...");
|
||||
}
|
||||
}
|
||||
// Do not leak the setup page's fixed physical text scale into the normal
|
||||
// UI after WebConfig exits.
|
||||
companion_setup_display->setCompactText(false);
|
||||
companion_setup_display->endFrame();
|
||||
}
|
||||
@@ -1779,7 +1684,8 @@ void setup() {
|
||||
DisplayDriver* disp = NULL;
|
||||
if (display.begin()) {
|
||||
disp = &display;
|
||||
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_WEBCONFIG)
|
||||
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_WEBCONFIG) \
|
||||
&& UI_WIFI_SETUP_HOME_PAGE != 1
|
||||
companion_setup_display = disp;
|
||||
#endif
|
||||
disp->startFrame();
|
||||
@@ -2133,7 +2039,8 @@ void loop() {
|
||||
// main/UI task.
|
||||
display.serviceFontRecovery();
|
||||
#endif
|
||||
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_WEBCONFIG)
|
||||
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_WEBCONFIG) \
|
||||
&& UI_WIFI_SETUP_HOME_PAGE != 1
|
||||
if (isCompanionWiFiEnabled() && (the_mesh.isWebConfigSetupActive()
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
|| !the_mesh.isMQTTConfigured()
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
#ifdef WIFI_SSID
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
#if UI_WIFI_SETUP_HOME_PAGE == 1
|
||||
#include <helpers/esp32/WebConfigServer.h>
|
||||
#include <helpers/ui/WiFiSetupQrPayload.h>
|
||||
#endif
|
||||
#if defined(ESP32)
|
||||
#include <esp_timer.h>
|
||||
#endif
|
||||
@@ -113,6 +117,70 @@ static void drawCompanionTransportChoice(DisplayDriver& display,
|
||||
}
|
||||
#endif
|
||||
|
||||
#if UI_WIFI_SETUP_HOME_PAGE == 1
|
||||
static void drawCompanionWiFiSetupPage(DisplayDriver& display) {
|
||||
char setup_ssid[33] = {0};
|
||||
char setup_ip[16] = {0};
|
||||
const bool setup_active = WebConfigServer::getSetupInfo(
|
||||
setup_ssid, sizeof(setup_ssid), setup_ip, sizeof(setup_ip));
|
||||
|
||||
// Keep the setup content below the shared title bar and page dots. Compact
|
||||
// text makes its physical size identical on the 480 canvas and the
|
||||
// 320-to-480 emergency canvas.
|
||||
display.setCompactText(true);
|
||||
display.setColor(UIColor::primary_txt);
|
||||
|
||||
if (setup_active) {
|
||||
display.setTextSize(2);
|
||||
display.drawTextCentered(display.width() / 2, 20, "WIFI SETUP");
|
||||
display.drawTextCentered(display.width() / 2, 37, setup_ssid);
|
||||
|
||||
char payload[256];
|
||||
const char* setup_password = nullptr;
|
||||
#ifdef WEBCONFIG_AP_PASSWORD
|
||||
setup_password = WEBCONFIG_AP_PASSWORD;
|
||||
#endif
|
||||
if (mesh::ui::buildWiFiSetupQrPayload(
|
||||
payload, sizeof(payload), setup_ssid, setup_password)) {
|
||||
static constexpr int qr_size = 105;
|
||||
const int qr_x = (display.width() - qr_size) / 2;
|
||||
if (display.drawQrCode(payload, qr_x, 55, qr_size)) {
|
||||
display.setCompactText(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
display.setColor(UIColor::warning_txt);
|
||||
display.setTextSize(2);
|
||||
display.drawTextCentered(display.width() / 2, 70, "QR UNAVAILABLE");
|
||||
display.setColor(UIColor::secondary_txt);
|
||||
display.drawTextCentered(display.width() / 2, 100, setup_ip);
|
||||
} else if (!isCompanionWiFiEnabled()) {
|
||||
display.setTextSize(3);
|
||||
display.drawTextCentered(display.width() / 2, 45, "WIFI OFF");
|
||||
display.setTextSize(2);
|
||||
display.setColor(UIColor::secondary_txt);
|
||||
display.drawTextCentered(display.width() / 2, 90, "SELECT WIFI");
|
||||
display.drawTextCentered(display.width() / 2, 112, "THEN REBOOT");
|
||||
} else if (WiFi.status() == WL_CONNECTED) {
|
||||
display.setTextSize(2);
|
||||
display.drawTextCentered(display.width() / 2, 25, "WIFI READY");
|
||||
const String ssid = WiFi.SSID();
|
||||
display.drawTextEllipsized(4, 60, display.width() - 8, ssid.c_str());
|
||||
const String ip = WiFi.localIP().toString();
|
||||
display.drawTextCentered(display.width() / 2, 95, ip.c_str());
|
||||
} else {
|
||||
display.setTextSize(3);
|
||||
display.drawTextCentered(display.width() / 2, 45, "WIFI");
|
||||
display.setTextSize(2);
|
||||
display.setColor(UIColor::secondary_txt);
|
||||
display.drawTextCentered(display.width() / 2, 90, "CONNECTING");
|
||||
}
|
||||
|
||||
display.setCompactText(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
#include "icons.h"
|
||||
|
||||
class SplashScreen : public UIScreen {
|
||||
@@ -190,6 +258,9 @@ class HomeScreen : public UIScreen {
|
||||
#endif
|
||||
#ifndef UI_NO_HIBERNATE
|
||||
SHUTDOWN,
|
||||
#endif
|
||||
#if UI_WIFI_SETUP_HOME_PAGE == 1
|
||||
WIFI_SETUP,
|
||||
#endif
|
||||
Count // keep as last
|
||||
};
|
||||
@@ -200,6 +271,9 @@ class HomeScreen : public UIScreen {
|
||||
CompanionNodePrefs* _node_prefs;
|
||||
uint8_t _page;
|
||||
bool _shutdown_init;
|
||||
#if UI_WIFI_SETUP_HOME_PAGE == 1
|
||||
bool _wifi_setup_was_active;
|
||||
#endif
|
||||
uint32_t _uptime_last_millis;
|
||||
uint64_t _uptime_millis;
|
||||
AdvertPath recent[UI_RECENT_LIST_SIZE];
|
||||
@@ -329,8 +403,17 @@ class HomeScreen : public UIScreen {
|
||||
public:
|
||||
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, CompanionNodePrefs* node_prefs)
|
||||
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
|
||||
_shutdown_init(false), _uptime_last_millis(millis()), _uptime_millis(0),
|
||||
sensors_lpp(200) { }
|
||||
_shutdown_init(false),
|
||||
#if UI_WIFI_SETUP_HOME_PAGE == 1
|
||||
_wifi_setup_was_active(false),
|
||||
#endif
|
||||
_uptime_last_millis(millis()), _uptime_millis(0), sensors_lpp(200) {
|
||||
#if UI_WIFI_SETUP_HOME_PAGE == 1
|
||||
_wifi_setup_was_active = WebConfigServer::getSetupInfo(
|
||||
nullptr, 0, nullptr, 0);
|
||||
if (_wifi_setup_was_active) _page = HomePage::WIFI_SETUP;
|
||||
#endif
|
||||
}
|
||||
|
||||
void showFirstPage() { _page = HomePage::FIRST; }
|
||||
|
||||
@@ -343,6 +426,17 @@ public:
|
||||
}
|
||||
|
||||
void poll() override {
|
||||
#if UI_WIFI_SETUP_HOME_PAGE == 1
|
||||
const bool wifi_setup_active = WebConfigServer::getSetupInfo(
|
||||
nullptr, 0, nullptr, 0);
|
||||
if (wifi_setup_active && !_wifi_setup_was_active) {
|
||||
// Focus a newly opened setup portal once. Keeping this edge-triggered
|
||||
// lets the user swipe away while setup and LoRa continue running.
|
||||
_page = HomePage::WIFI_SETUP;
|
||||
_task->gotoHomeScreen();
|
||||
}
|
||||
_wifi_setup_was_active = wifi_setup_active;
|
||||
#endif
|
||||
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
|
||||
_task->shutdown();
|
||||
}
|
||||
@@ -817,6 +911,10 @@ public:
|
||||
if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb;
|
||||
else sensors_scroll_offset = 0;
|
||||
#endif
|
||||
#if UI_WIFI_SETUP_HOME_PAGE == 1
|
||||
} else if (_page == HomePage::WIFI_SETUP) {
|
||||
drawCompanionWiFiSetupPage(display);
|
||||
#endif
|
||||
#ifndef UI_NO_HIBERNATE
|
||||
} else if (_page == HomePage::SHUTDOWN) {
|
||||
display.setColor(UIColor::corp_blue);
|
||||
@@ -834,6 +932,9 @@ public:
|
||||
if (_page == HomePage::RADIO) return UI_RADIO_REFRESH_MILLIS;
|
||||
#if UI_MESSAGES_HOME_PAGE == 1
|
||||
if (_page == HomePage::MESSAGES) return 1000;
|
||||
#endif
|
||||
#if UI_WIFI_SETUP_HOME_PAGE == 1
|
||||
if (_page == HomePage::WIFI_SETUP) return 1000;
|
||||
#endif
|
||||
return 5000;
|
||||
}
|
||||
|
||||
+1128
-1128
File diff suppressed because it is too large.
Load diff
@@ -2,6 +2,9 @@
|
||||
|
||||
#ifdef WITH_WEBCONFIG
|
||||
|
||||
static_assert(sizeof(WEBCONFIG_AP_PREFIX) <= 28,
|
||||
"WEBCONFIG_AP_PREFIX must be at most 27 bytes");
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <AsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
@@ -773,7 +776,8 @@ bool WebConfigServer::startSetupMode(char reply[]) {
|
||||
WiFi.setAutoReconnect(false);
|
||||
bool disconnect_ok = WiFi.disconnect(false, true);
|
||||
delay(100);
|
||||
snprintf(_ap_ssid, sizeof(_ap_ssid), "MeshCore-Setup-%02X%02X", _pub_key[0], _pub_key[1]);
|
||||
snprintf(_ap_ssid, sizeof(_ap_ssid), "%s-%02X%02X",
|
||||
WEBCONFIG_AP_PREFIX, _pub_key[0], _pub_key[1]);
|
||||
bool ap_ok = false;
|
||||
for (uint8_t attempt = 1; attempt <= 6 && !ap_ok; ++attempt) {
|
||||
#ifdef WEBCONFIG_AP_PASSWORD
|
||||
|
||||
@@ -52,6 +52,9 @@ class DNSServer;
|
||||
#ifndef WEBCONFIG_SESSION_TTL_MS
|
||||
#define WEBCONFIG_SESSION_TTL_MS (20UL * 60UL * 1000UL)
|
||||
#endif
|
||||
#ifndef WEBCONFIG_AP_PREFIX
|
||||
#define WEBCONFIG_AP_PREFIX "MeshCore-Setup"
|
||||
#endif
|
||||
|
||||
class WebConfigServer {
|
||||
public:
|
||||
|
||||
@@ -67,6 +67,16 @@ public:
|
||||
virtual void fillRect(int x, int y, int w, int h) = 0;
|
||||
virtual void drawRect(int x, int y, int w, int h) = 0;
|
||||
virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0;
|
||||
// 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.
|
||||
virtual bool drawQrCode(const char* text, int x, int y, int size) {
|
||||
(void)text;
|
||||
(void)x;
|
||||
(void)y;
|
||||
(void)size;
|
||||
return false;
|
||||
}
|
||||
virtual uint16_t getTextWidth(const char* str) = 0;
|
||||
virtual bool getTouch(int* x, int* y) {
|
||||
(void)x;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "LGFXDisplay.h"
|
||||
#include "ColorTheme.h"
|
||||
#include "IndicatorRenderProfile.h"
|
||||
#include <lgfx/utility/lgfx_qrcode.h>
|
||||
|
||||
#ifndef DISPLAY_ROTATION
|
||||
#define DISPLAY_ROTATION 1
|
||||
@@ -345,6 +346,64 @@ void LGFXDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
||||
}
|
||||
}
|
||||
|
||||
bool LGFXDisplay::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;
|
||||
}
|
||||
|
||||
// LovyanGFX's convenience renderer hardcodes pure colors. On the
|
||||
// Indicator's indexed canvas its light color maps to palette slot 15,
|
||||
// which is deliberately reserved for transparent emoji pixels. Generate
|
||||
// the matrix directly so both colors use stable semantic palette entries.
|
||||
static constexpr uint8_t max_version = 10;
|
||||
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 (lgfx_qrcode_initText(&qr, modules, version, ECC_LOW, text) == 0) {
|
||||
initialized = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!initialized) return false;
|
||||
|
||||
static constexpr int quiet_zone = 4;
|
||||
const int scaled_size = size * _coordinateScale;
|
||||
const int module_size = scaled_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 * _coordinateScale + (scaled_size - used_size) / 2;
|
||||
const int top = y * _coordinateScale + (scaled_size - used_size) / 2;
|
||||
const uint32_t light = renderColor(UIColor::primary_txt);
|
||||
const uint32_t dark = renderColor(UIColor::window_bkg);
|
||||
|
||||
buffer.fillRect(x * _coordinateScale, y * _coordinateScale,
|
||||
scaled_size, scaled_size, light);
|
||||
const int matrix_left = left + quiet_zone * module_size;
|
||||
const int matrix_top = top + quiet_zone * module_size;
|
||||
for (uint_fast8_t row = 0; row < qr.size; ++row) {
|
||||
int run_start = -1;
|
||||
for (uint_fast16_t column = 0; column <= qr.size; ++column) {
|
||||
const bool filled = column < qr.size
|
||||
&& lgfx_qrcode_getModule(&qr, column, row);
|
||||
if (filled && run_start < 0) {
|
||||
run_start = column;
|
||||
} else if (!filled && run_start >= 0) {
|
||||
buffer.fillRect(
|
||||
matrix_left + run_start * module_size,
|
||||
matrix_top + row * module_size,
|
||||
(column - run_start) * module_size,
|
||||
module_size, dark);
|
||||
run_start = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t LGFXDisplay::getTextWidth(const char* str) {
|
||||
String mapped = mapText(str);
|
||||
return (buffer.textWidth(mapped.c_str()) + _coordinateScale - 1)
|
||||
|
||||
@@ -69,6 +69,10 @@ public:
|
||||
LGFXDisplay(int w, int h, LGFX_Device &disp)
|
||||
: DisplayDriver(w/(UI_ZOOM*UI_COORD_SCALE),
|
||||
h/(UI_ZOOM*UI_COORD_SCALE)), display(&disp) {}
|
||||
LGFXDisplay(int logical_w, int logical_h, uint8_t coordinate_scale,
|
||||
float output_zoom, LGFX_Device &disp)
|
||||
: DisplayDriver(logical_w, logical_h), display(&disp),
|
||||
_coordinateScale(coordinate_scale), _outputZoom(output_zoom) {}
|
||||
bool begin();
|
||||
bool isOn() override { return _isOn; }
|
||||
void turnOn() override;
|
||||
@@ -86,6 +90,7 @@ public:
|
||||
void fillRect(int x, int y, int w, int h) override;
|
||||
void drawRect(int x, int y, int w, int h) override;
|
||||
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||
bool drawQrCode(const char* text, int x, int y, int size) override;
|
||||
uint16_t getTextWidth(const char* str) override;
|
||||
void translateUTF8ToBlocks(char* dest, const char* src,
|
||||
size_t dest_size) override;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ui {
|
||||
|
||||
inline bool appendWiFiQrText(char*& output, size_t& remaining,
|
||||
const char* value, bool escape) {
|
||||
if (value == nullptr) return false;
|
||||
while (*value != 0) {
|
||||
const bool special = escape && strchr("\\;,\":", *value) != nullptr;
|
||||
const size_t required = special ? 2 : 1;
|
||||
if (remaining <= required) {
|
||||
*output = 0;
|
||||
return false;
|
||||
}
|
||||
if (special) {
|
||||
*output++ = '\\';
|
||||
--remaining;
|
||||
}
|
||||
*output++ = *value++;
|
||||
--remaining;
|
||||
}
|
||||
*output = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build the de-facto WiFi QR payload understood by Android and iOS cameras.
|
||||
// A null or empty password denotes an open network.
|
||||
inline bool buildWiFiSetupQrPayload(char* payload, size_t payload_size,
|
||||
const char* ssid,
|
||||
const char* password = nullptr) {
|
||||
if (payload == nullptr || payload_size == 0
|
||||
|| ssid == nullptr || ssid[0] == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char* output = payload;
|
||||
size_t remaining = payload_size;
|
||||
payload[0] = 0;
|
||||
const bool protected_network = password != nullptr && password[0] != 0;
|
||||
|
||||
if (!appendWiFiQrText(output, remaining,
|
||||
protected_network ? "WIFI:T:WPA;S:"
|
||||
: "WIFI:T:nopass;S:",
|
||||
false)
|
||||
|| !appendWiFiQrText(output, remaining, ssid, true)) {
|
||||
return false;
|
||||
}
|
||||
if (protected_network
|
||||
&& (!appendWiFiQrText(output, remaining, ";P:", false)
|
||||
|| !appendWiFiQrText(output, remaining, password, true))) {
|
||||
return false;
|
||||
}
|
||||
return appendWiFiQrText(output, remaining, ";;", false);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace mesh
|
||||
@@ -96,6 +96,16 @@ class ColorThemeTest(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(positions, sorted(positions))
|
||||
|
||||
def test_indicator_qr_avoids_transparent_palette_slot(self) -> None:
|
||||
source = (ROOT / "src/helpers/ui/LGFXDisplay.cpp").read_text()
|
||||
start = source.index("bool LGFXDisplay::drawQrCode")
|
||||
end = source.index("uint16_t LGFXDisplay::getTextWidth", start)
|
||||
qr = source[start:end]
|
||||
self.assertIn("renderColor(UIColor::primary_txt)", qr)
|
||||
self.assertIn("renderColor(UIColor::window_bkg)", qr)
|
||||
self.assertNotIn("TRANSPARENT_EMOJI_PALETTE_INDEX", qr)
|
||||
self.assertNotIn("buffer.qrcode", qr)
|
||||
|
||||
def test_pagination_uses_readable_accent_on_color_displays(self) -> None:
|
||||
source = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text()
|
||||
branch = re.search(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/CompanionHomeLayout.h>
|
||||
#include <helpers/ui/DisplayTextLayout.h>
|
||||
#include <helpers/ui/WiFiSetupQrPayload.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -98,6 +99,34 @@ TEST(DisplayDriver, EllipsizesOnlyAtUTF8CodepointBoundaries) {
|
||||
EXPECT_TRUE(isValidUTF8(display.printed.c_str()));
|
||||
}
|
||||
|
||||
TEST(DisplayDriver, QrCodeIsOptionalByDefault) {
|
||||
TestDisplay display;
|
||||
EXPECT_FALSE(display.drawQrCode("WIFI:T:nopass;S:MC-Set-90DF;;",
|
||||
27, 55, 105));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
TEST(WiFiSetupQrPayload, EscapesProtectedNetworkFields) {
|
||||
char payload[96];
|
||||
EXPECT_TRUE(mesh::ui::buildWiFiSetupQrPayload(
|
||||
payload, sizeof(payload), "Cafe;West", "p,ass"));
|
||||
EXPECT_STREQ("WIFI:T:WPA;S:Cafe\\;West;P:p\\,ass;;", payload);
|
||||
}
|
||||
|
||||
TEST(WiFiSetupQrPayload, RejectsMissingOrTruncatedFields) {
|
||||
char payload[8];
|
||||
EXPECT_FALSE(mesh::ui::buildWiFiSetupQrPayload(
|
||||
payload, sizeof(payload), ""));
|
||||
EXPECT_FALSE(mesh::ui::buildWiFiSetupQrPayload(
|
||||
payload, sizeof(payload), "MC-Set-90DF"));
|
||||
}
|
||||
|
||||
TEST(DisplayDriver, FixedBufferDoesNotSplitUTF8Codepoint) {
|
||||
TestDisplay display;
|
||||
std::string text(254, 'A');
|
||||
|
||||
@@ -164,31 +164,29 @@ class IndicatorDisplayProfileTest(unittest.TestCase):
|
||||
self.assertLessEqual(pairing_label_y + 29, pairing_value_y)
|
||||
self.assertLessEqual(pairing_value_y + 29, 160)
|
||||
|
||||
# Native setup mode intentionally uses most of the panel: a size-3
|
||||
# title and size-2 action/value rows replace the legacy size-1 page.
|
||||
for text, requested_size in (
|
||||
("SETUP", 3),
|
||||
("READY", 3),
|
||||
("WIFI", 3),
|
||||
("CONNECTING", 2),
|
||||
("JOIN WIFI", 2),
|
||||
("BROWSE", 2),
|
||||
("192.168.4.1", 2),
|
||||
):
|
||||
width, _height = dimensions(text, requested_size, 3, 1.2)
|
||||
# Page 8 retains the shared status bar and dots, then uses two equal
|
||||
# text rows and a version-2 WiFi QR. Compact mode deliberately bypasses
|
||||
# the native 1.2x chrome boost in both render profiles.
|
||||
for text in ("WIFI SETUP", "MC-Set-90DF"):
|
||||
width, _height = dimensions(text, 2, 3, 1.0)
|
||||
self.assertLessEqual(width, 160)
|
||||
|
||||
title_height = dimensions("SETUP", 3, 3, 1.2)[1]
|
||||
row_height = dimensions("JOIN WIFI", 2, 3, 1.2)[1]
|
||||
self.assertLessEqual(2 + title_height, 34)
|
||||
self.assertLessEqual(34 + row_height, 56)
|
||||
self.assertLessEqual(56 + row_height, 77)
|
||||
self.assertLessEqual(77 + row_height, 101)
|
||||
self.assertLessEqual(101 + row_height, 124)
|
||||
self.assertLessEqual(124 + row_height, 160)
|
||||
label_height = dimensions("WIFI SETUP", 2, 3, 1.0)[1]
|
||||
self.assertLessEqual(20 + label_height, 37)
|
||||
self.assertLessEqual(37 + label_height, 55)
|
||||
|
||||
long_ssid = dimensions("ABCDEFGHIJKLMNOPQRSTUVWXYZ123456", 1, 3, 1.2)
|
||||
self.assertLessEqual(long_ssid[0], 2 * 148)
|
||||
qr_size = 105
|
||||
qr_version_2_modules = 25
|
||||
quiet_zone_modules = 4
|
||||
native_module_size = qr_size * 3 // (
|
||||
qr_version_2_modules + quiet_zone_modules * 2
|
||||
)
|
||||
fallback_module_size = qr_size * 2 // (
|
||||
qr_version_2_modules + quiet_zone_modules * 2
|
||||
)
|
||||
self.assertEqual(native_module_size, 9)
|
||||
self.assertEqual(fallback_module_size * 1.5, 9)
|
||||
self.assertEqual(55 + qr_size, 160)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -148,17 +148,20 @@ profile_end = build.index("\napply_radio_overrides()", profile_start)
|
||||
profile = build[profile_start:profile_end]
|
||||
flag = "-DCOMPANION_EXCLUSIVE_WIFI_BLE=1"
|
||||
assert profile.count(flag) == 1
|
||||
exclusive_case = re.search(
|
||||
r'case "\$\{env_name,,\}" in\s+'
|
||||
r"sensecapindicator-espnow_companion_radio_full\|\\\s+"
|
||||
r"sensecapindicator-lora_companion_radio_full\)\s+"
|
||||
r'export PLATFORMIO_BUILD_FLAGS="\$\{PLATFORMIO_BUILD_FLAGS\} '
|
||||
r"-DCOMPANION_EXCLUSIVE_WIFI_BLE=1 "
|
||||
r"-DINDICATOR_TRANSPORT_RENDER_PROFILE=1\"\s+;;\s+esac",
|
||||
profile,
|
||||
exclusive_policy = profile.index(
|
||||
"# Both Indicator Full layouts select exactly one secondary"
|
||||
)
|
||||
assert exclusive_case
|
||||
assert "espnow" in exclusive_case.group(0).lower()
|
||||
exclusive_start = profile.index(
|
||||
"sensecapindicator-espnow_companion_radio_full|\\\n"
|
||||
" sensecapindicator-lora_companion_radio_full)",
|
||||
exclusive_policy,
|
||||
)
|
||||
exclusive_case = profile[exclusive_start:profile.index(";;", exclusive_start)]
|
||||
assert "espnow" in exclusive_case.lower()
|
||||
assert "-DCOMPANION_EXCLUSIVE_WIFI_BLE=1" in exclusive_case
|
||||
assert "-DINDICATOR_TRANSPORT_RENDER_PROFILE=1" in exclusive_case
|
||||
assert "-DUI_WIFI_SETUP_HOME_PAGE=1" in exclusive_case
|
||||
assert "-DWEBCONFIG_AP_PREFIX='\\\"MC-Set\\\"'" in exclusive_case
|
||||
|
||||
|
||||
assert "get companion.transport" in readme
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
@@ -10,7 +12,7 @@ UI = ROOT / "examples" / "companion_radio" / "ui-new" / "UITask.cpp"
|
||||
|
||||
|
||||
class IndicatorMessagesProfileTest(unittest.TestCase):
|
||||
def test_indicator_enables_seventh_messages_home_page(self):
|
||||
def test_indicator_enables_messages_home_page(self):
|
||||
profile = PROFILE.read_text()
|
||||
ui = UI.read_text()
|
||||
|
||||
@@ -19,6 +21,43 @@ class IndicatorMessagesProfileTest(unittest.TestCase):
|
||||
self.assertIn("#if UI_MESSAGES_HOME_PAGE == 1\n MESSAGES,", ui)
|
||||
self.assertIn("_page == HomePage::MESSAGES", ui)
|
||||
|
||||
def test_wifi_setup_is_eighth_without_reordering_existing_pages(self):
|
||||
ui = UI.read_text(encoding="utf-8")
|
||||
enum_start = ui.index(" enum HomePage {")
|
||||
enum_end = ui.index("\n };", enum_start) + len("\n };")
|
||||
enum_source = ui[enum_start:enum_end].replace(
|
||||
" enum HomePage", "enum HomePage"
|
||||
)
|
||||
source = f"""
|
||||
#define UI_MESSAGES_HOME_PAGE 1
|
||||
#define COMPANION_EXCLUSIVE_WIFI_BLE 1
|
||||
#define ENV_INCLUDE_GPS 0
|
||||
#define UI_SENSORS_PAGE 1
|
||||
#define UI_NO_HIBERNATE 1
|
||||
#define UI_WIFI_SETUP_HOME_PAGE 1
|
||||
{enum_source}
|
||||
static_assert(FIRST == 0, "first");
|
||||
static_assert(MESSAGES == 1, "messages");
|
||||
static_assert(RECENT == 2, "recent");
|
||||
static_assert(RADIO == 3, "radio");
|
||||
static_assert(TRANSPORT == 4, "transport");
|
||||
static_assert(ADVERT == 5, "advert");
|
||||
static_assert(SENSORS == 6, "sensors");
|
||||
static_assert(WIFI_SETUP == 7, "wifi setup");
|
||||
static_assert(Count == 8, "count");
|
||||
int main() {{ return 0; }}
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
executable = Path(temp_dir) / "home_pages"
|
||||
compiled = subprocess.run(
|
||||
["c++", "-std=c++11", "-x", "c++", "-", "-o", str(executable)],
|
||||
input=source,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stderr)
|
||||
|
||||
def test_full_message_chrome_uses_compact_visuals_only(self):
|
||||
ui = UI.read_text(encoding="utf-8")
|
||||
touch = (ROOT / "src/helpers/ui/TouchInput.h").read_text(
|
||||
|
||||
@@ -12,6 +12,16 @@ MAIN = ROOT / "examples" / "companion_radio" / "main.cpp"
|
||||
MESH = ROOT / "examples" / "companion_radio" / "MyMesh.cpp"
|
||||
DATA_STORE = ROOT / "examples" / "companion_radio" / "DataStore.cpp"
|
||||
PROFILE = ROOT / "variants" / "sensecap_indicator-espnow" / "platformio.ini"
|
||||
UI = ROOT / "examples" / "companion_radio" / "ui-new" / "UITask.cpp"
|
||||
INDICATOR_DISPLAY = (
|
||||
ROOT / "variants" / "sensecap_indicator-espnow" / "SCIndicatorDisplay.h"
|
||||
)
|
||||
DISPLAY_DRIVER = ROOT / "src" / "helpers" / "ui" / "DisplayDriver.h"
|
||||
LGFX_DISPLAY = ROOT / "src" / "helpers" / "ui" / "LGFXDisplay.h"
|
||||
LGFX_DISPLAY_CPP = ROOT / "src" / "helpers" / "ui" / "LGFXDisplay.cpp"
|
||||
WEBCONFIG_HEADER = ROOT / "src" / "helpers" / "esp32" / "WebConfigServer.h"
|
||||
WEBCONFIG_SOURCE = ROOT / "src" / "helpers" / "esp32" / "WebConfigServer.cpp"
|
||||
WIFI_QR_PAYLOAD = ROOT / "src" / "helpers" / "ui" / "WiFiSetupQrPayload.h"
|
||||
|
||||
|
||||
class IndicatorRenderProfileTest(unittest.TestCase):
|
||||
@@ -76,6 +86,7 @@ int main() {
|
||||
self.assertIn("-UUI_ZOOM -DUI_ZOOM=1.0f", build)
|
||||
self.assertIn("-UUI_COORD_SCALE -DUI_COORD_SCALE=3", build)
|
||||
self.assertIn("-DINDICATOR_TRANSPORT_RENDER_PROFILE=1", build)
|
||||
self.assertIn("-DUI_WIFI_SETUP_HOME_PAGE=1", build)
|
||||
|
||||
def test_runtime_selection_precedes_bluetooth_start(self):
|
||||
main = MAIN.read_text(encoding="utf-8")
|
||||
@@ -115,6 +126,23 @@ int main() {
|
||||
self.assertLess(fallback_scale, fallback_zoom)
|
||||
self.assertLess(fallback_zoom, retry)
|
||||
|
||||
def test_full_indicator_constructs_the_160_unit_viewport_explicitly(self):
|
||||
display = INDICATOR_DISPLAY.read_text(encoding="utf-8")
|
||||
self.assertIn("#if defined(INDICATOR_TRANSPORT_RENDER_PROFILE)", display)
|
||||
self.assertIn(
|
||||
"SCIndicatorDisplay() : LGFXDisplay(160, 160, 3, 1.0f, disp)",
|
||||
display,
|
||||
)
|
||||
|
||||
lgfx = LGFX_DISPLAY.read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"LGFXDisplay(int logical_w, int logical_h, uint8_t coordinate_scale,",
|
||||
lgfx,
|
||||
)
|
||||
self.assertIn("DisplayDriver(logical_w, logical_h)", lgfx)
|
||||
self.assertIn("_coordinateScale(coordinate_scale)", lgfx)
|
||||
self.assertIn("_outputZoom(output_zoom)", lgfx)
|
||||
|
||||
def test_native_font_increase_preserves_compact_chrome(self):
|
||||
display = (ROOT / "src/helpers/ui/LGFXDisplay.cpp").read_text(
|
||||
encoding="utf-8"
|
||||
@@ -123,33 +151,102 @@ int main() {
|
||||
self.assertIn("static_cast<uint16_t>(renderWidth()), _compactText", display)
|
||||
self.assertIn("* profile_scale;", display)
|
||||
|
||||
def test_indicator_setup_page_uses_large_reflow_even_after_canvas_fallback(self):
|
||||
def test_indicator_setup_is_a_normal_eighth_home_page(self):
|
||||
ui = UI.read_text(encoding="utf-8")
|
||||
page_start = ui.index("static void drawCompanionWiFiSetupPage")
|
||||
page_end = ui.index("\n}\n#endif\n\n#include \"icons.h\"", page_start) + 2
|
||||
page = ui[page_start:page_end]
|
||||
self.assertIn("WebConfigServer::getSetupInfo", page)
|
||||
self.assertIn('20, "WIFI SETUP"', page)
|
||||
self.assertIn("37, setup_ssid", page)
|
||||
self.assertIn("static constexpr int qr_size = 105;", page)
|
||||
self.assertIn("payload, qr_x, 55, qr_size", page)
|
||||
self.assertIn("mesh::ui::buildWiFiSetupQrPayload", page)
|
||||
self.assertIn("display.setCompactText(true);", page)
|
||||
self.assertGreaterEqual(page.count("display.setCompactText(false);"), 2)
|
||||
self.assertNotIn("startFrame", page)
|
||||
self.assertNotIn("endFrame", page)
|
||||
|
||||
enum_start = ui.index(" enum HomePage {")
|
||||
enum = ui[enum_start : ui.index("\n };", enum_start)]
|
||||
self.assertIn(
|
||||
"#if UI_WIFI_SETUP_HOME_PAGE == 1\n WIFI_SETUP,\n#endif\n Count",
|
||||
enum,
|
||||
)
|
||||
self.assertIn("_page == HomePage::WIFI_SETUP", ui)
|
||||
self.assertIn("if (_page == HomePage::WIFI_SETUP) return 1000;", ui)
|
||||
self.assertIn("wifi_setup_active && !_wifi_setup_was_active", ui)
|
||||
|
||||
main = MAIN.read_text(encoding="utf-8")
|
||||
start = main.index("static bool hasIndicatorSetupTypography()")
|
||||
end = main.index("static bool saveCompanionWiFi", start)
|
||||
setup = main[start:end]
|
||||
predicate = setup[: setup.index("static int drawIndicatorSetupValue")]
|
||||
self.assertIn("#if defined(INDICATOR_TRANSPORT_RENDER_PROFILE)", predicate)
|
||||
self.assertIn("return true;", predicate)
|
||||
self.assertNotIn("renderWidth()", predicate)
|
||||
self.assertNotIn("renderHeight()", predicate)
|
||||
self.assertIn("renderIndicatorSetupDisplay", setup)
|
||||
self.assertIn('setCompactText(true);', setup)
|
||||
self.assertIn('setTextSize(4);', setup)
|
||||
self.assertIn('35, "JOIN"', setup)
|
||||
self.assertIn('drawIndicatorSetupValue(70, wifi_name);', setup)
|
||||
self.assertIn('strncmp(value, "MeshCore-Setup-", 15) == 0', setup)
|
||||
self.assertIn('y, "MeshCore-"', setup)
|
||||
self.assertIn('y + 17, value + 9', setup)
|
||||
self.assertIn('getTextWidth(value) > max_width * 2', setup)
|
||||
self.assertIn('line_height = 9;', setup)
|
||||
self.assertIn('105, "OPEN"', setup)
|
||||
self.assertIn('108, "WAIT"', setup)
|
||||
self.assertIn('drawIndicatorSetupAddress(140, address);', setup)
|
||||
self.assertIn('setCompactText(false);', setup)
|
||||
self.assertIn('"SETUP", setup_ssid, setup_ip, false', setup)
|
||||
self.assertIn('"READY", configured_wifi_ssid, ip.c_str(), false', setup)
|
||||
self.assertIn('"WIFI", configured_wifi_ssid, nullptr, true', setup)
|
||||
loop = main[main.index("\nvoid loop()") :]
|
||||
display_dispatch = loop[loop.index("#ifdef DISPLAY_CLASS") :]
|
||||
display_dispatch = display_dispatch[: display_dispatch.index("rtc_clock.tick();")]
|
||||
self.assertIn("&& UI_WIFI_SETUP_HOME_PAGE != 1", display_dispatch)
|
||||
self.assertIn("renderCompanionSetupDisplay();", display_dispatch)
|
||||
self.assertIn("#else\n ui_task.loop();", display_dispatch)
|
||||
|
||||
def test_wifi_qr_payload_is_standard_and_escaped(self):
|
||||
setup = WIFI_QR_PAYLOAD.read_text(encoding="utf-8")
|
||||
self.assertIn("buildWiFiSetupQrPayload", setup)
|
||||
self.assertIn('"WIFI:T:WPA;S:"', setup)
|
||||
self.assertIn('"WIFI:T:nopass;S:"', setup)
|
||||
self.assertIn('strchr("\\\\;,\\\":", *value)', setup)
|
||||
self.assertIn('";P:"', setup)
|
||||
self.assertIn('";;"', setup)
|
||||
|
||||
def test_legacy_setup_overlay_is_not_built_for_page_enabled_profile(self):
|
||||
main = MAIN.read_text(encoding="utf-8")
|
||||
start = main.index("static DisplayDriver* companion_setup_display")
|
||||
declaration = main[start - 120 : start + 80]
|
||||
self.assertIn("&& UI_WIFI_SETUP_HOME_PAGE != 1", declaration)
|
||||
self.assertNotIn("renderIndicatorSetupDisplay", main)
|
||||
self.assertNotIn("renderIndicatorSetupQrDisplay", main)
|
||||
|
||||
def test_lgfx_qr_renderer_scales_coordinates_and_keeps_quiet_zone(self):
|
||||
driver = DISPLAY_DRIVER.read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"virtual bool drawQrCode(const char* text, int x, int y, int size)",
|
||||
driver,
|
||||
)
|
||||
|
||||
header = LGFX_DISPLAY.read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"bool drawQrCode(const char* text, int x, int y, int size) override;",
|
||||
header,
|
||||
)
|
||||
implementation = LGFX_DISPLAY_CPP.read_text(encoding="utf-8")
|
||||
qr = implementation[implementation.index("bool LGFXDisplay::drawQrCode") :]
|
||||
qr = qr[: qr.index("\nuint16_t LGFXDisplay::getTextWidth")]
|
||||
self.assertIn("lgfx_qrcode_initText", qr)
|
||||
self.assertIn("lgfx_qrcode_getModule", qr)
|
||||
self.assertIn("static constexpr int quiet_zone = 4;", qr)
|
||||
self.assertIn("renderColor(UIColor::primary_txt)", qr)
|
||||
self.assertIn("renderColor(UIColor::window_bkg)", qr)
|
||||
self.assertIn("const int scaled_size = size * _coordinateScale;", qr)
|
||||
self.assertIn("x > width() - size", qr)
|
||||
self.assertNotIn("buffer.qrcode", qr)
|
||||
self.assertNotIn("TFT_WHITE", qr)
|
||||
self.assertNotIn("TFT_BLACK", qr)
|
||||
|
||||
def test_indicator_uses_short_captive_setup_ssid(self):
|
||||
build = BUILD.read_text(encoding="utf-8")
|
||||
short_prefix_flag = "-DWEBCONFIG_AP_PREFIX='\\\"MC-Set\\\"'"
|
||||
self.assertEqual(build.count(short_prefix_flag), 1)
|
||||
full_case_start = build.index(
|
||||
"# Both Indicator Full layouts select exactly one secondary"
|
||||
)
|
||||
full_case = build[full_case_start : build.index(";;", full_case_start)]
|
||||
self.assertIn(short_prefix_flag, full_case)
|
||||
|
||||
profile = PROFILE.read_text(encoding="utf-8")
|
||||
self.assertNotIn("WEBCONFIG_AP_PREFIX", profile)
|
||||
|
||||
header = WEBCONFIG_HEADER.read_text(encoding="utf-8")
|
||||
self.assertIn('#define WEBCONFIG_AP_PREFIX "MeshCore-Setup"', header)
|
||||
source = WEBCONFIG_SOURCE.read_text(encoding="utf-8")
|
||||
self.assertIn('"%s-%02X%02X"', source)
|
||||
self.assertIn("WEBCONFIG_AP_PREFIX, _pub_key[0], _pub_key[1]", source)
|
||||
self.assertIn("sizeof(WEBCONFIG_AP_PREFIX) <= 28", source)
|
||||
|
||||
def test_lora_fresh_install_uses_compiled_cascade_defaults(self):
|
||||
profile = PROFILE.read_text(encoding="utf-8")
|
||||
|
||||
@@ -45,7 +45,7 @@ wakes it without selecting anything.
|
||||
|
||||
The message preview is a non-destructive local inbox, newest first:
|
||||
|
||||
- A seventh home page, immediately after the status page, shows the newest
|
||||
- A Messages home page, immediately after the status page, shows the newest
|
||||
retained message for each heard channel. Each two-line row contains the
|
||||
channel name, rollover-safe relative age, and a one-line ellipsized message
|
||||
preview. Direct messages are grouped by contact and labelled `Direct`.
|
||||
@@ -53,7 +53,7 @@ The message preview is a non-destructive local inbox, newest first:
|
||||
Companion drains its unread queue. The oldest history is overwritten and a
|
||||
reboot starts with an empty list; messages are not written to flash.
|
||||
- Tap the center of the message-summary page to open the existing full message
|
||||
view. Horizontal swipes continue to move among the seven home pages.
|
||||
view. Horizontal swipes continue to move among the eight home pages.
|
||||
- The first home page shows the retained local `INBOX` count, which remains
|
||||
useful when a USB host has already drained the protocol's unread count. Tap
|
||||
the center to open the inbox and channel selector even when the count is 0.
|
||||
@@ -99,14 +99,20 @@ than claiming hundredth-dB measurement accuracy.
|
||||
|
||||
## USB + WiFi Companion
|
||||
|
||||
Build `SenseCapIndicator-LoRa_comp_radio_usb_wifi` to retain the USB Companion
|
||||
Build `SenseCapIndicator-LoRa_companion_radio_full` to retain the USB Companion
|
||||
link and expose the same framed Companion protocol over WiFi TCP port `5000`.
|
||||
Fresh devices advertise an open `MeshCore-Setup-XXXX` access point, where the
|
||||
four-character suffix identifies the device; join it and open
|
||||
`http://192.168.4.1/` to save WiFi credentials. Saved credentials and the WiFi
|
||||
on/off preference survive application-only reflashes. Setup mode explicitly
|
||||
restores standard 2.4 GHz b/g/n operation, so a previously installed ESP-NOW
|
||||
image cannot leave the setup AP hidden behind its long-range protocol setting.
|
||||
Fresh Full devices advertise an open `MC-Set-XXXX` access point, where the
|
||||
four-character suffix identifies the device. WiFi Setup is the eighth normal
|
||||
home page and shows the SSID plus a WiFi-join QR while the captive AP is active.
|
||||
Fresh setup starts on that page, but ordinary swipes still reach the other
|
||||
seven pages and the LoRa modem continues running. The captive portal should
|
||||
open automatically after joining, or browse to `http://192.168.4.1/` to save
|
||||
WiFi credentials. The direct
|
||||
`SenseCapIndicator-LoRa_comp_radio_usb_wifi` environment retains the generic
|
||||
`MeshCore-Setup-XXXX` name. Saved credentials and the WiFi on/off preference
|
||||
survive application-only reflashes. Setup mode explicitly restores standard
|
||||
2.4 GHz b/g/n operation, so a previously installed ESP-NOW image cannot leave
|
||||
the setup AP hidden behind its long-range protocol setting.
|
||||
|
||||
The combined profile uses minimum WiFi modem power saving by default. USB
|
||||
remains available for recovery and management if the configured network is
|
||||
@@ -144,12 +150,11 @@ uses separate full-width size-4 `INBOX` and count rows plus a large lower
|
||||
action or BLE-status block. Long IP addresses retain a smaller bounded row
|
||||
instead of clipping.
|
||||
|
||||
The Indicator WiFi setup/status screen is reflowed separately: its title and
|
||||
short `JOIN`/`OPEN`/`WAIT` actions are size 4, while the setup SSID and IP
|
||||
address use size 2. This large sparse layout uses a fixed physical scale on
|
||||
both the native and 320x320 emergency canvases, so a failed native allocation
|
||||
cannot turn the instructions back into the generic size-1 page. Other display
|
||||
layouts retain their compact setup page.
|
||||
The Indicator WiFi setup/status page uses a fixed physical scale on both the
|
||||
native and 320x320 emergency canvases. Its content begins below the shared
|
||||
status bar and eight page dots. The page only reads connection state; it never
|
||||
starts or stops WebConfig, WiFi, ESP-NOW, or LoRa. Other display layouts retain
|
||||
their existing setup overlay.
|
||||
|
||||
LoRa remains the primary radio in both LoRa modes. On the ESP-NOW layout,
|
||||
selecting BLE leaves the primary ESP-NOW WiFi radio and its fixed channel
|
||||
|
||||
@@ -231,7 +231,16 @@ class SCIndicatorDisplay : public LGFXDisplay {
|
||||
}
|
||||
|
||||
public:
|
||||
#if defined(INDICATOR_TRANSPORT_RENDER_PROFILE)
|
||||
// Full builds always use the established 160x160 logical layout. State it
|
||||
// explicitly here because PlatformIO's inherited flag ordering can leave
|
||||
// the variant's historical UI_COORD_SCALE value visible in this header.
|
||||
// The 480px canvas is allocated before radio/transport startup; begin()
|
||||
// retains its 320px emergency fallback if that allocation is unavailable.
|
||||
SCIndicatorDisplay() : LGFXDisplay(160, 160, 3, 1.0f, disp) {}
|
||||
#else
|
||||
SCIndicatorDisplay() : LGFXDisplay(480, 480, disp) {}
|
||||
#endif
|
||||
|
||||
bool begin() {
|
||||
if (!prepareControllers()) return false;
|
||||
|
||||
+3
-3
@@ -586,7 +586,7 @@ body.tab-cli{padding-bottom:0}
|
||||
<li>Open Safari or Chrome on this device.</li>
|
||||
<li>Go to <b style="font-size:17px" id="exit-url">http://192.168.4.1</b></li>
|
||||
</ol>
|
||||
<div class="note" style="margin-top:14px">Stay connected to the <b>MeshCore-Setup</b> WiFi network. Your progress here isn't carried over — the setup restarts in the browser, where it survives the screen locking.</div>
|
||||
<div class="note" style="margin-top:14px">Stay connected to this device's <b>setup WiFi</b> network. Your progress here isn't carried over — the setup restarts in the browser, where it survives the screen locking.</div>
|
||||
<p style="text-align:center;margin-top:10px"><a href="#" onclick="document.getElementById('exit-ov').classList.remove('show');return false" style="font-size:13px">Go back</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1471,12 +1471,12 @@ function wizardSave(){
|
||||
if(acked){
|
||||
// The node accepted the batch (202), but without its result we cannot
|
||||
// know whether every field passed CLI validation.
|
||||
showReboot("The node accepted the settings for processing, but the confirmation reply was lost. If every setting passed, its screen shows \"Config saved!\" and it reboots within 30 seconds. If it stays in setup mode, reconnect to MeshCore-Setup and correct the rejected field.",
|
||||
showReboot("The node accepted the settings for processing, but the confirmation reply was lost. If every setting passed, its screen shows \"Config saved!\" and it reboots within 30 seconds. If it stays in setup mode, reconnect to its setup WiFi and correct the rejected field.",
|
||||
false,"Settings sent - verify the node");
|
||||
st.dirty={};
|
||||
return;
|
||||
}
|
||||
$("#wz-err").textContent="Couldn't reach the node to save. Check this device is still on the MeshCore-Setup WiFi, then retry.";
|
||||
$("#wz-err").textContent="Couldn't reach the node to save. Check this device is still on its setup WiFi, then retry.";
|
||||
return;
|
||||
}
|
||||
var fails=(r.results||[]).filter(function(x){return!/^OK/i.test(x.reply)});
|
||||
|
||||
Reference in new issue
Block a user