mirror of
https://github.com/pelgraine/Meck.git
synced 2026-06-27 05:11:13 +02:00
initial builds, standalone and ble twatch s3 plus
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user