Add T-Deck Pro board bring-up and sync pending UI fixes

This commit is contained in:
liu weikai committed 2026-03-28 10:53:25 +08:00
1 parent f6a70d0860
commit e2a586bc1d
19 files changed
+1560 -59

No files matched your search

+2
View File
@@ -194,7 +194,9 @@ set(trail_mate_idf_ui_shared_sources
set(trail_mate_idf_include_dirs
"include"
"${CMAKE_SOURCE_DIR}/platform/shared/include"
"${CMAKE_SOURCE_DIR}/boards/tab5/include"
"${CMAKE_SOURCE_DIR}/boards/t_display_p4/include"
"${CMAKE_SOURCE_DIR}/modules/ui_shared/include"
"${CMAKE_SOURCE_DIR}/modules/core_sys/include"
"${CMAKE_SOURCE_DIR}/modules/core_chat/include"
+38
View File
@@ -0,0 +1,38 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_qspi"
},
"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",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "LilyGo T-Deck Pro (16M Flash 8M QSPI PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.lilygo.cc",
"vendor": "LilyGo"
}
@@ -0,0 +1,221 @@
#pragma once
#include <cstdint>
namespace boards::tdeck_pro
{
struct BoardProfile
{
struct ProductIdentity
{
const char* long_name;
const char* short_name;
const char* ble_name;
};
struct I2cPins
{
int sda;
int scl;
};
struct SpiPins
{
int sck;
int mosi;
int miso;
};
struct UartPins
{
int tx;
int rx;
uint32_t baud;
};
struct EpdPins
{
int cs;
int dc;
int rst;
int busy;
};
struct TouchPins
{
int int_pin;
int rst;
uint8_t i2c_addr;
};
struct KeyboardPins
{
int int_pin;
int led;
uint8_t i2c_addr;
uint8_t rows;
uint8_t cols;
};
struct LoraPins
{
int cs;
int busy;
int rst;
int irq;
int enable;
};
struct GpsPins
{
int enable;
int pps;
UartPins uart;
};
struct MotionPins
{
int enable_1v8;
int int_pin;
uint8_t i2c_addr;
};
struct SdPins
{
int cs;
};
struct OptionalModulePins
{
bool has_a7682e;
bool has_pcm512a;
int a7682e_enable;
int a7682e_pwrkey;
int a7682e_rst;
int a7682e_ri;
int a7682e_dtr;
UartPins a7682e_uart;
int i2s_bclk;
int i2s_lrc;
int i2s_dout;
};
ProductIdentity identity;
I2cPins i2c;
SpiPins spi;
EpdPins epd;
TouchPins touch;
KeyboardPins keyboard;
LoraPins lora;
GpsPins gps;
MotionPins motion;
SdPins sd;
OptionalModulePins optional;
int motor_pin;
int keyboard_default_brightness;
int screen_width;
int screen_height;
};
inline BoardProfile makeA7682EProfile()
{
BoardProfile profile{};
profile.identity.long_name = "Trail Mate T-Deck Pro A7682E";
profile.identity.short_name = "TDP4G";
profile.identity.ble_name = "TrailMate-TDeckPro-4G";
profile.i2c.sda = 13;
profile.i2c.scl = 14;
profile.spi.sck = 36;
profile.spi.mosi = 33;
profile.spi.miso = 47;
profile.epd.cs = 34;
profile.epd.dc = 35;
profile.epd.rst = -1;
profile.epd.busy = 37;
profile.touch.int_pin = 12;
profile.touch.rst = 45;
profile.touch.i2c_addr = 0x1A;
profile.keyboard.int_pin = 15;
profile.keyboard.led = 42;
profile.keyboard.i2c_addr = 0x34;
profile.keyboard.rows = 4;
profile.keyboard.cols = 10;
profile.lora.cs = 3;
profile.lora.busy = 6;
profile.lora.rst = 4;
profile.lora.irq = 5;
profile.lora.enable = 46;
profile.gps.enable = 39;
profile.gps.pps = 1;
profile.gps.uart.tx = 43;
profile.gps.uart.rx = 44;
profile.gps.uart.baud = 38400;
profile.motion.enable_1v8 = 38;
profile.motion.int_pin = 21;
profile.motion.i2c_addr = 0x28;
profile.sd.cs = 48;
profile.optional.has_a7682e = true;
profile.optional.has_pcm512a = false;
profile.optional.a7682e_enable = 41;
profile.optional.a7682e_pwrkey = 40;
profile.optional.a7682e_rst = 9;
profile.optional.a7682e_ri = 7;
profile.optional.a7682e_dtr = 8;
profile.optional.a7682e_uart.tx = 11;
profile.optional.a7682e_uart.rx = 10;
profile.optional.a7682e_uart.baud = 115200;
profile.optional.i2s_bclk = -1;
profile.optional.i2s_lrc = -1;
profile.optional.i2s_dout = -1;
profile.motor_pin = 2;
profile.keyboard_default_brightness = 96;
profile.screen_width = 240;
profile.screen_height = 320;
return profile;
}
inline BoardProfile makePCM512AProfile()
{
BoardProfile profile = makeA7682EProfile();
profile.identity.long_name = "Trail Mate T-Deck Pro PCM512A";
profile.identity.short_name = "TDPA";
profile.identity.ble_name = "TrailMate-TDeckPro-Audio";
profile.optional.has_a7682e = false;
profile.optional.has_pcm512a = true;
profile.optional.a7682e_enable = -1;
profile.optional.a7682e_pwrkey = -1;
profile.optional.a7682e_rst = -1;
profile.optional.a7682e_ri = -1;
profile.optional.a7682e_dtr = -1;
profile.optional.a7682e_uart.tx = -1;
profile.optional.a7682e_uart.rx = -1;
profile.optional.a7682e_uart.baud = 0;
profile.optional.i2s_bclk = 7;
profile.optional.i2s_lrc = 9;
profile.optional.i2s_dout = 8;
profile.motor_pin = 40;
return profile;
}
#if defined(TRAIL_MATE_TDECK_PRO_A7682E)
static const BoardProfile kBoardProfile = makeA7682EProfile();
#elif defined(TRAIL_MATE_TDECK_PRO_PCM512A)
static const BoardProfile kBoardProfile = makePCM512AProfile();
#else
#error "T-Deck Pro profile requires TRAIL_MATE_TDECK_PRO_A7682E or TRAIL_MATE_TDECK_PRO_PCM512A"
#endif
} // namespace boards::tdeck_pro
@@ -0,0 +1,36 @@
#pragma once
#include "boards/tdeck_pro/tdeck_pro_board.h"
#include "platform/esp/boards/board_runtime.h"
#include "ui/LV_Helper.h"
namespace platform::esp::boards::detail
{
inline void initializeBoard(bool waking_from_sleep)
{
(void)waking_from_sleep;
::boards::tdeck_pro::board.begin();
}
inline void initializeDisplay()
{
beginLvglHelper(static_cast<LilyGo_Display&>(::boards::tdeck_pro::instance));
}
inline bool tryResolveAppContextInitHandles(AppContextInitHandles* out_handles)
{
if (!out_handles)
{
return false;
}
*out_handles = resolveAppContextInitHandles();
return true;
}
inline AppContextInitHandles resolveAppContextInitHandles()
{
return {&::boards::tdeck_pro::board, &::boards::tdeck_pro::instance, &::boards::tdeck_pro::instance, &::boards::tdeck_pro::instance};
}
} // namespace platform::esp::boards::detail
@@ -0,0 +1,166 @@
#pragma once
#define XPOWERS_CHIP_BQ25896
#include <Adafruit_TCA8418.h>
#include <GxEPD2_BW.h>
#include <GaugeBQ27220.hpp>
#include <RadioLib.h>
#include <SensorBHI260AP.hpp>
#include <TouchDrvCSTXXX.hpp>
#include <XPowersLib.h>
#include <SPI.h>
#include <memory>
#include <vector>
#include "board/BoardBase.h"
#include "board/GpsBoard.h"
#include "board/LoraBoard.h"
#include "board/MotionBoard.h"
#include "board/SdBoard.h"
#include "board/TLoRaPagerTypes.h"
#include "boards/tdeck_pro/board_profile.h"
#include "display/DisplayInterface.h"
#include "platform/esp/arduino_common/gps/GPS.h"
namespace boards::tdeck_pro
{
class SX1262Access : public SX1262
{
public:
using SX1262::SX1262;
using SX126x::readRegister;
using SX126x::setTxParams;
using SX126x::writeRegister;
};
class TDeckProBoard : public BoardBase,
public LoraBoard,
public GpsBoard,
public MotionBoard,
public SdBoard,
public LilyGo_Display
{
public:
static TDeckProBoard* getInstance();
static constexpr const BoardProfile& profile()
{
return kBoardProfile;
}
uint32_t begin(uint32_t disable_hw_init = 0) override;
void wakeUp() override {}
void handlePowerButton() override {}
void softwareShutdown() override {}
void setBrightness(uint8_t level) override;
uint8_t getBrightness() override { return brightness_; }
bool hasKeyboard() override { return keyboard_ready_; }
void keyboardSetBrightness(uint8_t level) override;
uint8_t keyboardGetBrightness() override { return keyboard_brightness_; }
bool isRTCReady() const override;
bool isCharging() override;
int getBatteryLevel() override;
bool isSDReady() const override { return sd_ready_; }
bool isCardReady() override;
bool isGPSReady() const override { return gps_ready_; }
void vibrator() override;
void stopVibrator() override;
void playMessageTone() override;
void setMessageToneVolume(uint8_t volume_percent) override;
uint8_t getMessageToneVolume() const override;
void setRotation(uint8_t rotation) override;
uint8_t getRotation() override { return rotation_; }
void pushColors(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t* color) override;
uint16_t width() override;
uint16_t height() override;
bool hasTouch() override { return touch_ready_; }
uint8_t getPoint(int16_t* x, int16_t* y, uint8_t get_point) override;
int getKeyChar(char* c) override;
bool isRadioOnline() const override { return radio_ready_; }
int transmitRadio(const uint8_t* data, size_t len) override;
int startRadioReceive() override;
uint32_t getRadioIrqFlags() override;
int getRadioPacketLength(bool update) override;
int readRadioData(uint8_t* buf, size_t len) override;
void clearRadioIrqFlags(uint32_t flags) override;
float getRadioRSSI() override;
float getRadioSNR() override;
void configureLoraRadio(float freq_mhz, float bw_khz, uint8_t sf, uint8_t cr_denom,
int8_t tx_power, uint16_t preamble_len, uint8_t sync_word,
uint8_t crc_len) override;
bool initGPS() override;
void setGPSOnline(bool online) override { gps_ready_ = online; }
GPS& getGPS() override { return gps_; }
void powerControl(PowerCtrlChannel_t ch, bool enable) override;
bool syncTimeFromGPS(uint32_t gps_task_interval_ms = 0) override;
SensorBHI260AP& getMotionSensor() override { return motion_; }
bool isSensorReady() const override { return motion_ready_; }
bool hasCellularModem() const { return profile().optional.has_a7682e; }
bool hasAudioDac() const { return profile().optional.has_pcm512a; }
private:
TDeckProBoard();
void initSharedPins();
bool initPower();
bool initDisplay();
bool initTouch();
bool initKeyboard();
bool initMotion();
bool initRadio();
bool initStorage();
bool installSD() override;
void uninstallSD() override;
void renderEpd();
void setBit(int16_t x, int16_t y, bool black);
bool keyEventToChar(uint8_t event, char* c, bool* pressed);
using EpdPanel = GxEPD2_BW<GxEPD2_310_GDEQ031T10, GxEPD2_310_GDEQ031T10::HEIGHT>;
uint8_t brightness_ = DEVICE_MAX_BRIGHTNESS_LEVEL;
uint8_t keyboard_brightness_ = static_cast<uint8_t>(kBoardProfile.keyboard_default_brightness);
uint8_t message_tone_volume_ = 45;
uint8_t rotation_ = 0;
bool power_ready_ = false;
bool display_ready_ = false;
bool touch_ready_ = false;
bool keyboard_ready_ = false;
bool radio_ready_ = false;
bool sd_ready_ = false;
bool gps_ready_ = false;
bool motion_ready_ = false;
bool rtc_ready_ = false;
bool battery_gauge_ready_ = false;
int last_battery_level_ = -1;
SPIClass& shared_spi_ = SPI;
Module lora_module_{static_cast<uint32_t>(profile().lora.cs),
static_cast<uint32_t>(profile().lora.irq),
static_cast<uint32_t>(profile().lora.rst),
static_cast<uint32_t>(profile().lora.busy),
shared_spi_};
SX1262Access radio_{&lora_module_};
EpdPanel epd_{GxEPD2_310_GDEQ031T10(profile().epd.cs, profile().epd.dc, profile().epd.rst, profile().epd.busy)};
TouchDrvCSTXXX touch_;
Adafruit_TCA8418 keyboard_;
GPS gps_;
SensorBHI260AP motion_;
PowersBQ25896 pmu_;
GaugeBQ27220 gauge_;
std::vector<uint8_t> mono_buffer_;
};
extern TDeckProBoard& instance;
extern BoardBase& board;
} // namespace boards::tdeck_pro
+57
View File
@@ -0,0 +1,57 @@
{
"name": "boards_tdeck_pro",
"version": "0.1.0",
"frameworks": [
"arduino"
],
"platforms": [
"espressif32"
],
"build": {
"includeDir": "include",
"srcDir": "src",
"flags": [
"-std=gnu++17",
"-Iinclude",
"-I../..",
"-I../../platform/shared/include",
"-I../../platform/esp/boards/include",
"-I../../modules/ui_shared/include",
"-I../../modules/core_sys/include",
"-I../../modules/core_chat/include",
"-I../../modules/core_gps/include",
"-I../../modules/core_hostlink/include",
"-I../../modules/core_team/include",
"-I../../platform/esp/arduino_common/include"
]
},
"dependencies": [
{
"name": "lvgl"
},
{
"name": "SensorLib"
},
{
"name": "XPowersLib"
},
{
"name": "RadioLib"
},
{
"name": "TinyGPSPlus"
},
{
"name": "GxEPD2"
},
{
"name": "Adafruit TCA8418"
},
{
"name": "Adafruit BusIO"
},
{
"name": "ESP8266Audio"
}
]
}
+851
View File
@@ -0,0 +1,851 @@
#if defined(ARDUINO_T_DECK_PRO)
#include "boards/tdeck_pro/tdeck_pro_board.h"
#include <Arduino.h>
#include <AudioFileSourcePROGMEM.h>
#include <AudioGeneratorRTTTL.h>
#include <AudioOutputI2S.h>
#include <SD.h>
#include <SPI.h>
#include <Wire.h>
#include <ctime>
#include <limits>
#include <sys/time.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
namespace boards::tdeck_pro
{
namespace
{
constexpr const char* kTag = "TDeckProBoard";
constexpr time_t kMinValidEpochSeconds = 1577836800; // 2020-01-01 UTC
constexpr uint8_t kKeyboardRows = 4;
constexpr uint8_t kKeyboardCols = 10;
constexpr uint32_t kEpdSpiHz = 2000000;
constexpr uint8_t kFlushLogLimit = 8;
SemaphoreHandle_t g_shared_spi_mutex = nullptr;
void sharedSpiReleaseAllCs()
{
digitalWrite(TDeckProBoard::profile().lora.cs, HIGH);
digitalWrite(TDeckProBoard::profile().sd.cs, HIGH);
digitalWrite(TDeckProBoard::profile().epd.cs, HIGH);
}
void sharedSpiBusInit()
{
if (g_shared_spi_mutex == nullptr)
{
g_shared_spi_mutex = xSemaphoreCreateRecursiveMutex();
if (g_shared_spi_mutex == nullptr)
{
Serial.printf("[%s] shared SPI mutex create failed\n", kTag);
return;
}
}
sharedSpiReleaseAllCs();
}
void sharedSpiLock()
{
if (g_shared_spi_mutex == nullptr)
{
sharedSpiBusInit();
}
if (g_shared_spi_mutex != nullptr)
{
xSemaphoreTakeRecursive(g_shared_spi_mutex, portMAX_DELAY);
}
}
void sharedSpiUnlock()
{
if (g_shared_spi_mutex != nullptr)
{
sharedSpiReleaseAllCs();
xSemaphoreGiveRecursive(g_shared_spi_mutex);
}
}
void sharedSpiPrepareDevice(int cs_pin)
{
sharedSpiReleaseAllCs();
if (cs_pin >= 0)
{
digitalWrite(cs_pin, HIGH);
}
}
int batteryPercentFromMv(int mv)
{
if (mv <= 0)
{
return -1;
}
int pct = static_cast<int>(((mv - 3300) / 900.0f) * 100.0f);
if (pct < 0)
{
pct = 0;
}
if (pct > 100)
{
pct = 100;
}
return pct;
}
bool isLeapYear(int year)
{
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}
uint8_t daysInMonth(int year, uint8_t month)
{
static constexpr uint8_t kDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (month < 1 || month > 12)
{
return 0;
}
if (month == 2 && isLeapYear(year))
{
return 29;
}
return kDays[month - 1];
}
bool gpsDatetimeValid(int year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second)
{
if (year < 2020 || year > 2100)
{
return false;
}
if (month < 1 || month > 12)
{
return false;
}
const uint8_t max_day = daysInMonth(year, month);
if (day < 1 || day > max_day)
{
return false;
}
return hour < 24 && minute < 60 && second < 60;
}
int64_t daysFromCivil(int year, unsigned month, unsigned day)
{
year -= month <= 2;
const int era = (year >= 0 ? year : year - 399) / 400;
const unsigned yoe = static_cast<unsigned>(year - era * 400);
const unsigned doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1;
const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
return static_cast<int64_t>(era) * 146097 + static_cast<int64_t>(doe) - 719468;
}
time_t gpsDatetimeToEpochUtc(int year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second)
{
const int64_t days = daysFromCivil(year, month, day);
const int64_t sec_of_day = static_cast<int64_t>(hour) * 3600 +
static_cast<int64_t>(minute) * 60 +
static_cast<int64_t>(second);
const int64_t epoch64 = days * 86400 + sec_of_day;
if (epoch64 < 0 || epoch64 > static_cast<int64_t>(std::numeric_limits<time_t>::max()))
{
return static_cast<time_t>(-1);
}
return static_cast<time_t>(epoch64);
}
char translateKey(uint8_t idx)
{
static constexpr char kMap[kKeyboardRows][kKeyboardCols] = {
{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'},
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'},
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\n'},
{'z', 'x', 'c', 'v', 'b', 'n', 'm', ' ', '\b', '.'},
};
const uint8_t row = idx / 10;
const uint8_t col = idx % 10;
if (row >= kKeyboardRows || col >= kKeyboardCols)
{
return '\0';
}
return kMap[row][col];
}
void applyTxPower(SX1262Access& radio, int8_t tx_power)
{
constexpr int8_t kTxPowerMinDbm = -9;
int8_t clipped = tx_power;
if (clipped < kTxPowerMinDbm)
{
clipped = kTxPowerMinDbm;
}
radio.setOutputPower(clipped);
}
} // namespace
TDeckProBoard::TDeckProBoard()
: LilyGo_Display(SPI_DRIVER, true)
{
mono_buffer_.resize(static_cast<size_t>(profile().screen_width * profile().screen_height) / 8U, 0xFF);
}
TDeckProBoard* TDeckProBoard::getInstance()
{
static TDeckProBoard singleton;
return &singleton;
}
void TDeckProBoard::initSharedPins()
{
pinMode(profile().lora.enable, OUTPUT);
digitalWrite(profile().lora.enable, HIGH);
pinMode(profile().gps.enable, OUTPUT);
digitalWrite(profile().gps.enable, HIGH);
pinMode(profile().motion.enable_1v8, OUTPUT);
digitalWrite(profile().motion.enable_1v8, HIGH);
if (profile().optional.has_a7682e && profile().optional.a7682e_enable >= 0)
{
pinMode(profile().optional.a7682e_enable, OUTPUT);
digitalWrite(profile().optional.a7682e_enable, HIGH);
}
if (profile().optional.has_a7682e && profile().optional.a7682e_pwrkey >= 0)
{
pinMode(profile().optional.a7682e_pwrkey, OUTPUT);
digitalWrite(profile().optional.a7682e_pwrkey, HIGH);
}
if (profile().motor_pin >= 0)
{
pinMode(profile().motor_pin, OUTPUT);
digitalWrite(profile().motor_pin, LOW);
}
pinMode(profile().sd.cs, OUTPUT);
digitalWrite(profile().sd.cs, HIGH);
pinMode(profile().epd.cs, OUTPUT);
digitalWrite(profile().epd.cs, HIGH);
pinMode(profile().lora.cs, OUTPUT);
digitalWrite(profile().lora.cs, HIGH);
sharedSpiBusInit();
}
bool TDeckProBoard::initPower()
{
Wire.begin(profile().i2c.sda, profile().i2c.scl);
delay(10);
power_ready_ = pmu_.begin(Wire, 0x6B, profile().i2c.sda, profile().i2c.scl);
battery_gauge_ready_ = gauge_.begin(Wire, profile().i2c.sda, profile().i2c.scl);
if (power_ready_)
{
pmu_.enableMeasure();
pmu_.disableOTG();
}
Serial.printf("[%s] power init charger=%d gauge=%d\n", kTag, power_ready_ ? 1 : 0, battery_gauge_ready_ ? 1 : 0);
return power_ready_ || battery_gauge_ready_;
}
bool TDeckProBoard::initDisplay()
{
SPI.begin(profile().spi.sck, profile().spi.miso, profile().spi.mosi, profile().epd.cs);
sharedSpiLock();
sharedSpiPrepareDevice(profile().epd.cs);
epd_.epd2.selectSPI(SPI, SPISettings(kEpdSpiHz, MSBFIRST, SPI_MODE0));
epd_.init(0, true, 2, false);
epd_.setRotation(rotation_);
epd_.setFullWindow();
epd_.firstPage();
do
{
epd_.fillScreen(GxEPD_WHITE);
} while (epd_.nextPage());
epd_.powerOff();
sharedSpiUnlock();
display_ready_ = true;
Serial.printf("[%s] epd init ok %ux%u spi=%luHz\n", kTag, width(), height(), (unsigned long)kEpdSpiHz);
return true;
}
bool TDeckProBoard::initTouch()
{
touch_.setPins(profile().touch.rst, profile().touch.int_pin);
touch_ready_ = touch_.begin(Wire, profile().touch.i2c_addr, profile().i2c.sda, profile().i2c.scl);
if (touch_ready_)
{
touch_.setMaxCoordinates(profile().screen_width, profile().screen_height);
}
Serial.printf("[%s] touch init %s\n", kTag, touch_ready_ ? "ok" : "fail");
return touch_ready_;
}
bool TDeckProBoard::initKeyboard()
{
keyboard_ready_ = keyboard_.begin(profile().keyboard.i2c_addr, &Wire);
if (!keyboard_ready_)
{
Serial.printf("[%s] keyboard init fail\n", kTag);
return false;
}
keyboard_.matrix(profile().keyboard.rows, profile().keyboard.cols);
keyboard_.flush();
keyboardSetBrightness(keyboard_brightness_);
Serial.printf("[%s] keyboard init ok\n", kTag);
return true;
}
bool TDeckProBoard::initMotion()
{
motion_ready_ = motion_.begin(Wire, profile().motion.i2c_addr, profile().i2c.sda, profile().i2c.scl);
Serial.printf("[%s] motion init %s\n", kTag, motion_ready_ ? "ok" : "fail");
return motion_ready_;
}
bool TDeckProBoard::initRadio()
{
SPI.begin(profile().spi.sck, profile().spi.miso, profile().spi.mosi, profile().lora.cs);
sharedSpiLock();
sharedSpiPrepareDevice(profile().lora.cs);
radio_.reset();
radio_ready_ = (radio_.begin() == RADIOLIB_ERR_NONE);
sharedSpiUnlock();
Serial.printf("[%s] radio init %s\n", kTag, radio_ready_ ? "ok" : "fail");
return radio_ready_;
}
bool TDeckProBoard::initStorage()
{
sd_ready_ = installSD();
Serial.printf("[%s] sd init %s\n", kTag, sd_ready_ ? "ok" : "fail");
return sd_ready_;
}
bool TDeckProBoard::installSD()
{
pinMode(profile().sd.cs, OUTPUT);
digitalWrite(profile().sd.cs, HIGH);
sharedSpiLock();
sharedSpiPrepareDevice(profile().sd.cs);
const bool ok = SD.begin(profile().sd.cs, SPI);
sharedSpiUnlock();
return ok;
}
void TDeckProBoard::uninstallSD()
{
SD.end();
}
uint32_t TDeckProBoard::begin(uint32_t disable_hw_init)
{
Serial.begin(115200);
delay(30);
Serial.printf("[%s] begin variant=%s\n", kTag,
#if defined(TRAIL_MATE_TDECK_PRO_A7682E)
"a7682e"
#else
"pcm512a"
#endif
);
initSharedPins();
(void)initPower();
(void)initDisplay();
if ((disable_hw_init & NO_HW_TOUCH) == 0)
{
(void)initTouch();
}
(void)initKeyboard();
(void)initMotion();
if ((disable_hw_init & NO_HW_SD) == 0)
{
(void)initStorage();
}
if ((disable_hw_init & NO_HW_GPS) == 0)
{
(void)initGPS();
}
(void)initRadio();
rtc_ready_ = time(nullptr) >= kMinValidEpochSeconds;
uint32_t probe = 0;
if (power_ready_) probe |= HW_PMU_ONLINE;
if (battery_gauge_ready_) probe |= HW_GAUGE_ONLINE;
if (touch_ready_) probe |= HW_TOUCH_ONLINE;
if (keyboard_ready_) probe |= HW_KEYBOARD_ONLINE;
if (motion_ready_) probe |= HW_BHI260AP_ONLINE;
if (sd_ready_) probe |= HW_SD_ONLINE;
if (gps_ready_) probe |= HW_GPS_ONLINE;
if (radio_ready_) probe |= HW_RADIO_ONLINE;
return probe;
}
void TDeckProBoard::setBrightness(uint8_t level)
{
brightness_ = level;
}
void TDeckProBoard::keyboardSetBrightness(uint8_t level)
{
keyboard_brightness_ = level;
if (profile().keyboard.led >= 0)
{
analogWrite(profile().keyboard.led, level);
}
}
bool TDeckProBoard::isRTCReady() const
{
return rtc_ready_ || (time(nullptr) >= kMinValidEpochSeconds);
}
bool TDeckProBoard::isCharging()
{
return power_ready_ ? pmu_.isCharging() : false;
}
int TDeckProBoard::getBatteryLevel()
{
if (battery_gauge_ready_)
{
int level = static_cast<int>(gauge_.getStateOfCharge());
if (level >= 0 && level <= 100)
{
last_battery_level_ = level;
return level;
}
}
if (power_ready_)
{
int level = batteryPercentFromMv(static_cast<int>(pmu_.getBattVoltage()));
if (level >= 0 && level <= 100)
{
last_battery_level_ = level;
return level;
}
}
return last_battery_level_;
}
bool TDeckProBoard::isCardReady()
{
return sd_ready_ && SD.cardType() != CARD_NONE;
}
void TDeckProBoard::vibrator()
{
if (profile().motor_pin >= 0)
{
digitalWrite(profile().motor_pin, HIGH);
}
}
void TDeckProBoard::stopVibrator()
{
if (profile().motor_pin >= 0)
{
digitalWrite(profile().motor_pin, LOW);
}
}
void TDeckProBoard::playMessageTone()
{
if (!profile().optional.has_pcm512a || message_tone_volume_ == 0)
{
return;
}
static const char kMessageToneRtttl[] = "Msg:d=4,o=6,b=200:32e,32g,32b,16c7";
AudioOutputI2S audio_out(1, AudioOutputI2S::EXTERNAL_I2S);
audio_out.SetPinout(profile().optional.i2s_bclk,
profile().optional.i2s_lrc,
profile().optional.i2s_dout);
float gain = static_cast<float>(message_tone_volume_) / 250.0f;
if (gain > 0.40f)
{
gain = 0.40f;
}
audio_out.SetGain(gain);
AudioFileSourcePROGMEM song(kMessageToneRtttl, sizeof(kMessageToneRtttl) - 1);
AudioGeneratorRTTTL generator;
if (generator.begin(&song, &audio_out))
{
const uint32_t deadline = millis() + 1600;
while (generator.isRunning() && millis() < deadline)
{
if (!generator.loop())
{
break;
}
delay(1);
}
generator.stop();
}
audio_out.stop();
}
void TDeckProBoard::setMessageToneVolume(uint8_t volume_percent)
{
if (volume_percent > 100)
{
volume_percent = 100;
}
message_tone_volume_ = volume_percent;
}
uint8_t TDeckProBoard::getMessageToneVolume() const
{
return message_tone_volume_;
}
void TDeckProBoard::setRotation(uint8_t rotation)
{
rotation_ = rotation & 0x3;
epd_.setRotation(rotation_);
}
uint16_t TDeckProBoard::width()
{
return rotation_ % 2 == 0 ? static_cast<uint16_t>(profile().screen_width)
: static_cast<uint16_t>(profile().screen_height);
}
uint16_t TDeckProBoard::height()
{
return rotation_ % 2 == 0 ? static_cast<uint16_t>(profile().screen_height)
: static_cast<uint16_t>(profile().screen_width);
}
void TDeckProBoard::setBit(int16_t x, int16_t y, bool black)
{
if (x < 0 || y < 0 || x >= profile().screen_width || y >= profile().screen_height)
{
return;
}
const size_t idx = static_cast<size_t>(y) * static_cast<size_t>(profile().screen_width) + static_cast<size_t>(x);
const size_t byte_idx = idx / 8U;
const uint8_t bit_mask = static_cast<uint8_t>(0x80U >> (idx % 8U));
if (black)
{
mono_buffer_[byte_idx] &= static_cast<uint8_t>(~bit_mask);
}
else
{
mono_buffer_[byte_idx] |= bit_mask;
}
}
void TDeckProBoard::renderEpd()
{
if (!display_ready_)
{
return;
}
sharedSpiLock();
sharedSpiPrepareDevice(profile().epd.cs);
epd_.setRotation(rotation_);
epd_.setFullWindow();
epd_.firstPage();
do
{
epd_.drawInvertedBitmap(0, 0, mono_buffer_.data(), profile().screen_width, profile().screen_height, GxEPD_BLACK);
} while (epd_.nextPage());
epd_.powerOff();
sharedSpiUnlock();
}
void TDeckProBoard::pushColors(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t* color)
{
if (!color)
{
return;
}
static uint8_t s_flush_log_count = 0;
uint32_t dark_pixels = 0;
for (uint16_t row = 0; row < y2; ++row)
{
for (uint16_t col = 0; col < x2; ++col)
{
const uint16_t pixel = color[static_cast<size_t>(row) * x2 + col];
const uint8_t r = static_cast<uint8_t>((pixel >> 11) & 0x1F);
const uint8_t g = static_cast<uint8_t>((pixel >> 5) & 0x3F);
const uint8_t b = static_cast<uint8_t>(pixel & 0x1F);
const uint16_t luminance = static_cast<uint16_t>(r * 299U + g * 587U + b * 114U);
const bool black = luminance < 16384U;
if (black)
{
dark_pixels++;
}
setBit(static_cast<int16_t>(x1 + col), static_cast<int16_t>(y1 + row), black);
}
}
if (s_flush_log_count < kFlushLogLimit)
{
Serial.printf("[%s] flush #%u area=(%u,%u %ux%u) dark=%lu/%lu\n",
kTag,
static_cast<unsigned>(s_flush_log_count + 1),
static_cast<unsigned>(x1),
static_cast<unsigned>(y1),
static_cast<unsigned>(x2),
static_cast<unsigned>(y2),
static_cast<unsigned long>(dark_pixels),
static_cast<unsigned long>(static_cast<uint32_t>(x2) * static_cast<uint32_t>(y2)));
s_flush_log_count++;
}
renderEpd();
}
uint8_t TDeckProBoard::getPoint(int16_t* x, int16_t* y, uint8_t get_point)
{
if (!touch_ready_)
{
return 0;
}
int16_t tx = 0;
int16_t ty = 0;
const uint8_t touched = touch_.getPoint(&tx, &ty, get_point);
if (!touched || !x || !y)
{
return touched;
}
switch (rotation_ & 0x3)
{
case 1:
*x = ty;
*y = static_cast<int16_t>(profile().screen_width - 1 - tx);
break;
case 2:
*x = static_cast<int16_t>(profile().screen_width - 1 - tx);
*y = static_cast<int16_t>(profile().screen_height - 1 - ty);
break;
case 3:
*x = static_cast<int16_t>(profile().screen_height - 1 - ty);
*y = tx;
break;
default:
*x = tx;
*y = ty;
break;
}
return touched;
}
bool TDeckProBoard::keyEventToChar(uint8_t event, char* c, bool* pressed)
{
if (!c || !pressed)
{
return false;
}
*pressed = (event & 0x80U) != 0;
uint8_t key = static_cast<uint8_t>(event & 0x7FU);
if (key == 0)
{
return false;
}
key -= 1;
*c = translateKey(key);
return *c != '\0';
}
int TDeckProBoard::getKeyChar(char* c)
{
if (!keyboard_ready_ || !c || keyboard_.available() == 0)
{
return -1;
}
bool pressed = false;
if (!keyEventToChar(keyboard_.getEvent(), c, &pressed))
{
return -1;
}
return pressed ? KEYBOARD_PRESSED : KEYBOARD_RELEASED;
}
int TDeckProBoard::transmitRadio(const uint8_t* data, size_t len)
{
sharedSpiLock();
sharedSpiPrepareDevice(profile().lora.cs);
const int rc = radio_.transmit(data, len);
sharedSpiUnlock();
return rc;
}
int TDeckProBoard::startRadioReceive()
{
sharedSpiLock();
sharedSpiPrepareDevice(profile().lora.cs);
const int rc = radio_.startReceive();
sharedSpiUnlock();
return rc;
}
uint32_t TDeckProBoard::getRadioIrqFlags()
{
sharedSpiLock();
sharedSpiPrepareDevice(profile().lora.cs);
const uint32_t flags = radio_.getIrqFlags();
sharedSpiUnlock();
return flags;
}
int TDeckProBoard::getRadioPacketLength(bool update)
{
sharedSpiLock();
sharedSpiPrepareDevice(profile().lora.cs);
const int len = static_cast<int>(radio_.getPacketLength(update));
sharedSpiUnlock();
return len;
}
int TDeckProBoard::readRadioData(uint8_t* buf, size_t len)
{
sharedSpiLock();
sharedSpiPrepareDevice(profile().lora.cs);
const int rc = radio_.readData(buf, len);
sharedSpiUnlock();
return rc;
}
void TDeckProBoard::clearRadioIrqFlags(uint32_t flags)
{
sharedSpiLock();
sharedSpiPrepareDevice(profile().lora.cs);
radio_.clearIrqFlags(flags);
sharedSpiUnlock();
}
float TDeckProBoard::getRadioRSSI()
{
sharedSpiLock();
sharedSpiPrepareDevice(profile().lora.cs);
const float rssi = radio_.getRSSI();
sharedSpiUnlock();
return rssi;
}
float TDeckProBoard::getRadioSNR()
{
sharedSpiLock();
sharedSpiPrepareDevice(profile().lora.cs);
const float snr = radio_.getSNR();
sharedSpiUnlock();
return snr;
}
void TDeckProBoard::configureLoraRadio(float freq_mhz, float bw_khz, uint8_t sf, uint8_t cr_denom,
int8_t tx_power, uint16_t preamble_len, uint8_t sync_word,
uint8_t crc_len)
{
sharedSpiLock();
sharedSpiPrepareDevice(profile().lora.cs);
radio_.setFrequency(freq_mhz);
radio_.setBandwidth(bw_khz);
radio_.setSpreadingFactor(sf);
radio_.setCodingRate(cr_denom);
applyTxPower(radio_, tx_power);
radio_.setPreambleLength(preamble_len);
radio_.setSyncWord(sync_word);
radio_.setCRC(crc_len);
sharedSpiUnlock();
}
bool TDeckProBoard::initGPS()
{
Serial2.begin(profile().gps.uart.baud, SERIAL_8N1, profile().gps.uart.rx, profile().gps.uart.tx);
delay(50);
gps_ready_ = gps_.init(&Serial2);
Serial.printf("[%s] gps init %s\n", kTag, gps_ready_ ? "ok" : "fail");
return gps_ready_;
}
void TDeckProBoard::powerControl(PowerCtrlChannel_t ch, bool enable)
{
switch (ch)
{
case POWER_GPS:
if (profile().gps.enable >= 0)
{
digitalWrite(profile().gps.enable, enable ? HIGH : LOW);
}
break;
case POWER_SENSOR:
if (profile().motion.enable_1v8 >= 0)
{
digitalWrite(profile().motion.enable_1v8, enable ? HIGH : LOW);
}
break;
case POWER_RADIO:
if (profile().lora.enable >= 0)
{
digitalWrite(profile().lora.enable, enable ? HIGH : LOW);
}
break;
default:
break;
}
}
bool TDeckProBoard::syncTimeFromGPS(uint32_t gps_task_interval_ms)
{
(void)gps_task_interval_ms;
if (!gps_.date.isValid() || !gps_.time.isValid())
{
return false;
}
const int year = gps_.date.year();
const uint8_t month = gps_.date.month();
const uint8_t day = gps_.date.day();
const uint8_t hour = gps_.time.hour();
const uint8_t minute = gps_.time.minute();
const uint8_t second = gps_.time.second();
if (!gpsDatetimeValid(year, month, day, hour, minute, second))
{
return false;
}
const time_t epoch = gpsDatetimeToEpochUtc(year, month, day, hour, minute, second);
if (epoch < kMinValidEpochSeconds)
{
return false;
}
timeval tv{};
tv.tv_sec = epoch;
tv.tv_usec = 0;
if (settimeofday(&tv, nullptr) != 0)
{
return false;
}
rtc_ready_ = true;
return true;
}
namespace
{
TDeckProBoard& getInstanceRef()
{
return *TDeckProBoard::getInstance();
}
} // namespace
TDeckProBoard& instance = getInstanceRef();
BoardBase& board = getInstanceRef();
} // namespace boards::tdeck_pro
BoardBase& board = ::boards::tdeck_pro::instance;
#endif // defined(ARDUINO_T_DECK_PRO)
@@ -179,13 +179,16 @@ void refresh_recent_widget()
std::snprintf(footer, sizeof(footer), "%u threads active", static_cast<unsigned>(total));
set_label_text_if_changed(recent.footer_label, footer);
// Keep the footer accent subtle; continuously animating it forces a redraw
// every dashboard tick even when no conversation state changed.
const lv_coord_t track_w = lv_obj_get_width(recent.scan_track);
const lv_coord_t runner_w = lv_obj_get_width(recent.scan_runner);
const lv_coord_t travel = track_w - runner_w;
const lv_coord_t x = travel > 0
? static_cast<lv_coord_t>(((dashboard_state().tick % 2U) == 0U) ? (travel / 3) : ((travel * 2) / 3))
: 0;
lv_obj_set_x(recent.scan_runner, x);
const lv_coord_t x = travel > 0 ? travel / 2 : 0;
if (lv_obj_get_x(recent.scan_runner) != x)
{
lv_obj_set_x(recent.scan_runner, x);
}
}
} // namespace ui::menu::dashboard
@@ -3,6 +3,7 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
namespace ui::menu::dashboard
{
@@ -98,7 +99,8 @@ DashboardCardChrome create_card_chrome(lv_obj_t* parent,
chrome.body = lv_obj_create(chrome.card);
lv_obj_set_size(chrome.body, LV_PCT(100), card_h - kHeaderH - kFooterH);
lv_obj_align_to(chrome.body, chrome.header, LV_ALIGN_OUT_BOTTOM_MID, 0, 0);
lv_obj_set_style_bg_opa(chrome.body, LV_OPA_TRANSP, 0);
lv_obj_set_style_bg_color(chrome.body, color_panel_bg(), 0);
lv_obj_set_style_bg_opa(chrome.body, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(chrome.body, 0, 0);
lv_obj_set_style_pad_left(chrome.body, 10, 0);
lv_obj_set_style_pad_right(chrome.body, 10, 0);
@@ -110,7 +112,7 @@ DashboardCardChrome create_card_chrome(lv_obj_t* parent,
lv_obj_set_size(chrome.footer, LV_PCT(100), kFooterH);
lv_obj_align(chrome.footer, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_bg_color(chrome.footer, color_soft_amber(), 0);
lv_obj_set_style_bg_opa(chrome.footer, LV_OPA_50, 0);
lv_obj_set_style_bg_opa(chrome.footer, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(chrome.footer, 0, 0);
lv_obj_set_style_radius(chrome.footer, 18, 0);
lv_obj_set_style_pad_left(chrome.footer, 10, 0);
@@ -128,10 +130,23 @@ void set_status_chip(DashboardCardChrome& chrome, const char* text, lv_color_t b
{
return;
}
lv_obj_set_style_bg_color(chrome.status_chip, bg, 0);
lv_obj_set_style_bg_opa(chrome.status_chip, LV_OPA_COVER, 0);
lv_obj_set_style_text_color(chrome.status_label, fg, 0);
lv_label_set_text(chrome.status_label, text);
if (lv_color_to_int(lv_obj_get_style_bg_color(chrome.status_chip, LV_PART_MAIN)) != lv_color_to_int(bg))
{
lv_obj_set_style_bg_color(chrome.status_chip, bg, 0);
}
if (lv_obj_get_style_bg_opa(chrome.status_chip, LV_PART_MAIN) != LV_OPA_COVER)
{
lv_obj_set_style_bg_opa(chrome.status_chip, LV_OPA_COVER, 0);
}
if (lv_color_to_int(lv_obj_get_style_text_color(chrome.status_label, LV_PART_MAIN)) != lv_color_to_int(fg))
{
lv_obj_set_style_text_color(chrome.status_label, fg, 0);
}
const char* current = lv_label_get_text(chrome.status_label);
if (text != nullptr && (current == nullptr || std::strcmp(current, text) != 0))
{
lv_label_set_text(chrome.status_label, text);
}
}
void style_stat_tile(lv_obj_t* tile, lv_color_t bg)
@@ -10,13 +10,17 @@
#include "ui/menu/dashboard/dashboard_state.h"
#include "ui/menu/dashboard/dashboard_widgets.h"
#include "ui/menu/menu_profile.h"
#include "ui/ui_theme.h"
namespace ui::menu::dashboard
{
namespace
{
constexpr uint32_t kDashboardTimerMs = 220;
// Tab5's 720x1280 menu can starve the LVGL task if we keep animating the
// dashboard aggressively while the menu grid is also visible. A slower cadence
// keeps the dashboard useful without continuously forcing large redraws.
constexpr uint32_t kDashboardTimerMs = 1000;
bool is_tab5_profile()
{
@@ -34,7 +38,6 @@ void refresh_dashboard(lv_timer_t* timer)
++dashboard.tick;
// Keep motion responsive, but avoid repainting the whole dashboard too aggressively.
refresh_compass_widget();
if ((dashboard.tick % 2U) == 1U)
@@ -68,7 +71,8 @@ void init(lv_obj_t* menu_panel, lv_obj_t* grid_panel, const ui::menu_layout::Ini
dashboard.dock_right = right_gap >= 240;
dashboard.panel = lv_obj_create(menu_panel);
lv_obj_set_style_bg_opa(dashboard.panel, LV_OPA_TRANSP, 0);
lv_obj_set_style_bg_color(dashboard.panel, ui::theme::page_bg(), 0);
lv_obj_set_style_bg_opa(dashboard.panel, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(dashboard.panel, 0, 0);
lv_obj_set_style_pad_all(dashboard.panel, 0, 0);
lv_obj_set_style_pad_row(dashboard.panel, 12, 0);
@@ -13,7 +13,7 @@ namespace ui::menu_profile
namespace
{
#if !defined(TRAIL_MATE_ESP_BOARD_TAB5) && !defined(ARDUINO_T_LORA_PAGER) && !defined(ARDUINO_T_DECK)
#if !defined(TRAIL_MATE_ESP_BOARD_TAB5) && !defined(ARDUINO_T_LORA_PAGER) && !defined(ARDUINO_T_DECK) && !defined(ARDUINO_T_DECK_PRO)
lv_coord_t display_width()
{
lv_coord_t width = lv_display_get_physical_horizontal_resolution(nullptr);
@@ -206,7 +206,7 @@ const MenuLayoutProfile& current()
return make_tab5_profile();
#elif defined(ARDUINO_T_LORA_PAGER)
return make_pager_profile();
#elif defined(ARDUINO_T_DECK)
#elif defined(ARDUINO_T_DECK) || defined(ARDUINO_T_DECK_PRO)
return make_tdeck_profile();
#else
return make_default_profile(display_width(), display_height());
@@ -163,7 +163,7 @@ const PageLayoutProfile& current()
return make_tab5_profile();
#elif defined(ARDUINO_T_LORA_PAGER)
return make_pager_profile();
#elif defined(ARDUINO_T_DECK)
#elif defined(ARDUINO_T_DECK) || defined(ARDUINO_T_DECK_PRO)
return make_tdeck_profile();
#else
lv_coord_t width = lv_display_get_physical_horizontal_resolution(nullptr);
@@ -15,6 +15,7 @@
#include "team/protocol/team_location_marker.h"
#include "team/usecase/team_controller.h"
#include "ui/page/page_profile.h"
#include "ui/assets/fonts/fonts.h"
#include "ui/screens/team/team_ui_store.h"
#include "ui/ui_common.h"
#include "ui/widgets/system_notification.h"
@@ -1516,7 +1517,11 @@ void UiController::openKeyVerificationInfoModal(chat::NodeId node_id, uint32_t n
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 0);
char num_buf[24] = {};
std::snprintf(num_buf, sizeof(num_buf), "%03u %03u", number / 1000U, number % 1000U);
std::snprintf(num_buf,
sizeof(num_buf),
"%03u %03u",
static_cast<unsigned>(number / 1000U),
static_cast<unsigned>(number % 1000U));
key_verify_desc_ = lv_label_create(key_verify_panel_);
std::string desc = resolve_contact_name(node_id) + "\nShare this number:\n";
desc += num_buf;
@@ -39,6 +39,7 @@ static lv_color16_t* buf1 = nullptr;
static void disp_flush(lv_display_t* disp_drv, const lv_area_t* area, uint8_t* color_p)
{
static uint8_t s_tdeck_pro_flush_log_count = 0;
#if LV_TEST_FLUSH_LOG
static uint32_t s_flush_count = 0;
static uint32_t s_flush_last_ms = 0;
@@ -56,6 +57,33 @@ static void disp_flush(lv_display_t* disp_drv, const lv_area_t* area, uint8_t* c
lv_draw_sw_rgb565_swap(color_p, len);
#endif
#if defined(ARDUINO_T_DECK_PRO)
if (s_tdeck_pro_flush_log_count < 8)
{
uint32_t blackish = 0;
if (color_p != nullptr)
{
const uint16_t* pix = reinterpret_cast<const uint16_t*>(color_p);
for (size_t i = 0; i < len; ++i)
{
if (pix[i] == 0x0000)
{
blackish++;
}
}
}
Serial.printf("[LVGL][TDeckPro] flush #%u area=(%d,%d)-(%d,%d) len=%lu rgb565_black=%lu\n",
static_cast<unsigned>(s_tdeck_pro_flush_log_count + 1),
static_cast<int>(area->x1),
static_cast<int>(area->y1),
static_cast<int>(area->x2),
static_cast<int>(area->y2),
static_cast<unsigned long>(len),
static_cast<unsigned long>(blackish));
s_tdeck_pro_flush_log_count++;
}
#endif
plane->pushColors(area->x1, area->y1, w, h, (uint16_t*)color_p);
lv_display_flush_ready(disp_drv);
@@ -118,7 +146,7 @@ static void touchpad_read(lv_indev_t* drv, lv_indev_data_t* data)
if (touched)
{
input::MorseEngine::notifyTouch();
#if defined(ARDUINO_T_DECK)
#if defined(ARDUINO_T_DECK) || defined(ARDUINO_T_DECK_PRO)
// T-Deck: touch can wake/show the screen saver, but only SPACE enters the main menu.
if (isScreenSaverActive())
{
@@ -172,7 +200,7 @@ static void lv_encoder_read(lv_indev_t* drv, lv_indev_data_t* data)
{
auto* plane = (LilyGo_Display*)lv_indev_get_user_data(drv);
RotaryMsg_t msg = plane->getRotary();
#if defined(ARDUINO_T_DECK)
#if defined(ARDUINO_T_DECK) || defined(ARDUINO_T_DECK_PRO)
data->enc_diff = 0;
data->state = LV_INDEV_STATE_RELEASED;
#endif
@@ -184,7 +212,7 @@ static void lv_encoder_read(lv_indev_t* drv, lv_indev_data_t* data)
{
wakeScreenSaver();
}
#if defined(ARDUINO_T_DECK)
#if defined(ARDUINO_T_DECK) || defined(ARDUINO_T_DECK_PRO)
// T-Deck path already reset above.
#else
data->enc_diff = 0;
@@ -196,7 +224,7 @@ static void lv_encoder_read(lv_indev_t* drv, lv_indev_data_t* data)
// If GPS is loading tiles, ignore input
if (isGPSLoadingTiles())
{
#if !defined(ARDUINO_T_DECK)
#if !defined(ARDUINO_T_DECK) && !defined(ARDUINO_T_DECK_PRO)
data->enc_diff = 0;
data->state = LV_INDEV_STATE_RELEASED;
#endif
@@ -224,7 +252,7 @@ static void lv_encoder_read(lv_indev_t* drv, lv_indev_data_t* data)
return;
}
#if !defined(ARDUINO_T_DECK)
#if !defined(ARDUINO_T_DECK) && !defined(ARDUINO_T_DECK_PRO)
data->enc_diff = 0;
data->state = LV_INDEV_STATE_RELEASED;
#endif
@@ -246,7 +274,7 @@ static void lv_encoder_read(lv_indev_t* drv, lv_indev_data_t* data)
data->enc_diff = -1;
break;
default:
#if !defined(ARDUINO_T_DECK)
#if !defined(ARDUINO_T_DECK) && !defined(ARDUINO_T_DECK_PRO)
data->state = LV_INDEV_STATE_RELEASED;
#endif
break;
@@ -271,30 +299,18 @@ static void keypad_read(lv_indev_t* drv, lv_indev_data_t* data)
// which could cause spurious wake閳姀nterFromScreenSaver() without user input.
if (isScreenSleeping() || isScreenSaverActive())
{
if (state == KEYBOARD_PRESSED || state == KEYBOARD_RELEASED)
if (state == KEYBOARD_PRESSED)
{
#if defined(ARDUINO_T_DECK)
// T-Deck policy: any key can wake into the saver shell, and SPACE can continue into the main menu.
// Accept both press/release for SPACE so a quick tap from sleep can still land in the menu.
// Keyboard wake policy: any key can wake into the saver shell, but
// only SPACE is allowed to continue into the main menu.
if (isScreenSaverActive() && c == ' ')
{
enterFromScreenSaver();
}
else if (state == KEYBOARD_PRESSED)
else
{
wakeScreenSaver();
}
#else
// Other keyboard devices keep the original behavior:
// first key wakes the screen saver, and any key exits it.
if (state == KEYBOARD_PRESSED)
{
if (isScreenSaverActive())
enterFromScreenSaver();
else
wakeScreenSaver();
}
#endif
}
data->state = LV_INDEV_STATE_REL; // Don't pass key to UI
return;
@@ -306,7 +322,7 @@ static void keypad_read(lv_indev_t* drv, lv_indev_data_t* data)
walkie::on_key_event(c, state);
}
#if defined(ARDUINO_T_DECK)
#if defined(ARDUINO_T_DECK) || defined(ARDUINO_T_DECK_PRO)
if (state == KEYBOARD_PRESSED && c == ' ' && ui_get_active_app() == nullptr)
{
updateUserActivity();
@@ -211,7 +211,7 @@ void init_screen_saver()
s_screen_saver_hint_label = lv_label_create(s_screen_saver_layer);
lv_obj_set_style_text_color(s_screen_saver_hint_label, lv_color_hex(0x8A6A3A), 0);
lv_obj_set_style_text_font(s_screen_saver_hint_label, &lv_font_montserrat_14, 0);
#if defined(ARDUINO_T_DECK)
#if defined(ARDUINO_T_DECK) || defined(ARDUINO_T_DECK_PRO)
lv_label_set_text(s_screen_saver_hint_label, "Press SPACE to enter main menu");
#else
lv_label_set_text(s_screen_saver_hint_label, "Press SPACE to enter");
@@ -412,12 +412,12 @@ void wakeScreenSaver()
if (xSemaphoreTake(s_activity_mutex, pdMS_TO_TICKS(10)) == pdTRUE)
{
was_sleeping = s_screen_sleeping;
// Treat the wake gesture as real activity so the background sleep
// task doesn't immediately force the panel back off before the
// 3-second saver window expires.
s_last_user_activity_time = millis();
s_screen_saver_active = true;
s_screen_sleeping = false;
// Keep the runtime in the logical sleep state while the transient
// saver is visible. This matches the 0.1.13 behavior: waking only
// shows the saver shell, and if the user does nothing for 3s we go
// right back to sleep instead of being treated as fully awake.
s_screen_sleeping = true;
xSemaphoreGive(s_activity_mutex);
}
}
@@ -803,7 +803,11 @@ void render_line_from_buffer(uint8_t line_rgb[320][4], e_mode mode, uint16_t lin
void sstv_task(void*)
{
#if defined(TRAIL_MATE_ESP_BOARD_TAB5)
TLoRaPagerBoard* board = TLoRaPagerBoard::getInstance();
#else
boards::tlora_pager::TLoRaPagerBoard* board = boards::tlora_pager::TLoRaPagerBoard::getInstance();
#endif
int prev_volume = -1;
bool prev_out_mute = false;
bool restore_amp = false;
@@ -4,6 +4,8 @@
#include "boards/tab5/platform_esp_board_runtime.h"
#elif defined(TRAIL_MATE_ESP_BOARD_T_DISPLAY_P4)
#include "boards/t_display_p4/platform_esp_board_runtime.h"
#elif defined(ARDUINO_T_DECK_PRO)
#include "boards/tdeck_pro/platform_esp_board_runtime.h"
#elif defined(ARDUINO_T_DECK)
#include "boards/tdeck/platform_esp_board_runtime.h"
#elif defined(ARDUINO_T_WATCH_S3)
+20 -13
View File
@@ -496,17 +496,15 @@ void append_bytes_and_process(const uint8_t* buffer, int length, char* line_buff
}
}
void ensure_ext5v_enabled()
bool configure_uart_hardware()
{
#if defined(TRAIL_MATE_ESP_BOARD_TAB5)
(void)::boards::tab5::Tab5Board::instance().acquireExt5vRail("gps_runtime");
#endif
}
void configure_uart_hardware()
{
#if defined(TRAIL_MATE_ESP_BOARD_TAB5)
(void)::boards::tab5::Tab5Board::instance().configureGpsUart();
if (!::boards::tab5::Tab5Board::instance().configureGpsUart())
{
ESP_LOGE(kTag, "Tab5 GNSS UART setup failed");
return false;
}
return true;
#else
const auto gps_uart = gps_uart_pins();
uart_config_t config{};
@@ -521,14 +519,14 @@ void configure_uart_hardware()
ESP_ERROR_CHECK(uart_set_pin(static_cast<uart_port_t>(gps_uart.port), gps_uart.tx, gps_uart.rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_driver_install(static_cast<uart_port_t>(gps_uart.port), 4096, 0, 0, nullptr, 0));
ESP_LOGI(kTag, "GNSS UART configured: port=%d tx=%d rx=%d baud=%d", gps_uart.port, gps_uart.tx, gps_uart.rx, config.baud_rate);
return true;
#endif
}
void teardown_uart_hardware()
{
#if defined(TRAIL_MATE_ESP_BOARD_TAB5)
::boards::tab5::Tab5Board::instance().teardownGpsUart();
::boards::tab5::Tab5Board::instance().releaseExt5vRail("gps_runtime");
// Tab5 keeps GNSS power and UART under board ownership for the whole runtime.
#else
const auto gps_uart = gps_uart_pins();
if (gps_uart.port >= 0)
@@ -540,9 +538,18 @@ void teardown_uart_hardware()
void worker_task(void*)
{
ensure_ext5v_enabled();
vTaskDelay(pdMS_TO_TICKS(kProbeWarmupMs));
configure_uart_hardware();
if (!configure_uart_hardware())
{
std::lock_guard<std::mutex> lock(s_mutex);
s_runtime.powered = false;
s_runtime.stop_requested = false;
s_runtime.probe_requested = false;
s_runtime.probe_deadline_ms = 0;
s_runtime.worker_handle = nullptr;
vTaskDelete(nullptr);
return;
}
{
std::lock_guard<std::mutex> lock(s_mutex);
s_runtime.powered = true;
+74
View File
@@ -0,0 +1,74 @@
[env:tdeck_pro_a7682e]
extends = arduino_base
board = T-Deck-Pro
upload_speed = 921600
build_flags =
${arduino_base.build_flags}
-DBOARD_HAS_PSRAM=1
-DARDUINO_T_DECK_PRO
-DARDUINO_USB_CDC_ON_BOOT=1
-DARDUINO_LILYGO_LORA_SX1262
-DSCREEN_WIDTH=240
-DSCREEN_HEIGHT=320
-DSENSOR_INT=21
-DGPS_TX=43
-DGPS_RX=44
-DLORA_CS=3
-DLORA_BUSY=6
-DLORA_RST=4
-DLORA_IRQ=5
-DSD_CS=48
-DDISP_CS=34
-DDISP_DC=35
-DDISP_RST=-1
-DDISP_BUSY=37
-DBOARD_TOUCH_INT=12
-DUSING_INPUT_DEV_TOUCHPAD
-DUSING_INPUT_DEV_KEYBOARD
-DTRAIL_MATE_TDECK_PRO=1
-DTRAIL_MATE_TDECK_PRO_A7682E=1
lib_deps =
${arduino_base.lib_deps}
zinggjm/GxEPD2 @ 1.5.9
adafruit/Adafruit TCA8418 @ ^1.0.2
adafruit/Adafruit BusIO @ ^1.17.0
mikalhart/TinyGPSPlus @ 1.0.3
[env:tdeck_pro_pcm512a]
extends = arduino_base
board = T-Deck-Pro
upload_speed = 921600
build_flags =
${arduino_base.build_flags}
-DBOARD_HAS_PSRAM=1
-DARDUINO_T_DECK_PRO
-DARDUINO_USB_CDC_ON_BOOT=1
-DARDUINO_LILYGO_LORA_SX1262
-DSCREEN_WIDTH=240
-DSCREEN_HEIGHT=320
-DSENSOR_INT=21
-DGPS_TX=43
-DGPS_RX=44
-DLORA_CS=3
-DLORA_BUSY=6
-DLORA_RST=4
-DLORA_IRQ=5
-DSD_CS=48
-DDISP_CS=34
-DDISP_DC=35
-DDISP_RST=-1
-DDISP_BUSY=37
-DBOARD_TOUCH_INT=12
-DDAC_I2S_BCK=7
-DDAC_I2S_WS=9
-DDAC_I2S_DOUT=8
-DUSING_INPUT_DEV_TOUCHPAD
-DUSING_INPUT_DEV_KEYBOARD
-DTRAIL_MATE_TDECK_PRO=1
-DTRAIL_MATE_TDECK_PRO_PCM512A=1
lib_deps =
${arduino_base.lib_deps}
zinggjm/GxEPD2 @ 1.5.9
adafruit/Adafruit TCA8418 @ ^1.0.2
adafruit/Adafruit BusIO @ ^1.17.0
mikalhart/TinyGPSPlus @ 1.0.3