Browse Source

Add LilyGo T-Watch S3 variant

ESP32-S3 + SX1262 companion-radio (BLE) support for the LilyGo T-Watch S3:
- AXP2101 PMU brought up via XPowersLib (enables the LoRa, display and
  sensor power rails)
- ST7789 240x240 display and FT5x06/FT6x36 capacitive touch via LovyanGFX
- on-device UI driven by touch, as the watch has no navigation buttons

Shared changes, both guarded/no-op for existing boards:
- DisplayDriver: add virtual getTouch() (default returns false; LGFXDisplay
  already implements it)
- companion ui-new: map touch taps to nav keys when UI_HAS_TOUCH is defined
pull/2799/head
djkazic 1 month ago
parent
commit
97a595ee99
  1. 41
      boards/lilygo_twatch_s3.json
  2. 32
      examples/companion_radio/ui-new/UITask.cpp
  3. 1
      src/helpers/ui/DisplayDriver.h
  4. 64
      variants/lilygo_twatch_s3/TWatchS3Board.cpp
  5. 56
      variants/lilygo_twatch_s3/TWatchS3Board.h
  6. 94
      variants/lilygo_twatch_s3/TWatchS3Display.h
  7. 76
      variants/lilygo_twatch_s3/platformio.ini
  8. 36
      variants/lilygo_twatch_s3/target.cpp
  9. 26
      variants/lilygo_twatch_s3/target.h

41
boards/lilygo_twatch_s3.json

@ -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 (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",
"vendor": "LilyGo"
}

32
examples/companion_radio/ui-new/UITask.cpp

@ -774,6 +774,38 @@ void UITask::loop() {
next_backlight_btn_check = millis() + 300;
}
#endif
#if defined(UI_HAS_TOUCH)
// touch nav: tap left/right/centre -> KEY_PREV/KEY_NEXT/KEY_ENTER, long press -> ENTER
{
static bool touch_was_down = false;
static int touch_down_x = 0, touch_down_y = 0;
static unsigned long touch_down_at = 0;
int tx = 0, ty = 0;
bool touch_now = (_display != NULL) && _display->getTouch(&tx, &ty);
if (touch_now && !touch_was_down) { // touch start
touch_down_x = tx; touch_down_y = ty;
touch_down_at = millis();
touch_was_down = true;
} else if (!touch_now && touch_was_down) { // touch release -> tap
touch_was_down = false;
unsigned long held = millis() - touch_down_at;
if (!_display->isOn()) {
c = checkDisplayOn(KEY_ENTER); // first tap just wakes the screen
} else if (held >= 800) {
c = handleLongPress(KEY_ENTER); // long press -> ENTER (CLI rescue in first 8s)
} else {
int w = _display->width();
if (touch_down_x < w / 3) {
c = checkDisplayOn(KEY_PREV);
} else if (touch_down_x > (2 * w) / 3) {
c = checkDisplayOn(KEY_NEXT);
} else {
c = checkDisplayOn(KEY_ENTER);
}
}
}
}
#endif
if (c != 0 && curr) {
curr->handleInput(c);

1
src/helpers/ui/DisplayDriver.h

@ -28,6 +28,7 @@ public:
virtual void drawRect(int x, int y, int w, int h) = 0;
virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0;
virtual uint16_t getTextWidth(const char* str) = 0;
virtual bool getTouch(int* x, int* y) { return false; } // touch panels override this
virtual void drawTextCentered(int mid_x, int y, const char* str) { // helper method (override to optimise)
int w = getTextWidth(str);
setCursor(mid_x - w/2, y);

64
variants/lilygo_twatch_s3/TWatchS3Board.cpp

@ -0,0 +1,64 @@
#include <Arduino.h>
#include "TWatchS3Board.h"
void TWatchS3Board::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 TWatchS3Board::power_init() {
PMU = new XPowersAXP2101(Wire, PIN_BOARD_SDA, PIN_BOARD_SCL, I2C_PMU_ADD);
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 (matches LilyGo / Meshtastic T-Watch S3)
PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); // LoRa radio
PMU->enablePowerOutput(XPOWERS_ALDO3);
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); // sensors, display, PCF8563 RTC
PMU->enablePowerOutput(XPOWERS_ALDO2);
PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); // 6-axis sensor / display rail
PMU->enablePowerOutput(XPOWERS_ALDO1);
PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); // DRV2605 haptic
PMU->enablePowerOutput(XPOWERS_BLDO2);
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); // must stay on or the radio loses power
PMU->enablePowerOutput(XPOWERS_ALDO4);
PMU->disablePowerOutput(XPOWERS_DCDC2);
PMU->disablePowerOutput(XPOWERS_DCDC3);
PMU->disablePowerOutput(XPOWERS_DCDC4);
PMU->disablePowerOutput(XPOWERS_DCDC5);
PMU->disablePowerOutput(XPOWERS_BLDO1);
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_500MA);
PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
PMU->disableTSPinMeasure();
PMU->enableSystemVoltageMeasure();
PMU->enableVbusVoltageMeasure();
PMU->enableBattVoltageMeasure();
PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S);
return true;
}

