From 973d02df6044cbd51d0b266b71bd5ec3a35e43e2 Mon Sep 17 00:00:00 2001 From: Pelgraine Date: Tue, 7 Jul 2026 21:58:32 +1000 Subject: [PATCH] T-Watch target.cpp: fixed preamble = (sf <= 8) ? 32 : 16 to (sf <= 9) which I should have done in the first place to match meshcore PR 1954. Testing on Watch before applying wider change. T-Watch: So many UI changes. Also swapped render battery indicator to use the AXP2101 gauge for hopefully higher accuracy. Also implemnted meck rx duty cycle for watch builds only to test out power saving effects first on watch build before implementing elsewhere. --- examples/companion_radio/main.cpp | 39 +++++++ .../companion_radio/ui-new/ContactsScreen.h | 9 ++ .../companion_radio/ui-new/DiscoveryScreen.h | 15 +++ .../ui-new/RepeaterAdminScreen.h | 10 ++ .../companion_radio/ui-new/SettingsScreen.h | 14 ++- examples/companion_radio/ui-new/UITask.cpp | 100 +++++++++++++++--- examples/companion_radio/ui-new/UITask.h | 1 + src/Dispatcher.cpp | 4 + src/helpers/radiolib/RadioLibWrappers.cpp | 19 +++- src/helpers/ui/LGFXDisplay.cpp | 2 +- src/helpers/ui/LGFXDisplay.h | 13 +++ .../TWatchComposeScreens.h | 51 ++++++++- .../TWatchS3PlusBoard.cpp | 46 +++++++- .../lilygo_twatch_s3_plus/TWatchS3PlusBoard.h | 11 ++ variants/lilygo_twatch_s3_plus/platformio.ini | 1 + variants/lilygo_twatch_s3_plus/target.cpp | 2 +- 16 files changed, 308 insertions(+), 29 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index ef4d92eb..5534f545 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1904,6 +1904,37 @@ static void lastHeardToggleContact() { } } return 0; +#elif defined(LILYGO_TWATCH_S3_PLUS) + // Watch: long press = DM (tw-keyboard) / admin / room action. + // purpose ints match TWatchKeyboardScreen::Purpose: DM=1. + { + int idx = cs->getSelectedContactIdx(); + uint8_t ctype = cs->getSelectedContactType(); + if (idx >= 0 && ctype == ADV_TYPE_CHAT) { + if (ui_task.hasDMUnread(idx)) { + char cname[32]; + cs->getSelectedContactName(cname, sizeof(cname)); + ui_task.clearDMUnread(idx); + ui_task.gotoDMConversation(cname); + return 0; + } + ui_task.openTWatchKeyboard(1, idx); // TWKB_DM + return 0; + } else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) { + ui_task.gotoRepeaterAdmin(idx); + return 0; + } else if (idx >= 0 && ctype == ADV_TYPE_ROOM) { + ui_task.gotoRepeaterAdmin(idx); + return 0; + } else if (idx >= 0 && ui_task.hasDMUnread(idx)) { + char cname[32]; + cs->getSelectedContactName(cname, sizeof(cname)); + ui_task.clearDMUnread(idx); + ui_task.gotoDMConversation(cname); + return 0; + } + } + return 0; #else // T-Deck Pro: long press enters select mode cs->enterSelectMode(); @@ -1949,10 +1980,18 @@ static void lastHeardToggleContact() { #if defined(LilyGo_T5S3_EPaper_Pro) ui_task.showVirtualKeyboard(VKB_ADMIN_PASSWORD, "Admin Password", "", 32); return 0; +#elif defined(LILYGO_TWATCH_S3_PLUS) + ui_task.openTWatchKeyboard(2, 0); // TWKB_ADMIN_PASSWORD + return 0; #else return KEY_ENTER; // T-Deck Pro: keyboard handles password entry #endif } +#if defined(LILYGO_TWATCH_S3_PLUS) + // Watch: any other admin state long-press opens the CLI keyboard. + ui_task.openTWatchKeyboard(3, 0); // TWKB_ADMIN_CLI + return 0; +#endif } } diff --git a/examples/companion_radio/ui-new/ContactsScreen.h b/examples/companion_radio/ui-new/ContactsScreen.h index ab1da8ba..fd5544bb 100644 --- a/examples/companion_radio/ui-new/ContactsScreen.h +++ b/examples/companion_radio/ui-new/ContactsScreen.h @@ -301,7 +301,11 @@ public: int selCount = getSelectedCount(); snprintf(tmp, sizeof(tmp), "%d Selected [%s]", selCount, filterLabel(_filter)); } else { +#if defined(LILYGO_TWATCH_S3_PLUS) + snprintf(tmp, sizeof(tmp), "Contacts[%s]", filterLabel(_filter)); +#else snprintf(tmp, sizeof(tmp), "Contacts [%s]", filterLabel(_filter)); +#endif } display.print(tmp); @@ -466,6 +470,11 @@ public: display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); } +#elif defined(LILYGO_TWATCH_S3_PLUS) + display.setCursor(0, footerY); + if (!_selectMode) { + display.print("Long Press: Enter"); + } #elif defined(LILYGO_TECHO_LITE) display.setCursor(0, footerY); if (_selectMode) { diff --git a/examples/companion_radio/ui-new/DiscoveryScreen.h b/examples/companion_radio/ui-new/DiscoveryScreen.h index a7f06e7a..b60579a3 100644 --- a/examples/companion_radio/ui-new/DiscoveryScreen.h +++ b/examples/companion_radio/ui-new/DiscoveryScreen.h @@ -103,6 +103,14 @@ public: display.setColor(DisplayDriver::LIGHT); display.setCursor(4, 28); display.print(active ? "Listening for adverts..." : "No nodes found"); +#if defined(LILYGO_TWATCH_S3_PLUS) + display.setCursor(4, 38); + display.print("Hold: Rescan"); + display.setCursor(4, 48); + display.print("Tap: Select"); + display.setCursor(4, 58); + display.print("Tap Again: Add"); +#else if (!active) { display.setCursor(4, 38); #if defined(LilyGo_T5S3_EPaper_Pro) @@ -111,6 +119,7 @@ public: display.print("F: Scan again Sh+Del: Back"); #endif } +#endif } else { // Center visible window around selected item int maxVisible = (maxY - headerHeight) / lineHeight; @@ -199,6 +208,12 @@ public: display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY); display.print(mid); + const char* right = "Hold:Rescan"; + display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); + display.print(right); +#elif defined(LILYGO_TWATCH_S3_PLUS) + display.print("Tap:Slct"); + const char* right = "Hold:Rescan"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); diff --git a/examples/companion_radio/ui-new/RepeaterAdminScreen.h b/examples/companion_radio/ui-new/RepeaterAdminScreen.h index dba9c241..931a15de 100644 --- a/examples/companion_radio/ui-new/RepeaterAdminScreen.h +++ b/examples/companion_radio/ui-new/RepeaterAdminScreen.h @@ -606,6 +606,8 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Exit"); renderFooterRight(display, footerY, "Hold:Type"); +#elif defined(LILYGO_TWATCH_S3_PLUS) + display.print("Long Press: login"); #else display.print("Sh+Del:Exit"); renderFooterRight(display, footerY, "Ent:Login"); @@ -1075,6 +1077,11 @@ private: void renderResponse(DisplayDriver& display, int y, int bodyHeight) { display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); int lineHeight = the_mesh.getNodePrefs()->smallLineH(); +#if defined(LILYGO_TWATCH_S3_PLUS) + // Watch: clip long response lines at the screen edge instead of wrapping + // them onto the row below. + ((LGFXDisplay*)&display)->setTextWrap(false); +#endif display.setColor((_state == STATE_ERROR) ? DisplayDriver::YELLOW : DisplayDriver::LIGHT); @@ -1112,6 +1119,9 @@ private: } display.setTextSize(1); +#if defined(LILYGO_TWATCH_S3_PLUS) + ((LGFXDisplay*)&display)->setTextWrap(true); // restore default +#endif } bool handleResponseInput(char c) { diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 05d34e1d..b0addb63 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -499,14 +499,18 @@ private: addRow(ROW_BACKLIGHT_BRIGHTNESS); addRow(ROW_KB_BACKLIGHT); #endif +#if !defined(LILYGO_TWATCH_S3_PLUS) addRow(ROW_MSG_NOTIFY); +#endif #if HAS_GPS addRow(ROW_GPS_BAUD); #endif addRow(ROW_PATH_HASH_SIZE); addRow(ROW_DEFAULT_SCOPE); +#if !defined(LILYGO_TWATCH_S3_PLUS) addRow(ROW_DARK_MODE); -#if !defined(LILYGO_TECHO_LITE) +#endif +#if !defined(LILYGO_TECHO_LITE) && !defined(LILYGO_TWATCH_S3_PLUS) addRow(ROW_LARGE_FONT); addRow(ROW_FONT_STYLE); #endif @@ -2936,9 +2940,17 @@ public: } else if (_subScreen != SUB_NONE) { display.print("Sh+Del:Back"); } else { +#if defined(LILYGO_TWATCH_S3_PLUS) + display.print("Hold:Edit"); +#else display.print("Sh+Del:Bk"); +#endif } +#if defined(LILYGO_TWATCH_S3_PLUS) + const char* r = (_subScreen == SUB_NONE) ? "Tap:Cycle" : "Tap/Ent:Edit"; +#else const char* r = "Tap/Ent:Edit"; +#endif display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY); display.print(r); } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index ecc2e71a..27c576ee 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -445,9 +445,8 @@ public: { LGFXDisplay* lcd = (LGFXDisplay*)&display; - // Battery % (voltage-derived, same source as the old icon), rightmost. - int mV = (int)_task->getBattMilliVolts(); - int pct = (mV - 3000) * 100 / 1200; + // Battery % from the AXP2101 fuel gauge, rightmost. + int pct = (int)_task->getBatteryPercent(); if (pct < 0) pct = 0; if (pct > 100) pct = 100; char battbuf[6]; @@ -1453,8 +1452,7 @@ public: } else { strcpy(buf, "--:--"); } - display.setTextSize(4); - display.drawTextCentered(display.width() / 2, display.height() / 2 - 30, buf); + ((LGFXDisplay*)&display)->printClockFont(display.width() / 2, display.height() / 2 - 30, buf); // day + date line below the clock if (now > 1700000000) { @@ -1472,8 +1470,7 @@ public: } // battery % and unread count - int mv = (int)_task->getBattMilliVolts(); - int pct = (mv - 3000) * 100 / 1200; + int pct = (int)_task->getBatteryPercent(); if (pct < 0) pct = 0; if (pct > 100) pct = 100; sprintf(buf, "%d%% | %d unread", pct, _task->getUnreadMsgCount()); @@ -1973,6 +1970,21 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i } if (_display != NULL && !suppressNotif) { +#if defined(LILYGO_TWATCH_S3_PLUS) + // Watch: a new message must not wake the screen. The unread counter has + // already been incremented above; leave the display asleep so the count + // is only seen on the next tilt-wake to the clock screen. Only refresh + // when the display is already on. + if (_display->isOn()) { + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + if (isRoomMsg) { + unsigned long earliest = millis() + 3000; // At most one refresh per 3s during sync + if (_next_refresh < earliest) _next_refresh = earliest; + } else { + _next_refresh = 100; // trigger refresh + } + } +#else if (!_display->isOn() && !hasConnection()) { _display->turnOn(); } @@ -1986,6 +1998,7 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _next_refresh = 100; // trigger refresh } } +#endif } // Keyboard flash notification (suppress for room sync and muted channels) @@ -2143,6 +2156,20 @@ bool UITask::isButtonPressed() const { void UITask::loop() { char c = 0; +#if defined(LILYGO_TWATCH_S3_PLUS) + // TEMP power-debug probe: every 60s, log display state + board power stats. + { + static unsigned long _pwr_dbg_next = 0; + if (millis() >= _pwr_dbg_next) { + _pwr_dbg_next = millis() + 60000; + Serial.printf("[PWR] uptime=%lus display=%s auto_off_in=%ldms\n", + millis() / 1000, + (_display && _display->isOn()) ? "ON" : "off", + (long)(_auto_off - millis())); + board.printPowerDebug(); + } + } +#endif #if defined(PIN_USER_BTN) int ev = user_btn.check(); if (ev == BUTTON_EVENT_CLICK) { @@ -2354,16 +2381,46 @@ if (curr) curr->poll(); } const char* sendText = nullptr; if (kb->consumeSendRequest(&sendText) && sendText) { - ChannelDetails ch; - if (the_mesh.getChannel(kb->getChannelIdx(), ch)) { - uint32_t ts = rtc_clock.getCurrentTime(); - the_mesh.sendGroupMessage(ts, ch.channel, the_mesh.getNodeName(), - sendText, strlen(sendText)); - showAlert("Sent!", 800); - addSentChannelMessage(kb->getChannelIdx(), the_mesh.getNodePrefs()->node_name, sendText); + TWatchKeyboardScreen::Purpose purpose = kb->getPurpose(); + int ctxIdx = kb->getContextIdx(); + if (purpose == TWatchKeyboardScreen::TWKB_CHANNEL) { + ChannelDetails ch; + if (the_mesh.getChannel(kb->getChannelIdx(), ch)) { + uint32_t ts = rtc_clock.getCurrentTime(); + the_mesh.sendGroupMessage(ts, ch.channel, the_mesh.getNodeName(), + sendText, strlen(sendText)); + showAlert("Sent!", 800); + addSentChannelMessage(kb->getChannelIdx(), the_mesh.getNodePrefs()->node_name, sendText); + } + kb->clearOutBuf(); + setCurrScreen(tw_channel); + } else if (purpose == TWatchKeyboardScreen::TWKB_DM) { + bool dmSuccess = false; + if (strlen(sendText) > 0 && the_mesh.uiSendDirectMessage((uint32_t)ctxIdx, sendText)) { + ContactInfo dmRecipient; + if (the_mesh.getContactByIdx(ctxIdx, dmRecipient)) { + addSentDM(dmRecipient.name, the_mesh.getNodePrefs()->node_name, sendText); + } + dmSuccess = true; + } + kb->clearOutBuf(); + ContactInfo dmContact; + if (the_mesh.getContactByIdx(ctxIdx, dmContact)) { + gotoDMConversation(dmContact.name, ctxIdx, 0); + } else { + gotoHomeScreen(); + } + showAlert(dmSuccess ? "DM sent!" : "DM failed!", 1500); + } else { // TWKB_ADMIN_PASSWORD or TWKB_ADMIN_CLI + RepeaterAdminScreen* admin = (RepeaterAdminScreen*)getRepeaterAdminScreen(); + if (admin) { + for (int i = 0; sendText[i]; i++) admin->handleInput(sendText[i]); + admin->handleInput('\r'); + } + kb->clearOutBuf(); + if (repeater_admin) setCurrScreen(repeater_admin); + else gotoHomeScreen(); } - kb->clearOutBuf(); - setCurrScreen(tw_channel); } } #endif @@ -3227,6 +3284,17 @@ void UITask::openTWatchPicker() { } setCurrScreen(tw_picker); } + +void UITask::openTWatchKeyboard(int purpose, int contextIdx) { + ((TWatchKeyboardScreen*)tw_keyboard)->activateFor( + (TWatchKeyboardScreen::Purpose)purpose, contextIdx); + setCurrScreen(tw_keyboard); + if (_display != NULL && !_display->isOn()) { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; + _next_refresh = 100; +} #endif void UITask::gotoDMTab() { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index cc444aa9..bb751348 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -196,6 +196,7 @@ public: void gotoChannelPickerScreen(); // Navigate to channel picker (bubble/list) #ifdef TWATCH_COMPOSE_ENABLED void openTWatchPicker(); + void openTWatchKeyboard(int purpose, int contextIdx); #endif void gotoDMTab(); // Navigate directly to DM tab on channel screen void gotoDMConversation(const char* contactName, int contactIdx = -1, uint8_t perms = 0); diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 7db9e8e2..36b39333 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -86,6 +86,7 @@ void Dispatcher::loop() { rx_stuck_count = 0; // radio recovered — reset counter } } +#if !defined(MECK_RX_DUTY_CYCLE) if (!is_recv && _ms->getMillis() - radio_nonrx_start > 8000) { // radio has not been in Rx mode for 8 seconds! _err_flags |= ERR_EVENT_STARTRX_TIMEOUT; @@ -105,6 +106,7 @@ void Dispatcher::loop() { cad_busy_start = 0; next_agc_reset_time = futureMillis(getAGCResetInterval()); } +#endif if (outbound) { // waiting for outbound send to be completed if (_radio->isSendComplete()) { @@ -152,10 +154,12 @@ void Dispatcher::loop() { next_agc_reset_time = futureMillis(getAGCResetInterval()); } +#if !defined(MECK_RX_DUTY_CYCLE) if (getAGCResetInterval() > 0 && millisHasNowPassed(next_agc_reset_time)) { _radio->resetAGC(); next_agc_reset_time = futureMillis(getAGCResetInterval()); } +#endif // check inbound (delayed) queue { diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index e3407821..81c28b53 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -1,4 +1,3 @@ - #define RADIOLIB_STATIC_ONLY 1 #include "RadioLibWrappers.h" @@ -63,6 +62,11 @@ void RadioLibWrapper::resetAGC() { } void RadioLibWrapper::loop() { +#if defined(MECK_RX_DUTY_CYCLE) + // Duty-cycle RX sleeps the radio between listen windows, so RSSI floor + // sampling would read garbage. Skip it; the noise floor keeps its default. + return; +#endif if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) { if (!isReceivingPacket()) { int rssi = getCurrentRSSI(); @@ -83,7 +87,14 @@ void RadioLibWrapper::loop() { } void RadioLibWrapper::startRecv() { +#if defined(MECK_RX_DUTY_CYCLE) + // Watch: SX126x hardware RX duty cycling. senderPreambleLength 0 = use the + // configured preamble; minSymbols 0 = library minimum. At preambles too + // short to sleep, RadioLib falls back to continuous startReceive() itself. + int err = ((SX126x*)_radio)->startReceiveDutyCycleAuto(0, 0); +#else int err = _radio->startReceive(); +#endif if (err == RADIOLIB_ERR_NONE) { state = STATE_RX; } else { @@ -114,7 +125,11 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { } if (state != STATE_RX) { +#if defined(MECK_RX_DUTY_CYCLE) + int err = ((SX126x*)_radio)->startReceiveDutyCycleAuto(0, 0); +#else int err = _radio->startReceive(); +#endif if (err == RADIOLIB_ERR_NONE) { state = STATE_RX; } else { @@ -188,4 +203,4 @@ float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) { auto collision_penalty = 1 - (packet_len / 256.0); // Assuming max packet of 256 bytes return max(0.0, min(1.0, success_rate_based_on_snr * collision_penalty)); -} +} \ No newline at end of file diff --git a/src/helpers/ui/LGFXDisplay.cpp b/src/helpers/ui/LGFXDisplay.cpp index 6f10dc59..7b6ac894 100644 --- a/src/helpers/ui/LGFXDisplay.cpp +++ b/src/helpers/ui/LGFXDisplay.cpp @@ -4,7 +4,7 @@ bool LGFXDisplay::begin() { turnOn(); display->init(); display->setRotation(1); - display->setBrightness(54); + display->setBrightness(30); display->setColorDepth(8); display->setTextColor(TFT_WHITE); diff --git a/src/helpers/ui/LGFXDisplay.h b/src/helpers/ui/LGFXDisplay.h index 04ca1835..d2a7caa8 100644 --- a/src/helpers/ui/LGFXDisplay.h +++ b/src/helpers/ui/LGFXDisplay.h @@ -59,6 +59,19 @@ public: // ticker turns wrap off so a long scrolling line is clipped at the screen // edge instead of wrapping onto the rows below. void setTextWrap(bool en) { buffer.setTextWrap(en, false); } + // Draw a string centred horizontally at mid_x with its top at y, in + // FreeSansBold 24pt (used for the clock HH:MM), then restore the default + // GLCD font + size so later text is unaffected. + void printClockFont(int mid_x, int y, const char* str) { + buffer.setFont(&fonts::FreeSansBold24pt7b); + buffer.setTextColor(_color); + buffer.setTextSize(1); + int w = buffer.textWidth(str); + buffer.setCursor(mid_x - w / 2, y); + buffer.print(str); + buffer.setFont(&fonts::Font0); // restore default 6x8 GLCD font + buffer.setTextSize(1); + } #endif void setCursor(int x, int y) override; void print(const char* str) override; diff --git a/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h b/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h index 592fd8db..ade7eceb 100644 --- a/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h +++ b/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h @@ -405,6 +405,17 @@ class TWatchKeyboardScreen : public UIScreen { DisplayDriver* _display; uint8_t _channelIdx; +public: + enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI }; + +private: + Purpose _purpose; + int _contextIdx; // channel idx (channel) or contact idx (DM/admin) + bool _mask; // render composed text as '*' (admin password) + unsigned long _lastCharAt; // when the last char was typed (for reveal window) + + static const unsigned long PW_REVEAL_MS = 2000; // show last char this long before masking + char _outBuf[TW_OUT_BUF_LEN]; uint16_t _outLen; @@ -436,10 +447,10 @@ class TWatchKeyboardScreen : public UIScreen { } void appendChar(char ch) { - if (_outLen < MAXLEN) { _outBuf[_outLen++] = ch; _outBuf[_outLen] = 0; } + if (_outLen < MAXLEN) { _outBuf[_outLen++] = ch; _outBuf[_outLen] = 0; _lastCharAt = millis(); } } void backspace() { - if (_outLen > 0) { _outLen--; _outBuf[_outLen] = 0; } + if (_outLen > 0) { _outLen--; _outBuf[_outLen] = 0; _lastCharAt = 0; } } void cycleMode() { _mode = (_mode == LOWER) ? UPPER : (_mode == UPPER ? SYM : LOWER); @@ -454,7 +465,9 @@ class TWatchKeyboardScreen : public UIScreen { public: TWatchKeyboardScreen(DisplayDriver* display) - : _display(display), _channelIdx(0), _outLen(0), _mode(LOWER), + : _display(display), _channelIdx(0), + _purpose(TWKB_CHANNEL), _contextIdx(0), _mask(false), _lastCharAt(0), + _outLen(0), _mode(LOWER), _touchDown(false), _downX(0), _downY(0), _wantsSend(false), _wantsExit(false) { _outBuf[0] = 0; @@ -464,11 +477,28 @@ public: void activate(uint8_t idx, const char* name) { (void)name; _channelIdx = idx; + _purpose = TWKB_CHANNEL; + _contextIdx = idx; + _mask = false; _outLen = 0; _outBuf[0] = 0; _mode = LOWER; _touchDown = false; _wantsSend = false; _wantsExit = false; } + // Enter text for a non-channel purpose (DM / admin password / admin CLI). + void activateFor(Purpose purpose, int contextIdx) { + _purpose = purpose; + _contextIdx = contextIdx; + _channelIdx = 0; + _mask = (purpose == TWKB_ADMIN_PASSWORD); + _outLen = 0; _outBuf[0] = 0; + _mode = LOWER; + _touchDown = false; _wantsSend = false; _wantsExit = false; + } + + Purpose getPurpose() const { return _purpose; } + int getContextIdx() const { return _contextIdx; } + uint8_t getChannelIdx() const { return _channelIdx; } bool consumeSendRequest(const char** textOut) { if (!_wantsSend) return false; @@ -515,15 +545,26 @@ public: display.setColor(DisplayDriver::LIGHT); { + // Mask the composed text with '*' for the admin password purpose. + char masked[TW_OUT_BUF_LEN]; + const char* src = _outBuf; + if (_mask) { + int m = (_outLen < TW_OUT_BUF_LEN - 1) ? _outLen : TW_OUT_BUF_LEN - 1; + for (int i = 0; i < m; i++) masked[i] = '*'; + // Reveal the most recently typed char briefly so it can be read. + if (m > 0 && (millis() - _lastCharAt < PW_REVEAL_MS)) masked[m - 1] = _outBuf[m - 1]; + masked[m] = 0; + src = masked; + } const int avail = W - 16 - 4; // room after the back arrow, minus cursor int len = _outLen; int fit = 0; for (int n = 1; n <= len; n++) { - if (display.getTextWidth(_outBuf + (len - n)) > avail) break; + if (display.getTextWidth(src + (len - n)) > avail) break; fit = n; } char tail[TW_OUT_BUF_LEN + 2]; - strncpy(tail, _outBuf + (len - fit), sizeof(tail) - 2); + strncpy(tail, src + (len - fit), sizeof(tail) - 2); tail[sizeof(tail) - 2] = 0; size_t tl = strlen(tail); tail[tl] = '_'; tail[tl + 1] = 0; diff --git a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.cpp b/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.cpp index 1a4d71eb..d8d17673 100644 --- a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.cpp +++ b/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.cpp @@ -1,6 +1,12 @@ #include #include "TWatchS3PlusBoard.h" #include +#include // TEMP power-debug: esp_bt_controller_get_status() + +volatile bool TWatchS3PlusBoard::_tilt_flag = false; +volatile uint32_t TWatchS3PlusBoard::_tilt_isr_count = 0; // TEMP diagnostic + +void IRAM_ATTR TWatchS3PlusBoard::onTiltISR() { _tilt_flag = true; _tilt_isr_count++; } void TWatchS3PlusBoard::begin() { ESP32Board::begin(); @@ -13,12 +19,16 @@ void TWatchS3PlusBoard::begin() { _accel->setRemapAxes(SensorRemap::BOTTOM_LAYER_TOP_RIGHT_CORNER); _accel->configAccelerometer(OperationMode::NORMAL, AccelFullScaleRange::FS_2G, 50.0f, AccelBandwidth::OSR2_AVG2, AccelPerfMode::CIC_AVG_MODE); - _accel->enableTiltDetector(true, true); // INT1 pin electrical config: level trigger, active high, push-pull, // output enabled. INT1_IO_CTRL resets to output-disabled, so without // this the pin never drives and INPUT_PULLDOWN reads low forever. _accel->setInterruptPinConfig(InterruptPinMap::PIN1, false, false, true, false); pinMode(PIN_ACCEL_IRQ, INPUT_PULLDOWN); + // Attach the edge ISR BEFORE enabling the tilt source, so the first + // assertion cannot occur before the handler is armed (a missed first edge + // on a self-clearing line otherwise locks tilt-wake out permanently). + attachInterrupt(digitalPinToInterrupt(PIN_ACCEL_IRQ), onTiltISR, RISING); + _accel->enableTiltDetector(true, true); } esp_reset_reason_t reason = esp_reset_reason(); @@ -80,9 +90,30 @@ bool TWatchS3PlusBoard::power_init() { PMU->enableBattVoltageMeasure(); PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S); + + // TEMP power-debug: one-shot rail dump at boot. Rail OFF means the attached + // chip has no power at all (not merely sleeping). + Serial.printf("[PWR] rails: ALDO2(bl)=%d ALDO3(disp/touch)=%d ALDO4(LoRa)=%d BLDO1(GPS)=%d BLDO2(haptic)=%d\n", + PMU->isPowerChannelEnable(XPOWERS_ALDO2), + PMU->isPowerChannelEnable(XPOWERS_ALDO3), + PMU->isPowerChannelEnable(XPOWERS_ALDO4), + PMU->isPowerChannelEnable(XPOWERS_BLDO1), + PMU->isPowerChannelEnable(XPOWERS_BLDO2)); + Serial.printf("[PWR] GPS rail (BLDO1) at boot: %s\n", + PMU->isPowerChannelEnable(XPOWERS_BLDO1) ? "ON" : "OFF (fully powered down)"); return true; } +// TEMP power-debug probe. +void TWatchS3PlusBoard::printPowerDebug() { + if (!PMU) return; + Serial.printf("[PWR] batt=%dmV %d%% vbus=%dmV charging=%d cpu=%dMHz bt=%d gps_rail=%s\n", + PMU->getBattVoltage(), PMU->getBatteryPercent(), + PMU->getVbusVoltage(), PMU->isCharging(), + getCpuFrequencyMhz(), (int)esp_bt_controller_get_status(), + PMU->isPowerChannelEnable(XPOWERS_BLDO1) ? "ON" : "OFF"); +} + void TWatchS3PlusBoard::gpsPowerOn() { if (PMU) { PMU->enablePowerOutput(XPOWERS_BLDO1); @@ -95,9 +126,18 @@ void TWatchS3PlusBoard::gpsPowerOff() { } bool TWatchS3PlusBoard::tiltFired() { - if (digitalRead(PIN_ACCEL_IRQ)) { // only the tilt IRQ is enabled - _accel->update(); // reading the status clears the latched INT + if (_tilt_flag) { // set by the GPIO14 rising-edge ISR + _tilt_flag = false; + _accel->update(); // reading the status clears the sensor INT return true; } + // TEMP diagnostic: periodically report the raw INT pin level and ISR count, + // to distinguish "pin stuck high / never re-arms" from "never asserts". + static unsigned long _tilt_dbg_next = 0; + if (millis() >= _tilt_dbg_next) { + _tilt_dbg_next = millis() + 5000; + Serial.printf("[TILT] pin=%d isr_count=%u\n", + digitalRead(PIN_ACCEL_IRQ), (unsigned)_tilt_isr_count); + } return false; } \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.h b/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.h index 734007f5..04c6123b 100644 --- a/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.h +++ b/variants/lilygo_twatch_s3_plus/TWatchS3PlusBoard.h @@ -17,6 +17,9 @@ class SensorBMA423; // full include kept in the .cpp to avoid a BLE-build heade class TWatchS3PlusBoard : public ESP32Board { XPowersLibInterface* PMU = NULL; SensorBMA423* _accel = nullptr; + static volatile bool _tilt_flag; + static volatile uint32_t _tilt_isr_count; // TEMP diagnostic + static void IRAM_ATTR onTiltISR(); // defined in the .cpp (IRAM relocation) bool power_init(); @@ -50,11 +53,19 @@ public: return PMU ? PMU->getBattVoltage() : 0; } + uint8_t getBatteryPercent() override { + return PMU ? PMU->getBatteryPercent() : 0; + } + // GPS power is the AXP2101 BLDO1 rail. Off at boot; toggled at runtime via // the gps_enabled pref (boot) and the "gps on/off" CLI command (live). void gpsPowerOn(); void gpsPowerOff(); + // TEMP power-debug probe: battery, VBUS, CPU clock, BT controller state and + // live rail states (incl. GPS/BLDO1). Called periodically from UITask::loop. + void printPowerDebug(); + const char* getManufacturerName() const override { return "LilyGo T-Watch S3 Plus"; } diff --git a/variants/lilygo_twatch_s3_plus/platformio.ini b/variants/lilygo_twatch_s3_plus/platformio.ini index 640bbf20..ab07e2be 100644 --- a/variants/lilygo_twatch_s3_plus/platformio.ini +++ b/variants/lilygo_twatch_s3_plus/platformio.ini @@ -12,6 +12,7 @@ build_flags = -I variants/lilygo_twatch_s3_plus -D LILYGO_TWATCH_S3_PLUS -D TWATCH_COMPOSE_ENABLED + -D MECK_RX_DUTY_CYCLE -D BOARD_HAS_PSRAM=1 -D CORE_DEBUG_LEVEL=1 -D FORMAT_SPIFFS_IF_FAILED=1 diff --git a/variants/lilygo_twatch_s3_plus/target.cpp b/variants/lilygo_twatch_s3_plus/target.cpp index 45fcd591..abe3f039 100644 --- a/variants/lilygo_twatch_s3_plus/target.cpp +++ b/variants/lilygo_twatch_s3_plus/target.cpp @@ -51,7 +51,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { // Longer preamble for low SF improves reliability -- each symbol is shorter // at low SF, so more symbols are needed for reliable detection. - uint16_t preamble = (sf <= 8) ? 32 : 16; + uint16_t preamble = (sf <= 9) ? 32 : 16; radio.setPreambleLength(preamble); }