diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2ae0afb5..e0641f36 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1490,7 +1490,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.autoadd_config = DEFAULT_AUTOADD_CONFIG; _prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE; _prefs.radio_fem_txgain = 0; - //_prefs.rx_delay_base = 10.0f; enable once new algo fixed +#ifdef DEFAULT_RX_DELAY_BASE + _prefs.rx_delay_base = DEFAULT_RX_DELAY_BASE; +#endif _prefs.setRepeatEn(false); #if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) \ || defined(USE_LR2021) || defined(SX126X_RX_BOOSTED_GAIN) \ diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 968df68f..fb7e7663 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -86,18 +86,28 @@ static void drawCompanionTransportChoice(DisplayDriver& display, } const bool large_transport_text = display.height() >= 96; - display.setTextSize(large_transport_text ? 3 : 1); const bool show_status_label = height >= 44; - const int label_y = show_status_label - ? y + (large_transport_text ? 17 : 12) - : y + (height - 8) / 2; - display.drawTextCentered(x + width / 2, label_y, label); + if (large_transport_text) { + display.setTextSize(4); + if (strcmp(label, "WiFi") == 0) { + // A single size-4 "WiFi" row is wider than one half of the screen. + // Reflow it into two large rows instead of shrinking both choices. + display.drawTextCentered(x + width / 2, y + 2, "Wi"); + display.drawTextCentered(x + width / 2, y + 36, "Fi"); + } else { + display.drawTextCentered(x + width / 2, y + 25, label); + } + } else { + display.setTextSize(1); + const int label_y = show_status_label ? y + 12 : y + (height - 8) / 2; + display.drawTextCentered(x + width / 2, label_y, label); + } if (show_status_label && (active || selected)) { - display.setTextSize(large_transport_text ? 2 : 1); + display.setTextSize(large_transport_text ? 3 : 1); display.drawTextCentered( x + width / 2, y + height - (large_transport_text ? 25 : 17), - active ? "ACTIVE" + active ? (large_transport_text ? "ON" : "ACTIVE") : (large_transport_text ? "NEXT" : "NEXT BOOT")); } } @@ -381,17 +391,19 @@ public: mesh::ui::usesExpandedCompanionHomeTypography( display.width(), display.height(), display.renderWidth(), display.renderHeight()); - sprintf(tmp, expanded_home ? "INBOX %d" : "INBOX: %d", - _task->getPreviewCount()); - display.setTextSize(expanded_home ? 3 : 2); - if (expanded_home && display.getTextWidth(tmp) > display.width() - 4) { + if (expanded_home) { + display.setTextSize(4); + display.drawTextCentered(display.width() / 2, 20, "INBOX"); + } else { + sprintf(tmp, "INBOX: %d", _task->getPreviewCount()); display.setTextSize(2); + display.drawTextCentered(display.width() / 2, 22, tmp); } #else display.setTextSize(2); sprintf(tmp, "INBOX: %d", _task->getPreviewCount()); -#endif display.drawTextCentered(display.width() / 2, 22, tmp); +#endif #ifdef UI_DEDICATED_PAIRING_BLOCK const mesh::ui::CompanionHomeLayout layout = mesh::ui::makeLargeCompanionHomeLayout(display.width(), @@ -405,27 +417,35 @@ public: mesh::ui::clearDisplayRegion(display, layout.info); mesh::ui::clearDisplayRegion(display, layout.pairing); - display.setTextSize(expanded_home ? 2 : 1); - display.setColor(UIColor::secondary_txt); - mesh::ui::drawTextCenteredEllipsized( - display, layout.info, layout.instruction_y, - expanded_home ? "tap inbox" : "tap center: inbox"); - - #ifdef WIFI_SSID - if (isCompanionWiFiEnabled()) { - IPAddress ip = WiFi.localIP(); - snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", - ip[0], ip[1], ip[2], ip[3]); - } else { - strcpy(tmp, "WiFi: OFF"); - } - display.setTextSize(expanded_home ? 2 : 1); + if (expanded_home) { + snprintf(tmp, sizeof(tmp), "%d", _task->getPreviewCount()); + display.setTextSize(4); if (display.getTextWidth(tmp) > layout.info.width) { - display.setTextSize(1); + display.setTextSize(3); } + display.setColor(UIColor::primary_txt); mesh::ui::drawTextCenteredEllipsized( - display, layout.info, layout.network_y, tmp); - #endif + display, layout.info, layout.instruction_y, tmp); + } else { + display.setTextSize(1); + display.setColor(UIColor::secondary_txt); + mesh::ui::drawTextCenteredEllipsized( + display, layout.info, layout.instruction_y, + "tap center: inbox"); + + #ifdef WIFI_SSID + if (isCompanionWiFiEnabled()) { + IPAddress ip = WiFi.localIP(); + snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", + ip[0], ip[1], ip[2], ip[3]); + } else { + strcpy(tmp, "WiFi: OFF"); + } + display.setTextSize(1); + mesh::ui::drawTextCenteredEllipsized( + display, layout.info, layout.network_y, tmp); + #endif + } const bool bluetooth_enabled = _task->isBluetoothEnabled(); const bool bluetooth_connected = _task->hasBluetoothConnection(); @@ -436,10 +456,11 @@ public: char pairing_pin[16]; if (bluetooth_connected) { pairing_label = expanded_home ? "BLE" : "BLUETOOTH"; - pairing_value = "CONNECTED"; + pairing_value = expanded_home ? "LINKED" : "CONNECTED"; + if (expanded_home) pairing_value_size = 3; } else if (mesh::ui::shouldDisplayBluetoothPairingPin( bluetooth_enabled, bluetooth_connected, bluetooth_pin)) { - pairing_label = expanded_home ? "BLE PIN" : "BLUETOOTH PIN"; + pairing_label = expanded_home ? "PIN" : "BLUETOOTH PIN"; snprintf(pairing_pin, sizeof(pairing_pin), "%06u", (unsigned int)bluetooth_pin); pairing_value = pairing_pin; @@ -450,12 +471,30 @@ public: display.setColor(UIColor::title_bkg); mesh::ui::clearDisplayRegion(display, layout.pairing); display.setColor(UIColor::title_txt); - display.setTextSize(expanded_home ? 2 : 1); + display.setTextSize(expanded_home ? 3 : 1); mesh::ui::drawTextCenteredEllipsized( display, layout.pairing, layout.pairing_label_y, pairing_label); display.setTextSize(pairing_value_size); mesh::ui::drawTextCenteredEllipsized( display, layout.pairing, layout.pairing_value_y, pairing_value); + } else if (expanded_home) { + display.setColor(UIColor::secondary_txt); + display.setTextSize(3); + mesh::ui::drawTextCenteredEllipsized( + display, layout.pairing, layout.pairing_label_y, "TAP"); +#ifdef WIFI_SSID + if (isCompanionWiFiEnabled()) { + IPAddress ip = WiFi.localIP(); + snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", + ip[0], ip[1], ip[2], ip[3]); + display.setTextSize(1); + } else { + strcpy(tmp, "OFF"); + display.setTextSize(3); + } + mesh::ui::drawTextCenteredEllipsized( + display, layout.pairing, layout.pairing_value_y, tmp); +#endif } #else const bool bluetooth_enabled = _task->isBluetoothEnabled(); @@ -611,16 +650,15 @@ public: mesh::ui::makeCompanionTransportSelectorLayout( display.width(), display.height()); - // Keep this page independent of the additional native-480 text boost: - // the size-3 choice names and size-2 status already consume nearly the - // full half-screen width, and should remain equally readable in the - // memory-saving 320 profile. + // Keep this page independent of the additional native-480 text boost. + // Its deliberately reflowed size-4 choices then retain the same large + // physical dimensions in the 320 and 480 render profiles. display.setCompactText(true); if (layout.show_title) { display.setColor(UIColor::primary_txt); - display.setTextSize(2); + display.setTextSize(3); display.drawTextCentered( - display.width() / 2, layout.title_y, "TRANSPORT"); + display.width() / 2, layout.title_y, "MODE"); } drawCompanionTransportChoice( display, layout.wifi.x, layout.wifi.y, @@ -632,10 +670,11 @@ public: "BLE", !wifi_active, !wifi_selected); display.setColor(UIColor::secondary_txt); - display.setTextSize(1); + display.setTextSize(layout.show_title ? 2 : 1); #ifdef HAS_TOUCH display.drawTextCentered( - display.width() / 2, layout.prompt_y, "tap a box"); + display.width() / 2, layout.prompt_y, + layout.show_title ? "TAP SIDE" : "tap a box"); #else display.drawTextCentered( display.width() / 2, layout.prompt_y, PRESS_LABEL); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 333d17fe..2974f908 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -48,16 +48,26 @@ class UITask : public AbstractUITask { #ifndef TOUCH_CENTER_ZONE_PERCENT #define TOUCH_CENTER_ZONE_PERCENT 34 #endif - #if defined(TOUCH_REVERSE_SWIPE) \ - && defined(TOUCH_SEPARATE_VERTICAL_SWIPES) - mesh::ui::TouchInput touch_input{true, true, TOUCH_CENTER_ZONE_PERCENT}; - #elif defined(TOUCH_REVERSE_SWIPE) - mesh::ui::TouchInput touch_input{true, false, TOUCH_CENTER_ZONE_PERCENT}; - #elif defined(TOUCH_SEPARATE_VERTICAL_SWIPES) - mesh::ui::TouchInput touch_input{false, true, TOUCH_CENTER_ZONE_PERCENT}; + #ifdef TOUCH_REVERSE_SWIPE + static constexpr bool TOUCH_REVERSE_SWIPE_ENABLED = true; #else - mesh::ui::TouchInput touch_input{false, false, TOUCH_CENTER_ZONE_PERCENT}; + static constexpr bool TOUCH_REVERSE_SWIPE_ENABLED = false; #endif + #ifdef TOUCH_SEPARATE_VERTICAL_SWIPES + static constexpr bool TOUCH_SEPARATE_VERTICAL_SWIPES_ENABLED = true; + #else + static constexpr bool TOUCH_SEPARATE_VERTICAL_SWIPES_ENABLED = false; + #endif + #ifdef TOUCH_MIRROR_TAP_X + static constexpr bool TOUCH_MIRROR_TAP_X_ENABLED = true; + #else + static constexpr bool TOUCH_MIRROR_TAP_X_ENABLED = false; + #endif + mesh::ui::TouchInput touch_input{ + TOUCH_REVERSE_SWIPE_ENABLED, + TOUCH_SEPARATE_VERTICAL_SWIPES_ENABLED, + TOUCH_CENTER_ZONE_PERCENT, + TOUCH_MIRROR_TAP_X_ENABLED}; unsigned long next_touch_check = 0; #endif #ifdef PIN_STATUS_LED diff --git a/src/helpers/ui/CompanionHomeLayout.h b/src/helpers/ui/CompanionHomeLayout.h index 9a2e4e57..f1a28e97 100644 --- a/src/helpers/ui/CompanionHomeLayout.h +++ b/src/helpers/ui/CompanionHomeLayout.h @@ -70,13 +70,17 @@ inline CompanionHomeLayout makeLargeCompanionHomeLayout(int width, int height, bool expanded = false) { if (expanded && width == 160 && height == 160) { + // Native 480 uses three large vertical bands: an INBOX title above this + // count region, then one lower region that is either the BLE state or a + // large tap/network hint. Reusing the lower region conditionally gives + // the prominent text room to grow without overlapping another status. return { - {2, 51, 156, 46}, - {8, 100, 144, 58}, - 52, - 76, + {2, 61, 156, 39}, + {8, 102, 144, 58}, + 61, + 61, 102, - 126, + 131, }; } diff --git a/src/helpers/ui/CompanionTransportSelectorLayout.h b/src/helpers/ui/CompanionTransportSelectorLayout.h index 57e8f5ed..6773536c 100644 --- a/src/helpers/ui/CompanionTransportSelectorLayout.h +++ b/src/helpers/ui/CompanionTransportSelectorLayout.h @@ -23,10 +23,10 @@ inline CompanionTransportSelectorLayout makeCompanionTransportSelectorLayout( const bool tall_display = height >= 96; const int margin = 2; const int gap = 4; - const int box_y = tall_display ? 36 : 20; - const int prompt_y = height - (tall_display ? 20 : 11); - int box_height = prompt_y - box_y - (tall_display ? 6 : 8); - if (box_height > 98) box_height = 98; + const int box_y = tall_display ? 40 : 20; + const int prompt_y = height - (tall_display ? 17 : 11); + int box_height = prompt_y - box_y - (tall_display ? 3 : 8); + if (box_height > 100) box_height = 100; if (box_height < 20) box_height = 20; const int box_width = (width - margin * 2 - gap) / 2; const int bluetooth_x = margin + box_width + gap; @@ -34,7 +34,7 @@ inline CompanionTransportSelectorLayout makeCompanionTransportSelectorLayout( return { {margin, box_y, box_width, box_height}, {bluetooth_x, box_y, box_width, box_height}, - tall_display ? 17 : 0, + tall_display ? 14 : 0, prompt_y, tall_display, }; diff --git a/src/helpers/ui/TouchInput.h b/src/helpers/ui/TouchInput.h index 6d24136f..c5a42238 100644 --- a/src/helpers/ui/TouchInput.h +++ b/src/helpers/ui/TouchInput.h @@ -32,6 +32,7 @@ class TouchInput { bool _active = false; bool _reverse_swipes; bool _separate_vertical_swipes; + bool _mirror_tap_x; uint8_t _center_zone_percent; uint8_t _touch_samples = 0; uint8_t _release_samples = 0; @@ -57,9 +58,11 @@ class TouchInput { public: explicit TouchInput(bool reverse_swipes = false, bool separate_vertical_swipes = false, - uint8_t center_zone_percent = 34) + uint8_t center_zone_percent = 34, + bool mirror_tap_x = false) : _reverse_swipes(reverse_swipes), _separate_vertical_swipes(separate_vertical_swipes), + _mirror_tap_x(mirror_tap_x), _center_zone_percent(center_zone_percent > 100 ? 100 : center_zone_percent) {} @@ -113,15 +116,21 @@ public: return verticalSwipeAction(dy < 0); } + // Some panels report a mirrored X axis. Swipe direction has its own + // independent policy because gesture direction and stationary screen + // coordinates are different concerns. Apply the panel correction only + // after movement has been ruled out so it cannot alter swipe detection. + const int tap_x = _mirror_tap_x ? width - 1 - _start_x : _start_x; + // Message screens may reserve the otherwise empty ends of their bottom // status bar as forgiving arrow buttons. Keep the label in the middle // inert so an imprecise arrow tap cannot accidentally close the screen. if (bottom_selector && _separate_vertical_swipes && _start_y >= (height * 3) / 4) { - if (_start_x < width / 4) { + if (tap_x < width / 4) { return TouchAction::VerticalPrevious; } - if (_start_x >= (width * 3) / 4) { + if (tap_x >= (width * 3) / 4) { return TouchAction::VerticalNext; } return TouchAction::None; @@ -135,13 +144,13 @@ public: || _start_y >= split_selector->top_y + split_selector->height) { return TouchAction::None; } - if (_start_x >= split_selector->left_x - && _start_x < split_selector->left_x + if (tap_x >= split_selector->left_x + && tap_x < split_selector->left_x + split_selector->left_width) { return TouchAction::SelectLeft; } - if (_start_x >= split_selector->right_x - && _start_x < split_selector->right_x + if (tap_x >= split_selector->right_x + && tap_x < split_selector->right_x + split_selector->right_width) { return TouchAction::SelectRight; } @@ -156,8 +165,8 @@ public: const int side_percent = (100 - _center_zone_percent) / 2; const int center_left = (width * side_percent) / 100; const int center_right = width - center_left; - if (_start_x < center_left) return TouchAction::Previous; - if (_start_x >= center_right) return TouchAction::Next; + if (tap_x < center_left) return TouchAction::Previous; + if (tap_x >= center_right) return TouchAction::Next; return TouchAction::Select; } diff --git a/test/test_companion_transport_selector.py b/test/test_companion_transport_selector.py index 3e738db6..34ba1425 100644 --- a/test/test_companion_transport_selector.py +++ b/test/test_companion_transport_selector.py @@ -20,8 +20,8 @@ int main() { if (!layout.show_title) return 1; if (layout.wifi.x != 2 || layout.wifi.width != 76) return 2; if (layout.bluetooth.x != 82 || layout.bluetooth.width != 76) return 3; - if (layout.wifi.y != 36 || layout.wifi.height != 98) return 4; - if (layout.title_y != 17 || layout.prompt_y != 140) return 5; + if (layout.wifi.y != 40 || layout.wifi.height != 100) return 4; + if (layout.title_y != 14 || layout.prompt_y != 143) return 5; const auto small = mesh::ui::makeCompanionTransportSelectorLayout(128, 64); if (small.show_title) return 6; @@ -56,21 +56,25 @@ int main() { ) self.assertEqual(run_result.returncode, 0, run_result.stderr) - choice_glyph_width = 6 * 3 - choice_glyph_height = 8 * 3 - for text in ("WiFi", "BLE"): + choice_glyph_width = 6 * 4 + choice_glyph_height = 8 * 4 + for text in ("Wi", "Fi", "BLE"): self.assertLessEqual(len(text) * choice_glyph_width, 76) - status_glyph_width = 6 * 2 - status_glyph_height = 8 * 2 - for text in ("ACTIVE", "NEXT"): + status_glyph_width = 6 * 3 + status_glyph_height = 8 * 3 + for text in ("ON", "NEXT"): self.assertLessEqual(len(text) * status_glyph_width, 76) - title_glyph_width = 6 * 2 - for text in ("TRANSPORT", "tap a box"): - self.assertLessEqual(len(text) * title_glyph_width, 160) - self.assertLessEqual(17 + 8 * 2, 36) - self.assertLessEqual(53 + choice_glyph_height, 134) - self.assertLessEqual(109 + status_glyph_height, 134) - self.assertLessEqual(140 + 8, 160) + self.assertLessEqual(len("MODE") * 6 * 3, 160) + self.assertLessEqual(len("TAP SIDE") * 6 * 2, 160) + self.assertLessEqual(14 + 8 * 3, 40) + wifi_first_row_y = 42 + wifi_second_row_y = 76 + self.assertLessEqual(wifi_first_row_y + choice_glyph_height, + wifi_second_row_y) + self.assertLessEqual(wifi_first_row_y + choice_glyph_height, 140) + self.assertLessEqual(76 + choice_glyph_height, 140) + self.assertLessEqual(115 + status_glyph_height, 140) + self.assertLessEqual(143 + 8 * 2, 160) def test_touch_split_selector_distinguishes_taps_from_swipes(self): source = r''' @@ -88,19 +92,19 @@ static TouchAction release(TouchInput& input, } int main() { - TouchInput input(true, true, 70); + TouchInput input(true, true, 70, true); const auto layout = mesh::ui::makeCompanionTransportSelectorLayout(160, 160); const TouchSplitSelector selector{ layout.wifi.x, layout.wifi.width, layout.bluetooth.x, layout.bluetooth.width, layout.wifi.y, layout.wifi.height}; - input.update(true, 30, 70, 160, 160, false, &selector); - input.update(true, 31, 70, 160, 160, false, &selector); + input.update(true, 129, 70, 160, 160, false, &selector); + input.update(true, 128, 70, 160, 160, false, &selector); if (release(input, &selector) != TouchAction::SelectLeft) return 1; - input.update(true, 130, 70, 160, 160, false, &selector); - input.update(true, 129, 70, 160, 160, false, &selector); + input.update(true, 30, 70, 160, 160, false, &selector); + input.update(true, 31, 70, 160, 160, false, &selector); if (release(input, &selector) != TouchAction::SelectRight) return 2; input.update(true, 80, 70, 160, 160, false, &selector); @@ -121,10 +125,10 @@ int main() { // A quick selector tap may occupy only one 25 ms poll. Its bounded target // remains unambiguous after the normal two-sample release debounce. - input.update(true, 30, 120, 160, 160, false, &selector); + input.update(true, 129, 120, 160, 160, false, &selector); if (release(input, &selector) != TouchAction::SelectLeft) return 7; - input.update(true, 130, 120, 160, 160, false, &selector); + input.update(true, 30, 120, 160, 160, false, &selector); if (release(input, &selector) != TouchAction::SelectRight) return 8; // The gap, title, and exclusive bottom edge remain inert even for a quick @@ -132,14 +136,14 @@ int main() { input.update(true, 80, 120, 160, 160, false, &selector); if (release(input, &selector) != TouchAction::None) return 9; - input.update(true, 30, layout.wifi.y - 1, 160, 160, false, &selector); + input.update(true, 129, layout.wifi.y - 1, 160, 160, false, &selector); if (release(input, &selector) != TouchAction::None) return 10; - input.update(true, 30, layout.wifi.y + layout.wifi.height, + input.update(true, 129, layout.wifi.y + layout.wifi.height, 160, 160, false, &selector); if (release(input, &selector) != TouchAction::None) return 11; - input.update(true, 30, layout.wifi.y + layout.wifi.height - 1, + input.update(true, 129, layout.wifi.y + layout.wifi.height - 1, 160, 160, false, &selector); if (release(input, &selector) != TouchAction::SelectLeft) return 12; @@ -182,12 +186,16 @@ int main() { self.assertIn('"WiFi", wifi_active, wifi_selected', source) self.assertIn('"BLE", !wifi_active, !wifi_selected', source) self.assertIn("display.fillRect(x, y, width, height);", source) - self.assertIn('"ACTIVE"', source) - self.assertIn('large_transport_text ? 3 : 1', source) + self.assertIn('large_transport_text ? "ON" : "ACTIVE"', source) + self.assertIn('display.setTextSize(4);', source) + self.assertIn('display.drawTextCentered(x + width / 2, y + 2, "Wi");', source) + self.assertIn('display.drawTextCentered(x + width / 2, y + 36, "Fi");', source) self.assertIn("const bool show_status_label = height >= 44;", source) - self.assertIn('active ? "ACTIVE"', source) + self.assertIn('active ? (large_transport_text ? "ON" : "ACTIVE")', source) self.assertIn('large_transport_text ? "NEXT" : "NEXT BOOT"', source) - self.assertIn('display.setTextSize(2);', source) + self.assertIn('display.setTextSize(3);', source) + self.assertIn('display.width() / 2, layout.title_y, "MODE"', source) + self.assertIn('layout.show_title ? "TAP SIDE" : "tap a box"', source) self.assertIn("makeCompanionTransportSelectorLayout", source) handler_start = source.index( @@ -221,6 +229,10 @@ int main() { def test_only_split_page_maps_box_taps_to_transport_keys(self): source = UI.read_text(encoding="utf-8") + profile = ( + ROOT / "variants" / "sensecap_indicator-espnow" / "platformio.ini" + ).read_text(encoding="utf-8") + self.assertIn("-D TOUCH_MIRROR_TAP_X", profile) self.assertIn( "curr == home\n" " && static_cast(home)" diff --git a/test/test_display_driver/test_display_driver.cpp b/test/test_display_driver/test_display_driver.cpp index bc3e5797..0de19162 100644 --- a/test/test_display_driver/test_display_driver.cpp +++ b/test/test_display_driver/test_display_driver.cpp @@ -192,23 +192,21 @@ TEST(DisplayDriver, NativeIndicatorExpandsHomeTypographyRegions) { const mesh::ui::CompanionHomeLayout layout = mesh::ui::makeLargeCompanionHomeLayout(160, 160, true); EXPECT_EQ(2, layout.info.x); - EXPECT_EQ(51, layout.info.y); + EXPECT_EQ(61, layout.info.y); EXPECT_EQ(156, layout.info.width); - EXPECT_EQ(46, layout.info.height); + EXPECT_EQ(39, layout.info.height); EXPECT_EQ(8, layout.pairing.x); - EXPECT_EQ(100, layout.pairing.y); + EXPECT_EQ(102, layout.pairing.y); EXPECT_EQ(144, layout.pairing.width); EXPECT_EQ(58, layout.pairing.height); EXPECT_FALSE(mesh::ui::displayRegionsOverlap(layout.info, layout.pairing)); EXPECT_TRUE(mesh::ui::displayRegionContainsLine( - layout.info, layout.instruction_y, 20)); + layout.info, layout.instruction_y, 39)); EXPECT_TRUE(mesh::ui::displayRegionContainsLine( - layout.info, layout.network_y, 20)); + layout.pairing, layout.pairing_label_y, 29)); EXPECT_TRUE(mesh::ui::displayRegionContainsLine( - layout.pairing, layout.pairing_label_y, 20)); - EXPECT_TRUE(mesh::ui::displayRegionContainsLine( - layout.pairing, layout.pairing_value_y, 30)); + layout.pairing, layout.pairing_value_y, 29)); } TEST(DisplayDriver, IndicatorPairingValuesFitAtRenderedTextSizes) { diff --git a/test/test_indicator_display_profile.py b/test/test_indicator_display_profile.py index ba11862f..ffd59492 100644 --- a/test/test_indicator_display_profile.py +++ b/test/test_indicator_display_profile.py @@ -135,24 +135,34 @@ class IndicatorDisplayProfileTest(unittest.TestCase): self.assertLessEqual(height, 30) # Native 480 reflows the spacious home page instead of globally - # enlarging every dense UI row. Its prominent strings fit at their - # requested sizes, while a two-digit inbox count takes the deliberate - # size-2 fallback. - native_title = dimensions("INBOX 0", 3, 3, 1.2) - native_two_digit_title = dimensions("INBOX 10", 3, 3, 1.2) - native_two_digit_fallback = dimensions("INBOX 10", 2, 3, 1.2) + # enlarging every dense UI row. The title, count, action, and pairing + # state each occupy their own large row. + native_title = dimensions("INBOX", 4, 3, 1.2) + native_count = dimensions("9999", 4, 3, 1.2) + native_count_fallback = dimensions("99999", 3, 3, 1.2) self.assertLessEqual(native_title[0], 156) - self.assertGreater(native_two_digit_title[0], 156) - self.assertLessEqual(native_two_digit_fallback[0], 156) + self.assertLessEqual(native_title[1], 39) + self.assertLessEqual(native_count[0], 156) + self.assertLessEqual(native_count_fallback[0], 156) - for text, region_width in ( - ("tap inbox", 156), - ("WiFi: OFF", 156), - ("BLE PIN", 144), + for text, requested_size, region_width in ( + ("TAP", 3, 144), + ("OFF", 3, 144), + ("PIN", 3, 144), + ("LINKED", 3, 144), + ("123456", 3, 144), + ("IP: 192.168.100.200", 1, 144), ): - width, height = dimensions(text, 2, 3, 1.2) + width, height = dimensions(text, requested_size, 3, 1.2) self.assertLessEqual(width, region_width) - self.assertLessEqual(height, 20) + self.assertLessEqual(height, 29 if requested_size == 3 else 10) + + # The two lower size-3 rows must not depend on blank pixels inside the + # checked-in VLW: the built-in fallback font can fill its whole cell. + pairing_label_y = 102 + pairing_value_y = 131 + self.assertLessEqual(pairing_label_y + 29, pairing_value_y) + self.assertLessEqual(pairing_value_y + 29, 160) if __name__ == "__main__": diff --git a/test/test_indicator_render_profile.py b/test/test_indicator_render_profile.py index bf5b3613..549cea88 100644 --- a/test/test_indicator_render_profile.py +++ b/test/test_indicator_render_profile.py @@ -9,6 +9,9 @@ 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" class IndicatorRenderProfileTest(unittest.TestCase): @@ -117,6 +120,74 @@ int main() { self.assertIn("static_cast(renderWidth()), _compactText", display) self.assertIn("* profile_scale;", display) + 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("-", 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 "$1" +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, "test", str(BUILD)], + 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", + "-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, + ) + if __name__ == "__main__": unittest.main() diff --git a/test/test_touch_input/test_touch_input.cpp b/test/test_touch_input/test_touch_input.cpp index fdb59d94..9b4620e2 100644 --- a/test/test_touch_input/test_touch_input.cpp +++ b/test/test_touch_input/test_touch_input.cpp @@ -218,6 +218,47 @@ TEST(TouchInput, CanReverseSwipesWithoutReversingTapZones) { EXPECT_EQ(release(input), TouchAction::Next); } +TEST(TouchInput, CanMirrorStationaryTapZonesIndependentlyOfSwipes) { + TouchInput input(true, true, 70, true); + const TouchSplitSelector selector{2, 76, 82, 76, 40, 100}; + + // The controller's raw-right coordinate is the visual-left WiFi box. + input.update(true, 129, 80, 160, 160, false, &selector); + EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector), + TouchAction::None); + EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector), + TouchAction::SelectLeft); + + // The controller's raw-left coordinate is the visual-right BLE box. + input.update(true, 30, 80, 160, 160, false, &selector); + input.update(true, 31, 80, 160, 160, false, &selector); + EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector), + TouchAction::None); + EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector), + TouchAction::SelectRight); + + // Generic side taps and the message footer arrows use the same visual-X + // correction, while their center zones stay centered. + input.update(true, 150, 60, 160, 160); + input.update(true, 150, 60, 160, 160); + EXPECT_EQ(release(input, 160, 160), TouchAction::Previous); + + input.update(true, 10, 140, 160, 160, true); + input.update(true, 10, 140, 160, 160, true); + EXPECT_EQ(input.update(false, -1, -1, 160, 160, true), + TouchAction::None); + EXPECT_EQ(input.update(false, -1, -1, 160, 160, true), + TouchAction::VerticalNext); + + // Swipe direction remains governed only by reverse_swipes. + input.update(true, 130, 70, 160, 160, false, &selector); + input.update(true, 30, 72, 160, 160, false, &selector); + EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector), + TouchAction::None); + EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector), + TouchAction::Previous); +} + TEST(TouchInput, KeepsGestureAcrossOneMissingTouchSample) { TouchInput input; diff --git a/variants/sensecap_indicator-espnow/README.md b/variants/sensecap_indicator-espnow/README.md index efbf4b93..4d0ad1e7 100644 --- a/variants/sensecap_indicator-espnow/README.md +++ b/variants/sensecap_indicator-espnow/README.md @@ -1,5 +1,22 @@ # SenseCAP Indicator controls +The primary mesh radio is selected by the firmware image, not by the WiFi/BLE +screen. Use `SenseCapIndicator-LoRa_companion_radio_full` for a LoRa-equipped +D1L or D1Pro. Build a fresh image explicitly with: + +```sh +bash build.sh build-firmware SenseCapIndicator-LoRa_companion_radio_full \ + --radio-preset usa-cascadia --profile cascade --clean +``` + +The build uses the live USA/Canada preset when the settings service is +available; its offline USA Cascadia fallback is 910.525 MHz, BW 62.5, SF7, +CR5. The Cascade profile also supplies its fresh-install defaults, including +RX delay 2. `SenseCapIndicator-ESPNow_companion_radio_full` remains the image +for an Indicator without the LoRa assembly. An application-only update +preserves the previously saved numeric radio tuple; erase/install the LoRa +merged image when the compiled fresh defaults must replace it. + The Indicator preserves the existing 160x160 UI coordinate system. Ordinary images use a 320x320, 4-bit internal canvas scaled 1.5x onto the 480x480 panel. Full Companion images first request a native 480x480 canvas, then normally @@ -16,6 +33,9 @@ wakes it without selecting anything. side performs Previous or Next; horizontal swipes remain available anywhere. - A gesture is committed after a stable release. Brief missing touch samples are ignored so one swipe cannot become a second, opposite endpoint tap. +- The SenseCAP controller reports a mirrored physical X coordinate. The board + profile corrects stationary visual left/right taps independently from its + existing swipe-direction correction. - The transport page is the exception for a very quick stationary contact: either tall, outlined WiFi/BLE box can accept one sampled point after the same stable-release debounce. Taps in the title, center gap, or footer stay @@ -117,10 +137,12 @@ available in both modes for recovery. The Indicator transport page presents WiFi and BLE as full-height side-by-side choices. Tapping either choice saves it and reboots only when it differs from -the active mode. Its size-3 choice labels render at the same physical size in +the active mode. WiFi is split over two size-4 rows, BLE uses size 4, and the +short `ON`/`NEXT` state uses size 3; all render at the same physical size in the 320 and 480 profiles. On a retained native 480 canvas, the first home page -also reflows its inbox heading and short status labels at larger sizes; long IP -addresses automatically fall back to the smaller row size instead of clipping. +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. 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 diff --git a/variants/sensecap_indicator-espnow/platformio.ini b/variants/sensecap_indicator-espnow/platformio.ini index bc8ce100..3fa4d329 100644 --- a/variants/sensecap_indicator-espnow/platformio.ini +++ b/variants/sensecap_indicator-espnow/platformio.ini @@ -53,6 +53,9 @@ build_flags = -D PIN_USER_BTN=38 -D HAS_TOUCH -D TOUCH_REVERSE_SWIPE + ; The panel's physical X axis is mirrored. Swipe direction is corrected + ; separately; stationary taps also need visual left/right hit testing. + -D TOUCH_MIRROR_TAP_X -D TOUCH_SEPARATE_VERTICAL_SWIPES ; The outer 15% remains available for Previous/Next taps; the middle 70% ; is Select. This makes the primary tap target much easier on a 480px panel.