Files
HaloKeymind/test/test_indicator_render_profile.py
T

401 lines
17 KiB
Python

#!/usr/bin/env python3
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
BUILD = ROOT / "build.sh"
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"
def find_bash():
# The legacy WindowsApps bash.exe bridge drops shell positional arguments
# and expands variables incorrectly. Prefer Git for Windows' real Bash;
# other platforms use the normal PATH lookup.
if os.name == "nt":
git_bash = (
Path(os.environ.get("ProgramFiles", r"C:\Program Files"))
/ "Git"
/ "bin"
/ "bash.exe"
)
if git_bash.is_file():
return str(git_bash)
return shutil.which("bash") or "bash"
BASH = find_bash()
class IndicatorRenderProfileTest(unittest.TestCase):
def test_transport_matrix(self):
source = r'''
#include <helpers/ui/IndicatorRenderProfile.h>
int main() {
using mesh::ui::selectIndicatorRenderProfile;
const auto lora_wifi = selectIndicatorRenderProfile(false, true);
const auto lora_ble = selectIndicatorRenderProfile(false, false);
const auto espnow_wifi = selectIndicatorRenderProfile(true, true);
const auto espnow_ble = selectIndicatorRenderProfile(true, false);
if (lora_wifi.canvas_size != 480 || lora_wifi.coordinate_scale != 3
|| lora_wifi.output_zoom != 1.0f) return 1;
if (lora_ble.canvas_size != 480 || lora_ble.coordinate_scale != 3
|| lora_ble.output_zoom != 1.0f) return 2;
if (espnow_wifi.canvas_size != 480 || espnow_wifi.coordinate_scale != 3
|| espnow_wifi.output_zoom != 1.0f) return 3;
if (espnow_ble.canvas_size != 320 || espnow_ble.coordinate_scale != 2
|| espnow_ble.output_zoom != 1.5f) return 4;
if (mesh::ui::selectIndicatorTextScale(480, false) != 1.2f) return 5;
if (mesh::ui::selectIndicatorTextScale(480, true) != 1.0f) return 6;
if (mesh::ui::selectIndicatorTextScale(320, false) != 1.0f) return 7;
if (!mesh::ui::usesNativeIndicatorTypography(160, 160, 480, 480)) return 8;
if (mesh::ui::usesNativeIndicatorTypography(160, 160, 320, 320)) return 9;
if (mesh::ui::usesNativeIndicatorTypography(128, 128, 480, 480)) return 10;
return 0;
}
'''
with tempfile.TemporaryDirectory() as temp_dir:
executable = Path(temp_dir) / "indicator_render_profile"
compiled = subprocess.run(
[
"c++",
"-std=c++11",
f"-I{ROOT / 'src'}",
"-x",
"c++",
"-",
"-o",
str(executable),
],
input=source,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(compiled.returncode, 0, compiled.stderr)
ran = subprocess.run(
[str(executable)], capture_output=True, text=True, check=False
)
self.assertEqual(ran.returncode, 0, ran.stderr)
def test_full_profiles_allocate_native_before_runtime_selection(self):
build = BUILD.read_text(encoding="utf-8")
self.assertIn(
"sensecapindicator-espnow_companion_radio_full|\\\n"
" sensecapindicator-lora_companion_radio_full)",
build,
)
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")
setup = main[main.index("void setup()") : main.index("\nvoid loop()")]
select = setup.index("selectIndicatorRenderProfile(")
resize = setup.index("disp->setRenderScale(", select)
bluetooth = setup.index("startCompanionBluetooth();", resize)
self.assertLess(select, resize)
self.assertLess(resize, bluetooth)
self.assertIn("true,", setup[select:resize])
self.assertIn("companionTransportWiFiActiveAtBoot()", setup[select:resize])
def test_canvas_stays_internal_and_can_rollback(self):
display = (ROOT / "src/helpers/ui/LGFXDisplay.cpp").read_text(
encoding="utf-8"
)
self.assertIn("buffer.setPsram(false);", display)
self.assertIn("if (!configurePalette()) {\n buffer.deleteSprite();", display)
self.assertIn("buffer.deleteSprite();", display)
self.assertIn("_coordinateScale = previous_scale;", display)
self.assertIn("_outputZoom = previous_zoom;", display)
self.assertIn("createRenderBuffer();\n return false;", display)
def test_failed_native_startup_retries_safe_canvas(self):
display = (ROOT / "src/helpers/ui/LGFXDisplay.cpp").read_text(
encoding="utf-8"
)
begin = display[display.index("bool LGFXDisplay::begin()") :]
begin = begin[: begin.index("\nbool LGFXDisplay::createRenderBuffer()")]
first_attempt = begin.index("if (createRenderBuffer()) return true;")
guard = begin.index("INDICATOR_TRANSPORT_RENDER_PROFILE", first_attempt)
fallback_scale = begin.index("_coordinateScale = 2;", guard)
fallback_zoom = begin.index("_outputZoom = 1.5f;", fallback_scale)
retry = begin.index("if (createRenderBuffer()) return true;", fallback_zoom)
self.assertLess(first_attempt, guard)
self.assertLess(guard, fallback_scale)
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"
)
self.assertIn("selectIndicatorTextScale(", display)
self.assertIn("static_cast<uint16_t>(renderWidth()), _compactText", display)
self.assertIn("* profile_scale;", display)
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)
self.assertIn("isCompanionWiFiConnected()", page)
self.assertIn("hasCompanionWiFiCredentials()", page)
self.assertIn("CompanionWiFiDisplayState::NotConfigured", page)
self.assertIn('"TAP TO START"', page)
self.assertIn("wifi_connected != _wifi_was_connected", ui)
self.assertIn("CompanionWiFiDisplayState::Ready", page)
self.assertIn("display.clear()", page)
self.assertIn("noteCompanionWiFiDisplayState(state)", page)
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
helper = main[main.index("bool isCompanionWiFiConnected()"):
main.index("bool toggleCompanionWiFi()")]
self.assertNotIn("!companion_wifi_active", helper)
self.assertIn("esp_wifi_sta_get_ap_info", helper)
mesh = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
self.assertIn('strcmp(command, "get display.wifi")', mesh)
self.assertIn("_task->gotoHomeScreen();", ui)
self.assertIn("requestCompanionWiFiSetup();", ui)
self.assertIn('strcpy(tmp, "WiFi: SETUP");', ui)
self.assertIn('strcpy(tmp, "SETUP");', ui)
main = MAIN.read_text(encoding="utf-8")
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)
self.assertIn('"not-configured"', main)
self.assertIn("companion_wifi_setup_requested", loop)
def test_wifi_qr_payload_is_compact_and_escaped(self):
setup = WIFI_QR_PAYLOAD.read_text(encoding="utf-8")
self.assertIn("buildWiFiSetupQrPayload", setup)
self.assertIn('"WIFI:T:WPA;S:"', setup)
# Omitting the optional open-network type keeps the setup payload in
# QR version 1 at low error correction on the 64-pixel OLED.
self.assertIn('"WIFI: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("ECC_LOW", 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.assertIn("module_size, module_size, dark", qr)
self.assertNotIn("run_start", 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")
lora = profile[profile.index("[SenseCapIndicator-LoRa]") :]
lora = lora[: lora.index("[env:")]
self.assertIn("-UMESH_PRIMARY_ESPNOW", lora)
self.assertIn("-D SENSECAP_INDICATOR_LORA", lora)
self.assertIn("-D USE_SX1262", lora)
self.assertIn("-<helpers/esp32/ESPNOWRadio.cpp>", lora)
mesh = MESH.read_text(encoding="utf-8")
self.assertIn("_prefs.freq = LORA_FREQ;", mesh)
self.assertIn("_prefs.sf = LORA_SF;", mesh)
self.assertIn("_prefs.bw = LORA_BW;", mesh)
self.assertIn("_prefs.cr = LORA_CR;", mesh)
guarded_rx_delay = (
"#ifdef DEFAULT_RX_DELAY_BASE\n"
" _prefs.rx_delay_base = DEFAULT_RX_DELAY_BASE;\n"
"#endif"
)
self.assertIn(guarded_rx_delay, mesh)
# Saved preferences load after constructor defaults, so an app-only
# update remains non-destructive while an erased install uses them.
begin = mesh[mesh.index("void MyMesh::begin(") :]
self.assertIn("_store->loadPrefs(_prefs", begin)
store = DATA_STORE.read_text(encoding="utf-8")
self.assertIn('if (_fs->exists("/new_prefs"))', store)
self.assertIn('loadPrefsInt("/new_prefs", prefs', store)
def test_explicit_usa_cascadia_and_cascade_build_flags(self):
command = r'''
set -e
source ./build.sh
PLATFORMIO_BUILD_FLAGS=""
RADIO_FREQ_OVERRIDE=910.525
RADIO_BW_OVERRIDE=62.5
RADIO_SF_OVERRIDE=7
RADIO_CR_OVERRIDE=5
FIRMWARE_PROFILE_OVERRIDE=cascade
apply_radio_overrides
apply_firmware_profile_overrides
printf '%s\n' "$PLATFORMIO_BUILD_FLAGS"
'''
result = subprocess.run(
[BASH, "-c", command],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
flags = result.stdout.split()
for expected in (
"-DLORA_FREQ=910.525",
"-DLORA_BW=62.5",
"-DLORA_SF=7",
"-DLORA_CR=5",
"-DMESHCORE_USA_RADIO_PRESET=1",
"-DCASCADE_PROFILE=1",
"-DDEFAULT_RX_DELAY_BASE=2.0f",
):
self.assertIn(expected, flags)
build = BUILD.read_text(encoding="utf-8")
self.assertIn(
"'SenseCapIndicator-LoRa_comp_radio_usb_wifi|"
"SenseCapIndicator-LoRa_companion_radio_full'",
build,
)
self.assertIn(
"'SenseCapIndicator-LoRa-N16R2_comp_radio_usb_wifi|"
"SenseCapIndicator-LoRa-N16R2_companion_radio_full'",
build,
)
def test_non_usa_radio_does_not_enable_wizard_skip(self):
command = r'''
set -e
source ./build.sh
PLATFORMIO_BUILD_FLAGS=""
RADIO_SETTING_TITLE="Australia"
RADIO_FREQ_OVERRIDE=915.800
RADIO_BW_OVERRIDE=250
RADIO_SF_OVERRIDE=10
RADIO_CR_OVERRIDE=5
apply_radio_overrides
printf '%s\n' "$PLATFORMIO_BUILD_FLAGS"
'''
result = subprocess.run(
[BASH, "-c", command],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn("-DMESHCORE_USA_RADIO_PRESET=1", result.stdout.split())
if __name__ == "__main__":
unittest.main()