T-Watch Ultra support (#8171)

* feat: T-Watch Ultra support

* fix init touch controller

* add framebuffer

* update to device-ui

* trunk fmt

* update amoled driver reference

* PMU cosmetics

* power off lora

* fix NodeDB defaults

* trySetRTC when fixedPosition

* haptic touch (only BaseUI)

* init lora RF switch

* update LovyanGFX 1.2.19

* earlyInitVariant() adaptations acc. #9438

* update device-ui / touch handling

* Set NFC_CS disabled on boot

* Get t-watch-ultra working better on BaseUI

* Fix compilation

* Fix flash reads on t-watch-ultra

* Get baseui drawing to the screen correctly again on t-watch and add touch IRQ handling

* Add PMU IRQ handling

* Add IMU support

* Change define to avoid collision

* BaseUI changes to support t-watch-s3 rounded screen (#10786)

* BaseUI changes to support t-watch-s3 rounded screen

* Extend margin work to CannedMessages

* Finish merge

* Get audio working on watch-ultra

* trunk fmt

* added custom_meshtastic boilerplate

* T-Echo-Plus: disable BHI260AP while assumingly not implemented

* Drop the duplicate origBold declaration from the merge

* Inset incoming message bubbles on rounded screens

* Fix RTTTL tempo, WiFi screen margins, PMU guard and a duplicate define

* fix compile errror (the 2nd time)

* fix SDcard

* fix/workaround CO5300 pixel flush to SPI

* trunk fmt

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
This commit is contained in:
Manuel
2026-08-20 12:28:57 +00:00
committed by GitHub
co-authored by Jonathan Bennett Thomas Göttgens
parent 389559bddb
commit 0b906b4d15
35 changed files with 1060 additions and 166 deletions
+40
View File
@@ -0,0 +1,40 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_qspi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "t-watch-ultra"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino"],
"name": "LilyGo T-Watch Ultra",
"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/en-pl/products/t-watch-ultra",
"vendor": "LilyGo"
}
+1
View File
@@ -79,6 +79,7 @@ class AudioThread : public concurrency::OSThread
auto sam = std::unique_ptr<ESP8266SAM>(new ESP8266SAM);
sam->Say(audioOut.get(), text);
setCPUFast(false);
audioOut->stop();
#ifdef AUDIO_AMP_ENABLE
AUDIO_AMP_ENABLE(false);
#endif
+46 -2
View File
@@ -1142,8 +1142,10 @@ int32_t Power::runOnce()
// cancel action also turns the screen on and off.
if (PMU->isPekeyShortPressIrq()) {
LOG_INFO("Input: Corona Button Click");
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
if (inputBroker) {
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
}
}
#endif
/*
@@ -1446,6 +1448,48 @@ bool Power::axpChipInit()
PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist
PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist
PMU->disablePowerOutput(XPOWERS_VBACKUP);
} else if (HW_VENDOR == meshtastic_HardwareModel_T_WATCH_ULTRA) {
PMU->clearIrqStatus();
// Turn off the PMU charging indicator light, no physical connection
PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF); // NO LED
PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); // SD Card
PMU->enablePowerOutput(XPOWERS_ALDO1);
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); // Display
PMU->enablePowerOutput(XPOWERS_ALDO2);
PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); // LoRa
PMU->enablePowerOutput(XPOWERS_ALDO3);
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 1800); // Sensor
PMU->enablePowerOutput(XPOWERS_ALDO4);
PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); // GPS
PMU->enablePowerOutput(XPOWERS_BLDO1);
PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); // Speaker
PMU->enablePowerOutput(XPOWERS_BLDO2);
PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300); // RTC Button battery
PMU->enablePowerOutput(XPOWERS_VBACKUP);
// PMU->enablePowerOutput(XPOWERS_DLDO1); // NFC
// UNUSED POWER CHANNEL
PMU->disablePowerOutput(XPOWERS_DCDC2);
PMU->disablePowerOutput(XPOWERS_DCDC3);
PMU->disablePowerOutput(XPOWERS_DCDC4);
PMU->disablePowerOutput(XPOWERS_DCDC5);
PMU->disablePowerOutput(XPOWERS_CPULDO);
// Enable Measure
PMU->enableBattDetection();
PMU->enableVbusVoltageMeasure();
PMU->enableBattVoltageMeasure();
PMU->enableSystemVoltageMeasure();
PMU->enableTemperatureMeasure();
} else if (HW_VENDOR == meshtastic_HardwareModel_TBEAM_BPF) {
// T-Beam BPF rail map (per schematic LilyGo_TBeam_BPF r2025-05-08):
// DCDC1 -> ESP32 + OLED 3V3 (always on, protected)
+22 -19
View File
@@ -62,20 +62,17 @@ const int DURATION_1_1 = 1000; // 1/1 note
#ifdef HAS_I2S
void playTonesRTTTL(const ToneDuration *tone_durations, int size)
{
// translate ToneDuration[] to RTTTL string and play using audioThread
static std::unordered_map<int, std::string> freqToNote = {
{NOTE_C3, "c4"}, {NOTE_CS3, "c#4"}, {NOTE_D3, "d4"}, {NOTE_DS3, "d#4"}, {NOTE_E3, "e4"}, {NOTE_F3, "f4"},
{NOTE_FS3, "f#4"}, {NOTE_G3, "g4"}, {NOTE_GS3, "g#4"}, {NOTE_A3, "a4"}, {NOTE_AS3, "a#4"}, {NOTE_B3, "b4"},
{NOTE_C4, "c5"}, {NOTE_E4, "e5"}, {NOTE_G4, "g5"}, {NOTE_A4, "a5"}, {NOTE_C5, "c6"}, {NOTE_E5, "e6"},
{NOTE_G5, "g6"}, {NOTE_F5, "f6"}, {NOTE_G6, "g7"}, {NOTE_E7, "e8"}};
// translate ToneDuration[] to a single RTTTL string and play it via audioThread
static std::unordered_map<int, const char *> freqToNote = {
{NOTE_SILENT, "p"}, // rest
{NOTE_C3, "c4"}, {NOTE_CS3, "c#4"}, {NOTE_D3, "d4"}, {NOTE_DS3, "d#4"}, {NOTE_E3, "e4"}, {NOTE_F3, "f4"},
{NOTE_FS3, "f#4"}, {NOTE_G3, "g4"}, {NOTE_GS3, "g#4"}, {NOTE_A3, "a4"}, {NOTE_AS3, "a#4"}, {NOTE_B3, "b4"},
{NOTE_C4, "c5"}, {NOTE_CS4, "c#5"}, {NOTE_E4, "e5"}, {NOTE_G4, "g5"}, {NOTE_A4, "a5"}, {NOTE_B4, "b5"},
{NOTE_C5, "c6"}, {NOTE_E5, "e6"}, {NOTE_G5, "g6"}, {NOTE_F5, "f6"}, {NOTE_G6, "g7"}, {NOTE_E7, "e8"}};
char rtttl[128] = "tone:d=32,o=4,b=200:"; // default duration and octave
char rtttl[128] = "tone:d=32,o=4,b=240:"; // b=240 makes 240000/(bpm*d) match the ms durations above
for (int i = 0; i < size; i++) {
const auto &td = tone_durations[i];
std::string note = "b4";
if (freqToNote.find(td.frequency_khz) != freqToNote.end()) {
note = freqToNote[td.frequency_khz];
}
int dur = 32; // default duration
if (td.duration_ms >= 1000)
dur = 1;
@@ -90,16 +87,22 @@ void playTonesRTTTL(const ToneDuration *tone_durations, int size)
else
dur = 32;
char noteStr[64];
snprintf(noteStr, sizeof(noteStr), "%s,%d", note.c_str(), dur);
strncat(rtttl, noteStr, sizeof(rtttl) - strlen(rtttl) - 1);
auto it = freqToNote.find(td.frequency_khz);
const char *note = (it != freqToNote.end()) ? it->second : "p"; // unknown freq -> rest
audioThread->beginRttl(rtttl, strlen(rtttl));
while (audioThread->isPlaying()) {
delay(10);
}
return;
// RTTTL grammar puts duration before the note; notes are comma-separated
char noteStr[64];
snprintf(noteStr, sizeof(noteStr), "%s%d%s", i ? "," : "", dur, note);
strncat(rtttl, noteStr, sizeof(rtttl) - strlen(rtttl) - 1);
}
// trailing rest flushes the last note out of the I2S DMA buffer before teardown
strncat(rtttl, ",32p", sizeof(rtttl) - strlen(rtttl) - 1);
audioThread->beginRttl(rtttl, strlen(rtttl));
while (audioThread->isPlaying()) {
delay(10);
}
audioThread->stop(); // release I2S so the amp goes silent instead of looping the last buffer
}
#endif
+3 -3
View File
@@ -37,9 +37,9 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
{
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, SC7A20, BMA423, LSM6DS3, BMX160, STK8BAXX,
ICM20948, BMM150, BMI270, ICM42607P, ISM330DHCX, QMA6100P, QMI8658};
return firstOfOrNONE(14, types);
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, SC7A20, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948,
BMM150, BMI270, BHI260AP, ICM42607P, ISM330DHCX, QMA6100P, QMI8658};
return firstOfOrNONE(15, types);
}
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
+2 -2
View File
@@ -1281,8 +1281,8 @@ void GPS::setPowerPMU(bool on)
} else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) {
// t-beam-s3-core GNSS power channel
on ? PMU->enablePowerOutput(XPOWERS_ALDO4) : PMU->disablePowerOutput(XPOWERS_ALDO4);
} else if (HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) {
// t-watch-s3-plus GNSS power channel
} else if (HW_VENDOR == meshtastic_HardwareModel_T_WATCH_ULTRA || HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) {
// t-watch-ultra / t-watch-s3-plus GNSS power channel
on ? PMU->enablePowerOutput(XPOWERS_BLDO1) : PMU->disablePowerOutput(XPOWERS_BLDO1);
}
} else if (model == XPOWERS_AXP192) {
+5 -4
View File
@@ -681,12 +681,13 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
if (on) {
LOG_INFO("Turn on screen");
powerMon->setState(meshtastic_PowerMon_State_Screen_On);
#ifdef T_WATCH_S3
PMU->enablePowerOutput(XPOWERS_ALDO2);
#if defined(T_WATCH_S3) || defined(T_WATCH_ULTRA)
if (PMU) // cleared when both AXP init attempts failed
PMU->enablePowerOutput(XPOWERS_ALDO2);
#endif
// some screens seem to need a kick in the pants to turn back on
#if defined(MUZI_BASE) || defined(M5STACK_CARDPUTER_ADV)
#if defined(MUZI_BASE) || defined(M5STACK_CARDPUTER_ADV) || defined(TFT_RESET_AFTER_SLEEP)
dispdev->init();
dispdev->setBrightness(brightness);
dispdev->flipScreenVertically();
@@ -819,7 +820,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
#endif
#endif
#ifdef T_WATCH_S3
#if defined(T_WATCH_S3) // on T_WATCH_ULTRA, powering down this pin seems to goober the i2c bus.
PMU->disablePowerOutput(XPOWERS_ALDO2);
#endif
enabled = false;
+3
View File
@@ -287,6 +287,9 @@ class Screen : public concurrency::OSThread
// FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class
char ourId[5];
// if we have a step counter, this stores the number of steps.
uint32_t steps = 0;
/// Initializes the UI, turns on the display, starts showing boot screen.
//
// Not thread safe - must be called before any other methods are called.
+5 -5
View File
@@ -104,14 +104,14 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date,
bool transparent_background, bool use_title_color_override, uint16_t title_color_override)
{
constexpr int HEADER_OFFSET_Y = 1;
constexpr int HEADER_OFFSET_Y = 1 + BASEUI_HEADER_MARGIN;
y += HEADER_OFFSET_Y;
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
const int xOffset = 4;
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
const int xOffset = 4 + BASEUI_HEADER_LR_MARGIN;
const int highlightHeight = FONT_HEIGHT_SMALL - 1 + BASEUI_HEADER_MARGIN;
const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
const bool isBold = config.display.heading_bold;
@@ -250,8 +250,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
}
#endif
int batteryX = 1;
int batteryY = HEADER_OFFSET_Y + 1;
int batteryX = x + 1 + BASEUI_HEADER_LR_MARGIN;
int batteryY = HEADER_OFFSET_Y + 1 + BASEUI_HEADER_MARGIN / 2;
#if !defined(OLED_TINY)
// === Battery Icons ===
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
+17 -1
View File
@@ -21,7 +21,7 @@ namespace graphics
#define textSixthLine (textFifthLine + (FONT_HEIGHT_SMALL - 5))
// Consistent Line Spacing for devices like T114 and TEcho/ThinkNode M1 of devices
#define textFirstLine_medium (FONT_HEIGHT_SMALL + 1)
#define textFirstLine_medium (FONT_HEIGHT_SMALL + 1 + BASEUI_HEADER_MARGIN)
#define textSecondLine_medium (textFirstLine_medium + FONT_HEIGHT_SMALL)
#define textThirdLine_medium (textSecondLine_medium + FONT_HEIGHT_SMALL)
#define textFourthLine_medium (textThirdLine_medium + FONT_HEIGHT_SMALL)
@@ -36,6 +36,22 @@ namespace graphics
#define textFifthLine_large (textFourthLine_large + (FONT_HEIGHT_SMALL + 5))
#define textSixthLine_large (textFifthLine_large + (FONT_HEIGHT_SMALL + 5))
#ifndef BASEUI_HEADER_MARGIN
#define BASEUI_HEADER_MARGIN 0
#endif
#ifndef BASEUI_HEADER_LR_MARGIN
#define BASEUI_HEADER_LR_MARGIN 0
#endif
#ifndef BASEUI_BODY_LR_MARGIN
#define BASEUI_BODY_LR_MARGIN 0
#endif
#ifndef BASEUI_BELOW_HEADER_MARGIN
#define BASEUI_BELOW_HEADER_MARGIN 0
#endif
#ifndef ROUNDED_SCREEN
#define ROUNDED_SCREEN false
#endif
// Quick screen access
#define SCREEN_WIDTH display->getWidth()
#define SCREEN_HEIGHT display->getHeight()
+162 -5
View File
@@ -17,6 +17,92 @@
extern SX1509 gpioExtender;
#endif
#ifdef TFT_MESH_OVERRIDE
uint16_t TFT_MESH = TFT_MESH_OVERRIDE;
#else
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
#endif
#if defined(CO5300_CS)
#include <LovyanGFX.hpp> // Graphics and font library for AMOLED driver chip
class LGFX : public lgfx::LGFX_Device
{
lgfx::Panel_CO5300 _panel_instance;
lgfx::Bus_SPI _bus_instance;
public:
LGFX(void)
{
{
auto cfg = _bus_instance.config();
// configure SPI
cfg.spi_host = CO5300_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST
cfg.spi_mode = SPI_MODE0;
cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing
// 80MHz by an integer)
cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving
cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin
cfg.use_lock = true; // Set to true to use transaction locking
cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch /
// SPI_DMA_CH_AUTO=auto setting)
cfg.pin_sclk = CO5300_SCK; // Set SPI SCLK pin number
cfg.pin_io0 = CO5300_IO0;
cfg.pin_io1 = CO5300_IO1;
cfg.pin_io2 = CO5300_IO2;
cfg.pin_io3 = CO5300_IO3;
_bus_instance.config(cfg); // applies the set value to the bus.
_panel_instance.setBus(&_bus_instance); // set the bus on the panel.
}
{ // Set the display panel control.
auto cfg = _panel_instance.config(); // Gets a structure for display panel settings.
cfg.pin_cs = CO5300_CS; // Pin number where CS is connected (-1 = disable)
cfg.pin_rst = CO5300_RESET; // Pin number where RST is connected (-1 = disable)
cfg.panel_width = TFT_WIDTH; // actual displayable width
cfg.panel_height = TFT_HEIGHT; // actual displayable height
cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is upside down)
cfg.offset_x = TFT_OFFSET_X;
cfg.offset_y = TFT_OFFSET_Y;
cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout
cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read
cfg.readable = true; // Set to true if data can be read
cfg.invert = false; // Set to true if the light/darkness of the panel is reversed
cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped
cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units
cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.)
// Set the following only when the display is shifted with a driver with a variable number of pixels
cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC
_panel_instance.config(cfg);
}
setPanel(&_panel_instance);
}
bool init()
{
#ifdef CO5300_RESET
LOG_DEBUG("LGFX_Panel_CO5300::init()");
lgfx::pinMode(CO5300_RESET, lgfx::pin_mode_t::output);
lgfx::gpio_hi(CO5300_RESET);
delay(20);
lgfx::gpio_lo(CO5300_RESET);
delay(30);
lgfx::gpio_hi(CO5300_RESET);
delay(20);
#endif
return lgfx::LGFX_Device::init();
}
};
static LGFX *tft = nullptr;
#endif
#if defined(ST7735S)
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
@@ -1394,6 +1480,70 @@ void TFTDisplay::display(bool fromBlank)
}
// Step 3: Copy only the changed span into the pixel line buffer.
#if defined(CO5300_CS)
constexpr uint32_t kCO5300MinTransferBytes = 80;
constexpr uint32_t kCO5300BytesPerColumn = sizeof(uint16_t) * 2; // two rows, RGB565
constexpr uint32_t kCO5300MinColumns = (kCO5300MinTransferBytes + kCO5300BytesPerColumn - 1) / kCO5300BytesPerColumn;
// CO5300 workaround: widen very small updates so LovyanGFX avoids tiny SPI writes.
uint32_t span = x_LastPixelUpdate - x_FirstPixelUpdate + 1;
if (span < kCO5300MinColumns) {
uint32_t needed = kCO5300MinColumns - span;
uint32_t growLeft = needed / 2;
uint32_t growRight = needed - growLeft;
const uint32_t availableLeft = x_FirstPixelUpdate;
if (growLeft > availableLeft)
growLeft = availableLeft;
x_FirstPixelUpdate -= growLeft;
needed -= growLeft;
const uint32_t availableRight = (displayWidth - 1) - x_LastPixelUpdate;
const uint32_t extendRight = (needed < availableRight) ? needed : availableRight;
x_LastPixelUpdate += extendRight;
needed -= extendRight;
const uint32_t extendLeft = (needed < x_FirstPixelUpdate) ? needed : x_FirstPixelUpdate;
x_FirstPixelUpdate -= extendLeft;
}
// Keep transfer edges aligned as before for DMA-friendly boundaries.
x_FirstPixelUpdate &= ~1U;
x_LastPixelUpdate = (x_LastPixelUpdate | 1U);
if (x_LastPixelUpdate >= displayWidth) {
x_LastPixelUpdate = displayWidth - 1;
}
// snap y down to the even-row pair (AMOLED requires 2-row aligned writes)
const uint32_t y_draw = y & ~1U;
span = x_LastPixelUpdate - x_FirstPixelUpdate + 1;
const int y_offset = (int)y_draw - (int)y;
for (x = x_FirstPixelUpdate; x <= x_LastPixelUpdate; x++) {
const uint32_t col = x - x_FirstPixelUpdate;
uint32_t bi = (y_draw / 8) * displayWidth;
isset = buffer[x + bi] & (1 << (y_draw & 7));
#if GRAPHICS_TFT_COLORING_ENABLED
linePixelBuffer[x_FirstPixelUpdate + col] =
hasColorRegions ? graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y_draw), isset,
colorTftWhite, colorTftBlack)
: (isset ? colorTftWhite : colorTftBlack);
#else
linePixelBuffer[x_FirstPixelUpdate + col] = isset ? colorTftWhite : colorTftBlack;
#endif
bi = ((y_draw + 1) / 8) * displayWidth;
isset = buffer[x + bi] & (1 << ((y_draw + 1) & 7));
#if GRAPHICS_TFT_COLORING_ENABLED
linePixelBuffer[x_FirstPixelUpdate + span + col] =
hasColorRegions ? graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y_draw + 1),
isset, colorTftWhite, colorTftBlack)
: (isset ? colorTftWhite : colorTftBlack);
#else
linePixelBuffer[x_FirstPixelUpdate + span + col] = isset ? colorTftWhite : colorTftBlack;
#endif
}
const uint8_t lines_updated = 2;
#else
int y_offset = 0;
#if GRAPHICS_TFT_COLORING_ENABLED
if (hasColorRegions)
graphics::beginTFTColorRow(static_cast<int16_t>(y));
@@ -1411,13 +1561,16 @@ void TFTDisplay::display(bool fromBlank)
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
#endif
}
const uint8_t lines_updated = 1;
#endif
#if defined(HACKADAY_COMMUNICATOR)
tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
(x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1);
#else
// Step 4: Send the changed pixels on this line to the screen as a single block transfer.
// This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
tft->pushImage(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
tft->pushImage(x_FirstPixelUpdate, y + y_offset, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), lines_updated,
&linePixelBuffer[x_FirstPixelUpdate]);
#endif
somethingChanged = true;
@@ -1485,7 +1638,7 @@ void TFTDisplay::sendCommand(uint8_t com)
// handle display on/off directly
switch (com) {
case DISPLAYON: {
// LOG_DEBUG("Display on");
LOG_DEBUG("Display on");
backlightEnable->set(true);
#if ARCH_PORTDUINO
display(true);
@@ -1513,7 +1666,7 @@ void TFTDisplay::sendCommand(uint8_t com)
break;
}
case DISPLAYOFF: {
// LOG_DEBUG("Display off");
LOG_DEBUG("Display off");
backlightEnable->set(false);
#if ARCH_PORTDUINO
tft->clear();
@@ -1620,8 +1773,8 @@ bool TFTDisplay::connect()
#endif
}
backlightEnable->set(true);
LOG_INFO("Power to TFT Backlight");
backlightEnable->set(true);
#ifdef UNPHONE
unphone.backlight(true); // using unPhone library
@@ -1649,7 +1802,7 @@ bool TFTDisplay::connect()
tft->setRotation(1); // T-Deck has the TFT in landscape
#elif defined(T_WATCH_S3)
tft->setRotation(2); // T-Watch S3 left-handed orientation
#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER)
#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER) || defined(T_WATCH_ULTRA)
tft->setRotation(0); // use config.yaml to set rotation
#else
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
@@ -1657,7 +1810,11 @@ bool TFTDisplay::connect()
tft->fillScreen(getThemeDefaultOffColor());
if (this->linePixelBuffer == NULL) {
#if defined(CO5300_CS)
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth * 2);
#else
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
#endif
if (!this->linePixelBuffer) {
LOG_ERROR("Not enough memory to create TFT line buffer");
+38 -30
View File
@@ -59,17 +59,18 @@ void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
y += BASEUI_BELOW_HEADER_MARGIN;
const char *wifiName = config.network.wifi_ssid;
if (WiFi.status() != WL_CONNECTED) {
display->drawString(x, getTextPositions(display)[line++], "WiFi: Not Connected");
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y, "WiFi: Not Connected");
} else {
display->drawString(x, getTextPositions(display)[line++], "WiFi: Connected");
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y, "WiFi: Connected");
char rssiStr[32];
snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI());
display->drawString(x, getTextPositions(display)[line++], rssiStr);
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y, rssiStr);
}
/*
@@ -87,36 +88,36 @@ void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i
if (WiFi.status() == WL_CONNECTED) {
char ipStr[64];
snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str());
display->drawString(x, getTextPositions(display)[line++], ipStr);
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y, ipStr);
} else if (WiFi.status() == WL_NO_SSID_AVAIL) {
display->drawString(x, getTextPositions(display)[line++], "SSID Not Found");
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y, "SSID Not Found");
} else if (WiFi.status() == WL_CONNECTION_LOST) {
display->drawString(x, getTextPositions(display)[line++], "Connection Lost");
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y, "Connection Lost");
} else if (WiFi.status() == WL_IDLE_STATUS) {
display->drawString(x, getTextPositions(display)[line++], "Idle ... Reconnecting");
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y, "Idle ... Reconnecting");
} else if (WiFi.status() == WL_CONNECT_FAILED) {
display->drawString(x, getTextPositions(display)[line++], "Connection Failed");
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y, "Connection Failed");
}
#ifdef ARCH_ESP32
else {
// Codes:
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code
display->drawString(x, getTextPositions(display)[line++],
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y,
WiFi.disconnectReasonName(static_cast<wifi_err_reason_t>(getWifiDisconnectReason())));
}
#else
else {
char statusStr[32];
snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status());
display->drawString(x, getTextPositions(display)[line++], statusStr);
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y, statusStr);
}
#endif
char ssidStr[64];
snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName);
display->drawString(x, getTextPositions(display)[line++], ssidStr);
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y, ssidStr);
display->drawString(x, getTextPositions(display)[line++], "URL: http://meshtastic.local");
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line++] + y, "URL: http://meshtastic.local");
graphics::drawCommonFooter(display, x, y);
@@ -144,9 +145,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
y += BASEUI_BELOW_HEADER_MARGIN;
// === First Row: Region / BLE Name ===
graphics::UIRenderer::drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, 0, true, "");
graphics::UIRenderer::drawNodes(display, x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line] + 2 + y, nodeStatus, 0,
true, "");
uint8_t dmac[6];
char shortnameble[35];
@@ -158,8 +161,8 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
}
int textWidth = display->getStringWidth(shortnameble);
int nameX = (SCREEN_WIDTH - textWidth);
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
int nameX = (SCREEN_WIDTH - textWidth - BASEUI_BODY_LR_MARGIN);
display->drawString(nameX, getTextPositions(display)[line++] + y, shortnameble);
if (!graphics::isCompactPanel(display)) {
// === Second Row: Role ===
@@ -168,7 +171,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
snprintf(device_role, sizeof(device_role), "Role: %s", role);
textWidth = display->getStringWidth(device_role);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], device_role);
display->drawString(nameX, getTextPositions(display)[line++] + y, device_role);
}
// === Third Row: Radio Preset ===
@@ -194,7 +197,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
}
textWidth = display->getStringWidth(regionradiopreset);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset);
display->drawString(nameX, getTextPositions(display)[line++] + y, regionradiopreset);
// === Fourth Row: Frequency / ChanNum ===
char frequencyslot[35];
@@ -220,14 +223,14 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
}
textWidth = display->getStringWidth(frequencyslot);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
display->drawString(nameX, getTextPositions(display)[line++] + y, frequencyslot);
#if !defined(OLED_TINY)
// === Fifth Row: Channel Utilization ===
if (!config.lora.tx_enabled) {
const char *txdisabled = "Transmit Disabled";
textWidth = display->getStringWidth(txdisabled);
display->drawString((SCREEN_WIDTH - textWidth) / 2, getTextPositions(display)[line], txdisabled);
display->drawString((SCREEN_WIDTH - textWidth) / 2, getTextPositions(display)[line] + y, txdisabled);
} else {
const char *chUtil = "ChUtil:";
@@ -236,7 +239,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10
: display->getStringWidth(chUtil) + 5;
int chUtil_y = getTextPositions(display)[line] + 3;
int chUtil_y = getTextPositions(display)[line] + 3 + y;
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50;
int chutil_bar_max_fill = chutil_bar_width - 2; // Account for border
@@ -250,7 +253,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
(chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
int starting_position = centerofscreen - total_line_content_width;
display->drawString(starting_position, getTextPositions(display)[line], chUtil);
display->drawString(starting_position, getTextPositions(display)[line] + y, chUtil);
// Force 61% or higher to show a full 100% bar, text would still show related percent.
if (chutil_percent >= 61) {
@@ -297,7 +300,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
}
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++],
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++] + y,
chUtilPercentage);
}
#endif
@@ -318,11 +321,12 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
y += BASEUI_BELOW_HEADER_MARGIN;
// === Layout ===
int line = 1;
const int barHeight = 6;
const int labelX = x;
const int labelX = x + BASEUI_BODY_LR_MARGIN;
int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0;
#ifdef USE_EINK
#ifndef T_DECK_PRO
@@ -353,7 +357,11 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
}
int textWidth = display->getStringWidth(combinedStr);
int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6;
int labelWidth = display->getStringWidth(label);
if (barX < BASEUI_BODY_LR_MARGIN + labelWidth) {
barX = BASEUI_BODY_LR_MARGIN + labelWidth;
}
int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6 - BASEUI_BODY_LR_MARGIN;
if (adjustedBarWidth < 10)
adjustedBarWidth = 10;
@@ -361,10 +369,10 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
// Label
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->drawString(labelX, getTextPositions(display)[line], label);
display->drawString(labelX, getTextPositions(display)[line] + y, label);
#if !defined(OLED_TINY)
// Bar
int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2;
int barY = getTextPositions(display)[line] + y + (FONT_HEIGHT_SMALL - barHeight) / 2;
display->setColor(WHITE);
display->drawRect(barX, barY, adjustedBarWidth, barHeight);
@@ -384,7 +392,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
#endif
// Value string
display->setTextAlignment(TEXT_ALIGN_RIGHT);
display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr);
display->drawString(SCREEN_WIDTH - BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line] + y, combinedStr);
};
// === Memory values ===
@@ -473,7 +481,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
int textWidth = display->getStringWidth(appversionstr);
int nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], appversionstr);
display->drawString(nameX, getTextPositions(display)[line++] + y, appversionstr);
if (!graphics::isCompactPanel(display) &&
(SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5))) { // Only show uptime if the screen can show it
@@ -481,7 +489,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr));
textWidth = display->getStringWidth(uptimeStr);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], uptimeStr);
display->drawString(nameX, getTextPositions(display)[line++] + y, uptimeStr);
}
if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show API state if the screen can show it
@@ -528,7 +536,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
}
#endif
if (api_state[0] != '\0') {
display->drawString((SCREEN_WIDTH - display->getStringWidth(api_state)) / 2, getTextPositions(display)[line++],
display->drawString((SCREEN_WIDTH - display->getStringWidth(api_state)) / 2, getTextPositions(display)[line++] + y,
api_state);
}
}
+12 -9
View File
@@ -437,12 +437,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
display->setFont(FONT_SMALL);
const bool compactPanel = graphics::isCompactPanel(display);
// Compact panels: no bottom nav row anymore (see UIRenderer::drawNavigationBar), full height available.
const int navHeight = compactPanel ? 0 : FONT_HEIGHT_SMALL;
const int navHeight = compactPanel ? 0 : FONT_HEIGHT_SMALL + BASEUI_BELOW_HEADER_MARGIN + BASEUI_HEADER_MARGIN;
const int scrollBottom = SCREEN_HEIGHT - navHeight;
const int contentTop = compactPanel ? 0 : getTextPositions(display)[1];
// Rounded screens start the body below the header margin; getTextPositions(display)[1] + BASEUI_BELOW_HEADER_MARGIN
const int contentTop = compactPanel ? 0 : navHeight;
const int usableHeight = compactPanel ? scrollBottom - contentTop : scrollBottom;
constexpr int LEFT_MARGIN = 2;
constexpr int RIGHT_MARGIN = 2;
constexpr int LEFT_MARGIN = 2 + BASEUI_BODY_LR_MARGIN;
constexpr int RIGHT_MARGIN = 2 + BASEUI_BODY_LR_MARGIN;
constexpr int SCROLLBAR_WIDTH = 3;
constexpr int BUBBLE_PAD_X = 3;
constexpr int BUBBLE_PAD_Y = 4;
@@ -453,6 +454,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// Check if bubbles are enabled
const bool showBubbles = config.display.enable_message_bubbles && !compactPanel;
const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN;
// Bubbles carry their own padding, so the rounded-screen inset has to come from here
const int contentLeft = x + (showBubbles ? BASEUI_BODY_LR_MARGIN : 0);
// Derived widths
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0);
@@ -873,10 +876,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
if (b.mine) {
bubbleX = rightEdge - bubbleW;
} else {
bubbleX = x;
bubbleX = contentLeft;
}
if (bubbleX < x)
bubbleX = x;
if (bubbleX < contentLeft)
bubbleX = contentLeft;
if (bubbleX + bubbleW > rightEdge)
bubbleW = std::max(1, rightEdge - bubbleX);
@@ -953,7 +956,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
if (headerX < LEFT_MARGIN)
headerX = LEFT_MARGIN;
} else {
headerX = x + textIndent;
headerX = contentLeft + textIndent;
}
graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1,
true);
@@ -1002,7 +1005,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
} else {
drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes);
drawStringWithEmotes(display, contentLeft + textIndent, lineY, cachedLines[i], emotes, numEmotes);
}
}
}
+28 -5
View File
@@ -47,6 +47,20 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *
}
}
void drawScaledXBitmap3x(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display)
{
for (int row = 0; row < height; row++) {
uint8_t rowMask = (1 << row);
for (int col = 0; col < width; col++) {
uint8_t colData = pgm_read_byte(&bitmapXBM[col]);
if (colData & rowMask) {
// Note: rows become X, columns become Y after transpose
display->fillRect(x + row * 3, y + col * 3, 3, 3);
}
}
}
}
// Static variables for dynamic cycling
static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD;
static ListMode_Location currentMode_Location = MODE_DISTANCE;
@@ -606,7 +620,7 @@ void drawCompassUnknown(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
EntryRenderer renderer, NodeExtrasRenderer extras, float headingRadian, double lat, double lon)
{
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1 + BASEUI_HEADER_MARGIN;
// Compact panels: 4 rows fit (0,9,18,27), a 5th pages instead of cramming in.
const int rowYOffset = graphics::isCompactPanel(display) ? (FONT_HEIGHT_SMALL - 4) : (FONT_HEIGHT_SMALL - 3);
bool locationScreen = false;
@@ -622,7 +636,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// Compact panels have no header (see drawCommonHeader) - don't reserve space for one.
if (!graphics::isCompactPanel(display))
y += COMMON_HEADER_HEIGHT;
y += COMMON_HEADER_HEIGHT + BASEUI_BELOW_HEADER_MARGIN;
firstRowY = y;
int totalColumns = 1; // Default to 1 column
@@ -638,7 +652,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
} else {
if (SCREEN_WIDTH <= 64) {
totalColumns = 1;
} else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) {
} else if ((SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) || ROUNDED_SCREEN) {
totalColumns = 2;
} else {
totalColumns = 3;
@@ -691,11 +705,20 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
auto *node = nodeDB->getMeshNode(nodeNum);
int xPos = x + (col * columnWidth);
int yPos = y + yOffset;
int effectiveColumnWidth = columnWidth;
if (BASEUI_BODY_LR_MARGIN) {
if (col == 0) {
xPos += BASEUI_BODY_LR_MARGIN;
effectiveColumnWidth -= BASEUI_BODY_LR_MARGIN;
} else if (col == (totalColumns - 1)) {
effectiveColumnWidth -= BASEUI_BODY_LR_MARGIN;
}
}
renderer(display, node, xPos, yPos, columnWidth);
renderer(display, node, xPos, yPos, effectiveColumnWidth);
if (extras)
extras(display, node, xPos, yPos, columnWidth, headingRadian, lat, lon);
extras(display, node, xPos, yPos, effectiveColumnWidth, headingRadian, lat, lon);
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
yOffset += rowYOffset;
+1
View File
@@ -65,6 +65,7 @@ void scrollDown();
// Bitmap drawing function
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display);
void drawScaledXBitmap3x(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display);
} // namespace NodeListRenderer
+81 -48
View File
@@ -451,7 +451,8 @@ static bool computeBottomCompassPlacement(OLEDDisplay *display, int16_t xOffset,
int16_t margin, int16_t *compassX, int16_t *compassY, int16_t *compassRadius)
{
// Return false when content leaves no room for a readable compass.
int availableHeight = SCREEN_HEIGHT - yBelowContent - bottomReserved - margin;
int availableHeight =
SCREEN_HEIGHT - yBelowContent - bottomReserved - margin - BASEUI_HEADER_MARGIN - BASEUI_BELOW_HEADER_MARGIN;
if (availableHeight < FONT_HEIGHT_SMALL * 2) {
return false;
}
@@ -581,12 +582,12 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
if (!gps->getIsConnected() && !config.position.fixed_position) {
if (strcmp(mode, "line1") == 0) {
strcpy(displayLine, "No GPS present");
display->drawString(x, y, displayLine);
display->drawString(x + BASEUI_BODY_LR_MARGIN, y, displayLine);
}
} else if (!gps->getHasLock() && !config.position.fixed_position) {
if (strcmp(mode, "line1") == 0) {
strcpy(displayLine, gps->getHasTime() ? "GPS Time Only" : "No GPS Lock");
display->drawString(x, y, displayLine);
display->drawString(x + BASEUI_BODY_LR_MARGIN, y, displayLine);
}
} else {
@@ -665,13 +666,14 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
}
if (strcmp(mode, "line1") == 0) {
display->drawString(x, y, coordinateLine_1);
display->drawString(x + BASEUI_BODY_LR_MARGIN, y, coordinateLine_1);
} else if (strcmp(mode, "line2") == 0) {
display->drawString(x, y, coordinateLine_2);
display->drawString(x + BASEUI_BODY_LR_MARGIN, y, coordinateLine_2);
} else if (strcmp(mode, "combined") == 0) {
display->drawString(x, y, coordinateLine_1);
if (coordinateLine_2[0] != '\0') {
display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2);
display->drawString(x + BASEUI_BODY_LR_MARGIN + display->getStringWidth(coordinateLine_1), y,
coordinateLine_2);
}
}
@@ -683,12 +685,12 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(),
geoCoord.getDMSLonMin(), geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP());
if (strcmp(mode, "line1") == 0) {
display->drawString(x, y, coordinateLine_1);
display->drawString(x + BASEUI_BODY_LR_MARGIN, y, coordinateLine_1);
} else if (strcmp(mode, "line2") == 0) {
display->drawString(x, y, coordinateLine_2);
display->drawString(x + BASEUI_BODY_LR_MARGIN, y, coordinateLine_2);
} else { // both
display->drawString(x, y, coordinateLine_1);
display->drawString(x, y + 10, coordinateLine_2);
display->drawString(x + BASEUI_BODY_LR_MARGIN, y, coordinateLine_1);
display->drawString(x + BASEUI_BODY_LR_MARGIN, y + 10, coordinateLine_2);
}
}
}
@@ -933,6 +935,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
}
#endif
y += BASEUI_BELOW_HEADER_MARGIN;
// ===== DYNAMIC ROW STACKING WITH YOUR MACROS =====
// 1. Each potential info row has a macro-defined Y position (not regular increments!).
// 2. Each row is only shown if it has valid data.
@@ -1304,7 +1307,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
}
// ****************************
// * Device Focused Screen *
// * Home Frame *
// ****************************
void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
@@ -1313,6 +1316,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setFont(FONT_SMALL);
int line = 1;
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
bool origBold = config.display.heading_bold;
// === Header ===
if (currentResolution == ScreenResolution::UltraLow) {
@@ -1320,11 +1324,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
} else {
graphics::drawCommonHeader(display, x, y, "");
}
y += BASEUI_BELOW_HEADER_MARGIN;
// === Content below header ===
// === First Row: Region / Channel Utilization and Uptime ===
bool origBold = config.display.heading_bold;
config.display.heading_bold = false;
const bool compactPanel = graphics::isCompactPanel(display);
@@ -1333,19 +1337,20 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
const char *txdisabled = "Transmit Disabled";
if (compactPanel) {
int textWidth = display->getStringWidth(txdisabled);
display->drawString((SCREEN_WIDTH - textWidth) / 2, getTextPositions(display)[line], txdisabled);
display->drawString((SCREEN_WIDTH - textWidth) / 2, getTextPositions(display)[line] + y, txdisabled);
} else {
display->drawString(x, getTextPositions(display)[line], txdisabled);
display->drawString(x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line] + y, txdisabled);
}
} else if (compactPanel) {
// No room for a separate left/right column layout - center it instead.
drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online", true);
drawNodes(display, x, getTextPositions(display)[line] + y + 2, nodeStatus, -1, false, "online", true);
} else {
// Display Region and Channel Utilization
if (currentResolution == ScreenResolution::UltraLow) {
drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
drawNodes(display, x, getTextPositions(display)[line] + y + 2, nodeStatus, -1, false, "online");
} else {
drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
drawNodes(display, x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line] + y + 2, nodeStatus, -1, false,
"online");
}
}
char uptimeStr[32] = "";
@@ -1353,7 +1358,8 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr));
}
if (!compactPanel) {
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr);
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr) - BASEUI_BODY_LR_MARGIN,
getTextPositions(display)[line++] + y, uptimeStr);
} else {
line++;
}
@@ -1362,7 +1368,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
config.display.heading_bold = false;
#if HAS_GPS
UIRenderer::drawGps(display, x, getTextPositions(display)[line], gpsStatus, compactPanel);
UIRenderer::drawGps(display, x + BASEUI_BODY_LR_MARGIN, getTextPositions(display)[line] + y, gpsStatus, compactPanel);
#endif
#if defined(OLED_TINY)
@@ -1374,7 +1380,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
char chUtilStr[16];
snprintf(chUtilStr, sizeof(chUtilStr), "ChUtil %d%%", chutil_percent);
int chUtilWidth = display->getStringWidth(chUtilStr);
display->drawString((SCREEN_WIDTH - chUtilWidth) / 2, getTextPositions(display)[line++], chUtilStr);
display->drawString((SCREEN_WIDTH - chUtilWidth) / 2, getTextPositions(display)[line++] + y, chUtilStr);
// === Node Identity: long name (falls back to short), truncated with "..." if too wide ===
const char *longName = (nodeInfoLiteHasUser(ourNode) && ourNode->long_name[0]) ? ourNode->long_name : "";
@@ -1384,14 +1390,14 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
UIRenderer::truncateStringWithEmotes(display, rawName, nodeName, sizeof(nodeName), SCREEN_WIDTH - 4);
int textWidth = UIRenderer::measureStringWithEmotes(display, nodeName);
int nameX = (SCREEN_WIDTH - textWidth) / 2;
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], nodeName, FONT_HEIGHT_SMALL, 1,
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++] + y, nodeName, FONT_HEIGHT_SMALL, 1,
false);
} else {
// === Node Identity ===
const char *shortName = owner.short_name[0] ? owner.short_name : "";
int textWidth = UIRenderer::measureStringWithEmotes(display, shortName);
int nameX = (SCREEN_WIDTH - textWidth) / 2;
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1,
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++] + y, shortName, FONT_HEIGHT_SMALL, 1,
false);
}
#else
@@ -1400,9 +1406,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int batV = powerStatus->getBatteryVoltageMv() / 1000;
int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10;
snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv);
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), getTextPositions(display)[line++], batStr);
display->drawString(x + SCREEN_WIDTH - BASEUI_BODY_LR_MARGIN - display->getStringWidth(batStr),
getTextPositions(display)[line++] + y, batStr);
} else {
display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), getTextPositions(display)[line++], "USB");
display->drawString(x + SCREEN_WIDTH - BASEUI_BODY_LR_MARGIN - display->getStringWidth("USB"),
getTextPositions(display)[line++] + y, "USB");
}
config.display.heading_bold = origBold;
@@ -1413,9 +1421,8 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int chutil_percent = static_cast<int>(airTime->channelUtilizationPercent() + 0.5f);
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%d%%", chutil_percent);
int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10
: display->getStringWidth(chUtil) + 5;
int chUtil_y = getTextPositions(display)[line] + 3;
int chUtil_width = display->getStringWidth(chUtil);
int chUtil_y = getTextPositions(display)[line] + 3 + y;
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50;
int chutil_bar_max_fill = chutil_bar_width - 2; // Account for border
@@ -1433,10 +1440,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
}
const int raw_chutil_percent = chutil_percent;
// With BT disabled we pin this row left to make room for the extra "BT off" indicator.
const int starting_position = config.bluetooth.enabled ? x : 0;
// Center the row; with BT disabled reserve the width of the extra "BT off" indicator.
int starting_position =
(SCREEN_WIDTH - chUtil_width - chutil_bar_width - extraoffset - display->getStringWidth(chUtilPercentage));
if (!config.bluetooth.enabled) {
starting_position -= (display->getStringWidth("BT off") + extraoffset);
}
starting_position /= 2;
display->drawString(starting_position, getTextPositions(display)[line], chUtil);
display->drawString(starting_position, getTextPositions(display)[line] + y, chUtil);
// Force 61% or higher to show a full 100% bar, text would still show related percent.
if (chutil_percent >= 61) {
@@ -1446,7 +1458,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int fillRight = computeChannelUtilizationFill(chutil_percent, chutil_bar_max_fill);
// Draw outline
display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height);
display->drawRect(starting_position + chUtil_width, chUtil_y, chutil_bar_width, chutil_bar_height);
// Fill progress
if (fillRight > 0) {
@@ -1458,16 +1470,18 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
UtilizationFillColor = TFTPalette::Medium;
}
setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black,
starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
starting_position + chUtil_width + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
#endif
display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
display->fillRect(starting_position + chUtil_width + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
}
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line],
display->drawString(starting_position + chUtil_width + chutil_bar_width + extraoffset, getTextPositions(display)[line] + y,
chUtilPercentage);
if (!config.bluetooth.enabled) {
display->drawString(SCREEN_WIDTH - display->getStringWidth("BT off"), getTextPositions(display)[line], "BT off");
display->drawString(starting_position + chUtil_width + chutil_bar_width + extraoffset +
display->getStringWidth(chUtilPercentage) + extraoffset,
getTextPositions(display)[line] + y, "BT off");
}
line += 1;
@@ -1491,21 +1505,28 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
if (SCREEN_WIDTH - UIRenderer::measureStringWithEmotes(display, combinedName) > 10) {
textWidth = UIRenderer::measureStringWithEmotes(display, combinedName);
nameX = (SCREEN_WIDTH - textWidth) / 2;
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++] + yOffset, combinedName,
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++] + yOffset + y, combinedName,
FONT_HEIGHT_SMALL, 1, false);
} else {
// === LongName Centered ===
textWidth = UIRenderer::measureStringWithEmotes(display, longName);
nameX = (SCREEN_WIDTH - textWidth) / 2;
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], longName, FONT_HEIGHT_SMALL, 1,
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++] + y, longName, FONT_HEIGHT_SMALL, 1,
false);
// === ShortName Centered ===
textWidth = UIRenderer::measureStringWithEmotes(display, shortName);
nameX = (SCREEN_WIDTH - textWidth) / 2;
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1,
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++] + y, shortName, FONT_HEIGHT_SMALL, 1,
false);
}
#ifdef SHOW_STEP_COUNTER
std::string stepsLine = "Steps: " + std::to_string(screen->steps);
textWidth = UIRenderer::measureStringWithEmotes(display, stepsLine.c_str());
nameX = (SCREEN_WIDTH - textWidth) / 2;
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++] + y, stepsLine.c_str(), FONT_HEIGHT_SMALL,
1, false);
#endif
#endif
graphics::drawCommonFooter(display, x, y);
}
@@ -1776,6 +1797,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
y += BASEUI_BELOW_HEADER_MARGIN;
const int *textPos = getTextPositions(display);
const bool compactPanel = graphics::isCompactPanel(display);
@@ -1804,7 +1826,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
bool origBold = config.display.heading_bold;
config.display.heading_bold = false;
UIRenderer::drawGps(display, x, textPos[line++], gpsStatus, compactPanel);
UIRenderer::drawGps(display, x + BASEUI_BODY_LR_MARGIN, textPos[line++] + y, gpsStatus, compactPanel);
config.display.heading_bold = origBold;
@@ -1914,18 +1936,18 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true);
#endif
display->drawString(0, textPos[line++], uptimeStr);
display->drawString(x + BASEUI_BODY_LR_MARGIN, textPos[line++] + y, uptimeStr);
} else {
display->drawString(0, textPos[line++], "Last: ?");
display->drawString(x + BASEUI_BODY_LR_MARGIN, textPos[line++] + y, "Last: ?");
}
// === Third Row: Line 1 GPS Info ===
UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line1");
UIRenderer::drawGpsCoordinates(display, x, textPos[line++] + y, gpsStatus, "line1");
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC &&
uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) {
// === Fourth Row: Line 2 GPS Info ===
UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line2");
UIRenderer::drawGpsCoordinates(display, x, textPos[line++] + y, gpsStatus, "line2");
}
// === Final Row: Altitude ===
@@ -1936,21 +1958,21 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
} else {
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt);
}
display->drawString(x, textPos[line++], altitudeLine);
display->drawString(x + BASEUI_BODY_LR_MARGIN, textPos[line++] + y, altitudeLine);
}
#if !defined(OLED_TINY)
// === Draw Compass ===
if (validHeading || statusLine1) {
// --- Compass Rendering: landscape (wide) screens use original side-aligned logic ---
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
const int16_t topY = textPos[1];
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height
const int16_t topY = textPos[1] + y;
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1) - y; // nav row height
const int16_t usableHeight = bottomY - topY - 5;
int16_t compassRadius = usableHeight / 2;
if (compassRadius < 8)
compassRadius = 8;
const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8;
const int16_t compassX = x + BASEUI_BODY_LR_MARGIN + SCREEN_WIDTH - compassRadius - 8;
// Center vertically and nudge down slightly to keep "N" clear of header
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
@@ -2085,7 +2107,11 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
lastFrameChangeTime = millis();
}
#ifdef OLED_HUGE
const int iconSize = 24;
#else
const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8;
#endif
const int spacing = (currentResolution == ScreenResolution::High) ? 8 : 4;
const int bigOffset = (currentResolution == ScreenResolution::High) ? 1 : 0;
const bool compactPanel = graphics::isCompactPanel(display);
@@ -2153,7 +2179,11 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
}
#endif
#if BASEUI_HEADER_LR_MARGIN
const int navPadding = BASEUI_HEADER_LR_MARGIN;
#else
const int navPadding = compactPanel ? 8 : ((currentResolution == ScreenResolution::High) ? 24 : 12);
#endif
int usableWidth = SCREEN_WIDTH - (navPadding * 2);
if (usableWidth < iconSize)
@@ -2253,12 +2283,15 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setColor(BLACK);
#endif
}
#ifdef OLED_HUGE
NodeListRenderer::drawScaledXBitmap3x(x, y, 8, 8, icon, display);
#else
if (currentResolution == ScreenResolution::High) {
NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display);
} else {
display->drawXbm(x, y, iconSize, iconSize, icon);
}
#endif
if (isActive) {
display->setColor(WHITE);
+4
View File
@@ -52,6 +52,10 @@ class UIRenderer
// though drawNavigationBar itself never ran while the screen (and its OSThread) was off.
static void notifyScreenWoke();
// screen frames
// First two pointers are self explanatory
// x and y are the offset everything should be drawn at, to support sliding transitions between frames.
static void drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Compact panels: toggle between compass+distance view and status/telemetry view
static void scrollFavoriteDown();
+1 -1
View File
@@ -192,7 +192,7 @@ int32_t TouchScreenBase::runOnce()
void TouchScreenBase::hapticFeedback()
{
#ifdef T_WATCH_S3
#if defined(T_WATCH_S3) || defined(T_WATCH_ULTRA)
drv.setWaveform(0, 75);
drv.setWaveform(1, 0); // end waveform
drv.go();
+2
View File
@@ -322,6 +322,8 @@ __attribute__((weak, noinline)) bool loopCanSleep()
__attribute__((noinline)) void lateInitVariant() __attribute__((weak));
__attribute__((noinline)) void lateInitVariant() {}
// earlyInitVariant() runs before consoleInit(): a LOG_* macro here CRASHES the device,
// it is not a silent no-op. Defer any logging to lateInitVariant() or later.
__attribute__((noinline)) void earlyInitVariant() __attribute__((weak));
__attribute__((noinline)) void earlyInitVariant() {}
+2 -2
View File
@@ -1033,7 +1033,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT) || defined(RAK_WISMESH_TAP_V2) || \
defined(ELECROW_ThinkNode_M9)) && \
defined(ELECROW_ThinkNode_M9) || defined(T_WATCH_ULTRA)) && \
HAS_TFT
// switch BT off by default; use TFT programming mode or hotkey to enable
config.bluetooth.enabled = false;
@@ -1117,7 +1117,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
config.display.wake_on_tap_or_motion = true;
#endif
#if defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR)
#if defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR) || defined(T_WATCH_ULTRA)
config.display.screen_on_secs = 30;
config.display.wake_on_tap_or_motion = true;
#endif
+42 -27
View File
@@ -211,10 +211,13 @@ void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y,
snprintf(header, sizeof(header), "To: @%s", getNodeName(this->dest));
}
const int maxWidth = std::max(0, display->getWidth() - x);
// First row of text: inset horizontally by the header L/R margin and pushed down by the header margin
const int headerX = x + BASEUI_HEADER_LR_MARGIN;
const int headerY = y + BASEUI_HEADER_MARGIN;
const int maxWidth = std::max(0, display->getWidth() - headerX - BASEUI_HEADER_LR_MARGIN);
char truncatedHeader[96];
graphics::UIRenderer::truncateStringWithEmotes(display, header, truncatedHeader, sizeof(truncatedHeader), maxWidth);
graphics::UIRenderer::drawStringWithEmotes(display, x, y, truncatedHeader, FONT_HEIGHT_SMALL, 1, false);
graphics::UIRenderer::drawStringWithEmotes(display, headerX, headerY, truncatedHeader, FONT_HEIGHT_SMALL, 1, false);
}
void CannedMessageModule::resetSearch()
@@ -1475,7 +1478,9 @@ void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState
{
int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet];
int xOffset = 0;
// Inset the key grid horizontally by the body L/R margin (keeps touch aligned since
// keyForCoordinates() reads the same per-key rects stored below)
int xOffset = BASEUI_BODY_LR_MARGIN;
int yOffset = 56;
@@ -1485,7 +1490,8 @@ void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState
display->setColor(OLEDDISPLAY_COLOR::WHITE);
display->drawStringMaxWidth(0, 0, display->getWidth(),
// Free text being typed is the first row of text: inset by header margins
display->drawStringMaxWidth(BASEUI_HEADER_LR_MARGIN, BASEUI_HEADER_MARGIN, display->getWidth() - 2 * BASEUI_HEADER_LR_MARGIN,
cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor));
display->setFont(FONT_MEDIUM);
@@ -1507,7 +1513,7 @@ void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState
}
}
int cellWidth = display->width() / innerSize;
int cellWidth = (display->width() - 2 * BASEUI_BODY_LR_MARGIN) / innerSize;
for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) {
xOffset += innerIndex > 0 ? cellWidth : 0;
@@ -1580,7 +1586,7 @@ void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState
}
}
xOffset = 0;
xOffset = BASEUI_BODY_LR_MARGIN;
}
this->highlight = 0x00;
@@ -1670,8 +1676,8 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
// Header
int titleY = 2;
// Header (first row): pushed down by the header margin; centered, so no L/R inset needed
int titleY = 2 + BASEUI_HEADER_MARGIN;
String titleText = "Select Destination";
titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]";
display->setTextAlignment(TEXT_ALIGN_CENTER);
@@ -1723,7 +1729,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
}
}
int availWidth = display->getWidth() -
int availWidth = display->getWidth() - 2 * BASEUI_BODY_LR_MARGIN -
((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) -
((nodeInfoLiteIsFavorite(node)) ? 10 : 0);
if (availWidth < 0)
@@ -1749,12 +1755,14 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
// Highlight background (if selected)
if (itemIndex == destIndex) {
int scrollPadding = 8; // Reserve space for scrollbar
display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5);
display->fillRect(BASEUI_BODY_LR_MARGIN, yOffset + 2, display->getWidth() - scrollPadding - 2 * BASEUI_BODY_LR_MARGIN,
FONT_HEIGHT_SMALL - 5);
display->setColor(BLACK);
}
// Draw entry text
graphics::UIRenderer::drawStringWithEmotes(display, xOffset + 2, yOffset, entryText.c_str(), FONT_HEIGHT_SMALL, 1, false);
graphics::UIRenderer::drawStringWithEmotes(display, xOffset + 2 + BASEUI_BODY_LR_MARGIN, yOffset, entryText.c_str(),
FONT_HEIGHT_SMALL, 1, false);
display->setColor(WHITE);
// Draw key icon (after highlight)
@@ -1783,7 +1791,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
if (totalEntries > visibleRows) {
int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4);
int totalScrollable = totalEntries;
int scrollTrackX = display->getWidth() - 6;
int scrollTrackX = display->getWidth() - 6 - BASEUI_BODY_LR_MARGIN;
display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight);
int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable;
int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable;
@@ -1801,8 +1809,8 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla
const int maxEmoteHeight = graphics::EmoteRenderer::maxEmoteHeight();
const int rowHeight = maxEmoteHeight + 2;
// Place header at top, then compute start of emote list
int headerY = y;
// Place header at top (pushed down by the header margin), then compute start of emote list
int headerY = y + BASEUI_HEADER_MARGIN;
int listTop = headerY + headerFontHeight + headerMargin;
int _visibleRows = (display->getHeight() - listTop - 2) / rowHeight;
@@ -1841,12 +1849,13 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla
// Draw highlight box 2px taller than emote (1px margin above and below)
if (emoteIdx == emotePickerIndex) {
display->fillRect(x, rowY, display->getWidth() - 8, emote.height + 2);
display->fillRect(x + BASEUI_BODY_LR_MARGIN, rowY, display->getWidth() - 8 - 2 * BASEUI_BODY_LR_MARGIN,
emote.height + 2);
display->setColor(BLACK);
}
// Emote bitmap (left), centered inside the row
int labelStartX = x + bitmapGapX;
int labelStartX = x + BASEUI_BODY_LR_MARGIN + bitmapGapX;
const int emoteY = rowY + ((rowHeight - emote.height) / 2);
display->drawXbm(labelStartX, emoteY, emote.width, emote.height, emote.bitmap);
labelStartX += emote.width;
@@ -1863,7 +1872,7 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla
// Draw scrollbar if needed
if (numEmotes > _visibleRows) {
int scrollbarHeight = _visibleRows * rowHeight;
int scrollTrackX = display->getWidth() - 6;
int scrollTrackX = display->getWidth() - 6 - BASEUI_BODY_LR_MARGIN;
display->drawRect(scrollTrackX, listTop, 4, scrollbarHeight);
int scrollBarLen = std::max(6, (scrollbarHeight * _visibleRows) / numEmotes);
int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes;
@@ -1900,7 +1909,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) {
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled.");
display->drawString(10 + x + BASEUI_BODY_LR_MARGIN, y + FONT_HEIGHT_SMALL + BASEUI_HEADER_MARGIN,
"Canned Message\nModule disabled.");
return;
}
@@ -1927,7 +1937,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
uint16_t charsLeft =
meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0);
snprintf(buffer, sizeof(buffer), "%d left", charsLeft);
display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer);
display->drawString(x + display->getWidth() - display->getStringWidth(buffer) - BASEUI_HEADER_LR_MARGIN,
y + BASEUI_HEADER_MARGIN, buffer);
}
#if INPUTBROKER_SERIAL_TYPE == 1
@@ -2014,9 +2025,11 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
// Draw Free Text input with multi-emote support and proper line wrapping
display->setColor(WHITE);
{
int inputY = 0 + y + FONT_HEIGHT_SMALL;
int inputY = y + FONT_HEIGHT_SMALL + BASEUI_HEADER_MARGIN;
int inputX = x + BASEUI_BODY_LR_MARGIN;
String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor);
drawWrappedEmoteText(display, x, inputY, msgWithCursor.c_str(), display->getWidth() - x, FONT_HEIGHT_SMALL);
drawWrappedEmoteText(display, inputX, inputY, msgWithCursor.c_str(),
display->getWidth() - inputX - BASEUI_BODY_LR_MARGIN, FONT_HEIGHT_SMALL);
}
#endif
return;
@@ -2037,7 +2050,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
drawHeader(display, x, y, buffer);
// Shift message list upward by 3 pixels to reduce spacing between header and first message
const int listYOffset = y + FONT_HEIGHT_SMALL - 3;
// Push the list below the header margin so the body starts clear of the reserved top area
const int listYOffset = y + FONT_HEIGHT_SMALL - 3 + BASEUI_HEADER_MARGIN;
_visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing;
// Figure out which messages are visible and their needed heights
@@ -2059,16 +2073,17 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2;
#ifdef USE_EINK
int nextX = x + (_highlight ? 12 : 0);
int nextX = x + BASEUI_BODY_LR_MARGIN + (_highlight ? 12 : 0);
if (_highlight)
display->drawString(x + 0, lineY + textYOffset, ">");
display->drawString(x + BASEUI_BODY_LR_MARGIN, lineY + textYOffset, ">");
#else
int scrollPadding = 8;
if (_highlight) {
display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight);
display->fillRect(x + BASEUI_BODY_LR_MARGIN, lineY,
display->getWidth() - scrollPadding - 2 * BASEUI_BODY_LR_MARGIN, rowHeight);
display->setColor(BLACK);
}
int nextX = x + (_highlight ? 2 : 0);
int nextX = x + BASEUI_BODY_LR_MARGIN + (_highlight ? 2 : 0);
#endif
if (msg && *msg)
@@ -2084,7 +2099,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
// Scrollbar
if (messagesCount > _visibleRows) {
int scrollHeight = display->getHeight() - listYOffset;
int scrollTrackX = display->getWidth() - 6;
int scrollTrackX = display->getWidth() - 6 - BASEUI_BODY_LR_MARGIN;
display->drawRect(scrollTrackX, listYOffset, 4, scrollHeight);
int barHeight = (scrollHeight * _visibleRows) / messagesCount;
int scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount;
+1 -1
View File
@@ -71,7 +71,7 @@ bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
if (config.position.fixed_position) {
LOG_DEBUG("Ignore own position update except time: position.fixed_position true");
#ifdef T_WATCH_S3
#if defined(T_WATCH_S3) || defined(T_WATCH_ULTRA)
// Since we return early if position.fixed_position is true, set the T-Watch's RTC to the time received from the
// client device here
if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) {
+8
View File
@@ -30,6 +30,9 @@
#ifdef HAS_STK8XXX
#include "STK8XXXSensor.h"
#endif
#ifdef HAS_BHI260AP
#include "BHI260APSensor.h"
#endif
extern ScanI2C::DeviceAddress accelerometer_found;
@@ -152,6 +155,11 @@ class AccelerometerThread : public concurrency::OSThread
case ScanI2C::DeviceType::QMI8658:
sensor.reset(new QMI8658Sensor(device));
break;
#endif
#ifdef HAS_BHI260AP
case ScanI2C::DeviceType::BHI260AP:
sensor.reset(new BHI260APSensor(device));
break;
#endif
default:
disable();
+80
View File
@@ -0,0 +1,80 @@
#include "BHI260APSensor.h"
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BHI260AP) && __has_include(<SensorBHI260AP.hpp>)
#define BOSCH_BHI260_KLIO
#define USING_DATA_HELPER
#include <BoschFirmware.h>
BHI260APSensor::BHI260APSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
// https://github.com/lewisxhe/SensorLib/blob/master/examples/Sensors/IMU/BHI260AP_InterruptSettings/BHI260AP_InterruptSettings.ino
bool BHI260APSensor::init()
{
LOG_WARN("Initializing BHI260AP sensor %u", deviceAddress());
sensor.setFirmware(bosch_firmware_image, bosch_firmware_size, bosch_firmware_type);
sensor.setBootFromFlash(bosch_firmware_type);
if (sensor.begin(Wire, deviceAddress())) {
sensor.setRemapAxes(SensorBHI260AP::TOP_LAYER_BOTTOM_RIGHT_CORNER);
BoschSensorInfo info = sensor.getSensorInfo();
LOG_INFO("Product ID : %02x\n", info.product_id);
LOG_INFO("Kernel version : %04u\n", info.kernel_version);
LOG_INFO("User version : %04u\n", info.user_version);
LOG_INFO("ROM version : %04u\n", info.rom_version);
LOG_INFO("Power state : %s\n", (info.host_status & BHY2_HST_POWER_STATE) ? "sleeping" : "active");
LOG_INFO("Host interface : %s\n", (info.host_status & BHY2_HST_HOST_PROTOCOL) ? "SPI" : "I2C");
LOG_INFO("Feature status : 0x%02x\n", info.feat_status);
stepCounter = new SensorStepCounter(sensor);
// stepDetector = new SensorStepDetector(sensor);
// sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE);
// sensor.enableAccelerometer();
// sensor.configInterrupt();
#ifdef BHI260AP_INT
pinMode(BHI260AP_INT, INPUT);
attachInterrupt(
BHI260AP_INT,
[] {
// Set interrupt to set irq value to true
},
RISING); // Select the interrupt mode according to the actual circuit
#endif
#ifdef T_WATCH_S3
// Need to raise the wrist function, need to set the correct axis
sensor.setRemapAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER);
#else
// sensor.setRemapAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER);
#endif
// stepDetector->enable(1.0, 0);
stepCounter->enable(1.0, 0);
LOG_DEBUG("BHI260AP init ok");
return true;
}
LOG_DEBUG("BHI260AP init failed");
return false;
}
int32_t BHI260APSensor::runOnce()
{
sensor.update();
if (stepCounter->hasUpdated()) {
steps = stepCounter->getStepCount();
LOG_WARN("Step count updated: %u", steps);
if (screen)
screen->steps = steps;
}
// LOG_WARN("Step count: %u", stepCounter->getStepCount());
// if (sensor.readIrqStatus()) {
// if (sensor.isTilt() || sensor.isDoubleTap()) {
// wakeScreen();
// return 500;
// }
//}
return 1000;
}
#endif
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#ifndef _BHI260AP_SENSOR_H_
#define _BHI260AP_SENSOR_H_
#include "MotionSensor.h"
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BHI260AP) && __has_include(<SensorBHI260AP.hpp>)
// Sensor lib
#include <SensorBHI260AP.hpp>
#include <Wire.h>
#include <bosch/BoschSensorDataHelper.hpp>
class BHI260APSensor : public MotionSensor
{
private:
SensorBHI260AP sensor;
volatile bool BHI_IRQ = false;
SensorStepCounter *stepCounter;
SensorStepDetector *stepDetector;
uint32_t steps = 0;
public:
explicit BHI260APSensor(ScanI2C::FoundDevice foundDevice);
virtual bool init() override;
virtual int32_t runOnce() override;
};
#endif
#endif
+2
View File
@@ -208,6 +208,8 @@
#define HW_VENDOR meshtastic_HardwareModel_M5STACK_C6L
#elif defined(HELTEC_WIRELESS_TRACKER_V2)
#define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2
#elif defined(T_WATCH_ULTRA)
#define HW_VENDOR meshtastic_HardwareModel_T_WATCH_ULTRA
#elif defined(M5STACK_CARDPUTER_ADV)
#define HW_VENDOR meshtastic_HardwareModel_M5STACK_CARDPUTER_ADV
#elif defined(MESHNOLOGY_W10)
@@ -0,0 +1,42 @@
// Workaround for the IDF 5.5 manual esp_flash read regression on t-watch-ultra.
//
// On this board (Winbond W25Q128JW, ef:8018), the IDF 5.5 *direct* flash read path
// (esp_flash_read / esp_partition_read) returns 0x00 for data that is physically
// correct on flash. We proved the *memory-mapped* (cache) read returns the right
// data, writes work, and it's not read-mode/HPM/timing-tuning/PSRAM. So route every
// esp_partition_read through esp_partition_mmap + memcpy, which uses the working
// cache path. Activated by `-Wl,--wrap=esp_partition_read` (t-watch-ultra only).
#if defined(T_WATCH_ULTRA)
#include "esp_partition.h"
#include <string.h>
extern esp_err_t __real_esp_partition_read(const esp_partition_t *partition, size_t src_offset, void *dst, size_t size);
esp_err_t __wrap_esp_partition_read(const esp_partition_t *partition, size_t src_offset, void *dst, size_t size)
{
if (partition == NULL || dst == NULL)
return ESP_ERR_INVALID_ARG;
if (size == 0)
return ESP_OK;
// mmap requires a 64KB-aligned start; map the containing page span and copy
// out from the requested offset.
const size_t PAGE = 0x10000;
size_t aligned = src_offset & ~(PAGE - 1);
size_t delta = src_offset - aligned;
const void *ptr = NULL;
esp_partition_mmap_handle_t handle;
esp_err_t err = esp_partition_mmap(partition, aligned, delta + size, ESP_PARTITION_MMAP_DATA, &ptr, &handle);
if (err != ESP_OK) {
// Encrypted partitions / regions mmap can't serve: fall back to the real
// read (may be wrong on this board, but better than failing the call).
return __real_esp_partition_read(partition, src_offset, dst, size);
}
memcpy(dst, (const uint8_t *)ptr + delta, size);
esp_partition_munmap(handle);
return ESP_OK;
}
#endif // T_WATCH_ULTRA
+2
View File
@@ -7,6 +7,8 @@ This directory tree is designed to solve two problems.
So we are borrowing the initVariant() ideas here (by using weak gcc references). You can now define earlyInitVariant() and lateInitVariant() if your board needs them. earlyInitVariant() runs at the beginning of setup() directly after waitUntilPowerLevelSafe(); while lateInitVariant() runs after the LoRa radio is initialized.
**Important:** earlyInitVariant() runs _before_ consoleInit(), so the logging subsystem isn't set up yet. Calling a `LOG_*` macro there **crashes the device** - it is not a silent no-op. Never use `LOG_*` in earlyInitVariant(); defer any logging to lateInitVariant() or later.
If you'd like a board specific variant to be run, add the variant.cpp file to an appropriately named
subdirectory and check for \_VARIANT_boardname in the cpp file (so that your code is only built for your board).
You'll need to define \_VARIANT_boardname in your corresponding variant.h file.
@@ -0,0 +1,83 @@
#include "configuration.h"
#ifdef T_WATCH_ULTRA
// Board-specific init lives here (rather than in variants/esp32s3/t-watch-ultra/variant.cpp)
// so that PlatformIO's library dependency finder can resolve headers such as
// input/TouchScreenImpl1.h (which transitively pulls in the ArduinoThread "Thread.h"),
// ExtensionIOXL9555.hpp and TouchDrvCSTXXX.hpp. Files compiled from outside src/ only get
// include paths for libraries they reference directly, so the transitive Thread.h include
// is not found there. See src/platform/extra_variants/README.md.
#include "TouchDrvCSTXXX.hpp"
#include "input/TouchScreenImpl1.h"
#include <ExtensionIOXL9555.hpp>
#include <Wire.h>
static ExtensionIOXL9555 io;
static TouchDrvCST92xx touchDrv;
void earlyInitVariant()
{
pinMode(LORA_CS, OUTPUT);
digitalWrite(LORA_CS, HIGH);
pinMode(DISP_CS, OUTPUT);
digitalWrite(DISP_CS, HIGH);
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH);
pinMode(NFC_CS, OUTPUT);
digitalWrite(NFC_CS, HIGH);
pinMode(I2C_SDA, INPUT_PULLUP);
pinMode(I2C_SCL, INPUT_PULLUP);
if (io.begin(Wire, XL9555_SLAVE_ADDRESS0)) {
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
delay(1);
io.pinMode(EXPANDS_DISP_EN, OUTPUT);
io.digitalWrite(EXPANDS_DISP_EN, HIGH);
delay(1);
io.pinMode(EXPANDS_TOUCH_RST, OUTPUT);
io.digitalWrite(EXPANDS_TOUCH_RST, LOW);
delay(20);
io.digitalWrite(EXPANDS_TOUCH_RST, HIGH);
delay(60);
io.pinMode(EXPANDS_LORA_RF_SW, OUTPUT);
io.digitalWrite(EXPANDS_LORA_RF_SW, HIGH); // set RF switch to built-in LoRa antenna
// io.pinMode(EXPANDS_SD_DET, INPUT);
}
// NOTE: deliberately no LOG_* on the io.begin() failure path. earlyInitVariant() runs
// before consoleInit(), where calling a LOG_* macro crashes the device (see
// extra_variants/README.md). On failure the EXPANDS_* pins stay on their defaults.
}
static bool readTouch(int16_t *x, int16_t *y)
{
int16_t x_array[1], y_array[1];
uint8_t touched = touchDrv.getPoint(x_array, y_array, 1);
if (touched > 0) {
*x = (x_array[0]);
*y = (y_array[0]);
// Check bounds
if (*x < 0 || *x >= TFT_WIDTH || *y < 0 || *y >= TFT_HEIGHT) {
return false;
}
return true; // Valid touch detected
}
return false; // No valid touch data
}
void lateInitVariant()
{
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP);
touchDrv.setPins(-1, SCREEN_TOUCH_INT);
if (touchDrv.begin(Wire, TOUCH_SLAVE_ADDRESS, -1, -1)) {
touchScreenImpl1 = new TouchScreenImpl1(TFT_WIDTH, TFT_HEIGHT, readTouch);
touchScreenImpl1->init();
} else {
LOG_ERROR("failed to initialize CST92xx");
}
}
}
#endif
+1 -1
View File
@@ -370,7 +370,7 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
// t-beam v1.2 radio power channel
PMU->disablePowerOutput(XPOWERS_ALDO2); // lora radio power channel
} else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE ||
HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) {
HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3 || HW_VENDOR == meshtastic_HardwareModel_T_WATCH_ULTRA) {
PMU->disablePowerOutput(XPOWERS_ALDO3); // lora radio power channel
}
} else if (model == XPOWERS_AXP192) {
+1
View File
@@ -30,6 +30,7 @@
// vibration motor
#define PIN_VIBRATION 2
#define HAS_DRV2605 1
// Have SPI interface SD card slot
#define HAS_SDCARD
@@ -0,0 +1,94 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
// #ifndef digitalPinToInterrupt
// #define digitalPinToInterrupt(p) (((p) < 48) ? (p) : -1)
// #endif
#define USB_VID 0x303a
#define USB_PID 0x8227
#define USB_MANUFACTURER "LILYGO"
#define USB_PRODUCT "T-Watch-Ultra"
#define DISP_WIDTH 502
#define DISP_HEIGHT 410
// QSPI interface display
#define DISP_D0 (38)
#define DISP_D1 (39)
#define DISP_D2 (42)
#define DISP_D3 (45)
#define DISP_SCK (40)
#define DISP_CS (41)
#define DISP_RST (37)
#define DISP_TE (6)
// Interrupt IO port
#define TP_INT (12)
#define RTC_INT (1)
#define PMU_INT (7)
#define NFC_INT (5)
#define SENSOR_INT (8)
#define NFC_CS (4)
// PDM microphone
#define MIC_SCK (17)
#define MIC_DAT (18)
// MAX98357A
#define I2S_BCLK (9)
#define I2S_WCLK (10)
#define I2S_DOUT (11)
#define SD_CS (21)
// TX, RX pin connected to GPS
static const uint8_t TX = 43;
static const uint8_t RX = 44;
// BHI260,PCF85063,AXP2101,DRV2605L share I2C Bus
static const uint8_t SDA = 3;
static const uint8_t SCL = 2;
// Default sd cs pin
static const uint8_t SS = SD_CS;
static const uint8_t MOSI = 34;
static const uint8_t MISO = 33;
static const uint8_t SCK = 35;
#define GPS_TX (TX)
#define GPS_RX (RX)
#define GPS_PPS (13)
#define TP_SDA (SDA)
#define TP_SCL (SCL)
// LoRa and SD card share SPI bus -> variant.h
// #define LORA_SCK (SCK) // share spi bus
// #define LORA_MISO (MISO) // share spi bus
// #define LORA_MOSI (MOSI) // share spi bus
// #define LORA_CS (36)
// #define LORA_RST (47)
// #define LORA_BUSY (48)
// #define LORA_IRQ (14)
// External expansion chip IO definition
#define EXPANDS_DRV_EN (6)
#define EXPANDS_DISP_EN (7)
#define EXPANDS_TOUCH_RST (8)
#define EXPANDS_SD_DET (10)
#define EXPANDS_LORA_RF_SW (11)
// Peripheral definition exists
#define USING_XL9555_EXPANDS
#define USING_PCM_AMPLIFIER
#define USING_PDM_MICROPHONE
#define USING_PMU_MANAGE
#define USING_INPUT_DEV_TOUCHPAD
#define USING_ST25R3916
#define USING_BHI260_SENSOR
#define HAS_SD_CARD_SOCKET
#endif /* Pins_Arduino_h */
@@ -0,0 +1,96 @@
; LilyGo T-Watch S3
[env:t-watch-ultra]
custom_meshtastic_hw_model = 114
custom_meshtastic_hw_model_slug = T_WATCH_ULTRA
custom_meshtastic_architecture = esp32-s3
custom_meshtastic_actively_supported = true
custom_meshtastic_support_level = 1
custom_meshtastic_display_name = T-Watch Ultra
custom_meshtastic_images = t-watch-ultra.svg
custom_meshtastic_tags = LilyGo
custom_meshtastic_requires_dfu = false
custom_meshtastic_partition_scheme = 16MB
custom_meshtastic_has_mui = true
extends = esp32s3_base
board = t-watch-ultra
board_level = release
board_build.partitions = default_16MB.csv
upload_protocol = esptool
custom_sdkconfig =
${esp32s3_base.custom_sdkconfig}
; Keep esp_littlefs buffers in internal RAM (off the shared-bus PSRAM).
CONFIG_LITTLEFS_MALLOC_STRATEGY_INTERNAL=y
CONFIG_SPI_FLASH_SHARE_SPI1_BUS=y
build_flags = ${esp32_base.build_flags} -Ivariants/esp32s3/t-watch-ultra
; Route flash reads through the cache/mmap path (esp_partition_read_mmap_wrap.c)
; to dodge the IDF 5.5 manual-read regression on this board's flash.
-Wl,--wrap=esp_partition_read
-D T_WATCH_ULTRA
-D RADIOLIB_EXCLUDE_SX128X=1
-D RADIOLIB_EXCLUDE_SX127X=1
-D RADIOLIB_EXCLUDE_LR11X0=1
-UMESHTASTIC_EXCLUDE_ACCELEROMETER
-D HAS_SDCARD
-D SDCARD_USE_SPI1
-D SD_SPI_FREQUENCY=75000000
-D SPI_MISO=33
-D SPI_MOSI=34
-D SPI_SCK=35
-D SDCARD_CS=21
; -DHAS_BMA423=1
build_src_filter =
${esp32s3_base.build_src_filter}
+<../variants/esp32s3/t-watch-ultra>
lib_deps = ${esp32s3_base.lib_deps}
https://github.com/lovyan03/LovyanGFX/archive/tags/1.2.27.zip
adafruit/Adafruit DRV2605 Library@^1.2.4
# renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix
https://github.com/earlephilhower/ESP8266Audio/archive/05f2fb0045cc294b4e0d1a1a9747b89c22c1fea4.zip
# renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM
earlephilhower/ESP8266SAM@1.1.0
lewisxhe/SensorLib@0.3.1
[env:t-watch-ultra-tft]
board_level = extra
extends = env:t-watch-ultra
build_flags =
${env:t-watch-ultra.build_flags}
-D CONFIG_DISABLE_HAL_LOCKS=1
-D INPUTDRIVER_BUTTON_TYPE=0
-D HAS_SCREEN=1
-D HAS_TFT=1
-D USE_I2S_BUZZER
-D RAM_SIZE=5120
-D LV_LVGL_H_INCLUDE_SIMPLE
-D LV_CONF_INCLUDE_SIMPLE
-D LV_COMP_CONF_INCLUDE_SIMPLE
-D LV_USE_SYSMON=0
-D LV_USE_PROFILER=0
-D LV_USE_PERF_MONITOR=0
-D LV_USE_MEM_MONITOR=0
-D LV_USE_LOG=0
-D USE_LOG_DEBUG
-D LOG_DEBUG_INC=\"DebugConfiguration.h\"
-D RADIOLIB_SPI_PARANOID=0
-D LGFX_SCREEN_WIDTH=410
-D LGFX_SCREEN_HEIGHT=502
-D LGFX_AMOLED_ROUNDER=1
-D LGFX_BUFSIZE=308732
-D DISPLAY_SIZE=410x502 ; portrait mode
-D DISPLAY_SET_RESOLUTION
-D LGFX_DRIVER=LGFX_TWATCH_ULTRA
-D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_T_WATCH_ULTRA.h\"
; -D LVGL_DRIVER=LVGL_T_WATCH_ULTRA
-D VIEW_320x240
-D USE_PACKET_API
-D MAP_FULL_REDRAW
-D CUSTOM_TOUCH_DRIVER
lib_deps =
${env:t-watch-ultra.lib_deps}
${device-ui_base.lib_deps}
+101
View File
@@ -0,0 +1,101 @@
// CO5300 TFT AMOLED
#define CO5300_CS 41
#define CO5300_SCK 40
#define CO5300_RESET 37
#define CO5300_TE 6
#define CO5300_IO0 38
#define CO5300_IO1 39
#define CO5300_IO2 42
#define CO5300_IO3 45
#define CO5300_SPI_HOST SPI2_HOST
#define SPI_FREQUENCY 75000000
#define SPI_READ_FREQUENCY 16000000 // irrelevant
#define TFT_HEIGHT 502
#define TFT_WIDTH 410
#define TFT_OFFSET_X 22
#define TFT_OFFSET_Y 0
#define TFT_OFFSET_ROTATION 0
#define SCREEN_TRANSITION_FRAMERATE 5 // fps
#define USE_TFTDISPLAY 1
#define HAS_SCREEN 1
#define TFT_RESET_AFTER_SLEEP
#define OLED_HUGE
#define ROUNDED_SCREEN true
#define BASEUI_HEADER_MARGIN 15
#define BASEUI_HEADER_LR_MARGIN 55
#define BASEUI_BELOW_HEADER_MARGIN 15
#define BASEUI_BODY_LR_MARGIN 35
#define HAS_TOUCHSCREEN 1
#define HAS_SPI_TFT 1
#define ENABLE_TOUCH_INT 1
#define VARIANT_TOUCHSCREEN 1
#define SCREEN_TOUCH_INT 12
#define TOUCH_I2C_PORT 0
#define TOUCH_SLAVE_ADDRESS 0x1A
#define WAKE_ON_TOUCH
#define BUTTON_PIN 0
#define USE_POWERSAVE
#define SLEEP_TIME 120
// External expansion chip XL9555
#define USE_XL9555
// PCF85063 RTC Module
#define PCF85063_RTC 0x51
#define HAS_RTC 1
// MAX98357A
#define HAS_I2S
#define DAC_I2S_BCK 9
#define DAC_I2S_WS 10
#define DAC_I2S_DOUT 11
#define DAC_I2S_MCLK -1 // TODO
#define HAS_AXP2101
#define PMU_IRQ 7
#define PMU_POWER_BUTTON_IS_CANCEL
#define HAS_DRV2605 1
#define HAS_BHI260AP
#define BHI260AP_INT 8
#undef MESHTASTIC_EXCLUDE_ACCELEROMETER
#define SHOW_STEP_COUNTER
#define I2C_SDA 3
#define I2C_SCL 2
#define I2C_NO_RESCAN
#define HAS_GPS 1
#define GPS_BAUDRATE 38400
#define GPS_RX_PIN 44
#define GPS_TX_PIN 43
#define PIN_GPS_PPS 13
#define USE_SX1262
// #define USE_SX1280
#define HW_SPI1_DEVICE
#define LORA_SCK 35
#define LORA_MISO 33
#define LORA_MOSI 34
#define LORA_CS 36
#define LORA_DIO0 -1 // a No connect on the SX1262 module
#define LORA_RESET 47
#define LORA_DIO1 14 // SX1262 IRQ
#define LORA_DIO2 48 // SX1262 BUSY
#define LORA_DIO3
#define SX126X_CS LORA_CS
#define SX126X_DIO1 LORA_DIO1
#define SX126X_BUSY LORA_DIO2
#define SX126X_RESET LORA_RESET
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
#define USE_VIRTUAL_KEYBOARD 1
#define DISPLAY_CLOCK_FRAME 1
+1 -1
View File
@@ -59,7 +59,7 @@ static const uint8_t A0 = PIN_A0;
#define WIRE_INTERFACES_COUNT 1
#define PIN_WIRE_SDA (0 + 26)
#define PIN_WIRE_SCL (0 + 27)
#define HAS_BHI260AP
// #define HAS_BHI260AP ; lewisxhe/SensorLib too big for nrf52
#define TP_SER_IO (0 + 11)