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.
This commit is contained in:
Pelgraine committed 2026-07-07 21:58:32 +10:00
1 parent d4286fdf84
commit 973d02df60
16 files changed
+308 -29

No files matched your search

+39
View File
@@ -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
}
}
@@ -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) {
@@ -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);
@@ -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) {
@@ -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);
}
+84 -16
View File
@@ -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() {
+1
View File
@@ -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);
+4
View File
@@ -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
{
+17 -2
View File
@@ -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));
}
}
+1 -1
View File
@@ -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);
+13
View File
@@ -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;
@@ -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;
@@ -1,6 +1,12 @@
#include <Arduino.h>
#include "TWatchS3PlusBoard.h"
#include <SensorBMA423.hpp>
#include <esp_bt.h> // 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;
}
@@ -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";
}
@@ -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
+1 -1
View File
@@ -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);
}