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
+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