diff --git a/boards/t-deck_pro_max.json b/boards/t-deck_pro_max.json new file mode 100644 index 00000000..ef57d9a3 --- /dev/null +++ b/boards/t-deck_pro_max.json @@ -0,0 +1,40 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_qspi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "LilyGo T-Deck Pro MAX (16MB Flash 8MB QSPI PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.lilygo.cc/products/t-deck-pro", + "vendor": "LilyGo" +} diff --git a/variants/lilygo_tdeck_pro_max/TDeckProMaxBoard.cpp b/variants/lilygo_tdeck_pro_max/TDeckProMaxBoard.cpp new file mode 100644 index 00000000..9d7e34f2 --- /dev/null +++ b/variants/lilygo_tdeck_pro_max/TDeckProMaxBoard.cpp @@ -0,0 +1,347 @@ +#include +#include "variant.h" +#include "TDeckProMaxBoard.h" +#include // For MESH_DEBUG_PRINTLN + +// LEDC channel for e-ink backlight PWM (Arduino ESP32 core 2.x channel-based API) +#ifdef PIN_EINK_BL + #define EINK_BL_LEDC_CHANNEL 0 +#endif + +// ============================================================================= +// TDeckProMaxBoard::begin() — Boot sequence for T-Deck Pro MAX V0.1 +// +// Critical ordering: +// 1. I2C bus init (XL9555, BQ27220, and all sensors share this bus) +// 2. XL9555 init (must be up before ANY peripheral that depends on it) +// 3. Touch reset pulse via XL9555 (needed before touch driver init) +// 4. Keyboard reset pulse via XL9555 (clean keyboard state) +// 5. LoRa power enable via XL9555 (must be on before SPI radio init) +// 6. GPS power + UART init +// 7. Parent class init (ESP32Board::begin) +// 8. LoRa SPI pin config + deep sleep wake handling +// 9. BQ27220 fuel gauge check +// 10. Low-voltage protection +// +// NOTE: We do NOT call TDeckBoard::begin() — we reimplement the boot sequence +// to handle XL9555-routed pins. BQ27220 methods are inherited unchanged. +// ============================================================================= + +void TDeckProMaxBoard::begin() { + + MESH_DEBUG_PRINTLN("TDeckProMaxBoard::begin() - T-Deck Pro MAX V0.1"); + + // ------ Step 1: I2C bus ------ + // All I2C devices (XL9555, BQ27220, TCA8418, CST328, DRV2605, ES8311, + // BQ25896, BHI260AP) share SDA=13, SCL=14. + Wire.begin(I2C_SDA, I2C_SCL); + Wire.setClock(100000); // 100kHz — safe for all devices on the bus + MESH_DEBUG_PRINTLN(" I2C initialized (SDA=%d SCL=%d)", I2C_SDA, I2C_SCL); + + // ------ Step 2: XL9555 I/O Expander ------ + // This must happen before anything that needs peripheral power or resets. + if (!xl9555_init()) { + Serial.println("CRITICAL: XL9555 init failed — peripherals will not work!"); + // Continue anyway; some things (display, keyboard INT) might still work + // without XL9555, but LoRa/GPS/modem will be dead. + } + + // ------ Step 3: Touch reset pulse ------ + // The touch controller (CST328) needs a clean reset via XL9555 IO07 + // before the touch driver tries to communicate with it. + touchReset(); + + // ------ Step 4: Keyboard reset pulse ------ + keyboardReset(); + + // ------ Step 5: Parent class init ------ + // ESP32Board::begin() handles common ESP32 setup. + // We skip TDeckBoard::begin() because it uses PIN_PERF_POWERON and + // direct GPIO for LoRa/GPS power that don't exist on MAX. + ESP32Board::begin(); + + // ------ Step 6: GPS UART init ------ + // GPS power was already enabled by XL9555 boot defaults (GPS_EN HIGH). + // Now init the UART with the MAX-specific pins. + #if HAS_GPS + Serial2.begin(GPS_BAUDRATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); + MESH_DEBUG_PRINTLN(" GPS Serial2 initialized (RX=%d TX=%d @ %d baud)", + GPS_RX_PIN, GPS_TX_PIN, GPS_BAUDRATE); + #endif + + // ------ Step 7: Configure user button ------ + pinMode(PIN_USER_BTN, INPUT); + + // ------ Step 8: Configure LoRa SPI pins ------ + // LoRa power is already enabled via XL9555 (LORA_EN HIGH in boot defaults). + pinMode(P_LORA_MISO, INPUT_PULLUP); + + // ------ Step 9: Handle wake from deep sleep ------ + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + uint64_t wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1ULL << P_LORA_DIO_1)) { + startup_reason = BD_STARTUP_RX_PACKET; + } + rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } + + // ------ Step 10: BQ27220 fuel gauge ------ + #if HAS_BQ27220 + uint16_t voltage = getBattMilliVolts(); + MESH_DEBUG_PRINTLN(" Battery voltage: %d mV", voltage); + configureFuelGauge(); // Inherited from TDeckBoard — sets 1500 mAh + #endif + + // ------ Step 11: Early low-voltage protection ------ + #if HAS_BQ27220 && defined(AUTO_SHUTDOWN_MILLIVOLTS) + { + uint16_t bootMv = getBattMilliVolts(); + if (bootMv > 0 && bootMv < AUTO_SHUTDOWN_MILLIVOLTS) { + Serial.printf("CRITICAL: Boot voltage %dmV < %dmV — sleeping immediately\n", + bootMv, AUTO_SHUTDOWN_MILLIVOLTS); + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + esp_sleep_enable_ext1_wakeup(1ULL << PIN_USER_BTN, ESP_EXT1_WAKEUP_ANY_HIGH); + esp_deep_sleep_start(); + } + } + #endif + + // ------ Step 12: E-ink backlight (working on MAX!) ------ + // Configure LEDC PWM for backlight brightness control. + // Start with backlight OFF — UI code can enable it when needed. + #ifdef PIN_EINK_BL + // Arduino ESP32 core 2.x uses channel-based LEDC API + ledcSetup(EINK_BL_LEDC_CHANNEL, 1000, 8); // Channel 0, 1kHz, 8-bit resolution + ledcAttachPin(PIN_EINK_BL, EINK_BL_LEDC_CHANNEL); + ledcWrite(EINK_BL_LEDC_CHANNEL, 0); // Off by default + MESH_DEBUG_PRINTLN(" Backlight PWM configured on IO%d", PIN_EINK_BL); + #endif + + MESH_DEBUG_PRINTLN("TDeckProMaxBoard::begin() - complete"); +} + + +// ============================================================================= +// XL9555 I/O Expander — Lightweight I2C Driver +// ============================================================================= + +bool TDeckProMaxBoard::xl9555_writeReg(uint8_t reg, uint8_t val) { + Wire.beginTransmission(I2C_ADDR_XL9555); + Wire.write(reg); + Wire.write(val); + return Wire.endTransmission() == 0; +} + +uint8_t TDeckProMaxBoard::xl9555_readReg(uint8_t reg) { + Wire.beginTransmission(I2C_ADDR_XL9555); + Wire.write(reg); + Wire.endTransmission(false); + Wire.requestFrom((uint8_t)I2C_ADDR_XL9555, (uint8_t)1); + return Wire.available() ? Wire.read() : 0xFF; +} + +bool TDeckProMaxBoard::xl9555_init() { + MESH_DEBUG_PRINTLN(" XL9555: Initializing I/O expander at 0x%02X", I2C_ADDR_XL9555); + + // Verify XL9555 is present on the bus + Wire.beginTransmission(I2C_ADDR_XL9555); + if (Wire.endTransmission() != 0) { + Serial.println(" XL9555: NOT FOUND on I2C bus!"); + _xlReady = false; + return false; + } + + // Set ALL pins as outputs (config register: 0 = output) + // Port 0 (pins 0-7): all output + if (!xl9555_writeReg(XL9555_REG_CONFIG_0, 0x00)) return false; + // Port 1 (pins 8-15): all output + if (!xl9555_writeReg(XL9555_REG_CONFIG_1, 0x00)) return false; + + // Apply boot defaults + _xlPort0 = XL9555_BOOT_PORT0; + _xlPort1 = XL9555_BOOT_PORT1; + if (!xl9555_writeReg(XL9555_REG_OUTPUT_0, _xlPort0)) return false; + if (!xl9555_writeReg(XL9555_REG_OUTPUT_1, _xlPort1)) return false; + + _xlReady = true; + + MESH_DEBUG_PRINTLN(" XL9555: Ready (Port0=0x%02X Port1=0x%02X)", _xlPort0, _xlPort1); + MESH_DEBUG_PRINTLN(" XL9555: LoRa=%s GPS=%s 1V8=%s Modem=%s Antenna=%s", + (_xlPort0 & (1 << XL_PIN_LORA_EN)) ? "ON" : "OFF", + (_xlPort0 & (1 << XL_PIN_GPS_EN)) ? "ON" : "OFF", + (_xlPort0 & (1 << XL_PIN_1V8_EN)) ? "ON" : "OFF", + (_xlPort0 & (1 << XL_PIN_6609_EN)) ? "ON" : "OFF", + (_xlPort0 & (1 << XL_PIN_LORA_SEL)) ? "internal" : "external"); + + return true; +} + +void TDeckProMaxBoard::xl9555_digitalWrite(uint8_t pin, bool value) { + if (!_xlReady) return; + + if (pin < 8) { + // Port 0 + if (value) _xlPort0 |= (1 << pin); + else _xlPort0 &= ~(1 << pin); + xl9555_writeReg(XL9555_REG_OUTPUT_0, _xlPort0); + } else if (pin < 16) { + // Port 1 (subtract 8 for bit position) + uint8_t bit = pin - 8; + if (value) _xlPort1 |= (1 << bit); + else _xlPort1 &= ~(1 << bit); + xl9555_writeReg(XL9555_REG_OUTPUT_1, _xlPort1); + } +} + +bool TDeckProMaxBoard::xl9555_digitalRead(uint8_t pin) const { + if (pin < 8) return (_xlPort0 >> pin) & 1; + if (pin < 16) return (_xlPort1 >> (pin - 8)) & 1; + return false; +} + +void TDeckProMaxBoard::xl9555_writePort0(uint8_t val) { + _xlPort0 = val; + if (_xlReady) xl9555_writeReg(XL9555_REG_OUTPUT_0, val); +} + +void TDeckProMaxBoard::xl9555_writePort1(uint8_t val) { + _xlPort1 = val; + if (_xlReady) xl9555_writeReg(XL9555_REG_OUTPUT_1, val); +} + + +// ============================================================================= +// High-level peripheral control +// ============================================================================= + +// ---- Modem (A7682E) ---- + +void TDeckProMaxBoard::modemPowerOn() { + MESH_DEBUG_PRINTLN(" XL9555: Modem power ON (6609_EN HIGH)"); + xl9555_digitalWrite(XL_PIN_6609_EN, HIGH); + delay(100); // Allow SGM6609 boost to stabilise +} + +void TDeckProMaxBoard::modemPowerOff() { + MESH_DEBUG_PRINTLN(" XL9555: Modem power OFF (6609_EN LOW)"); + xl9555_digitalWrite(XL_PIN_6609_EN, LOW); +} + +void TDeckProMaxBoard::modemPwrkeyPulse() { + // A7682E power-on sequence: pulse PWRKEY LOW for >= 500ms + // (Some datasheets say pull HIGH then LOW; LilyGo factory sets HIGH then toggles.) + MESH_DEBUG_PRINTLN(" XL9555: Modem PWRKEY pulse"); + xl9555_digitalWrite(XL_PIN_PWRKEY_EN, HIGH); + delay(100); + xl9555_digitalWrite(XL_PIN_PWRKEY_EN, LOW); + delay(1200); + xl9555_digitalWrite(XL_PIN_PWRKEY_EN, HIGH); +} + +// ---- Audio output selection ---- + +void TDeckProMaxBoard::selectAudioES8311() { + MESH_DEBUG_PRINTLN(" XL9555: Audio select → ES8311"); + xl9555_digitalWrite(XL_PIN_AUDIO_SEL, LOW); +} + +void TDeckProMaxBoard::selectAudioModem() { + MESH_DEBUG_PRINTLN(" XL9555: Audio select → A7682E"); + xl9555_digitalWrite(XL_PIN_AUDIO_SEL, HIGH); +} + +void TDeckProMaxBoard::amplifierEnable() { + xl9555_digitalWrite(XL_PIN_AMPLIFIER, HIGH); +} + +void TDeckProMaxBoard::amplifierDisable() { + xl9555_digitalWrite(XL_PIN_AMPLIFIER, LOW); +} + +// ---- LoRa antenna selection ---- + +void TDeckProMaxBoard::loraAntennaInternal() { + MESH_DEBUG_PRINTLN(" XL9555: LoRa antenna → internal"); + xl9555_digitalWrite(XL_PIN_LORA_SEL, HIGH); +} + +void TDeckProMaxBoard::loraAntennaExternal() { + MESH_DEBUG_PRINTLN(" XL9555: LoRa antenna → external"); + xl9555_digitalWrite(XL_PIN_LORA_SEL, LOW); +} + +// ---- Motor (DRV2605) ---- + +void TDeckProMaxBoard::motorEnable() { + xl9555_digitalWrite(XL_PIN_MOTOR_EN, HIGH); +} + +void TDeckProMaxBoard::motorDisable() { + xl9555_digitalWrite(XL_PIN_MOTOR_EN, LOW); +} + +// ---- Touch reset ---- + +void TDeckProMaxBoard::touchReset() { + if (!_xlReady) return; + MESH_DEBUG_PRINTLN(" XL9555: Touch reset pulse"); + xl9555_digitalWrite(XL_PIN_TOUCH_RST, LOW); + delay(20); + xl9555_digitalWrite(XL_PIN_TOUCH_RST, HIGH); + delay(50); // Allow touch controller to come out of reset +} + +// ---- Keyboard reset ---- + +void TDeckProMaxBoard::keyboardReset() { + if (!_xlReady) return; + MESH_DEBUG_PRINTLN(" XL9555: Keyboard reset pulse"); + xl9555_digitalWrite(XL_PIN_KEY_RST, LOW); + delay(20); + xl9555_digitalWrite(XL_PIN_KEY_RST, HIGH); + delay(50); +} + +// ---- GPS power ---- + +void TDeckProMaxBoard::gpsPowerOn() { + xl9555_digitalWrite(XL_PIN_GPS_EN, HIGH); + delay(100); +} + +void TDeckProMaxBoard::gpsPowerOff() { + xl9555_digitalWrite(XL_PIN_GPS_EN, LOW); +} + +// ---- LoRa power ---- + +void TDeckProMaxBoard::loraPowerOn() { + xl9555_digitalWrite(XL_PIN_LORA_EN, HIGH); + delay(10); +} + +void TDeckProMaxBoard::loraPowerOff() { + xl9555_digitalWrite(XL_PIN_LORA_EN, LOW); +} + +// ---- E-ink backlight (working on MAX!) ---- + +void TDeckProMaxBoard::backlightOn() { + #ifdef PIN_EINK_BL + ledcWrite(EINK_BL_LEDC_CHANNEL, 255); + #endif +} + +void TDeckProMaxBoard::backlightOff() { + #ifdef PIN_EINK_BL + ledcWrite(EINK_BL_LEDC_CHANNEL, 0); + #endif +} + +void TDeckProMaxBoard::backlightSetBrightness(uint8_t duty) { + #ifdef PIN_EINK_BL + ledcWrite(EINK_BL_LEDC_CHANNEL, duty); + #endif +} diff --git a/variants/lilygo_tdeck_pro_max/TDeckProMaxBoard.h b/variants/lilygo_tdeck_pro_max/TDeckProMaxBoard.h new file mode 100644 index 00000000..91126284 --- /dev/null +++ b/variants/lilygo_tdeck_pro_max/TDeckProMaxBoard.h @@ -0,0 +1,108 @@ +#pragma once + +// ============================================================================= +// TDeckProMaxBoard — Board support for LilyGo T-Deck Pro MAX V0.1 +// +// Extends TDeckBoard (which provides all BQ27220 fuel gauge methods) with: +// - XL9555 I/O expander initialisation and control +// - XL9555-routed peripheral power management +// - Touch/keyboard reset via XL9555 +// - Modem power/PWRKEY via XL9555 +// - LoRa antenna selection via XL9555 +// - Audio output mux (ES8311 vs A7682E) via XL9555 +// - Speaker amplifier enable via XL9555 +// +// The XL9555 must be initialised before LoRa, GPS, modem, or touch are used. +// All power enables, resets, and switches go through I2C — not direct GPIO. +// ============================================================================= + +#include "variant.h" +#include "TDeckBoard.h" // Inherits BQ27220 fuel gauge, deep sleep, power management + +class TDeckProMaxBoard : public TDeckBoard { +public: + void begin(); + + const char* getManufacturerName() const { + return "LilyGo T-Deck Pro MAX"; + } + + // ------------------------------------------------------------------------- + // XL9555 I/O Expander — lightweight inline driver + // + // The XL9555 has 16 I/O pins across two 8-bit ports. + // Pin 0-7 = Port 0, Pin 8-15 = Port 1. + // We shadow the output state in _xlPort0/_xlPort1 to allow + // single-bit set/clear without read-modify-write over I2C. + // ------------------------------------------------------------------------- + + // Initialise XL9555: set all used pins as outputs, apply boot defaults. + // Returns true if I2C communication with XL9555 succeeded. + bool xl9555_init(); + + // Set a single XL9555 pin HIGH or LOW (pin 0-15). + void xl9555_digitalWrite(uint8_t pin, bool value); + + // Read the current output state of a pin (from shadow, not I2C read). + bool xl9555_digitalRead(uint8_t pin) const; + + // Write raw port values (for batch updates). + void xl9555_writePort0(uint8_t val); + void xl9555_writePort1(uint8_t val); + + // ------------------------------------------------------------------------- + // High-level peripheral control (delegates to XL9555) + // ------------------------------------------------------------------------- + + // Modem (A7682E) power control + void modemPowerOn(); // Enable SGM6609 boost (6609_EN HIGH) + void modemPowerOff(); // Disable SGM6609 boost (6609_EN LOW) + void modemPwrkeyPulse(); // Toggle PWRKEY: HIGH 100ms → LOW 1200ms → HIGH + + // Audio output selection + void selectAudioES8311(); // AUDIO_SEL LOW → ES8311 output to speaker/headphones + void selectAudioModem(); // AUDIO_SEL HIGH → A7682E output to speaker/headphones + void amplifierEnable(); // NS4150B amplifier ON (louder speaker) + void amplifierDisable(); // NS4150B amplifier OFF (saves power) + + // LoRa antenna selection (SKY13453 RF switch) + void loraAntennaInternal(); // LORA_SEL HIGH → internal PCB antenna (default) + void loraAntennaExternal(); // LORA_SEL LOW → external IPEX antenna + + // Motor (DRV2605) power + void motorEnable(); // MOTOR_EN HIGH + void motorDisable(); // MOTOR_EN LOW + + // Touch controller reset via XL9555 + void touchReset(); // Pulse TOUCH_RST: LOW 20ms → HIGH, then 50ms settle + + // Keyboard reset via XL9555 + void keyboardReset(); // Pulse KEY_RST: LOW 20ms → HIGH, then 50ms settle + + // GPS power control via XL9555 + void gpsPowerOn(); // GPS_EN HIGH + void gpsPowerOff(); // GPS_EN LOW + + // LoRa power control via XL9555 + void loraPowerOn(); // LORA_EN HIGH + void loraPowerOff(); // LORA_EN LOW + + // ------------------------------------------------------------------------- + // E-ink front-light control + // On MAX, IO41 has a working backlight circuit (boost converter + LEDs). + // PWM control for brightness is possible via ledc. + // ------------------------------------------------------------------------- + void backlightOn(); + void backlightOff(); + void backlightSetBrightness(uint8_t duty); // 0-255, via LEDC PWM + +private: + // Shadow registers for XL9555 output ports (avoid I2C read-modify-write) + uint8_t _xlPort0 = XL9555_BOOT_PORT0; + uint8_t _xlPort1 = XL9555_BOOT_PORT1; + bool _xlReady = false; + + // Low-level I2C helpers + bool xl9555_writeReg(uint8_t reg, uint8_t val); + uint8_t xl9555_readReg(uint8_t reg); +}; diff --git a/variants/lilygo_tdeck_pro_max/Tca8418keyboard.h b/variants/lilygo_tdeck_pro_max/Tca8418keyboard.h new file mode 100644 index 00000000..4e5635a6 --- /dev/null +++ b/variants/lilygo_tdeck_pro_max/Tca8418keyboard.h @@ -0,0 +1,360 @@ +#pragma once + +#include +#include + +// TCA8418 Register addresses +#define TCA8418_REG_CFG 0x01 +#define TCA8418_REG_INT_STAT 0x02 +#define TCA8418_REG_KEY_LCK_EC 0x03 +#define TCA8418_REG_KEY_EVENT_A 0x04 +#define TCA8418_REG_KP_GPIO1 0x1D +#define TCA8418_REG_KP_GPIO2 0x1E +#define TCA8418_REG_KP_GPIO3 0x1F +#define TCA8418_REG_DEBOUNCE 0x29 +#define TCA8418_REG_GPI_EM1 0x20 +#define TCA8418_REG_GPI_EM2 0x21 +#define TCA8418_REG_GPI_EM3 0x22 + +// Key codes for special keys +#define KB_KEY_NONE 0 +#define KB_KEY_BACKSPACE '\b' +#define KB_KEY_ENTER '\r' +#define KB_KEY_SPACE ' ' +#define KB_KEY_EMOJI 0x01 // Non-printable code for $ key (emoji picker) +#define KB_KEY_BACKLIGHT 0x02 // Non-printable code for Alt+B (backlight toggle, MAX only) + +class TCA8418Keyboard { +private: + uint8_t _addr; + TwoWire* _wire; + bool _initialized; + bool _shiftActive; // Sticky shift (one-shot or held) + bool _shiftConsumed; // Was shift active for the last returned key + bool _shiftHeld; // Shift key physically held down + bool _shiftUsedWhileHeld; // Was shift consumed by any key while held + bool _altActive; // Sticky alt (one-shot) + bool _symActive; // Sticky sym (one-shot) + unsigned long _lastShiftTime; // For Shift+key combos + + uint8_t readReg(uint8_t reg) { + _wire->beginTransmission(_addr); + _wire->write(reg); + _wire->endTransmission(); + _wire->requestFrom(_addr, (uint8_t)1); + return _wire->available() ? _wire->read() : 0; + } + + void writeReg(uint8_t reg, uint8_t val) { + _wire->beginTransmission(_addr); + _wire->write(reg); + _wire->write(val); + _wire->endTransmission(); + } + + // Map raw key codes to characters (from working reader firmware) + char getKeyChar(uint8_t keyCode) { + switch (keyCode) { + // Row 1 - QWERTYUIOP + case 10: return 'q'; // Q (was 97 on different hardware) + case 9: return 'w'; + case 8: return 'e'; + case 7: return 'r'; + case 6: return 't'; + case 5: return 'y'; + case 4: return 'u'; + case 3: return 'i'; + case 2: return 'o'; + case 1: return 'p'; + + // Row 2 - ASDFGHJKL + Backspace + case 20: return 'a'; // A (was 98 on different hardware) + case 19: return 's'; + case 18: return 'd'; + case 17: return 'f'; + case 16: return 'g'; + case 15: return 'h'; + case 14: return 'j'; + case 13: return 'k'; + case 12: return 'l'; + case 11: return '\b'; // Backspace + + // Row 3 - Alt ZXCVBNM Sym Enter + case 30: return 0; // Alt - handled separately + case 29: return 'z'; + case 28: return 'x'; + case 27: return 'c'; + case 26: return 'v'; + case 25: return 'b'; + case 24: return 'n'; + case 23: return 'm'; + case 22: return 0; // Symbol key - handled separately + case 21: return '\r'; // Enter + + // Row 4 - Shift Mic Space Sym Shift + case 35: return 0; // Left shift - handled separately + case 34: return 0; // Mic + case 33: return ' '; // Space + case 32: return 0; // Sym - handled separately + case 31: return 0; // Right shift - handled separately + + default: return 0; + } + } + + // Map key with Alt modifier - same as Sym for this keyboard + char getAltChar(uint8_t keyCode) { + return getSymChar(keyCode); // Alt does same as Sym + } + + // Map key with Sym modifier - based on actual T-Deck Pro keyboard silk-screen + char getSymChar(uint8_t keyCode) { + switch (keyCode) { + // Row 1: Q W E R T Y U I O P + case 10: return '#'; // Q -> # + case 9: return '1'; // W -> 1 + case 8: return '2'; // E -> 2 + case 7: return '3'; // R -> 3 + case 6: return '('; // T -> ( + case 5: return ')'; // Y -> ) + case 4: return '_'; // U -> _ + case 3: return '-'; // I -> - + case 2: return '+'; // O -> + + case 1: return '@'; // P -> @ + + // Row 2: A S D F G H J K L + case 20: return '*'; // A -> * + case 19: return '4'; // S -> 4 + case 18: return '5'; // D -> 5 + case 17: return '6'; // F -> 6 + case 16: return '/'; // G -> / + case 15: return ':'; // H -> : + case 14: return ';'; // J -> ; + case 13: return '\''; // K -> ' + case 12: return '"'; // L -> " + + // Row 3: Z X C V B N M + case 29: return '7'; // Z -> 7 + case 28: return '8'; // X -> 8 + case 27: return '9'; // C -> 9 + case 26: return '?'; // V -> ? + case 25: return '!'; // B -> ! + case 24: return ','; // N -> , + case 23: return '.'; // M -> . + + // Row 4: Mic key -> 0 + case 34: return '0'; // Mic -> 0 + + default: return 0; + } + } + +public: + TCA8418Keyboard(uint8_t addr = 0x34, TwoWire* wire = &Wire) + : _addr(addr), _wire(wire), _initialized(false), + _shiftActive(false), _shiftConsumed(false), _shiftHeld(false), _shiftUsedWhileHeld(false), _altActive(false), _symActive(false), _lastShiftTime(0) {} + + bool begin() { + // Check if device responds + _wire->beginTransmission(_addr); + if (_wire->endTransmission() != 0) { + Serial.println("TCA8418: Device not found"); + return false; + } + + // --- Warm-reboot safe init sequence --- + // The TCA8418 stays powered across ESP32 resets (no dedicated RST pin), + // so the scanner may still be active from the previous session. + // We must disable it before reconfiguring the matrix. + + // 1. Disable scanner — stop all scanning before touching config + writeReg(TCA8418_REG_CFG, 0x00); + + // 2. Drain any stale events from the previous session + for (int i = 0; i < 16; i++) { + if ((readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break; + readReg(TCA8418_REG_KEY_EVENT_A); + } + writeReg(TCA8418_REG_INT_STAT, 0x1F); // Clear all interrupt flags + + // 3. Explicitly clear GPI event masks (prevent phantom GPI events) + writeReg(TCA8418_REG_GPI_EM1, 0x00); + writeReg(TCA8418_REG_GPI_EM2, 0x00); + writeReg(TCA8418_REG_GPI_EM3, 0x00); + + // 4. Configure keyboard matrix (8 rows x 10 cols) + writeReg(TCA8418_REG_KP_GPIO1, 0xFF); // Rows 0-7 as keypad + writeReg(TCA8418_REG_KP_GPIO2, 0xFF); // Cols 0-7 as keypad + writeReg(TCA8418_REG_KP_GPIO3, 0x03); // Cols 8-9 as keypad + + // 5. Set debounce + writeReg(TCA8418_REG_DEBOUNCE, 0x03); + + // 6. Final pre-enable cleanup + writeReg(TCA8418_REG_INT_STAT, 0x1F); + + // 7. Enable scanner — matrix config is stable, safe to start scanning + writeReg(TCA8418_REG_CFG, 0x11); // KE_IEN + INT_CFG + + // 8. Let scanner stabilise, then flush any spurious first-scan events + delay(5); + for (int i = 0; i < 16; i++) { + if ((readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break; + readReg(TCA8418_REG_KEY_EVENT_A); + } + writeReg(TCA8418_REG_INT_STAT, 0x1F); + + _initialized = true; + Serial.println("TCA8418: Keyboard initialized OK"); + return true; + } + + // Read a key press - returns character or 0 if no key + char readKey() { + if (!_initialized) return 0; + + // Check for key events in FIFO + uint8_t keyCount = readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F; + if (keyCount == 0) return 0; + + // Read key event from FIFO + uint8_t keyEvent = readReg(TCA8418_REG_KEY_EVENT_A); + + // Bit 7: 1 = press, 0 = release + bool pressed = (keyEvent & 0x80) != 0; + uint8_t keyCode = keyEvent & 0x7F; + + // Clear interrupt + writeReg(TCA8418_REG_INT_STAT, 0x1F); + + Serial.printf("KB raw: event=0x%02X code=%d pressed=%d count=%d\n", + keyEvent, keyCode, pressed, keyCount); + + // Track shift release (before the general release-ignore) + if (!pressed && (keyCode == 35 || keyCode == 31)) { + _shiftHeld = false; + // If shift was used while held (e.g. cursor nav), clear it completely + // so the next bare keypress isn't treated as shifted. + // If shift was NOT used (tap-then-release), keep _shiftActive for one-shot. + if (_shiftUsedWhileHeld) { + _shiftActive = false; + } + _shiftUsedWhileHeld = false; + return 0; + } + + // Only act on key press, not release + if (!pressed || keyCode == 0) { + return 0; + } + + // Handle modifier keys - set sticky state and return 0 + if (keyCode == 35 || keyCode == 31) { // Shift keys + _shiftActive = true; + _shiftHeld = true; + _shiftUsedWhileHeld = false; + _lastShiftTime = millis(); + Serial.println("KB: Shift activated"); + return 0; + } + if (keyCode == 30) { // Alt key + _altActive = true; + Serial.println("KB: Alt activated"); + return 0; + } + if (keyCode == 32) { // Sym key (bottom row) + _symActive = true; + Serial.println("KB: Sym activated"); + return 0; + } + + // Handle dedicated $ key (key code 22, next to M) + // Bare press = emoji picker, Sym+$ = literal '$' + if (keyCode == 22) { + if (_symActive) { + _symActive = false; + Serial.println("KB: Sym+$ -> '$'"); + return '$'; + } + Serial.println("KB: $ key -> emoji"); + return KB_KEY_EMOJI; + } + + // Handle Mic key - always produces '0' (silk-screened on key) + // Sym+Mic also produces '0' (consumes sym so it doesn't leak) + if (keyCode == 34) { + _symActive = false; + Serial.println("KB: Mic -> '0'"); + return '0'; + } + + // Get the character + char c = 0; + + // Alt+B -> backlight toggle (T-Deck Pro MAX only — working front-light on IO41) + if (_altActive && keyCode == 25) { // keyCode 25 = B + _altActive = false; + Serial.println("KB: Alt+B -> backlight toggle"); + return KB_KEY_BACKLIGHT; + } + + if (_altActive) { + c = getAltChar(keyCode); + _altActive = false; // Reset sticky alt + if (c != 0) { + Serial.printf("KB: Alt+key -> '%c'\n", c); + return c; + } + } + + if (_symActive) { + c = getSymChar(keyCode); + _symActive = false; // Reset sticky sym + if (c != 0) { + Serial.printf("KB: Sym+key -> '%c'\n", c); + return c; + } + } + + c = getKeyChar(keyCode); + + if (c != 0 && _shiftActive) { + // Apply shift - uppercase letters + if (c >= 'a' && c <= 'z') { + c = c - 'a' + 'A'; + } + // Track that shift was used while physically held + if (_shiftHeld) { + _shiftUsedWhileHeld = true; + } + // Only clear shift if it's one-shot (tap), not held down + if (!_shiftHeld) { + _shiftActive = false; + } + _shiftConsumed = true; // Record that shift was active for this key + } else { + _shiftConsumed = false; + } + + if (c != 0) { + Serial.printf("KB: code %d -> '%c' (0x%02X)\n", keyCode, c >= 32 ? c : '?', c); + } else { + Serial.printf("KB: code %d -> UNMAPPED\n", keyCode); + } + + return c; + } + + bool isReady() const { return _initialized; } + + // Check if shift was pressed within the last N milliseconds + bool wasShiftRecentlyPressed(unsigned long withinMs = 500) const { + return (millis() - _lastShiftTime) < withinMs; + } + + // Check if shift was active when the most recent key was produced + // (immune to e-ink refresh timing unlike wasShiftRecentlyPressed) + bool wasShiftConsumed() const { + return _shiftConsumed; + } +}; \ No newline at end of file diff --git a/variants/lilygo_tdeck_pro_max/platformio.ini b/variants/lilygo_tdeck_pro_max/platformio.ini new file mode 100644 index 00000000..5b5889f6 --- /dev/null +++ b/variants/lilygo_tdeck_pro_max/platformio.ini @@ -0,0 +1,232 @@ +; ============================================================================= +; T-Deck Pro MAX V0.1 — Meck Build Environments +; +; Hardware: ESP32-S3 + XL9555 I/O expander + combined 4G (A7682E) + Audio (ES8311) +; +; Key differences from LilyGo_TDeck_Pro (V1.1): +; - Peripheral power controlled via XL9555 (not direct GPIO) +; - 4G modem and ES8311 audio coexist (no longer mutually exclusive) +; - ES8311 I2C codec replaces PCM5102A (different I2S pins, needs I2C config) +; - Several GPIO reassignments (see variant.h for full map) +; - 1500 mAh battery (was 1400) +; - Working e-ink front-light on IO41 +; +; WHAT WORKS OUT OF THE BOX: +; LoRa mesh, keyboard, e-ink display, GPS, touchscreen, battery management, +; SD card, text reader, notes, contacts, channels, settings, discovery, +; last heard, repeater admin, web reader (WiFi builds), OTA update. +; +; NEEDS ADAPTATION (future work): +; - HAS_4G_MODEM: ModemManager uses direct GPIO for MODEM_POWER_EN/PWRKEY +; which are XL9555-routed on MAX. Needs board.modemPowerOn() etc. +; - MECK_AUDIO_VARIANT: ES8311 needs I2C codec init (PCM5102A didn't). +; I2S pins are different. AudiobookPlayerScreen needs ES8311 driver. +; - Combined 4G+audio: existing #ifdef guards treat them as mutually +; exclusive. Needs restructuring for coexistence. +; ============================================================================= + +; --------------------------------------------------------------------------- +; Base environment for T-Deck Pro MAX +; --------------------------------------------------------------------------- +[LilyGo_TDeck_Pro_Max] +extends = esp32_base +extra_scripts = post:merge_firmware.py +board = t-deck_pro_max +board_build.flash_mode = qio +board_build.f_flash = 80000000L +board_build.arduino.memory_type = qio_qspi +board_upload.flash_size = 16MB +build_flags = + ${esp32_base.build_flags} + ${sensor_base.build_flags} + ; Include MAX variant first (for variant.h, target.h, TDeckProMaxBoard.h) + ; then V1.1 variant (for TDeckBoard.h, which TDeckProMaxBoard inherits from) + -I variants/LilyGo_TDeck_Pro_Max + -I variants/LilyGo_TDeck_Pro + ; Both defines needed: LilyGo_TDeck_Pro for existing UI code guards, + ; LilyGo_TDeck_Pro_Max for MAX-specific code paths + -D LilyGo_TDeck_Pro + -D LilyGo_TDeck_Pro_Max + -D HAS_XL9555=1 + -D HAS_GPS=1 + -D BOARD_HAS_PSRAM=1 + -D CORE_DEBUG_LEVEL=1 + -D FORMAT_SPIFFS_IF_FAILED=1 + -D FORMAT_LITTLEFS_IF_FAILED=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_DIO2_AS_RF_SWITCH + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_DIO3_TCXO_VOLTAGE=2.4f + ; LoRa SPI pins (direct GPIO — unchanged from V1.1) + -D P_LORA_DIO_1=5 + -D P_LORA_NSS=3 + -D P_LORA_RESET=4 + -D P_LORA_BUSY=6 + -D P_LORA_SCLK=36 + -D P_LORA_MISO=47 + -D P_LORA_MOSI=33 + ; P_LORA_EN deliberately NOT defined — LoRa power via XL9555 in board.begin() + ; GPS pins (direct GPIO — changed from V1.1!) + -D ENV_INCLUDE_GPS=1 + -D ENV_SKIP_GPS_DETECT=1 + -D PIN_GPS_RX=2 + -D PIN_GPS_TX=16 + -D GPS_BAUD_RATE=38400 + ; Sensor exclusions (same as V1.1) + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_SHTC3=0 + -D ENV_INCLUDE_SHT4X=0 + -D ENV_INCLUDE_LPS22HB=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_INA226=0 + -D ENV_INCLUDE_INA260=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_BME680=0 + -D ENV_INCLUDE_BMP085=0 + ; E-ink display (pin changes from V1.1: RST=9, BL=41) + -D USE_EINK + -D DISPLAY_CLASS=GxEPDDisplay + -D EINK_DISPLAY_MODEL=GxEPD2_310_GDEQ031T10 + -D EINK_WIDTH=240 + -D EINK_HEIGHT=320 + -D EINK_CS=34 + -D EINK_DC=35 + -D EINK_RST=9 + -D EINK_BUSY=37 + -D EINK_SCLK=36 + -D EINK_MOSI=33 + -D EINK_BL=41 + -D EINK_NOT_HIBERNATE=1 + ; Battery (1500 mAh on MAX, was 1400 on V1.1) + -D HAS_BQ27220=1 + -D AUTO_SHUTDOWN_MILLIVOLTS=2800 + ; Display rendering parameters + -D EINK_LIMIT_FASTREFRESH=10 + -D EINK_LIMIT_GHOSTING_PX=2000 + -D DISPLAY_ROTATION=0 + -D EINK_ROTATION=0 + -D EINK_SCALE_X=1.875f + -D EINK_SCALE_Y=2.5f + -D EINK_X_OFFSET=0 + -D EINK_Y_OFFSET=5 + ; Legacy display pin aliases (for GxEPDDisplay.cpp) + -D PIN_DISPLAY_CS=34 + -D PIN_DISPLAY_DC=35 + -D PIN_DISPLAY_RST=9 + -D PIN_DISPLAY_BUSY=37 + -D PIN_DISPLAY_SCLK=36 + -D PIN_DISPLAY_MISO=-1 + -D PIN_DISPLAY_MOSI=33 + -D PIN_DISPLAY_BL=41 + -D PIN_USER_BTN=0 + ; Touch (INT is direct GPIO; RST is XL9555, handled by board class) + -D HAS_TOUCHSCREEN=1 + -D CST328_PIN_INT=12 + -D CST328_PIN_RST=-1 + -D ARDUINO_LOOP_STACK_SIZE=32768 +build_src_filter = ${esp32_base.build_src_filter} + ; Include TDeckBoard.cpp from V1.1 (parent class with BQ27220 code) + +<../variants/LilyGo_TDeck_Pro/TDeckBoard.cpp> + ; Include MAX variant (target.cpp + TDeckProMaxBoard.cpp) + +<../variants/LilyGo_TDeck_Pro_Max> + + +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + zinggjm/GxEPD2@^1.5.9 + adafruit/Adafruit GFX Library@^1.11.0 + bitbank2/PNGdec@^1.0.1 + WebServer + Update + + +; =========================================================================== +; Meck MAX builds — LoRa mesh works out of the box on all variants. +; 4G modem and ES8311 audio need adaptation before they can be enabled. +; =========================================================================== + +; MAX + BLE companion (standard BLE phone bridging) +; Both 4G + audio hardware present but not yet enabled in firmware. +; BLE_PIN_CODE limit: MAX_CONTACTS=500 (BLE protocol ceiling). +[env:meck_max_ble] +extends = LilyGo_TDeck_Pro_Max +build_flags = + ${LilyGo_TDeck_Pro_Max.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 + -D MAX_GROUP_CHANNELS=20 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 + -D MECK_WEB_READER=1 + -D MECK_OTA_UPDATE=1 + -D FIRMWARE_VERSION='"Meck v1.3.MAX"' +build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + +lib_deps = + ${LilyGo_TDeck_Pro_Max.lib_deps} + densaugeo/base64 @ ~1.4.0 + https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6 + bitbank2/JPEGDEC + +; MAX + WiFi companion (WiFi app bridging — no BLE, higher contact limit) +; WiFi credentials loaded from SD card (/web/wifi.cfg). +; Connect via MeshCore web app, meshcore.js, or Python CLI. +[env:meck_max_wifi] +extends = LilyGo_TDeck_Pro_Max +build_flags = + ${LilyGo_TDeck_Pro_Max.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=1500 + -D MAX_GROUP_CHANNELS=20 + -D MECK_WIFI_COMPANION=1 + -D TCP_PORT=5000 + -D WIFI_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D MECK_WEB_READER=1 + -D MECK_OTA_UPDATE=1 + -D FIRMWARE_VERSION='"Meck v1.3.MAX.WiFi"' +build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + +lib_deps = + ${LilyGo_TDeck_Pro_Max.lib_deps} + densaugeo/base64 @ ~1.4.0 + https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6 + bitbank2/JPEGDEC + +; MAX standalone (no BLE/WiFi — maximum battery life, LoRa mesh only) +; Contacts in PSRAM (1500 capacity). OTA enabled (WiFi AP on demand). +[env:meck_max_standalone] +extends = LilyGo_TDeck_Pro_Max +build_flags = + ${LilyGo_TDeck_Pro_Max.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=1500 + -D MAX_GROUP_CHANNELS=20 + -D OFFLINE_QUEUE_SIZE=1 + -D MECK_OTA_UPDATE=1 + -D FIRMWARE_VERSION='"Meck v1.3.MAX.SA"' +build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + +lib_deps = + ${LilyGo_TDeck_Pro_Max.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/lilygo_tdeck_pro_max/target.cpp b/variants/lilygo_tdeck_pro_max/target.cpp new file mode 100644 index 00000000..6f06a995 --- /dev/null +++ b/variants/lilygo_tdeck_pro_max/target.cpp @@ -0,0 +1,91 @@ +#include +#include "variant.h" +#include "target.h" + +TDeckProMaxBoard board; + +#if defined(P_LORA_SCLK) + static SPIClass loraSpi(HSPI); + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, loraSpi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if HAS_GPS + // Wrap Serial2 with a sentence counter so the UI can show NMEA throughput. + // MicroNMEALocationProvider reads through this wrapper transparently. + GPSStreamCounter gpsStream(Serial2); + MicroNMEALocationProvider gps(gpsStream, &rtc_clock); + EnvironmentSensorManager sensors(gps); +#else + SensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + MESH_DEBUG_PRINTLN("radio_init() - starting"); + + // NOTE: board.begin() is called by main.cpp setup() before radio_init() + // I2C is already initialized there with correct pins + + fallback_clock.begin(); + MESH_DEBUG_PRINTLN("radio_init() - fallback_clock started"); + + // Wire already initialized in board.begin() - just use it for RTC + rtc_clock.begin(Wire); + MESH_DEBUG_PRINTLN("radio_init() - rtc_clock started"); + +#if defined(P_LORA_SCLK) + MESH_DEBUG_PRINTLN("radio_init() - initializing LoRa SPI..."); + loraSpi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS); + MESH_DEBUG_PRINTLN("radio_init() - SPI initialized, calling radio.std_init()..."); + bool result = radio.std_init(&loraSpi); + MESH_DEBUG_PRINTLN("radio_init() - radio.std_init() returned: %s", result ? "SUCCESS" : "FAILED"); + return result; +#else + MESH_DEBUG_PRINTLN("radio_init() - calling radio.std_init() without custom SPI..."); + bool result = radio.std_init(); + return result; +#endif +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); + + // Longer preamble for low SF improves reliability — each symbol is shorter + // at low SF, so more symbols are needed for reliable detection. + // SF <= 8 gets 32 symbols (~65ms at SF7/62.5kHz); SF >= 9 keeps 16 (already ~131ms+). + // See: https://github.com/meshcore-dev/MeshCore/pull/1954 + uint16_t preamble = (sf <= 8) ? 32 : 16; + radio.setPreambleLength(preamble); + MESH_DEBUG_PRINTLN("radio_set_params() - bw=%.1f sf=%u preamble=%u", bw, sf, preamble); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} + +void radio_reset_agc() { + radio.setRxBoostedGainMode(true); +} diff --git a/variants/lilygo_tdeck_pro_max/target.h b/variants/lilygo_tdeck_pro_max/target.h new file mode 100644 index 00000000..4daa7ae0 --- /dev/null +++ b/variants/lilygo_tdeck_pro_max/target.h @@ -0,0 +1,47 @@ +#pragma once + +// Include variant.h first to ensure all board-specific defines are available +#include "variant.h" + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include + +#ifdef DISPLAY_CLASS + #include + #include +#endif + +#if HAS_GPS + #include "helpers/sensors/EnvironmentSensorManager.h" + #include "helpers/sensors/MicroNMEALocationProvider.h" + #include "GPSStreamCounter.h" +#else + #include +#endif + +extern TDeckProMaxBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; + +#if HAS_GPS + extern GPSStreamCounter gpsStream; + extern EnvironmentSensorManager sensors; +#else + extern SensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); +void radio_reset_agc(); diff --git a/variants/lilygo_tdeck_pro_max/variant.h b/variants/lilygo_tdeck_pro_max/variant.h new file mode 100644 index 00000000..2f48ce5f --- /dev/null +++ b/variants/lilygo_tdeck_pro_max/variant.h @@ -0,0 +1,301 @@ +#pragma once + +// ============================================================================= +// LilyGo T-Deck Pro MAX V0.1 - Pin Definitions +// Hardware revision: HD-V3-250911 +// +// KEY DIFFERENCES FROM T-Deck Pro V1.1: +// - XL9555 I/O expander (0x20) controls peripheral power, resets, and switches +// (LoRa EN, GPS EN, modem power, touch RST, keyboard RST, antenna sel, etc.) +// - 4G (A7682E) and audio (ES8311) coexist on ONE board — no longer mutually exclusive +// - ES8311 I2C codec replaces PCM5102A (needs I2C config, different I2S pins) +// - E-ink RST moved: IO9 (was IO16) +// - E-ink BL moved: IO41 (was IO45, now has working front-light hardware!) +// - GPS UART moved: RX=IO2, TX=IO16 (was RX=IO44, TX=IO43) +// - GPS/LoRa power via XL9555 (was direct GPIO 39/46) +// - Touch RST via XL9555 IO07 (was GPIO 38) +// - Modem power/PWRKEY via XL9555 (was direct GPIO 41/40) +// - No PIN_PERF_POWERON (IO10 is now modem UART RX) +// - Battery: 1500 mAh (was 1400 mAh) +// - LoRa antenna switch (SKY13453) controlled by XL9555 IO04 +// - Audio output mux (A7682E vs ES8311) controlled by XL9555 IO12 +// - Speaker amplifier (NS4150B) enable via XL9555 IO06 +// ============================================================================= + +// ----------------------------------------------------------------------------- +// E-Ink Display (GDEQ031T10 - 240x320) +// E-ink SHARES the SPI bus with LoRa and SD card (SCK=36, MOSI=33, MISO=47) +// They use different chip selects: E-ink CS=34, LoRa CS=3, SD CS=48 +// ----------------------------------------------------------------------------- +#define PIN_EINK_CS 34 +#define PIN_EINK_DC 35 +#define PIN_EINK_RES 9 // MAX: IO9 (was IO16 on V1.1) +#define PIN_EINK_BUSY 37 +#define PIN_EINK_SCLK 36 // Shared with LoRa + SD +#define PIN_EINK_MOSI 33 // Shared with LoRa + SD +#define PIN_EINK_BL 41 // MAX: IO41 — working front-light! (was IO45 non-functional on V1.1) + +// Legacy aliases for MeshCore compatibility +#define PIN_DISPLAY_CS PIN_EINK_CS +#define PIN_DISPLAY_DC PIN_EINK_DC +#define PIN_DISPLAY_RST PIN_EINK_RES +#define PIN_DISPLAY_BUSY PIN_EINK_BUSY +#define PIN_DISPLAY_SCLK PIN_EINK_SCLK +#define PIN_DISPLAY_MOSI PIN_EINK_MOSI + +// Display dimensions - native resolution of GDEQ031T10 +#define LCD_HOR_SIZE 240 +#define LCD_VER_SIZE 320 + +// E-ink model for GxEPD2 +#define EINK_DISPLAY_MODEL GxEPD2_310_GDEQ031T10 + +// ----------------------------------------------------------------------------- +// SPI Bus - Shared by LoRa, SD Card, AND E-ink display +// ----------------------------------------------------------------------------- +#define BOARD_SPI_SCLK 36 +#define BOARD_SPI_MISO 47 +#define BOARD_SPI_MOSI 33 + +// ----------------------------------------------------------------------------- +// I2C Bus +// ----------------------------------------------------------------------------- +#define I2C_SDA 13 +#define I2C_SCL 14 + +// Aliases for ESP32Board base class compatibility +#define PIN_BOARD_SDA I2C_SDA +#define PIN_BOARD_SCL I2C_SCL + +// I2C Device Addresses +#define I2C_ADDR_ES8311 0x18 // ES8311 audio codec (NEW on MAX) +#define I2C_ADDR_TOUCH 0x1A // CST328 +#define I2C_ADDR_XL9555 0x20 // XL9555 I/O expander (NEW on MAX) +#define I2C_ADDR_GYROSCOPE 0x28 // BHI260AP +#define I2C_ADDR_KEYBOARD 0x34 // TCA8418 +#define I2C_ADDR_BQ27220 0x55 // Fuel gauge +#define I2C_ADDR_DRV2605 0x5A // Motor driver (haptic) +#define I2C_ADDR_BQ25896 0x6B // Charger + +// ----------------------------------------------------------------------------- +// XL9555 I/O Expander — Pin Assignments +// +// The XL9555 replaces direct GPIO control of peripheral power enables, +// resets, and switches. It must be initialised over I2C before LoRa, GPS, +// modem, or touch can be used. +// +// Port 0: pins 0-7, registers 0x02 (output) / 0x06 (direction) +// Port 1: pins 8-15, registers 0x03 (output) / 0x07 (direction) +// Direction: 0 = output, 1 = input +// ----------------------------------------------------------------------------- +#define HAS_XL9555 1 + +// XL9555 I2C registers +#define XL9555_REG_INPUT_0 0x00 +#define XL9555_REG_INPUT_1 0x01 +#define XL9555_REG_OUTPUT_0 0x02 +#define XL9555_REG_OUTPUT_1 0x03 +#define XL9555_REG_INVERT_0 0x04 +#define XL9555_REG_INVERT_1 0x05 +#define XL9555_REG_CONFIG_0 0x06 // 0=output, 1=input +#define XL9555_REG_CONFIG_1 0x07 + +// XL9555 pin assignments (0-7 = Port 0, 8-15 = Port 1) +#define XL_PIN_6609_EN 0 // HIGH: Enable A7682E power supply (SGM6609 boost) +#define XL_PIN_LORA_EN 1 // HIGH: Enable SX1262 power supply +#define XL_PIN_GPS_EN 2 // HIGH: Enable GPS power supply +#define XL_PIN_1V8_EN 3 // HIGH: Enable BHI260AP 1.8V power supply +#define XL_PIN_LORA_SEL 4 // HIGH: internal antenna, LOW: external antenna (SKY13453) +#define XL_PIN_MOTOR_EN 5 // HIGH: Enable DRV2605 power supply +#define XL_PIN_AMPLIFIER 6 // HIGH: Enable NS4150B speaker power amplifier +#define XL_PIN_TOUCH_RST 7 // LOW: Reset touch controller (active-low) +#define XL_PIN_PWRKEY_EN 8 // HIGH: A7682E POWERKEY toggle +#define XL_PIN_KEY_RST 9 // LOW: Reset keyboard (active-low) +#define XL_PIN_AUDIO_SEL 10 // HIGH: A7682E audio out, LOW: ES8311 audio out +// Pins 11-15 are reserved + +// Default XL9555 output state at boot (all power enables ON, resets de-asserted) +// Bit layout: [P07..P00] = TOUCH_RST=1, AMP=0, MOTOR_EN=0, LORA_SEL=1, 1V8=1, GPS=1, LORA=1, 6609=0 +// [P17..P10] = reserved=0, AUDIO_SEL=0, KEY_RST=1, PWRKEY=0 +// +// Conservative boot defaults for Meck: +// - LoRa ON, GPS ON, 1.8V ON, internal antenna +// - Modem OFF (6609_EN LOW), PWRKEY LOW (toggled later if needed) +// - Motor OFF, Amplifier OFF (saves power, enabled on demand) +// - Touch RST HIGH (not resetting), Keyboard RST HIGH (not resetting) +// - Audio select LOW (ES8311 by default — Meck controls this when needed) +#define XL9555_BOOT_PORT0 0b10011110 // 0x9E: T_RST=1, AMP=0, MOT=0, LSEL=1, 1V8=1, GPS=1, LORA=1, 6609=0 +#define XL9555_BOOT_PORT1 0b00000010 // 0x02: ..., ASEL=0, KRST=1, PKEY=0 + +// ----------------------------------------------------------------------------- +// Touch Controller (CST328) +// NOTE: Touch RST is via XL9555 pin 7, NOT a direct GPIO! +// CST328_PIN_RST is defined as -1 to signal "not a direct GPIO". +// The board class handles touch reset via XL9555 in begin(). +// ----------------------------------------------------------------------------- +#define HAS_TOUCHSCREEN 1 +#define CST328_PIN_INT 12 +#define CST328_PIN_RST -1 // MAX: Routed through XL9555 IO07 — handled by board class + +// ----------------------------------------------------------------------------- +// GPS +// NOTE: GPS power enable is via XL9555 pin 2, NOT a direct GPIO! +// PIN_GPS_EN is intentionally NOT defined — the board class handles it via XL9555. +// ----------------------------------------------------------------------------- +#define HAS_GPS 1 +#define GPS_BAUDRATE 38400 +// #define PIN_GPS_EN — NOT a direct GPIO on MAX (XL9555 IO02) +#define GPS_RX_PIN 2 // MAX: IO2 (was IO44 on V1.1) — ESP32 receives from GPS +#define GPS_TX_PIN 16 // MAX: IO16 (was IO43 on V1.1) — ESP32 sends to GPS +#define PIN_GPS_PPS 1 + +// ----------------------------------------------------------------------------- +// Buttons & Controls +// ----------------------------------------------------------------------------- +#define BUTTON_PIN 0 +#define PIN_USER_BTN 0 + +// Vibration Motor — DRV2605 driver (same as V1.1) +// Motor power enable is via XL9555 pin 5, not a direct GPIO. +#define HAS_DRV2605 1 + +// ----------------------------------------------------------------------------- +// SD Card +// ----------------------------------------------------------------------------- +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SPI_MOSI 33 +#define SPI_SCK 36 +#define SPI_MISO 47 +#define SPI_CS 48 +#define SDCARD_CS SPI_CS + +// ----------------------------------------------------------------------------- +// Keyboard (TCA8418) +// NOTE: Keyboard RST is via XL9555 pin 9 (active-low). +// The board class handles keyboard reset via XL9555 in begin(). +// ----------------------------------------------------------------------------- +#define KB_BL_PIN 42 +#define BOARD_KEYBOARD_INT 15 +#define HAS_PHYSICAL_KEYBOARD 1 + +// ----------------------------------------------------------------------------- +// Audio — ES8311 I2C Codec (NEW on MAX — replaces PCM5102A) +// +// ES8311 is an I2C-controlled audio codec (unlike PCM5102A which needed no config). +// It requires I2C register setup for input source, gain, volume, etc. +// Speaker/headphone output is shared with A7682E modem audio, selected via +// XL9555 pin AUDIO_SEL: LOW = ES8311, HIGH = A7682E. +// Power amplifier (NS4150B) for speaker enabled via XL9555 pin AMPLIFIER. +// +// I2S pin mapping for ES8311 (completely different from V1.1 PCM5102A!): +// MCLK = IO38 (master clock — ES8311 needs this, PCM5102A didn't) +// SCLK = IO39 (bit clock, aka BCLK) +// LRCK = IO18 (word select, aka LRC/WS) +// DSDIN = IO17 (DAC serial data in — ESP32 sends audio TO codec) +// ASDOUT= IO40 (ADC serial data out — codec sends mic audio TO ESP32) +// ----------------------------------------------------------------------------- +#define HAS_ES8311_AUDIO 1 + +#define BOARD_ES8311_MCLK 38 +#define BOARD_ES8311_SCLK 39 +#define BOARD_ES8311_LRCK 18 +#define BOARD_ES8311_DSDIN 17 // ESP32 → ES8311 (speaker/headphone output) +#define BOARD_ES8311_ASDOUT 40 // ES8311 → ESP32 (microphone input) + +// Compatibility aliases for ESP32-audioI2S library (setPinout expects BCLK, LRC, DOUT) +#define BOARD_I2S_BCLK BOARD_ES8311_SCLK // IO39 +#define BOARD_I2S_LRC BOARD_ES8311_LRCK // IO18 +#define BOARD_I2S_DOUT BOARD_ES8311_DSDIN // IO17 +#define BOARD_I2S_MCLK BOARD_ES8311_MCLK // IO38 (ESP32-audioI2S may need setMCLK) + +// Microphone — ES8311 built-in ADC (replaces separate PDM mic on V1.1) +// Mic data comes through I2S ASDOUT pin, not a separate PDM interface. +#define BOARD_MIC_I2S_DIN BOARD_ES8311_ASDOUT // IO40 + +// ----------------------------------------------------------------------------- +// Sensors +// ----------------------------------------------------------------------------- +#define HAS_BHI260AP // Gyroscope/IMU (1.8V power via XL9555 IO03) +#define BOARD_GYRO_INT 21 + +// ----------------------------------------------------------------------------- +// Power Management +// ----------------------------------------------------------------------------- +#define HAS_BQ27220 1 +#define BQ27220_I2C_ADDR 0x55 +#define BQ27220_I2C_SDA I2C_SDA +#define BQ27220_I2C_SCL I2C_SCL +#define BQ27220_DESIGN_CAPACITY 1500 // MAX: 1500 mAh (was 1400 on V1.1) +#define BQ27220_DESIGN_CAPACITY_MAH 1500 // Alias used by TDeckBoard.h + +#define HAS_PPM 1 +#define XPOWERS_CHIP_BQ25896 + +// ----------------------------------------------------------------------------- +// LoRa Radio (SX1262) +// NOTE: LoRa power enable is via XL9555 pin 1, NOT GPIO 46! +// The board class enables LoRa power via XL9555 in begin(). +// P_LORA_EN is intentionally NOT defined here — handled by board class. +// Antenna selection: XL9555 pin 4 (HIGH=internal, LOW=external via SKY13453). +// ----------------------------------------------------------------------------- +#define USE_SX1262 +#define USE_SX1268 + +// LORA_EN is NOT a direct GPIO on MAX — omit the define entirely. +// If any code references P_LORA_EN, it must be guarded with #ifndef HAS_XL9555. +// #define LORA_EN — NOT DEFINED (was GPIO 46 on V1.1) + +#define LORA_SCK 36 +#define LORA_MISO 47 +#define LORA_MOSI 33 // Shared with e-ink and SD card +#define LORA_CS 3 +#define LORA_RESET 4 +#define LORA_DIO0 -1 // Not connected on SX1262 +#define LORA_DIO1 5 // SX1262 IRQ +#define LORA_DIO2 6 // SX1262 BUSY + +// SX126X driver aliases (Meshtastic compatibility) +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +// RadioLib/MeshCore compatibility aliases +#define P_LORA_NSS LORA_CS +#define P_LORA_DIO_1 LORA_DIO1 +#define P_LORA_RESET LORA_RESET +#define P_LORA_BUSY LORA_DIO2 +#define P_LORA_SCLK LORA_SCK +#define P_LORA_MISO LORA_MISO +#define P_LORA_MOSI LORA_MOSI +// P_LORA_EN is NOT defined — LoRa power is via XL9555, handled in board begin() + +// ----------------------------------------------------------------------------- +// 4G Modem — A7682E (ALWAYS PRESENT on MAX — no longer optional!) +// +// On V1.1, 4G and audio were mutually exclusive hardware configurations. +// On MAX, both coexist. The XL9555 controls: +// - 6609_EN (XL pin 0): modem power supply (SGM6609 boost converter) +// - PWRKEY (XL pin 8): modem power key toggle +// Audio output from modem vs ES8311 is selected by AUDIO_SEL (XL pin 10). +// +// MODEM_POWER_EN and MODEM_PWRKEY are NOT direct GPIOs — ModemManager +// needs MAX-aware paths (see integration guide). +// MODEM_RST does not exist on MAX (IO9 is now LCD_RST). +// ----------------------------------------------------------------------------- +// Direct GPIO modem pins (still accessible as regular GPIO): +#define MODEM_RI 7 // Ring indicator (interrupt input) +#define MODEM_DTR 8 // Data terminal ready (output) +#define MODEM_RX 10 // UART RX (ESP32 receives from modem) +#define MODEM_TX 11 // UART TX (ESP32 sends to modem) + +// XL9555-routed modem pins — these are NOT direct GPIO! +// MODEM_POWER_EN and MODEM_PWRKEY are intentionally NOT defined. +// Existing code guarded by #ifdef MODEM_POWER_EN / #ifdef HAS_4G_MODEM will +// be skipped. Use board.modemPowerOn()/modemPwrkeyPulse() instead. +// MODEM_RST does not exist on MAX (IO9 is LCD_RST). + +// Compatibility: PIN_PERF_POWERON does not exist on MAX (IO10 is modem UART RX). +// Defined as -1 so TDeckBoard.cpp compiles (parent class), but never used at runtime. +#define PIN_PERF_POWERON -1