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
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user