diff --git a/apps/esp_idf/CMakeLists.txt b/apps/esp_idf/CMakeLists.txt index d333a335..c97a0ec9 100644 --- a/apps/esp_idf/CMakeLists.txt +++ b/apps/esp_idf/CMakeLists.txt @@ -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" diff --git a/boards/T-Deck-Pro.json b/boards/T-Deck-Pro.json new file mode 100644 index 00000000..11592bdb --- /dev/null +++ b/boards/T-Deck-Pro.json @@ -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" +} diff --git a/boards/tdeck_pro/include/boards/tdeck_pro/board_profile.h b/boards/tdeck_pro/include/boards/tdeck_pro/board_profile.h new file mode 100644 index 00000000..c9ce5f95 --- /dev/null +++ b/boards/tdeck_pro/include/boards/tdeck_pro/board_profile.h @@ -0,0 +1,221 @@ +#pragma once + +#include + +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 diff --git a/boards/tdeck_pro/include/boards/tdeck_pro/platform_esp_board_runtime.h b/boards/tdeck_pro/include/boards/tdeck_pro/platform_esp_board_runtime.h new file mode 100644 index 00000000..436b6058 --- /dev/null +++ b/boards/tdeck_pro/include/boards/tdeck_pro/platform_esp_board_runtime.h @@ -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(::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 diff --git a/boards/tdeck_pro/include/boards/tdeck_pro/tdeck_pro_board.h b/boards/tdeck_pro/include/boards/tdeck_pro/tdeck_pro_board.h new file mode 100644 index 00000000..721a134f --- /dev/null +++ b/boards/tdeck_pro/include/boards/tdeck_pro/tdeck_pro_board.h @@ -0,0 +1,166 @@ +#pragma once + +#define XPOWERS_CHIP_BQ25896 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; + + uint8_t brightness_ = DEVICE_MAX_BRIGHTNESS_LEVEL; + uint8_t keyboard_brightness_ = static_cast(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(profile().lora.cs), + static_cast(profile().lora.irq), + static_cast(profile().lora.rst), + static_cast(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 mono_buffer_; +}; + +extern TDeckProBoard& instance; +extern BoardBase& board; + +} // namespace boards::tdeck_pro diff --git a/boards/tdeck_pro/library.json b/boards/tdeck_pro/library.json new file mode 100644 index 00000000..df517ac3 --- /dev/null +++ b/boards/tdeck_pro/library.json @@ -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" + } + ] +} diff --git a/boards/tdeck_pro/src/tdeck_pro_board.cpp b/boards/tdeck_pro/src/tdeck_pro_board.cpp new file mode 100644 index 00000000..41cb0b33 --- /dev/null +++ b/boards/tdeck_pro/src/tdeck_pro_board.cpp @@ -0,0 +1,851 @@ +#if defined(ARDUINO_T_DECK_PRO) + +#include "boards/tdeck_pro/tdeck_pro_board.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(((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(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(era) * 146097 + static_cast(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(hour) * 3600 + + static_cast(minute) * 60 + + static_cast(second); + const int64_t epoch64 = days * 86400 + sec_of_day; + if (epoch64 < 0 || epoch64 > static_cast(std::numeric_limits::max())) + { + return static_cast(-1); + } + return static_cast(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(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(gauge_.getStateOfCharge()); + if (level >= 0 && level <= 100) + { + last_battery_level_ = level; + return level; + } + } + if (power_ready_) + { + int level = batteryPercentFromMv(static_cast(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(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(profile().screen_width) + : static_cast(profile().screen_height); +} + +uint16_t TDeckProBoard::height() +{ + return rotation_ % 2 == 0 ? static_cast(profile().screen_height) + : static_cast(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(y) * static_cast(profile().screen_width) + static_cast(x); + const size_t byte_idx = idx / 8U; + const uint8_t bit_mask = static_cast(0x80U >> (idx % 8U)); + if (black) + { + mono_buffer_[byte_idx] &= static_cast(~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(row) * x2 + col]; + const uint8_t r = static_cast((pixel >> 11) & 0x1F); + const uint8_t g = static_cast((pixel >> 5) & 0x3F); + const uint8_t b = static_cast(pixel & 0x1F); + const uint16_t luminance = static_cast(r * 299U + g * 587U + b * 114U); + const bool black = luminance < 16384U; + if (black) + { + dark_pixels++; + } + setBit(static_cast(x1 + col), static_cast(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(s_flush_log_count + 1), + static_cast(x1), + static_cast(y1), + static_cast(x2), + static_cast(y2), + static_cast(dark_pixels), + static_cast(static_cast(x2) * static_cast(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(profile().screen_width - 1 - tx); + break; + case 2: + *x = static_cast(profile().screen_width - 1 - tx); + *y = static_cast(profile().screen_height - 1 - ty); + break; + case 3: + *x = static_cast(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(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(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) diff --git a/modules/ui_shared/src/ui/menu/dashboard/dashboard_recent_widget.cpp b/modules/ui_shared/src/ui/menu/dashboard/dashboard_recent_widget.cpp index cb5aebe9..935a7e68 100644 --- a/modules/ui_shared/src/ui/menu/dashboard/dashboard_recent_widget.cpp +++ b/modules/ui_shared/src/ui/menu/dashboard/dashboard_recent_widget.cpp @@ -179,13 +179,16 @@ void refresh_recent_widget() std::snprintf(footer, sizeof(footer), "%u threads active", static_cast(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(((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 diff --git a/modules/ui_shared/src/ui/menu/dashboard/dashboard_style.cpp b/modules/ui_shared/src/ui/menu/dashboard/dashboard_style.cpp index 0e898b77..11397402 100644 --- a/modules/ui_shared/src/ui/menu/dashboard/dashboard_style.cpp +++ b/modules/ui_shared/src/ui/menu/dashboard/dashboard_style.cpp @@ -3,6 +3,7 @@ #include #include #include +#include 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) diff --git a/modules/ui_shared/src/ui/menu/menu_dashboard.cpp b/modules/ui_shared/src/ui/menu/menu_dashboard.cpp index 11b2e4bc..1991e6e1 100644 --- a/modules/ui_shared/src/ui/menu/menu_dashboard.cpp +++ b/modules/ui_shared/src/ui/menu/menu_dashboard.cpp @@ -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); diff --git a/modules/ui_shared/src/ui/menu/menu_profile.cpp b/modules/ui_shared/src/ui/menu/menu_profile.cpp index ad98eec7..05fb7832 100644 --- a/modules/ui_shared/src/ui/menu/menu_profile.cpp +++ b/modules/ui_shared/src/ui/menu/menu_profile.cpp @@ -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()); diff --git a/modules/ui_shared/src/ui/page/page_profile.cpp b/modules/ui_shared/src/ui/page/page_profile.cpp index 917e5e3e..7e20ed71 100644 --- a/modules/ui_shared/src/ui/page/page_profile.cpp +++ b/modules/ui_shared/src/ui/page/page_profile.cpp @@ -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); diff --git a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp index 1226165c..70c5ac3d 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp @@ -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(number / 1000U), + static_cast(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; diff --git a/platform/esp/arduino_common/src/LV_Helper_v9.cpp b/platform/esp/arduino_common/src/LV_Helper_v9.cpp index 60001df0..d0bfd57d 100644 --- a/platform/esp/arduino_common/src/LV_Helper_v9.cpp +++ b/platform/esp/arduino_common/src/LV_Helper_v9.cpp @@ -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(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(s_tdeck_pro_flush_log_count + 1), + static_cast(area->x1), + static_cast(area->y1), + static_cast(area->x2), + static_cast(area->y2), + static_cast(len), + static_cast(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(); diff --git a/platform/esp/arduino_common/src/screen_sleep.cpp b/platform/esp/arduino_common/src/screen_sleep.cpp index ac5ec27b..2a14d72b 100644 --- a/platform/esp/arduino_common/src/screen_sleep.cpp +++ b/platform/esp/arduino_common/src/screen_sleep.cpp @@ -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); } } diff --git a/platform/esp/arduino_common/src/sstv/sstv_service.cpp b/platform/esp/arduino_common/src/sstv/sstv_service.cpp index 4ed619aa..cbf4720d 100644 --- a/platform/esp/arduino_common/src/sstv/sstv_service.cpp +++ b/platform/esp/arduino_common/src/sstv/sstv_service.cpp @@ -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; diff --git a/platform/esp/boards/src/board_runtime.cpp b/platform/esp/boards/src/board_runtime.cpp index 67cbb954..fefa8dd4 100644 --- a/platform/esp/boards/src/board_runtime.cpp +++ b/platform/esp/boards/src/board_runtime.cpp @@ -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) diff --git a/platform/esp/idf_common/src/gps_runtime.cpp b/platform/esp/idf_common/src/gps_runtime.cpp index c7b325a4..fb85b321 100644 --- a/platform/esp/idf_common/src/gps_runtime.cpp +++ b/platform/esp/idf_common/src/gps_runtime.cpp @@ -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(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(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 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 lock(s_mutex); s_runtime.powered = true; diff --git a/variants/tdeck_pro/envs/tdeck_pro.ini b/variants/tdeck_pro/envs/tdeck_pro.ini new file mode 100644 index 00000000..7a83f863 --- /dev/null +++ b/variants/tdeck_pro/envs/tdeck_pro.ini @@ -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