56
variants/lilygo_twatch_s3/TWatchS3Board.h

@ -0,0 +1,56 @@
#pragma once
// Pin mappings must be defined before including ESP32Board.h so begin() brings up
// Wire on the correct I2C pins and sleep() can reference P_LORA_DIO_1.
// Main I2C bus (AXP2101 PMU, PCF8563 RTC)
#define PIN_BOARD_SDA 10
#define PIN_BOARD_SCL 11
#define I2C_PMU_ADD 0x34
#ifndef PIN_PMU_IRQ
#define PIN_PMU_IRQ 21
#endif
#include <Wire.h>
#include <Arduino.h>
#include "XPowersLib.h"
#include "helpers/ESP32Board.h"
#include <driver/rtc_io.h>
class TWatchS3Board : 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((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH);
} else {
esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH);
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
esp_deep_sleep_start();
}
uint16_t getBattMilliVolts() override {
return PMU ? PMU->getBattVoltage() : 0;
}
const char* getManufacturerName() const override {
return "LilyGo T-Watch S3";
}
};

94
variants/lilygo_twatch_s3/TWatchS3Display.h

@ -0,0 +1,94 @@
#pragma once
#include <helpers/ui/LGFXDisplay.h>
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
// ST7789 240x240 on SPI3 + PWM backlight (GPIO 45) + FT5x06/FT6x36 capacitive
// touch on a separate I2C bus (Wire1: SDA 39 / SCL 40).
class LGFX_TWatchS3 : 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_TWatchS3(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 LCD's 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 TWatchS3Display : public LGFXDisplay {
LGFX_TWatchS3 disp;
public:
TWatchS3Display() : LGFXDisplay(240, 240, disp) {}
};

76
variants/lilygo_twatch_s3/platformio.ini

@ -0,0 +1,76 @@
[LilyGo_TWatchS3]
extends = esp32_base
board = lilygo_twatch_s3
build_flags =
${esp32_base.build_flags}
${sensor_base.build_flags}
-I variants/lilygo_twatch_s3
-D LILYGO_TWATCH_S3
-D T_WATCH_S3
-D BOARD_HAS_PSRAM=1
-D CORE_DEBUG_LEVEL=1
-D ARDUINO_USB_CDC_ON_BOOT=1
-D ESP32_CPU_FREQ=80 ; 80MHz keeps BLE + USB working while reducing power
; ---- LoRa SX1262 ----
-D USE_SX1262
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_DIO2_AS_RF_SWITCH=true
-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 ; SX1262 IRQ
-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) + capacitive touch via LovyanGFX ----
-D DISPLAY_CLASS=TWatchS3Display
-D UI_HAS_TOUCH=1
-D PIN_PMU_IRQ=21
; ---- No GPS / env sensors on the base watch ----
-D ENV_INCLUDE_GPS=0
-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>
+<helpers/sensors/*.cpp>
+<helpers/ui/LGFXDisplay.cpp>
lib_deps =
${esp32_base.lib_deps}
${sensor_base.lib_deps}
lewisxhe/XPowersLib @ ^0.2.7
lovyan03/LovyanGFX @ ^1.2.0
[env:LilyGo_TWatchS3_companion_radio_ble]
extends = LilyGo_TWatchS3
build_flags =
${LilyGo_TWatchS3.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${LilyGo_TWatchS3.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${LilyGo_TWatchS3.lib_deps}
densaugeo/base64 @ ~1.4.0

36
variants/lilygo_twatch_s3/target.cpp

@ -0,0 +1,36 @@
#include <Arduino.h>
#include "target.h"
TWatchS3Board board;
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
#if ENV_INCLUDE_GPS
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors;
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
#endif
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
return radio.std_init(&spi);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}

26
variants/lilygo_twatch_s3/target.h

@ -0,0 +1,26 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <TWatchS3Board.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#ifdef DISPLAY_CLASS
#include "TWatchS3Display.h"
#endif
#include "helpers/sensors/EnvironmentSensorManager.h"
#include "helpers/sensors/MicroNMEALocationProvider.h"
extern TWatchS3Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
#endif
bool radio_init();
mesh::LocalIdentity radio_new_identity();
Loading…
Cancel
Save