diff --git a/DEVICES.md b/DEVICES.md index dd2560b..32cb953 100644 --- a/DEVICES.md +++ b/DEVICES.md @@ -40,7 +40,11 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases see [variants/lilygo_tdeck_pro/PORT.md](variants/lilygo_tdeck_pro/PORT.md). - **Heltec V4 + TFT**: touch UI with the on-screen keyboard; the optional Expansion Kit adds environment sensors (home-screen chart) and a piezo - buzzer. V4.3 boards get the switchable high-gain receive LNA toggle. + buzzer. On the original Expansion Kit, tap **IO** for Back or hold it for one + second to return Home; this reuses GPIO35, so that target no longer drives the + same line as a TX LED. **PWR** remains the expansion board's hardware power + control and is not readable by firmware. V4.3 boards get the switchable + high-gain receive LNA toggle. - **Tanmatsu**: keyboard-driven UI (no touchscreen) with the coloured function keys mapped to tabs, ALT accent picker, UI scaling (Normal/Large/Huge), microSD for all persistent data. Ships through the Tanmatsu launcher store, diff --git a/platformio.ini b/platformio.ini index d0e76c8..3dcabbd 100644 --- a/platformio.ini +++ b/platformio.ini @@ -72,7 +72,12 @@ build_flags = -D ESP32_CPU_FREQ=80 -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper - -D P_LORA_TX_LED=35 + ; Original Expansion Kit SW3 "IO": active-low KEY net on GPIO35. This pin was + ; previously treated as a TX LED, which drove the button line as an output. + ; Navigation is more valuable on the cramped 240 px touch UI: short=Back, + ; hold=Home. The PWR switch drives the EC190708 power controller directly and + ; cannot provide a firmware short-press. The V4-R8 uses GPIO46 for its TX LED. + -D HELTEC_V4_EXPANSION_IO_PIN=35 -D P_LORA_DIO_1=14 -D P_LORA_NSS=8 -D P_LORA_RESET=12 diff --git a/src/ui-touch/KeyboardLayouts.cpp b/src/ui-touch/KeyboardLayouts.cpp index 3eaf7ab..77dd40e 100644 --- a/src/ui-touch/KeyboardLayouts.cpp +++ b/src/ui-touch/KeyboardLayouts.cpp @@ -221,6 +221,139 @@ static const lv_btnmatrix_ctrl_t kb_en_upper_ctrl[] = { LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2 }; +#if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) +// Original V4 Expansion Kit: the 240 px portrait keyboard's stock first row +// spends most of its width on mode/backspace controls, leaving ~18 px letters. +// Keep QWERTY order but move controls to the bottom so letters get 24-27 px. +static const char* const kb_v4_lower[] = { + "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "\n", + "a", "s", "d", "f", "g", "h", "j", "k", "l", "\n", + "z", "x", "c", "v", "b", "n", "m", ".", ",", "\n", + "ABC", "1#", LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, + LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, "" +}; +static const char* const kb_v4_upper[] = { + "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "\n", + "A", "S", "D", "F", "G", "H", "J", "K", "L", "\n", + "Z", "X", "C", "V", "B", "N", "M", ".", ",", "\n", + "abc", "1#", LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, + LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, "" +}; +static const lv_btnmatrix_ctrl_t kb_v4_text_ctrl[] = { + EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), + EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), + EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), + EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), + EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), + EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), + LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), + LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2, + LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 5, + LV_BTNMATRIX_CTRL_CHECKED | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, + LV_KEYBOARD_CTRL_BTN_FLAGS | 2 +}; + +// Five columns give each number/punctuation key roughly 48 px. "#+" switches +// to the second page; "123" returns. UITask intercepts those two controls +// before LVGL's default handler so their labels are never inserted as text. +static const char* const kb_v4_special[] = { + "1", "2", "3", "4", "5", "\n", + "6", "7", "8", "9", "0", "\n", + ".", ",", "?", "!", "@", "\n", + "-", "_", "+", "=", "/", "\n", + "#+", "abc", LV_SYMBOL_BACKSPACE, " ", LV_SYMBOL_OK, "" +}; +static const char* const kb_v4_special_more[] = { + "#", "$", "%", "&", "*", "\n", + "(", ")", "[", "]", "{", "\n", + "}", ":", ";", "'", "\"", "\n", + "\\", "|", "<", ">", "^", "\n", + "123", "abc", LV_SYMBOL_BACKSPACE, " ", LV_SYMBOL_OK, "" +}; +static const lv_btnmatrix_ctrl_t kb_v4_special_ctrl[] = { + EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), + EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), + EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), + EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), + LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2, + LV_BTNMATRIX_CTRL_CHECKED | 2, 5, LV_KEYBOARD_CTRL_BTN_FLAGS | 2 +}; +static_assert(sizeof(kb_v4_lower) / sizeof(kb_v4_lower[0]) == + sizeof(kb_v4_text_ctrl) / sizeof(kb_v4_text_ctrl[0]) + 4, + "V4 lower map/control count mismatch"); +static_assert(sizeof(kb_v4_upper) / sizeof(kb_v4_upper[0]) == + sizeof(kb_v4_text_ctrl) / sizeof(kb_v4_text_ctrl[0]) + 4, + "V4 upper map/control count mismatch"); +static_assert(sizeof(kb_v4_special) / sizeof(kb_v4_special[0]) == + sizeof(kb_v4_special_ctrl) / sizeof(kb_v4_special_ctrl[0]) + 5, + "V4 symbols map/control count mismatch"); +static_assert(sizeof(kb_v4_special_more) / sizeof(kb_v4_special_more[0]) == + sizeof(kb_v4_special_ctrl) / sizeof(kb_v4_special_ctrl[0]) + 5, + "V4 more-symbols map/control count mismatch"); + +static const char* kb_v4_secondary_lower[64]; +static const char* kb_v4_secondary_upper[64]; +static lv_btnmatrix_ctrl_t kb_v4_secondary_lower_ctrl[56]; +static lv_btnmatrix_ctrl_t kb_v4_secondary_upper_ctrl[56]; + +static bool kbV4TextControl(const char* key) { + return strcmp(key, "1#") == 0 || strcmp(key, "ABC") == 0 || + strcmp(key, "abc") == 0 || strcmp(key, LV_SYMBOL_UP) == 0 || + strcmp(key, LV_SYMBOL_NEW_LINE) == 0 || + strcmp(key, LV_SYMBOL_KEYBOARD) == 0 || + strcmp(key, LV_SYMBOL_LEFT) == 0 || strcmp(key, " ") == 0 || + strcmp(key, LV_SYMBOL_RIGHT) == 0 || + strcmp(key, LV_SYMBOL_BACKSPACE) == 0 || + strcmp(key, LV_SYMBOL_OK) == 0; +} + +static bool kbV4InlinePunctuation(const char* key) { + return strcmp(key, "_") == 0 || strcmp(key, "-") == 0 || + strcmp(key, ".") == 0 || strcmp(key, ",") == 0 || + strcmp(key, ":") == 0; +} + +static void kbV4BuildSecondary(const char* const* source, bool upper, + const char** map, lv_btnmatrix_ctrl_t* controls) { + size_t map_count = 0; + size_t control_count = 0; + bool row_has_key = false; + for (size_t i = 0; source[i] && source[i][0]; ++i) { + const char* key = source[i]; + if (strcmp(key, "\n") == 0) { + if (row_has_key) map[map_count++] = "\n"; + row_has_key = false; + continue; + } + if (kbV4TextControl(key) || kbV4InlinePunctuation(key)) continue; + map[map_count++] = key; + controls[control_count++] = EN_KB_BTN(1); + row_has_key = true; + } + if (row_has_key) map[map_count++] = "\n"; + + static const char* const footer_lower[] = { + "ABC", "1#", LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, + LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK + }; + static const char* const footer_upper[] = { + "abc", "1#", LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, + LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK + }; + const char* const* footer = upper ? footer_upper : footer_lower; + for (size_t i = 0; i < 8; ++i) map[map_count++] = footer[i]; + map[map_count] = ""; + controls[control_count++] = LV_KEYBOARD_CTRL_BTN_FLAGS | 2; + controls[control_count++] = LV_KEYBOARD_CTRL_BTN_FLAGS | 2; + controls[control_count++] = LV_KEYBOARD_CTRL_BTN_FLAGS | 2; + controls[control_count++] = LV_BTNMATRIX_CTRL_CHECKED | 2; + controls[control_count++] = 5; + controls[control_count++] = LV_BTNMATRIX_CTRL_CHECKED | 2; + controls[control_count++] = LV_BTNMATRIX_CTRL_CHECKED | 2; + controls[control_count] = LV_KEYBOARD_CTRL_BTN_FLAGS | 2; +} +#endif + /* ---------- French (AZERTY) on-screen keyboard ---------- */ /* Keeps the proven LVGL control rows/punctuation, but swaps the alpha rows to * a familiar French AZERTY order. Accents still come from the existing popup. */ @@ -806,8 +939,30 @@ void keyboardLayoutsApply(lv_obj_t* keyboard, KeyboardLayoutId id) { if (!keyboard) return; const OsKeyboardLayout& lo = k_os_layouts[static_cast(id)]; +#if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) + if (id == KeyboardLayoutId::EN) { + lv_keyboard_set_map(keyboard, LV_KEYBOARD_MODE_TEXT_LOWER, + const_cast(kb_v4_lower), kb_v4_text_ctrl); + lv_keyboard_set_map(keyboard, LV_KEYBOARD_MODE_TEXT_UPPER, + const_cast(kb_v4_upper), kb_v4_text_ctrl); + } else { + kbV4BuildSecondary(lo.lower_map, false, kb_v4_secondary_lower, + kb_v4_secondary_lower_ctrl); + kbV4BuildSecondary(lo.upper_map, true, kb_v4_secondary_upper, + kb_v4_secondary_upper_ctrl); + lv_keyboard_set_map(keyboard, LV_KEYBOARD_MODE_TEXT_LOWER, + kb_v4_secondary_lower, kb_v4_secondary_lower_ctrl); + lv_keyboard_set_map(keyboard, LV_KEYBOARD_MODE_TEXT_UPPER, + kb_v4_secondary_upper, kb_v4_secondary_upper_ctrl); + } + lv_keyboard_set_map(keyboard, LV_KEYBOARD_MODE_SPECIAL, + const_cast(kb_v4_special), kb_v4_special_ctrl); + lv_keyboard_set_map(keyboard, LV_KEYBOARD_MODE_USER_1, + const_cast(kb_v4_special_more), kb_v4_special_ctrl); +#else lv_keyboard_set_map(keyboard, LV_KEYBOARD_MODE_TEXT_LOWER, const_cast(lo.lower_map), lo.lower_ctrl); // maps now in flash; LVGL never writes them lv_keyboard_set_map(keyboard, LV_KEYBOARD_MODE_TEXT_UPPER, const_cast(lo.upper_map), lo.upper_ctrl); +#endif } KeyboardLayoutId keyboardLayoutsGetCurrent() { diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index dfcfc35..d2f2a83 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -1918,7 +1918,11 @@ static inline lv_coord_t chatComposerBaseH() { // composer automatically reclaims space from the message list. Reset to // CHAT_COMP_H on each chat-panel (re)build; updated by chatComposerAutoGrow(). static lv_coord_t s_comp_h = CHAT_COMP_H; +#if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) +constexpr int CHAT_KB_H = 160; // larger original-V4 touch targets +#else constexpr int CHAT_KB_H = 130; // on-screen keyboard (portrait) +#endif // Bottom tab-bar height (matches lv_tabview_create in buildUiTree). A tab // page's usable content area is the screen minus the status bar and tab bar — // queried live so it tracks the current rotation (240×260 portrait / @@ -10379,6 +10383,21 @@ static void kbBackspaceSelCb(lv_event_t* e) { lv_event_stop_processing(e); } +#if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) +static void kbV4SpecialPageCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + lv_obj_t* kb = lv_event_get_target(e); + const uint32_t button = lv_btnmatrix_get_selected_btn(kb); + if (button == LV_BTNMATRIX_BTN_NONE) return; + const char* text = lv_btnmatrix_get_btn_text(kb, button); + if (!text) return; + if (strcmp(text, "#+") == 0) lv_keyboard_set_mode(kb, LV_KEYBOARD_MODE_USER_1); + else if (strcmp(text, "123") == 0) lv_keyboard_set_mode(kb, LV_KEYBOARD_MODE_SPECIAL); + else return; + lv_event_stop_processing(e); +} +#endif + static void closeSettingsModal() { hideKb(); #if CAP_KEYPAD_NAV @@ -40711,6 +40730,7 @@ static bool drawerPopupOpen() { // once behaves consistently everywhere. Adding a popup = one registry row. static bool popupRegistryAny(); static bool popupRegistryDismissTop(); +static int popupRegistryDismissTopAboveAppDrawer(); static bool popupRegistryBlocksSwipe(); // True if any popup/modal is currently up (rows flagged PF_COUNT). @@ -47310,6 +47330,11 @@ static void buildGlobalStatusBar() { // vertical centreline with the clock and connection/battery indicators. const lv_coord_t BH = 20, BW = 26, GAP = 3, BX0 = 6; const lv_coord_t BY = (STATUSBAR_H - BH) / 2; + #elif defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) + // Original V4 Expansion Kit: partition row 2 into larger, non-overlapping + // cells; the former 30x20 actions were too easy to miss or hit beside. + const lv_coord_t BH = 22, BW = 42, GAP = 8, BX0 = 4; + const lv_coord_t BY = STATUSBAR_H; #else const bool portrait = lv_disp_get_hor_res(nullptr) < lv_disp_get_ver_res(nullptr); const lv_coord_t BH = portrait ? 20 : (lv_coord_t)((STATUSBAR_H * 2 - 4) * 7 / 10); @@ -47327,6 +47352,10 @@ static void buildGlobalStatusBar() { styleButton(b); lv_obj_set_style_radius(b, 9, LV_PART_MAIN); // softer corners lv_obj_set_style_border_opa(b, LV_OPA_20, LV_PART_MAIN); // dim the edge so it blends + #if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) + // Three pixels each side still leave two pixels between adjacent hits. + lv_obj_set_ext_click_area(b, 3); + #endif lv_obj_add_flag(b, LV_OBJ_FLAG_HIDDEN); return b; }; @@ -47400,7 +47429,11 @@ static void buildGlobalStatusBar() { lv_obj_align(g_statusbar.chan_gear, LV_ALIGN_LEFT_MID, 44, 0); // gap after the back chevron #endif lv_obj_add_flag(g_statusbar.chan_gear, LV_OBJ_FLAG_CLICKABLE); +#if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) + lv_obj_set_ext_click_area(g_statusbar.chan_gear, 12); +#else lv_obj_set_ext_click_area(g_statusbar.chan_gear, 10); +#endif lv_obj_add_flag(g_statusbar.chan_gear, LV_OBJ_FLAG_HIDDEN); lv_obj_add_event_cb(g_statusbar.chan_gear, channelGearCb, LV_EVENT_CLICKED, nullptr); #if defined(HAS_TANMATSU) @@ -50776,11 +50809,20 @@ static void buildUiTree() { lv_obj_set_size(g_lv.keyboard, lv_disp_get_hor_res(nullptr), CHAT_KB_H); lv_obj_align(g_lv.keyboard, LV_ALIGN_BOTTOM_MID, 0, 0); lv_obj_add_flag(g_lv.keyboard, LV_OBJ_FLAG_HIDDEN); +#if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) + // Magnify the selected key above the finger before release. The compact V4 + // maps mark character keys as POPOVER-capable; LVGL leaves the feature off + // unless it is explicitly enabled on the keyboard widget. + lv_keyboard_set_popovers(g_lv.keyboard, true); +#endif // Insert our backspace-deletes-selection interceptor AHEAD of the keyboard's // own default handler: remove the auto-registered default, add the interceptor // first, then re-add the default after it. The interceptor runs first and can // stop_processing to replace the default single-char delete when text is selected. lv_obj_remove_event_cb(g_lv.keyboard, lv_keyboard_def_event_cb); +#if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) + lv_obj_add_event_cb(g_lv.keyboard, kbV4SpecialPageCb, LV_EVENT_VALUE_CHANGED, nullptr); +#endif lv_obj_add_event_cb(g_lv.keyboard, kbBackspaceSelCb, LV_EVENT_VALUE_CHANGED, nullptr); lv_obj_add_event_cb(g_lv.keyboard, lv_keyboard_def_event_cb, LV_EVENT_VALUE_CHANGED, nullptr); lv_obj_add_event_cb(g_lv.keyboard, keyboardCb, LV_EVENT_READY, nullptr); @@ -59462,6 +59504,74 @@ void UITask::loop() { snprintf(msg, sizeof(msg), TR("No reply from %s"), s_ui_ping_target_name); showAlert(msg, 3500); } +#if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) + // Original V4 Expansion Kit SW3 "IO" (GPIO35, active-low). Debounce before + // starting the hold clock: short release walks Back; a one-second hold goes + // directly Home. A press that only wakes an idle-dimmed screen is consumed. + { + static bool s_io_inited = false; + static uint8_t s_io_raw = HIGH; + static uint8_t s_io_stable = HIGH; + static uint32_t s_io_changed_ms = 0; + static uint32_t s_io_down_ms = 0; + static bool s_io_long_fired = false; + static bool s_io_wake_consumed = false; + constexpr uint32_t kIoDebounceMs = 25; + constexpr uint32_t kIoHomeHoldMs = 1000; + + const uint8_t raw = digitalRead(HELTEC_V4_EXPANSION_IO_PIN); + if (!s_io_inited) { + s_io_raw = s_io_stable = raw; + s_io_down_ms = now; + s_io_wake_consumed = raw == LOW; // held through boot: wait for a clean release + s_io_inited = true; + } + if (raw != s_io_raw) { + s_io_raw = raw; + s_io_changed_ms = now; + } + if (s_io_stable != s_io_raw && now - s_io_changed_ms >= kIoDebounceMs) { + const uint8_t previous = s_io_stable; + s_io_stable = s_io_raw; + if (s_io_stable == LOW) { + s_io_down_ms = now; + s_io_long_fired = false; + s_io_wake_consumed = _screen_off; + if (_screen_off && !_manual_lock) wakeScreen(); + } else if (previous == LOW && !s_io_long_fired && !s_io_wake_consumed) { + // Match the established keypad Back ladder: frontmost controls first, + // then app page, popup, chat, and finally the Home tab. + const int popup_result = popupRegistryDismissTopAboveAppDrawer(); + if (popup_result == 0) { + if (s_apppage_close) s_apppage_close(); + else if (anyPopupOpen()) popupRegistryDismissTop(); + else if (g_lv.dm.detail_open) closeChatPanel(&g_lv.dm); + else if (g_lv.ch.detail_open) closeChatPanel(&g_lv.ch); + else if (getActiveTab() != HOME_TAB_INDEX) goToTab(HOME_TAB_INDEX); + } + noteUserInput(); + } + } + if (s_io_stable == LOW && !s_io_long_fired && !s_io_wake_consumed && + now - s_io_down_ms >= kIoHomeHoldMs) { + // Home is a deliberate root action. Respect an undismissable progress + // overlay, otherwise close every layer and return to Commander. + bool clear = true; + for (int i = 0; i < 8 && anyPopupOpen(); ++i) { + if (!popupRegistryDismissTop()) { clear = false; break; } + } + if (clear && !anyPopupOpen()) { + if (s_apppage_close) s_apppage_close(); + if (g_lv.dm.detail_open) closeChatPanel(&g_lv.dm); + if (g_lv.ch.detail_open) closeChatPanel(&g_lv.ch); + goToTab(HOME_TAB_INDEX); + setHomeDrawer(false); + noteUserInput(); + } + s_io_long_fired = true; + } + } +#endif #if CAP_SD || defined(TLORA_PAGER) // Manual telemetry request timed out — flip the open window to "failed" (it // still shows the history). Auto-poll has no deadline, so it never lands here. @@ -60832,6 +60942,20 @@ static bool popupRegistryDismissTop() { } return false; } +static int popupRegistryDismissTopAboveAppDrawer() { + // The app drawer is the registry's final row and intentionally stays open + // beneath a launched app. Dismiss a visible popup above that app first, but + // leave the hidden drawer for the app-page rung in the caller. + const size_t count = sizeof(k_popup_registry) / sizeof(k_popup_registry[0]); + for (size_t i = 0; i + 1 < count; ++i) { + const PopupEnt& entry = k_popup_registry[i]; + if (!entry.is_open()) continue; + if (!entry.close) return -1; // an active progress overlay blocks Back + entry.close(); + return 1; + } + return 0; +} static bool popupRegistryBlocksSwipe() { for (const auto& e : k_popup_registry) if ((e.flags & PF_SWIPE) && e.is_open()) return true; diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index f280d69..471a6e9 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -11,6 +11,10 @@ void HeltecV4Board::begin() { loRaFEMControl.init(); +#if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) + pinMode(HELTEC_V4_EXPANSION_IO_PIN, INPUT_PULLUP); +#endif + #if defined(HELTEC_LORA_V4_R8) && defined(PIN_SD_CS) // Park the shared-FSPI micro-SD's chip-select HIGH before LGFX starts // driving the bus (display.begin + logo blit run before the first @@ -48,12 +52,16 @@ void HeltecV4Board::begin() { } void HeltecV4Board::onBeforeTransmit(void) { +#ifdef P_LORA_TX_LED digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on +#endif loRaFEMControl.setTxModeEnable(); } void HeltecV4Board::onAfterTransmit(void) { +#ifdef P_LORA_TX_LED digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off +#endif loRaFEMControl.setRxModeEnable(); }