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); }