Add Indicator WiFi setup as eighth home page

This commit is contained in:
mikecarper committed 2026-08-31 17:31:48 -07:00
1 parent a38e0370f9
commit 42888e6e3a
20 files changed
+1660 -1311

No files matched your search

+10
View File
@@ -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');
+20 -22
View File
@@ -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__":
+13 -10
View File
@@ -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
+40 -1
View File
@@ -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(
+123 -26
View File
@@ -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")