initial builds, standalone and ble twatch s3 plus

This commit is contained in:
pelgraine
2026-06-26 19:28:24 +10:00
parent 7005bd065e
commit b80184cf96
18 changed files with 1007 additions and 18 deletions
+41
View File
@@ -0,0 +1,41 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "LilyGo T-Watch S3 Plus (16M Flash 8M PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"speed": 921600
},
"url": "https://www.lilygo.cc/products/t-watch-s3-plus",
"vendor": "LilyGo"
}
+4
View File
@@ -2426,6 +2426,10 @@ void MyMesh::handleCmdFrame(size_t len) {
if (strcmp(sp, "gps") == 0) {
_prefs.gps_enabled = (np[0] == '1') ? 1 : 0;
savePrefs();
#if defined(LILYGO_TWATCH_S3_PLUS)
// Apply the BLDO1 GPS rail live so the toggle takes effect now, no reboot
if (_prefs.gps_enabled) board.gpsPowerOn(); else board.gpsPowerOff();
#endif
} else if (strcmp(sp, "gps_interval") == 0) {
uint32_t interval_seconds = atoi(np);
_prefs.gps_interval = constrain(interval_seconds, 0, 86400);
+79 -3
View File
@@ -709,10 +709,32 @@
// =============================================================================
// Define MECK_TOUCH_ENABLED for any platform with touch support
#if defined(LilyGo_T5S3_EPaper_Pro) || (defined(LilyGo_TDeck_Pro) && defined(HAS_TOUCHSCREEN))
#if defined(LilyGo_T5S3_EPaper_Pro) || (defined(LilyGo_TDeck_Pro) && defined(HAS_TOUCHSCREEN)) || defined(LILYGO_TWATCH_S3_PLUS)
#define MECK_TOUCH_ENABLED 1
#endif
// --- T-Watch S3 Plus: screen headers for the touch UI ---
// The watch needs the same concrete screen types as the gesture machine casts
// to, but none of the T5S3/T-Deck hardware baggage (GT911, SD, TCA8418 keyboard).
#if defined(LILYGO_TWATCH_S3_PLUS)
#include "TextReaderScreen.h"
#include "NotesScreen.h"
#include "ContactsScreen.h"
#include "ChannelScreen.h"
#include "ChannelPickerScreen.h"
#include "MeckExport.h"
#include "MeckImport.h"
#include "SettingsScreen.h"
#include "RepeaterAdminScreen.h"
#include "DiscoveryScreen.h"
#include "LastHeardScreen.h"
#include "PathEditorScreen.h"
#include "Tracescreen.h"
#include "GamesMenuScreen.h"
#include "SnakeScreen.h"
#include "MinesweeperScreen.h"
#endif
// --- T5S3: GT911 capacitive touch driver ---
#if defined(LilyGo_T5S3_EPaper_Pro)
#include "TouchDrvGT911.hpp"
@@ -879,6 +901,8 @@
#define TOUCH_LONG_PRESS_MS 750
#if defined(LilyGo_T5S3_EPaper_Pro)
#define TOUCH_SWIPE_THRESHOLD 60 // T5S3: 960×540 — 60px ≈ 6% of width
#elif defined(LILYGO_TWATCH_S3_PLUS)
#define TOUCH_SWIPE_THRESHOLD 16 // Watch: getTouch() returns 0..119 (240px/UI_ZOOM); 16 is ~13% of width
#else
#define TOUCH_SWIPE_THRESHOLD 30 // T-Deck Pro: 240×320 — 30px ≈ 12.5% of width
#endif
@@ -915,6 +939,18 @@
}
#elif defined(LilyGo_TDeck_Pro)
return touchInput.getPoint(*outX, *outY);
#elif defined(LILYGO_TWATCH_S3_PLUS)
{
// FT6336U is read through the LovyanGFX panel backing the display.
// display.getTouch() returns coordinates already divided by UI_ZOOM.
int tx, ty;
if (display.getTouch(&tx, &ty)) {
*outX = (int16_t)tx;
*outY = (int16_t)ty;
return true;
}
return false;
}
#else
return false;
#endif
@@ -928,6 +964,11 @@
#elif defined(LilyGo_TDeck_Pro)
float sx = (float)EINK_WIDTH / 128.0f; // 240/128 = 1.875
float sy = (float)EINK_HEIGHT / 128.0f; // 320/128 = 2.5
#elif defined(LILYGO_TWATCH_S3_PLUS)
// display.getTouch() already divides by UI_ZOOM, so px/py span the
// 240/UI_ZOOM logical canvas (0..119 at UI_ZOOM=2). Scale to 128 virtual.
float sx = (240.0f / UI_ZOOM) / 128.0f;
float sy = (240.0f / UI_ZOOM) / 128.0f;
#endif
vx = (int)(px / sx);
#if defined(LilyGo_TDeck_Pro)
@@ -1081,7 +1122,7 @@ static uint32_t _atoi(const char* sp) {
/* GLOBAL OBJECTS */
#ifdef DISPLAY_CLASS
#include "UITask.h"
#if HAS_GPS && !defined(LILYGO_TECHO_CARD)
#if HAS_GPS && !defined(LILYGO_TECHO_CARD) && !defined(LILYGO_TWATCH_S3_PLUS)
#include "MapScreen.h" // After BLE -- PNGdec headers conflict with BLE if included earlier
#endif
UITask ui_task(&board, &serial_interface);
@@ -1683,6 +1724,35 @@ static void lastHeardToggleContact() {
return 'q';
}
#if defined(LILYGO_TWATCH_S3_PLUS)
// Watch FIRST page: long-press on a coloured tile opens its screen.
// The grid renders in the logical (display.width()) space; touchToVirtual
// yields 128-space coords, so scale them back to render space to hit-test.
if (ui_task.isOnHomeScreen() && ui_task.isHomeShowingTiles()) {
int vx, vy;
touchToVirtual(x, y, vx, vy);
const int W = display.width(); // logical width (120)
int rvx = vx * W / 128;
int rvy = vy * W / 128;
const int cols = 2, rows = 3;
const int tileW = 56, tileH = 24, gapX = 4, gapY = 3;
const int gridW = tileW * cols + gapX * (cols - 1); // 116
const int gridX = (W - gridW) / 2; // 2
int gridY = ui_task.getTileGridVY();
if (rvx >= gridX && rvx < gridX + gridW &&
rvy >= gridY && rvy < gridY + rows * (tileH + gapY)) {
int col = (rvx - gridX) / (tileW + gapX); if (col > 1) col = 1;
int row = (rvy - gridY) / (tileH + gapY); if (row > 2) row = 2;
if (row == 0 && col == 0) { ui_task.gotoChannelPickerScreen(); return 0; }
if (row == 0 && col == 1) { ui_task.gotoContactsScreen(); return 0; }
if (row == 1 && col == 0) { ui_task.gotoSettingsScreen(); return 0; }
if (row == 1 && col == 1) { ui_task.gotoDiscoveryScreen(); return 0; }
if (row == 2 && col == 0) { ui_task.gotoTraceScreen(); return 0; }
// row 2 col 1 = Maps -- TODO subscreen not yet built; no-op for now.
}
return 0; // consume long-press on the tile page
}
#endif
// Home screen: long press = activate current page action
// (BLE toggle, send advert, hibernate, GPS toggle, etc.)
if (ui_task.isOnHomeScreen()) {
@@ -2587,6 +2657,9 @@ void setup() {
#ifdef PIN_GPS_EN
digitalWrite(PIN_GPS_EN, GPS_EN_ACTIVE);
#endif
#if defined(LILYGO_TWATCH_S3_PLUS)
board.gpsPowerOn(); // GPS power is the AXP2101 BLDO1 rail
#endif
#if defined(LilyGo_TDeck_Pro_Max)
// Speed up / improve the MAX GPS fix: enable multi-constellation
// (GPS + GLONASS + BeiDou) and EASY predicted ephemeris (warm/hot starts
@@ -2604,6 +2677,9 @@ void setup() {
#if defined(LilyGo_TDeck_Pro_Max)
board.gpsPowerOff(); // MAX: GPS power is XL9555-routed, not PIN_GPS_EN
#endif
#if defined(LILYGO_TWATCH_S3_PLUS)
board.gpsPowerOff(); // GPS power is the AXP2101 BLDO1 rail
#endif
sensors.setSettingValue("gps", "0");
}
Serial.printf("GPS: power %s\n", gps_wanted ? "ON" : "OFF");
@@ -2779,7 +2855,7 @@ void loop() {
// Map screen: periodically update own GPS position and contact markers
#ifdef DISPLAY_CLASS
#if HAS_GPS && !defined(LILYGO_TECHO_CARD)
#if HAS_GPS && !defined(LILYGO_TECHO_CARD) && !defined(LILYGO_TWATCH_S3_PLUS)
if (ui_task.isOnMapScreen()) {
static unsigned long lastMapUpdate = 0;
if (millis() - lastMapUpdate > 30000) { // Every 30 seconds
@@ -54,6 +54,12 @@ static const uint8_t icon_alarm[] PROGMEM = {
0x40,0x40, 0x20,0x80, 0x1F,0x00, 0x00,0x00, 0x20,0x40, 0x40,0x20,
};
// Mountain (Maps)
static const uint8_t icon_map[] PROGMEM = {
0x00,0x00, 0x00,0x00, 0x00,0x00, 0x08,0x00, 0x14,0x00, 0x22,0x40,
0x41,0xA0, 0x42,0x50, 0x84,0x30, 0xFF,0xF0, 0x00,0x00, 0x00,0x00,
};
// ➡ Right arrow (Trace Route)
static const uint8_t icon_trace[] PROGMEM = {
0x00,0x00, 0x00,0x00, 0x00,0x80, 0x00,0xC0, 0x00,0xE0, 0xFF,0xF0,
+82 -6
View File
@@ -16,14 +16,14 @@
#ifdef MECK_WEB_READER
#include "WebReaderScreen.h"
#endif
#if HAS_GPS && !defined(LILYGO_TECHO_CARD)
#if HAS_GPS && !defined(LILYGO_TECHO_CARD) && !defined(LILYGO_TWATCH_S3_PLUS)
#include "MapScreen.h"
#endif
#include "target.h"
#if defined(LilyGo_TDeck_Pro_Max)
#include "DRV2605Haptic.h" // haptic motor for "Buzzer (vibrate)" channels
#endif
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_AUDIO_VARIANT)
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_AUDIO_VARIANT) || defined(LILYGO_TWATCH_S3_PLUS)
#include "HomeIcons.h"
#endif
#if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION)
@@ -131,7 +131,11 @@ public:
// version info
display.setColor(DisplayDriver::LIGHT);
#if defined(LILYGO_TWATCH_S3_PLUS)
display.setTextSize(1); // 240x240: size 2 overflows the 120px virtual width and wraps
#else
display.setTextSize(2);
#endif
display.drawTextCentered(display.width()/2, 22, _version_info);
display.setTextSize(1);
@@ -402,11 +406,29 @@ public:
#define HOME_HDR_Y 1
#elif defined(LILYGO_TECHO_LITE)
#define HOME_HDR_Y 0
#elif defined(LILYGO_TWATCH_S3_PLUS)
#define HOME_HDR_Y 1
#else
#define HOME_HDR_Y -3
#endif
display.setCursor(0, HOME_HDR_Y);
#if defined(LILYGO_TWATCH_S3_PLUS)
// Watch: render the name in a very small font so long names fit beside the
// centred clock instead of overrunning it. Colour was set above (GREEN).
((LGFXDisplay*)&display)->printSmallFont(0, HOME_HDR_Y, filtered_name);
#else
display.print(filtered_name);
#endif
#if defined(LILYGO_TWATCH_S3_PLUS)
// P4-style compact MSG count, stacked under the node name (top-left).
{
char msgbuf[16];
sprintf(msgbuf, "MSG: %d", _task->getUnreadMsgCount());
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, HOME_HDR_Y + 9);
display.print(msgbuf);
}
#endif
// battery voltage + status icons
#ifdef MECK_AUDIO_VARIANT
@@ -453,6 +475,8 @@ public:
int y = 13; // Below header
#elif defined(LilyGo_T5S3_EPaper_Pro)
int y = 14; // Closer to header
#elif defined(LILYGO_TWATCH_S3_PLUS)
int y = 18; // below the stacked name + MSG header
#else
int y = 14;
#endif
@@ -466,7 +490,7 @@ public:
}
if (_page == HomePage::FIRST) {
#if defined(LilyGo_T5S3_EPaper_Pro)
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LILYGO_TWATCH_S3_PLUS)
_task->setHomeShowingTiles(true);
#endif
#if defined(LilyGo_T5S3_EPaper_Pro)
@@ -477,13 +501,17 @@ public:
#endif
#elif defined(LILYGO_TECHO_LITE)
int y = 18; // Below page dots
#elif defined(LILYGO_TWATCH_S3_PLUS)
int y = 21; // tiles start below header (MSG shown small in header)
#else
int y = 20;
#endif
#if !defined(LILYGO_TWATCH_S3_PLUS)
display.setColor(DisplayDriver::YELLOW);
display.setTextSize(2);
sprintf(tmp, "MSG: %d", _task->getUnreadMsgCount());
display.drawTextCentered(display.width() / 2, y, tmp);
#endif
#if defined(LILYGO_TECHO_LITE)
y += 12; // Compact
#elif defined(LilyGo_TDeck_Pro_Max)
@@ -600,6 +628,47 @@ public:
}
display.setTextSize(1);
#elif defined(LILYGO_TWATCH_S3_PLUS)
// ----- T-Watch S3 Plus: P4-style coloured tile grid (3x2) -----
// Border colours approximate the Meck P4 home palette (RGB565): a white
// icon + label on a dark navy fill, each tile a distinct bright border.
// setRawColor() pushes exact RGB565 past the Color enum + watch grey remap.
{
struct Tile { const uint8_t* icon; const char* label; uint16_t color; };
const Tile tiles[3][2] = {
{ {icon_envelope, "Messages", 0x0CB1}, {icon_people, "Contacts", 0xCAA0} },
{ {icon_gear, "Settings", 0x0560}, {icon_search, "Discover", 0xF81F} },
{ {icon_trace, "Trace", 0xF800}, {icon_map, "Maps", 0x231D} },
};
const uint16_t TILE_FILL = 0x18C5; // dark navy
const int cols = 2, rows = 3;
const int tileW = 56, tileH = 24, gapX = 4, gapY = 3;
const int gridW = tileW * cols + gapX * (cols - 1);
const int gridX = (display.width() - gridW) / 2;
const int gridY = y + 2;
_task->setTileGridVY(gridY);
LGFXDisplay* lcd = (LGFXDisplay*)&display; // watch display is always LGFXDisplay
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
int tx = gridX + col * (tileW + gapX);
int ty = gridY + row * (tileH + gapY);
lcd->setRawColor(TILE_FILL);
lcd->fillRoundRect(tx, ty, tileW, tileH, 4);
lcd->setRawColor(tiles[row][col].color);
lcd->drawRoundRect(tx, ty, tileW, tileH, 4);
display.setColor(DisplayDriver::LIGHT);
int iconX = tx + (tileW - HOME_ICON_W) / 2;
int iconY = ty + 3;
display.drawXbm(iconX, iconY, tiles[row][col].icon, HOME_ICON_W, HOME_ICON_H);
display.setTextSize(_node_prefs->smallTextSize());
display.drawTextCentered(tx + tileW / 2, ty + 16, tiles[row][col].label);
}
}
display.setTextSize(1);
}
#else
// Non-T5S3: keyboard shortcut menu
#if defined(LILYGO_TECHO_LITE)
@@ -786,6 +855,9 @@ public:
#if defined(LilyGo_T5S3_EPaper_Pro)
display.drawTextCentered(display.width() / 2, display.height() - 24,
"Tap here for full Last Heard list");
#elif defined(LILYGO_TWATCH_S3_PLUS)
display.drawTextCentered(display.width() / 2, display.height() - 24,
"Long Press: Full Last Heard List");
#else
display.drawTextCentered(display.width() / 2, display.height() - 24,
"H: Full Last Heard list");
@@ -844,6 +916,8 @@ public:
display.setTextSize(1);
#if defined(LilyGo_T5S3_EPaper_Pro)
display.drawTextCentered(display.width() / 2, 80, "toggle: " PRESS_LABEL);
#elif defined(LILYGO_TWATCH_S3_PLUS)
display.drawTextCentered(display.width() / 2, 68, "toggle: " PRESS_LABEL);
#else
display.drawTextCentered(display.width() / 2, 68, "toggle: " PRESS_LABEL);
display.drawTextCentered(display.width() / 2, 78, "or press Enter key");
@@ -897,6 +971,8 @@ public:
#endif
#if defined(LilyGo_T5S3_EPaper_Pro)
display.drawTextCentered(display.width() / 2, 64, "advert: " PRESS_LABEL);
#elif defined(LILYGO_TWATCH_S3_PLUS)
display.drawTextCentered(display.width() / 2, 57, "advert: " PRESS_LABEL);
#else
display.drawTextCentered(display.width() / 2, 57, "advert: " PRESS_LABEL);
display.drawTextCentered(display.width() / 2, 67, "or press Enter key");
@@ -1478,7 +1554,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#ifdef HAS_4G_MODEM
sms_screen = new SMSScreen(this, node_prefs);
#endif
#if HAS_GPS && !defined(LILYGO_TECHO_CARD)
#if HAS_GPS && !defined(LILYGO_TECHO_CARD) && !defined(LILYGO_TWATCH_S3_PLUS)
map_screen = new MapScreen(this);
#else
map_screen = nullptr;
@@ -3326,8 +3402,8 @@ void UITask::gotoWebReader() {
#if HAS_GPS
void UITask::gotoMapScreen() {
if (!map_screen) return; // Not available on this platform (T-Echo Card)
#if !defined(LILYGO_TECHO_CARD)
if (!map_screen) return; // Not available on this platform (T-Echo Card, T-Watch)
#if !defined(LILYGO_TECHO_CARD) && !defined(LILYGO_TWATCH_S3_PLUS)
MapScreen* map = (MapScreen*)map_screen;
if (_display != NULL) {
map->enter(*_display);
+4 -4
View File
@@ -34,7 +34,7 @@
#include "AlarmScreen.h"
#endif
#if defined(LilyGo_T5S3_EPaper_Pro)
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LILYGO_TWATCH_S3_PLUS)
#include "VirtualKeyboard.h"
#endif
@@ -112,7 +112,7 @@ class UITask : public AbstractUITask {
UIScreen* curr;
bool _homeShowingTiles = false; // Set by HomeScreen render when tile grid is visible
int _tileGridVY = 44; // Virtual Y of tile grid top (updated each render)
#if defined(LilyGo_T5S3_EPaper_Pro)
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LILYGO_TWATCH_S3_PLUS)
UIScreen* lock_screen; // Lock screen (big clock + battery + unread)
UIScreen* _screenBeforeLock = nullptr;
bool _locked = false;
@@ -274,12 +274,12 @@ public:
bool isOnSnakeScreen() const { return curr == snake_screen; }
bool isOnMinesweeperScreen() const { return curr == minesweeper_screen; }
bool isOnMapScreen() const { return curr == map_screen; }
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro)
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) || defined(LILYGO_TWATCH_S3_PLUS)
bool isLocked() const { return _locked; }
void lockScreen();
void unlockScreen();
#endif
#if defined(LilyGo_T5S3_EPaper_Pro)
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LILYGO_TWATCH_S3_PLUS)
bool isVKBActive() const { return _vkbActive; }
unsigned long vkbOpenedAt() const { return _vkbOpenedAt; }
VirtualKeyboard& getVKB() { return _vkb; }
@@ -12,7 +12,7 @@
// if (keyboard.status() == VKB_SUBMITTED) { ... keyboard.getText() ... }
// =============================================================================
#if defined(LilyGo_T5S3_EPaper_Pro)
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LILYGO_TWATCH_S3_PLUS)
#ifndef VIRTUAL_KEYBOARD_H
#define VIRTUAL_KEYBOARD_H
@@ -536,4 +536,4 @@ private:
};
#endif // VIRTUAL_KEYBOARD_H
#endif // LilyGo_T5S3_EPaper_Pro
#endif // LilyGo_T5S3_EPaper_Pro || LILYGO_TWATCH_S3_PLUS
+14 -2
View File
@@ -56,16 +56,28 @@ void LGFXDisplay::setColor(Color c) {
_color = TFT_WHITE;
break;
case RED:
#if defined(LILYGO_TWATCH_S3_PLUS)
_color = 0x7BEF; // dark grey -- the watch UI uses a grey/white/black palette
#else
_color = TFT_RED;
#endif
break;
case GREEN:
#if defined(LILYGO_TWATCH_S3_PLUS)
_color = 0xC618; // light grey
#else
_color = TFT_GREEN;
#endif
break;
case BLUE:
_color = TFT_BLUE;
break;
case YELLOW:
#if defined(LILYGO_TWATCH_S3_PLUS)
_color = TFT_WHITE;
#else
_color = TFT_YELLOW;
#endif
break;
case ORANGE:
_color = TFT_ORANGE;
@@ -113,7 +125,7 @@ void LGFXDisplay::endFrame() {
bool LGFXDisplay::getTouch(int *x, int *y) {
lgfx::v1::touch_point_t point;
display->getTouch(&point);
if (!display->getTouch(&point)) return false;
if (UI_ZOOM != 1) {
*x = point.x / UI_ZOOM;
*y = point.y / UI_ZOOM;
@@ -121,5 +133,5 @@ bool LGFXDisplay::getTouch(int *x, int *y) {
*x = point.x;
*y = point.y;
}
return (*x >= 0) && (*y >= 0);
return true;
}
+21 -1
View File
@@ -28,6 +28,26 @@ public:
void startFrame(Color bkg = DARK) override;
void setTextSize(int sz) override;
void setColor(Color c) override;
#if defined(LILYGO_TWATCH_S3_PLUS)
// Set an exact RGB565 colour, bypassing the Color enum + watch grey remap.
// Used by the watch P4-style tile grid for per-tile border/fill colours.
void setRawColor(uint16_t c) { _color = c; }
// Rounded-rect helpers for the P4-style tile grid (LovyanGFX buffer).
void fillRoundRect(int x, int y, int w, int h, int r) { buffer.fillRoundRect(x, y, w, h, r, _color); }
void drawRoundRect(int x, int y, int w, int h, int r) { buffer.drawRoundRect(x, y, w, h, r, _color); }
// Render a string in a very small font at (x,y) so long node names fit
// beside the centred clock, then restore the default GLCD font + size so
// later text is unaffected. Uses the colour set via setColor()/setRawColor().
void printSmallFont(int x, int y, const char* str) {
buffer.setFont(&fonts::TomThumb);
buffer.setTextColor(_color);
buffer.setTextSize(1);
buffer.setCursor(x, 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;
void fillRect(int x, int y, int w, int h) override;
@@ -36,4 +56,4 @@ public:
uint16_t getTextWidth(const char* str) override;
void endFrame() override;
virtual bool getTouch(int *x, int *y);
};
};
@@ -0,0 +1,113 @@
#pragma once
#include <Arduino.h>
// CPU Frequency Scaling for ESP32-S3
//
// Typical current draw (CPU only, rough):
// 240 MHz ~70-80 mA
// 160 MHz ~50-60 mA
// 80 MHz ~30-40 mA
// 40 MHz ~15-20 mA (low-power / lock screen mode)
//
// SPI peripherals and UART use their own clock dividers from the APB clock,
// so LoRa, e-ink, and GPS serial all work fine at 80MHz and 40MHz.
#ifdef ESP32
#ifndef CPU_FREQ_IDLE
#define CPU_FREQ_IDLE 80 // MHz — normal mesh listening
#endif
#ifndef CPU_FREQ_BOOST
#define CPU_FREQ_BOOST 240 // MHz — heavy processing
#endif
#ifndef CPU_FREQ_LOW_POWER
#define CPU_FREQ_LOW_POWER 80 // MHz — lock screen / idle standby (40 MHz breaks I2C)
#endif
#ifndef CPU_BOOST_TIMEOUT_MS
#define CPU_BOOST_TIMEOUT_MS 10000 // 10 seconds
#endif
class CPUPowerManager {
public:
CPUPowerManager() : _boosted(false), _lowPower(false), _boost_started(0) {}
void begin() {
setCpuFrequencyMhz(CPU_FREQ_IDLE);
_boosted = false;
_lowPower = false;
MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE);
}
void loop() {
if (_boosted && (millis() - _boost_started >= CPU_BOOST_TIMEOUT_MS)) {
// Return to low-power if locked, otherwise normal idle
if (_lowPower) {
setCpuFrequencyMhz(CPU_FREQ_LOW_POWER);
MESH_DEBUG_PRINTLN("CPU power: boost expired, returning to low-power %d MHz", CPU_FREQ_LOW_POWER);
} else {
setCpuFrequencyMhz(CPU_FREQ_IDLE);
MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE);
}
_boosted = false;
}
}
void setBoost() {
if (!_boosted) {
setCpuFrequencyMhz(CPU_FREQ_BOOST);
_boosted = true;
MESH_DEBUG_PRINTLN("CPU power: boosted to %d MHz", CPU_FREQ_BOOST);
}
_boost_started = millis();
}
void setIdle() {
if (_boosted) {
setCpuFrequencyMhz(CPU_FREQ_IDLE);
_boosted = false;
MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE);
}
if (_lowPower) {
_lowPower = false;
}
}
// Low-power mode — drops CPU to 40 MHz for lock screen standby.
// If currently boosted, the boost timeout will return to 40 MHz
// instead of 80 MHz.
void setLowPower() {
_lowPower = true;
if (!_boosted) {
setCpuFrequencyMhz(CPU_FREQ_LOW_POWER);
MESH_DEBUG_PRINTLN("CPU power: low-power at %d MHz", CPU_FREQ_LOW_POWER);
}
// If boosted, the loop() timeout will drop to low-power instead of idle
}
// Exit low-power mode — returns to normal idle (80 MHz).
// If currently boosted, the boost timeout will return to idle
// instead of low-power.
void clearLowPower() {
_lowPower = false;
if (!_boosted) {
setCpuFrequencyMhz(CPU_FREQ_IDLE);
MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz (low-power cleared)", CPU_FREQ_IDLE);
}
// If boosted, the loop() timeout will drop to idle as normal
}
bool isBoosted() const { return _boosted; }
bool isLowPower() const { return _lowPower; }
uint32_t getFrequencyMHz() const { return getCpuFrequencyMhz(); }
private:
bool _boosted;
bool _lowPower;
unsigned long _boost_started;
};
#endif // ESP32
@@ -0,0 +1,72 @@
#pragma once
#include <Arduino.h>
// Transparent Stream wrapper that counts NMEA sentences (newline-delimited)
// flowing from the GPS serial port to the MicroNMEA parser.
//
// Usage: Instead of MicroNMEALocationProvider gps(Serial2, &rtc_clock);
// Use: GPSStreamCounter gpsStream(Serial2);
// MicroNMEALocationProvider gps(gpsStream, &rtc_clock);
//
// Every read() call passes through to the underlying stream; when a '\n'
// is seen the sentence counter increments. This lets the UI display a
// live "nmea" count so users can confirm the baud rate is correct and
// the GPS module is actually sending data.
class GPSStreamCounter : public Stream {
public:
GPSStreamCounter(Stream& inner)
: _inner(inner), _sentences(0), _sentences_snapshot(0),
_last_snapshot(0), _sentences_per_sec(0) {}
// --- Stream read interface (passes through) ---
int available() override { return _inner.available(); }
int peek() override { return _inner.peek(); }
int read() override {
int c = _inner.read();
if (c == '\n') {
_sentences++;
}
return c;
}
// --- Stream write interface (pass through for NMEA commands if needed) ---
size_t write(uint8_t b) override { return _inner.write(b); }
// --- Sentence counting API ---
// Total sentences received since boot (or last reset)
uint32_t getSentenceCount() const { return _sentences; }
// Sentences received per second (updated each time you call it,
// with a 1-second rolling window)
uint16_t getSentencesPerSec() {
unsigned long now = millis();
unsigned long elapsed = now - _last_snapshot;
if (elapsed >= 1000) {
uint32_t delta = _sentences - _sentences_snapshot;
// Scale to per-second if interval wasn't exactly 1000ms
_sentences_per_sec = (uint16_t)((delta * 1000UL) / elapsed);
_sentences_snapshot = _sentences;
_last_snapshot = now;
}
return _sentences_per_sec;
}
// Reset all counters (e.g. when GPS hardware power cycles)
void resetCounters() {
_sentences = 0;
_sentences_snapshot = 0;
_sentences_per_sec = 0;
_last_snapshot = millis();
}
private:
Stream& _inner;
volatile uint32_t _sentences;
uint32_t _sentences_snapshot;
unsigned long _last_snapshot;
uint16_t _sentences_per_sec;
};
@@ -0,0 +1,79 @@
#include <Arduino.h>
#include "TWatchS3PlusBoard.h"
void TWatchS3PlusBoard::begin() {
ESP32Board::begin();
power_init();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) {
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
bool TWatchS3PlusBoard::power_init() {
PMU = new XPowersAXP2101(Wire, PIN_BOARD_SDA, PIN_BOARD_SCL, I2C_ADDR_PMU);
if (!PMU->init()) {
MESH_DEBUG_PRINTLN("Warning: Failed to find AXP2101 power management");
delete PMU;
PMU = NULL;
return false;
}
PMU->setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG);
// Power rails per the T-Watch S3 Plus PowerManage table:
// ALDO2 = display backlight, ALDO3 = display + touch,
// ALDO4 = LoRa, BLDO1 = GNSS, BLDO2 = DRV2605, ALDO1 = unused.
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); // LoRa radio
PMU->enablePowerOutput(XPOWERS_ALDO4);
PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); // display + touch
PMU->enablePowerOutput(XPOWERS_ALDO3);
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); // display backlight
PMU->enablePowerOutput(XPOWERS_ALDO2);
PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); // DRV2605 haptic
PMU->enablePowerOutput(XPOWERS_BLDO2);
// GNSS (MIA-M10Q) on BLDO1 -- set the rail voltage but leave it OFF at boot.
// It is powered on demand via gpsPowerOn() when gps_enabled is set.
PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
PMU->disablePowerOutput(XPOWERS_BLDO1);
PMU->disablePowerOutput(XPOWERS_DCDC2);
PMU->disablePowerOutput(XPOWERS_DCDC3);
PMU->disablePowerOutput(XPOWERS_DCDC4);
PMU->disablePowerOutput(XPOWERS_DCDC5);
PMU->disablePowerOutput(XPOWERS_ALDO1); // unused on the Plus
PMU->disablePowerOutput(XPOWERS_DLDO1);
PMU->disablePowerOutput(XPOWERS_DLDO2);
PMU->disablePowerOutput(XPOWERS_VBACKUP);
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
PMU->clearIrqStatus();
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_125MA);
PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
PMU->disableTSPinMeasure();
PMU->enableSystemVoltageMeasure();
PMU->enableVbusVoltageMeasure();
PMU->enableBattVoltageMeasure();
PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S);
return true;
}
void TWatchS3PlusBoard::gpsPowerOn() {
if (PMU) {
PMU->enablePowerOutput(XPOWERS_BLDO1);
delay(100); // allow the module to boot before we expect NMEA
}
}
void TWatchS3PlusBoard::gpsPowerOff() {
if (PMU) PMU->disablePowerOutput(XPOWERS_BLDO1);
}
@@ -0,0 +1,55 @@
#pragma once
#include "variant.h" // Board-specific pin definitions (I2C, user btn, addresses)
#include <Wire.h>
#include <Arduino.h>
#include "XPowersLib.h"
#include "helpers/ESP32Board.h"
#include <driver/rtc_io.h>
// LilyGo T-Watch S3 Plus board.
//
// Power is managed by an AXP2101 PMU on the main I2C bus. The PMU power rails
// (per the T-Watch S3 Plus PowerManage table) are brought up in power_init().
class TWatchS3PlusBoard : public ESP32Board {
XPowersLibInterface* PMU = NULL;
bool power_init();
public:
void begin();
void enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH);
} else {
esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1) | (1ULL << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH);
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000ULL);
}
esp_deep_sleep_start();
}
uint16_t getBattMilliVolts() override {
return PMU ? PMU->getBattVoltage() : 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();
const char* getManufacturerName() const override {
return "LilyGo T-Watch S3 Plus";
}
};
@@ -0,0 +1,96 @@
#pragma once
// LovyanGFX display + touch for the LilyGo T-Watch S3 Plus.
// ST7789V3 240x240 on SPI3 + PWM backlight (GPIO45) + FT6336U capacitive touch
// on a separate I2C bus (Wire1: SDA 39 / SCL 40, INT 16, no RST pin).
//
// LGFXDisplay.h pulls in LovyanGFX (it defines LGFX_USE_V1 and includes
// LovyanGFX.hpp), so we do not redefine those here.
#include <helpers/ui/LGFXDisplay.h>
class LGFX_TWatchS3Plus : public lgfx::LGFX_Device {
lgfx::Panel_ST7789 _panel_instance;
lgfx::Bus_SPI _bus_instance;
lgfx::Light_PWM _light_instance;
lgfx::Touch_FT5x06 _touch_instance;
public:
LGFX_TWatchS3Plus(void) {
{
auto cfg = _bus_instance.config();
cfg.spi_host = SPI3_HOST;
cfg.spi_mode = 0;
cfg.freq_write = 40000000;
cfg.freq_read = 16000000;
cfg.spi_3wire = true;
cfg.use_lock = true;
cfg.dma_channel = SPI_DMA_CH_AUTO;
cfg.pin_sclk = 18;
cfg.pin_mosi = 13;
cfg.pin_miso = -1;
cfg.pin_dc = 38;
_bus_instance.config(cfg);
_panel_instance.setBus(&_bus_instance);
}
{
auto cfg = _panel_instance.config();
cfg.pin_cs = 12;
cfg.pin_rst = -1;
cfg.pin_busy = -1;
// ST7789 GRAM is 240x320; memory_height must be 320 so the 80px rotation
// offset is applied (otherwise a strip of the panel shows noise).
cfg.memory_width = 240;
cfg.memory_height = 320;
cfg.panel_width = 240;
cfg.panel_height = 240;
cfg.offset_x = 0;
cfg.offset_y = 0;
cfg.offset_rotation = 1;
cfg.readable = false;
cfg.invert = true;
cfg.rgb_order = false;
cfg.dlen_16bit = false;
cfg.bus_shared = false;
_panel_instance.config(cfg);
}
{
auto cfg = _light_instance.config();
cfg.pin_bl = 45;
cfg.invert = false;
cfg.freq = 44100;
cfg.pwm_channel = 7;
_light_instance.config(cfg);
_panel_instance.setLight(&_light_instance);
}
{
auto cfg = _touch_instance.config();
cfg.x_min = 0;
cfg.x_max = 239;
cfg.y_min = 0;
cfg.y_max = 239;
cfg.pin_int = 16;
cfg.pin_rst = -1;
cfg.bus_shared = false;
cfg.offset_rotation = 2; // touch IC mounted 180deg vs the LCD horizontal axis
cfg.i2c_port = 1;
cfg.i2c_addr = 0x38;
cfg.pin_sda = 39;
cfg.pin_scl = 40;
cfg.freq = 400000;
_touch_instance.config(cfg);
_panel_instance.setTouch(&_touch_instance);
}
setPanel(&_panel_instance);
}
};
class TWatchS3PlusDisplay : public LGFXDisplay {
LGFX_TWatchS3Plus disp;
public:
TWatchS3PlusDisplay() : LGFXDisplay(240, 240, disp) {}
};
@@ -0,0 +1,144 @@
[LilyGo_TWatchS3Plus]
extends = esp32_base
extra_scripts = post:merge_firmware.py
board = lilygo_twatch_s3_plus
board_build.flash_mode = qio
board_build.f_flash = 80000000L
board_build.arduino.memory_type = qio_opi
board_upload.flash_size = 16MB
build_flags =
${esp32_base.build_flags}
${sensor_base.build_flags}
-I variants/lilygo_twatch_s3_plus
-D LILYGO_TWATCH_S3_PLUS
-D BOARD_HAS_PSRAM=1
-D CORE_DEBUG_LEVEL=1
-D FORMAT_SPIFFS_IF_FAILED=1
-D FORMAT_LITTLEFS_IF_FAILED=1
-D ARDUINO_USB_CDC_ON_BOOT=1
; ---- LoRa SX1262 ----
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_DIO2_AS_RF_SWITCH
-D SX126X_DIO3_TCXO_VOLTAGE=1.8f
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D P_LORA_NSS=5
-D P_LORA_DIO_1=9
-D P_LORA_RESET=8
-D P_LORA_BUSY=7
-D P_LORA_SCLK=3
-D P_LORA_MISO=4
-D P_LORA_MOSI=1
; ---- Display (ST7789 240x240) + FT6336U touch via LovyanGFX ----
-D DISPLAY_CLASS=TWatchS3PlusDisplay
-D UI_ZOOM=2
; ---- Misc ----
-D AUTO_SHUTDOWN_MILLIVOLTS=2800
-D ARDUINO_LOOP_STACK_SIZE=32768
; ---- GPS (u-blox MIA-M10Q on Serial2) ----
; Rail-managed: BLDO1 is off at boot and gated by the gps_enabled pref
; (defaults to 0) plus the "gps on/off" CLI command.
; HAS_GPS is a build flag (not only variant.h) so it is defined globally when
; UITask.h is parsed -- the gotoMapScreen() declaration is behind #if HAS_GPS,
; and UITask.h is included before target.h/variant.h in the .cpp TUs.
; PIN_GPS_RX/PIN_GPS_TX feed the shared initBasicGPS() Serial1 path and mirror
; the GPS_RX_PIN/GPS_TX_PIN values in variant.h (used by the Serial2 provider).
-D HAS_GPS=1
-D ENV_INCLUDE_GPS=1
-D ENV_SKIP_GPS_DETECT=1
-D PIN_GPS_RX=41
-D PIN_GPS_TX=42
-D ENV_INCLUDE_AHTX0=0
-D ENV_INCLUDE_BME280=0
-D ENV_INCLUDE_BMP280=0
-D ENV_INCLUDE_SHTC3=0
-D ENV_INCLUDE_SHT4X=0
-D ENV_INCLUDE_LPS22HB=0
-D ENV_INCLUDE_INA3221=0
-D ENV_INCLUDE_INA219=0
-D ENV_INCLUDE_INA226=0
-D ENV_INCLUDE_INA260=0
-D ENV_INCLUDE_MLX90614=0
-D ENV_INCLUDE_VL53L0X=0
-D ENV_INCLUDE_BME680=0
-D ENV_INCLUDE_BMP085=0
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/lilygo_twatch_s3_plus>
+<helpers/sensors/*.cpp>
lib_deps =
${esp32_base.lib_deps}
${sensor_base.lib_deps}
lovyan03/LovyanGFX @ ^1.2.0
lewisxhe/XPowersLib @ ^0.2.7
adafruit/Adafruit GFX Library @ ^1.11.0
bitbank2/PNGdec @ ^1.0.1
WebServer
DNSServer
Update
; ---------------------------------------------------------------------------
; Phase 1: standalone (no BLE companion), touch + display + lock screen.
; MAX_CONTACTS=2000 -- contact + sort arrays allocated in PSRAM via
; BaseChatMesh::initContacts(). GPS is compiled in but its BLDO1 rail stays
; off until toggled on ("gps on"); maps, tiles and the on-screen keyboard are
; deferred to later phases.
; ---------------------------------------------------------------------------
[env:meck_twatch_standalone]
extends = LilyGo_TWatchS3Plus
build_flags =
${LilyGo_TWatchS3Plus.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=2000
-D MAX_GROUP_CHANNELS=40
-D OFFLINE_QUEUE_SIZE=1
-D FIRMWARE_VERSION='"Meck TWatch v0.1"'
build_src_filter = ${LilyGo_TWatchS3Plus.build_src_filter}
+<helpers/esp32/*.cpp>
-<helpers/esp32/SerialBLEInterface.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
+<helpers/ui/LGFXDisplay.cpp>
lib_deps =
${LilyGo_TWatchS3Plus.lib_deps}
densaugeo/base64 @ ~1.4.0
lib_ignore =
AsyncTCP
ESPAsyncWebServer
ESP32 BLE Arduino
; ---------------------------------------------------------------------------
; BLE companion build -- same touch UI as the standalone, plus the BLE serial
; interface so a phone app can connect on occasion. Differs from standalone:
; BLE_PIN_CODE set, OFFLINE_QUEUE_SIZE raised, SerialBLEInterface.cpp compiled
; in (no longer excluded), and the BLE Arduino lib no longer in lib_ignore.
; Contacts stay PSRAM-allocated via initContacts(), covering the BLE stack's
; internal-SRAM use. ESP32_CPU_FREQ=80 sets the boot clock low; note that
; CPUPowerManager already governs the idle clock to 80 MHz at runtime.
; Flash: pio run -e meck_twatch_ble -t upload
; ---------------------------------------------------------------------------
[env:meck_twatch_ble]
extends = LilyGo_TWatchS3Plus
build_flags =
${LilyGo_TWatchS3Plus.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=2000
-D MAX_GROUP_CHANNELS=20
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
-D ESP32_CPU_FREQ=80
-D FIRMWARE_VERSION='"Meck TWatch BLE v0.1"'
build_src_filter = ${LilyGo_TWatchS3Plus.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
+<helpers/ui/LGFXDisplay.cpp>
lib_deps =
${LilyGo_TWatchS3Plus.lib_deps}
densaugeo/base64 @ ~1.4.0
lib_ignore =
AsyncTCP
ESPAsyncWebServer
+69
View File
@@ -0,0 +1,69 @@
#include <Arduino.h>
#include "variant.h"
#include "target.h"
TWatchS3PlusBoard board;
// LoRa SX1262 on its own SPI bus (the display uses SPI3_HOST separately).
static SPIClass loraSpi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, loraSpi);
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
// GPS: u-blox MIA-M10Q on Serial2 (the GPS pins are reclaimed from Serial1 to
// Serial2 in main.cpp setup(), matching the Meck T-Deck Pro pattern). The
// BLDO1 rail that powers the module is off at boot and gated by gps_enabled.
#if HAS_GPS
GPSStreamCounter gpsStream(Serial2);
MicroNMEALocationProvider gps(gpsStream, &rtc_clock);
EnvironmentSensorManager sensors(gps);
#else
SensorManager sensors;
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {
// NOTE: board.begin() is called by main.cpp setup() before radio_init();
// Wire is already initialised there with the correct pins.
fallback_clock.begin();
rtc_clock.begin(Wire);
loraSpi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS);
return radio.std_init(&loraSpi);
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(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;
radio.setPreambleLength(preamble);
}
void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng);
}
void radio_reset_agc() {
radio.setRxBoostedGainMode(true);
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
// Include variant.h first to ensure all board-specific defines are available
#include "variant.h"
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <TWatchS3PlusBoard.h>
#include <helpers/AutoDiscoverRTCClock.h>
#ifdef DISPLAY_CLASS
#include <TWatchS3PlusDisplay.h>
#include <helpers/ui/MomentaryButton.h>
#endif
#if HAS_GPS
#include "helpers/sensors/EnvironmentSensorManager.h"
#include "helpers/sensors/MicroNMEALocationProvider.h"
#include "GPSStreamCounter.h"
#else
#include <helpers/SensorManager.h>
#endif
extern TWatchS3PlusBoard board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
#if HAS_GPS
extern GPSStreamCounter gpsStream;
extern EnvironmentSensorManager sensors;
#else
extern SensorManager sensors;
#endif
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();
void radio_reset_agc();
+79
View File
@@ -0,0 +1,79 @@
#pragma once
// =============================================================================
// LilyGo T-Watch S3 Plus - Board-level pin definitions
// Source: LilyGo hardware doc (lilygo-t-watch-s3-plus.md) and the working
// MeshCore companion build.
//
// NOTE: LoRa (P_LORA_*) pins and the SX126x radio parameters are supplied as
// -D build flags in this variant's platformio.ini, matching the Meck T-Deck
// Pro convention. The display SPI/backlight and FT6336U touch pins live inline
// in TWatchS3PlusDisplay.h because LovyanGFX needs them in its panel config.
// =============================================================================
// -----------------------------------------------------------------------------
// Main I2C bus (shared): AXP2101 PMU, PCF8563 RTC, BMA423 accel, DRV2605 haptic
// The FT6336U touch panel is on a SEPARATE bus (Wire1), configured in the
// display class -- it is not on this bus.
// -----------------------------------------------------------------------------
#define I2C_SDA 10
#define I2C_SCL 11
// Aliases for ESP32Board base class compatibility
#define PIN_BOARD_SDA I2C_SDA
#define PIN_BOARD_SCL I2C_SCL
// -----------------------------------------------------------------------------
// I2C device addresses (7-bit)
// -----------------------------------------------------------------------------
#define I2C_ADDR_PMU 0x34 // AXP2101 power management
#define I2C_ADDR_RTC 0x51 // PCF8563 real-time clock
#define I2C_ADDR_ACCEL 0x19 // BMA423 accelerometer
#define I2C_ADDR_HAPTIC 0x5A // DRV2605 haptic driver
#define I2C_ADDR_TOUCH 0x38 // FT6336U capacitive touch (on Wire1)
// -----------------------------------------------------------------------------
// Interrupt / control pins
// -----------------------------------------------------------------------------
#ifndef PIN_PMU_IRQ
#define PIN_PMU_IRQ 21 // AXP2101 interrupt
#endif
#define PIN_RTC_IRQ 17 // PCF8563 interrupt
#define PIN_ACCEL_IRQ 14 // BMA423 interrupt
// User button -- GPIO0 "Custom Button" on the T-Watch S3 Plus (idles HIGH,
// active LOW), the same arrangement Meck uses on the T-Deck Pro.
#define PIN_USER_BTN 0
// -----------------------------------------------------------------------------
// Display dimensions (ST7789V3 IPS, 240x240)
// -----------------------------------------------------------------------------
#define LCD_HOR_SIZE 240
#define LCD_VER_SIZE 240
// -----------------------------------------------------------------------------
// Storage
// -----------------------------------------------------------------------------
// The T-Watch S3 Plus has no SD card slot. Notes/Reader/Epub screens reference
// SDCARD_CS unconditionally, so it must be defined for them to compile. -1 is a
// safe no-op on ESP32 (digitalWrite/pinMode reject out-of-range pins), and SD
// mounts will simply fail at runtime. Persistent storage arrives in Phase 3 via
// a LittleFS partition.
#define SDCARD_CS -1
// -----------------------------------------------------------------------------
// GPS (u-blox MIA-M10Q on Serial2)
// Pin values mirror the working MeshCore companion build. Power is on the
// AXP2101 BLDO1 rail and is gated at runtime via the gps_enabled pref +
// board.gpsPowerOn()/gpsPowerOff() -- it is NOT a GPIO enable line, so
// PIN_GPS_EN is deliberately left undefined (the main.cpp PIN_GPS_EN blocks
// are skipped and the watch branch toggles BLDO1 instead).
//
// NOTE: GPS RX/TX wiring labelling is ambiguous between the LilyGo doc and the
// MeshCore build. These values match the verified MeshCore config; if the
// module never acquires, swapping GPS_RX_PIN/GPS_TX_PIN is the first thing to try.
// -----------------------------------------------------------------------------
#define HAS_GPS 1
#define GPS_BAUDRATE 38400
#define GPS_RX_PIN 41
#define GPS_TX_PIN 42