diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 94390a2e..20e304c3 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1612,6 +1612,28 @@ static void lastHeardToggleContact() { return 0; } +#if defined(MECK_TWATCH) + // Trace screen (watch): tap to select row, tap same to activate. Mode row + // toggles 1/2-byte; Type Path opens the keyboard (handleInput -> wantsKeyboard). + // Guarded to the watch: selectRowAtVY uses the watch's 128-space coords, and + // T5S3/T-Deck keep their existing boot-button/keyboard trace handling. + if (ui_task.isOnTraceScreen()) { + TraceScreen* ts = (TraceScreen*)ui_task.getTraceScreen(); + if (ts) { + int result = ts->selectRowAtVY(vy); + if (result == 1) { + ui_task.forceRefresh(); + return 0; + } + if (result == 2) { + if (ts->isOnModeRow()) return 'd'; // toggle 1-byte / 2-byte + return KEY_ENTER; // Type Path / Add / Remove / Run / Exit / hop + } + } + return 0; + } +#endif + // Discovery screen: tap to select, tap same to add if (ui_task.isOnDiscoveryScreen()) { DiscoveryScreen* ds = (DiscoveryScreen*)ui_task.getDiscoveryScreen(); diff --git a/examples/companion_radio/ui-new/DiscoveryScreen.h b/examples/companion_radio/ui-new/DiscoveryScreen.h index 8b2f5173..6b0f7f48 100644 --- a/examples/companion_radio/ui-new/DiscoveryScreen.h +++ b/examples/companion_radio/ui-new/DiscoveryScreen.h @@ -104,11 +104,11 @@ public: display.setCursor(4, 28); display.print(active ? "Listening for adverts..." : "No nodes found"); #if defined(MECK_TWATCH) - display.setCursor(4, 38); - display.print("Hold: Rescan"); display.setCursor(4, 48); - display.print("Tap: Select"); + display.print("Hold: Rescan"); display.setCursor(4, 58); + display.print("Tap: Select"); + display.setCursor(4, 68); display.print("Tap Again: Add"); #else if (!active) { diff --git a/examples/companion_radio/ui-new/Tracescreen.h b/examples/companion_radio/ui-new/Tracescreen.h index 9dbc4ab4..498a9f08 100644 --- a/examples/companion_radio/ui-new/Tracescreen.h +++ b/examples/companion_radio/ui-new/Tracescreen.h @@ -91,6 +91,9 @@ private: int _resultScroll; bool _wantExit; +#if defined(MECK_TWATCH) + bool _wantKeyboard; // Watch: Type Path tapped -> UITask opens the on-screen keyboard +#endif // --- Menu helpers (STATE_BUILD) --- // Menu layout: @@ -313,6 +316,56 @@ public: return _state == STATE_BUILD && menuItemAt(_menuSel) == MENU_TYPE_PATH; } + // True if the highlighted menu row is the Mode selector (STATE_BUILD only). + bool isOnModeRow() const { + return _state == STATE_BUILD && menuItemAt(_menuSel) == MENU_PATH_SIZE; + } + +#if defined(MECK_TWATCH) + bool wantsKeyboard() const { return _wantKeyboard; } + void clearWantKeyboard() { _wantKeyboard = false; } + + // Tap-to-select row mapping for the watch (mirrors PathEditor/Discovery). + // vy is in the 128-tall virtual touch space. Returns: + // 0 = miss, 1 = moved the cursor, 2 = tapped the already-selected row. + int selectRowAtVY(int vy) { + if (_state == STATE_BUILD) return selectBuildRowAtVY(vy); + if (_state == STATE_PICK_HOP) return selectPickerRowAtVY(vy); + return 0; + } + + int selectBuildRowAtVY(int vy) { + int menuCount = buildMenuCount(); + if (menuCount == 0) return 0; + const int headerH = 14, footerH = 14, lineH = 11; + const int bodyTop = headerH; // renderBuild draws row 0 at y=14 + if (vy < bodyTop || vy >= 128 - footerH) return 0; + int maxVisible = (128 - headerH - footerH) / lineH; + if (maxVisible < 1) maxVisible = 1; + // Replicate renderBuild's scroll window so a tap maps to the drawn row. + int scrollTop = 0; + if (_menuSel >= scrollTop + maxVisible) scrollTop = _menuSel - maxVisible + 1; + if (_menuSel < scrollTop) scrollTop = _menuSel; + int tappedRow = scrollTop + (vy - bodyTop) / lineH; + if (tappedRow < 0 || tappedRow >= menuCount) return 0; + if (tappedRow == _menuSel) return 2; + _menuSel = tappedRow; + return 1; + } + + int selectPickerRowAtVY(int vy) { + if (_repCount == 0) return 0; + const int headerH = 14, footerH = 14, lineH = 11; + const int bodyTop = headerH; // renderPicker draws row 0 at y=14 + if (vy < bodyTop || vy >= 128 - footerH) return 0; + int tappedRow = _repScroll + (vy - bodyTop) / lineH; + if (tappedRow < 0 || tappedRow >= _repCount) return 0; + if (tappedRow == _repSel) return 2; + _repSel = tappedRow; + return 1; + } +#endif + // Returns the current path formatted as a comma-separated string, suitable // for pre-populating an external text editor (e.g. the T5S3 virtual keyboard). // The returned pointer references an internal buffer and is valid until the @@ -343,6 +396,9 @@ public: _repSel = 0; _repScroll = 0; _wantExit = false; +#if defined(MECK_TWATCH) + _wantKeyboard = false; +#endif _resultScroll = 0; memset(_pathBuf, 0, sizeof(_pathBuf)); memset(&_result, 0, sizeof(_result)); @@ -462,6 +518,8 @@ private: } else { #if defined(LilyGo_T5S3_EPaper_Pro) snprintf(tmp, sizeof(tmp), "%c Type Path: [Long press]", prefix); +#elif defined(MECK_TWATCH) + snprintf(tmp, sizeof(tmp), "%c Type Path: [Tap]", prefix); #else snprintf(tmp, sizeof(tmp), "%c Type Path: [Press Enter]", prefix); #endif @@ -533,6 +591,8 @@ private: } else { #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Exit Tap:Sel"); +#elif defined(MECK_TWATCH) + display.print("Tap:Sel Hold:Go"); #else display.print("Sh+Del:Exit W/S:Nav Ent:Sel"); #endif @@ -596,6 +656,8 @@ private: display.setCursor(0, footerY); #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Back Tap:Add"); +#elif defined(MECK_TWATCH) + display.print("Tap:Sel Hold:Add"); #else display.print("Sh+Del:Back W/S:Scroll Ent:Add"); #endif @@ -748,6 +810,8 @@ private: display.setCursor(0, footerY); #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Back Tap:New Trace"); +#elif defined(MECK_TWATCH) + display.print("Hold: New Trace"); #else display.print("Sh+Del:Back Ent:New Trace"); #endif @@ -843,9 +907,15 @@ private: MenuItem item = menuItemAt(_menuSel); switch (item) { case MENU_TYPE_PATH: - // Enter edit mode -- pre-fill with current path if any pathToEditBuf(); +#if defined(MECK_TWATCH) + // Watch: no physical keys -- request the on-screen keyboard. The main + // loop polls wantsKeyboard() and opens it, pre-filled with the path. + _wantKeyboard = true; +#else + // Enter inline edit mode -- pre-fill with current path if any _editing = true; +#endif return true; case MENU_ADD_HOP: diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 5a3ed474..3fe1a43e 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1694,7 +1694,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no _sensors = sensors; _auto_off = millis() + AUTO_OFF_MILLIS; -#if (defined(PIN_USER_BTN) || defined(MECK_PMU_BUTTON)) && !defined(MECK_DIAG_NO_PMU) +#if (defined(PIN_USER_BTN) || defined(MECK_PMU_BUTTON)) user_btn.begin(); #endif #if defined(PIN_USER_BTN_ANA) @@ -2291,7 +2291,7 @@ void UITask::shutdown(bool restart){ } bool UITask::isButtonPressed() const { -#if (defined(PIN_USER_BTN) || defined(MECK_PMU_BUTTON)) && !defined(MECK_DIAG_NO_PMU) +#if (defined(PIN_USER_BTN) || defined(MECK_PMU_BUTTON)) return user_btn.isPressed(); #else return false; @@ -2314,7 +2314,7 @@ void UITask::loop() { } } #endif -#if (defined(PIN_USER_BTN) || defined(MECK_PMU_BUTTON)) && !defined(MECK_DIAG_NO_PMU) +#if (defined(PIN_USER_BTN) || defined(MECK_PMU_BUTTON)) int ev = user_btn.check(); if (ev == BUTTON_EVENT_CLICK) { #if defined(LilyGo_T5S3_EPaper_Pro) @@ -2511,7 +2511,7 @@ void UITask::loop() { if (buzzer.isPlaying()) buzzer.loop(); #endif -#if defined(LILYGO_TWATCH_S3) +#if defined(LILYGO_TWATCH_S3) && !defined(MECK_DIAG_NO_ALARM) // Alarms are checked every loop, not from the screen's poll(), so one fires // with the display off or another screen showing. if (watch_alarm_screen) { @@ -2626,6 +2626,12 @@ if (curr) curr->poll(); if (watch_channel_cfg_screen) setCurrScreen(watch_channel_cfg_screen); else gotoHomeScreen(); if (serr) showAlert(serr, 1200); + } else if (purpose == TWatchKeyboardScreen::TWKB_TRACE_PATH) { + TraceScreen* ts = (TraceScreen*)getTraceScreen(); + if (ts) ts->setTypedPath(sendText); + kb->clearOutBuf(); + if (trace_screen) setCurrScreen(trace_screen); + else gotoHomeScreen(); } else { // TWKB_ADMIN_PASSWORD or TWKB_ADMIN_CLI RepeaterAdminScreen* admin = (RepeaterAdminScreen*)getRepeaterAdminScreen(); if (admin) { @@ -2666,6 +2672,14 @@ if (curr) curr->poll(); gotoSettingsScreen(); } } + else if (curr == trace_screen && trace_screen != nullptr) { + TraceScreen* ts = (TraceScreen*)trace_screen; + if (ts->wantsKeyboard()) { + ts->clearWantKeyboard(); + openTWatchKeyboard(TWatchKeyboardScreen::TWKB_TRACE_PATH, 0); + ((TWatchKeyboardScreen*)tw_keyboard)->setInitialText(ts->getCurrentPathAsText()); + } + } else if (curr == settings_screen && settings_screen != nullptr) { SettingsScreen* ss = (SettingsScreen*)settings_screen; if (ss->wantsWatchChannels()) { diff --git a/variants/lilygo_twatch_s3/TWatchComposeScreens.h b/variants/lilygo_twatch_s3/TWatchComposeScreens.h index 880c1bb9..4c7a4a9e 100644 --- a/variants/lilygo_twatch_s3/TWatchComposeScreens.h +++ b/variants/lilygo_twatch_s3/TWatchComposeScreens.h @@ -492,7 +492,7 @@ class TWatchKeyboardScreen : public UIScreen { uint8_t _channelIdx; public: - enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE, TWKB_SCOPE }; + enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE, TWKB_SCOPE, TWKB_TRACE_PATH }; private: Purpose _purpose; diff --git a/variants/lilygo_twatch_s3/TWatchS3Board.cpp b/variants/lilygo_twatch_s3/TWatchS3Board.cpp index 68194965..0644282f 100644 --- a/variants/lilygo_twatch_s3/TWatchS3Board.cpp +++ b/variants/lilygo_twatch_s3/TWatchS3Board.cpp @@ -180,17 +180,11 @@ bool TWatchS3Board::power_init() { PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); PMU->clearIrqStatus(); -#if !defined(MECK_DIAG_NO_PMU) // SHORT gives the click; NEGATIVE/POSITIVE are the press/release edges that // back PMUButton::isPressed(). PMU->enableIRQ(XPOWERS_AXP2101_PKEY_SHORT_IRQ | XPOWERS_AXP2101_PKEY_NEGATIVE_IRQ | XPOWERS_AXP2101_PKEY_POSITIVE_IRQ); -#endif - // TEMP DIAGNOSTIC (MECK_DIAG_NO_PMU, power bisect): with the PKEY IRQs left - // disabled above and PMUButton::check() skipped in UITask, the PMU-button - // subsystem is fully inert -- no IRQ config, no 30 ms polling, no dangling - // latch. disableIRQ(ALL)+clearIrqStatus above already left the PMU quiet. // 150 mA = 0.32C on the 470 mAh cell -- a modest bump over the 125 mA // vendor default (LilyGoWatchS3.cpp uses 125), well under the 0.5C rule. diff --git a/variants/lilygo_twatch_s3/platformio.ini b/variants/lilygo_twatch_s3/platformio.ini index 28bd989e..4ab3d877 100644 --- a/variants/lilygo_twatch_s3/platformio.ini +++ b/variants/lilygo_twatch_s3/platformio.ini @@ -138,20 +138,19 @@ lib_deps = lib_ignore = AsyncTCP ESPAsyncWebServer + ; --------------------------------------------------------------------------- ; TEMP DIAGNOSTIC BUILD -- power-drain bisect (remove when concluded). -; The S3 standalone firmware with the PMU-button subsystem fully disabled, -; gated by MECK_DIAG_NO_PMU: the PKEY IRQ enable in power_init is skipped and -; PMUButton::check()/begin()/isPressed() are compiled out of UITask, so there -; is no IRQ config and no 30 ms I2C polling of the PMU (and no dangling latch). -; Run the same wear test as the standalone S3 build; a materially slower drain -; implicates the PMU-button subsystem in the S3-only excess. The PWR key does -; nothing on this build (no click, no wake, no path-editor open) by design. -; Revert: delete this env and grep MECK_DIAG_NO_PMU (one block in -; TWatchS3Board.cpp, three in UITask.cpp). -; Flash: pio run -e meck_twatch_s3_diag_nopmu -t upload +; The S3 standalone firmware with the per-loop WatchAlarmScreen tick() disabled, +; gated by MECK_DIAG_NO_ALARM: the alarm-check block in UITask::loop is compiled +; out, so localNow() (an RTC read) and the slot scan no longer run every loop. +; The alarm screen is still created and menu-reachable, but no alarm can fire on +; this build by design. Run the same wear test as the standalone S3 build; a +; materially slower drain implicates the alarm tick() in the S3-only excess. +; Revert: delete this env and grep MECK_DIAG_NO_ALARM (one block in UITask.cpp). +; Flash: pio run -e meck_twatch_s3_diag_noalarm -t upload ; --------------------------------------------------------------------------- -[env:meck_twatch_s3_diag_nopmu] +[env:meck_twatch_s3_diag_noalarm] extends = LilyGo_TWatchS3 build_flags = ${LilyGo_TWatchS3.build_flags} @@ -160,8 +159,8 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=1 -D ESP32_CPU_FREQ=80 - -D MECK_DIAG_NO_PMU - -D FIRMWARE_VERSION='"Meck TWatch S3 DiagNoPMU v0.1"' + -D MECK_DIAG_NO_ALARM + -D FIRMWARE_VERSION='"Meck TWatch S3 DiagNoAlarm v0.1"' build_src_filter = ${LilyGo_TWatchS3.build_src_filter} + - diff --git a/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h b/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h index 880c1bb9..4c7a4a9e 100644 --- a/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h +++ b/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h @@ -492,7 +492,7 @@ class TWatchKeyboardScreen : public UIScreen { uint8_t _channelIdx; public: - enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE, TWKB_SCOPE }; + enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE, TWKB_SCOPE, TWKB_TRACE_PATH }; private: Purpose _purpose;