From d66f202081248d64a43d6e1d9fe27ffbffd6f831 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Wed, 3 Jun 2026 01:52:48 +1000 Subject: [PATCH] decoupled the Max from the Pro variant; fixed e-ink offset in the max --- examples/companion_radio/main.cpp | 3 +- .../lilygo_tdeck_max/TDeckProMaxBoard.cpp | 519 +++++++++++++++++- variants/lilygo_tdeck_max/TDeckProMaxBoard.h | 99 +++- variants/lilygo_tdeck_max/platformio.ini | 10 +- variants/lilygo_tdeck_max/variant.h | 3 + variants/lilygo_tdeck_pro/variant.h | 3 + 6 files changed, 621 insertions(+), 16 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 48a84714..6b376b0d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1865,6 +1865,7 @@ void setup() { MESH_DEBUG_PRINTLN("setup() - E-Ink reset pin initialized"); // Initialize Touch reset pin (GPIO 38) + Serial.printf(">>> TOUCH DIAG: compiled CST328_PIN_RST = %d (MAX expects -1; a real GPIO means stale Pro variant)\n", (int)CST328_PIN_RST); #ifdef CST328_PIN_RST pinMode(CST328_PIN_RST, OUTPUT); digitalWrite(CST328_PIN_RST, HIGH); @@ -2503,7 +2504,7 @@ void setup() { #endif sensors.setSettingValue("gps", "0"); } - Serial.printf("GPS: power %s, PIN_GPS_EN=%d\n", gps_wanted ? "ON" : "OFF", PIN_GPS_EN); + Serial.printf("GPS: power %s\n", gps_wanted ? "ON" : "OFF"); } #endif diff --git a/variants/lilygo_tdeck_max/TDeckProMaxBoard.cpp b/variants/lilygo_tdeck_max/TDeckProMaxBoard.cpp index da845921..021183a8 100644 --- a/variants/lilygo_tdeck_max/TDeckProMaxBoard.cpp +++ b/variants/lilygo_tdeck_max/TDeckProMaxBoard.cpp @@ -23,8 +23,10 @@ // 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. +// NOTE: We do NOT call any parent board begin() beyond ESP32Board::begin(); +// the boot sequence is reimplemented here to handle XL9555-routed pins. +// The BQ27220 fuel-gauge methods are defined in this file (MAX is standalone, +// no longer inheriting TDeckBoard). // ============================================================================= void TDeckProMaxBoard::begin() { @@ -69,9 +71,9 @@ for (uint8_t a = 0x6A; a <= 0x6B; a++) { 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() handles common ESP32 setup. The MAX reimplements its + // own boot sequence above for XL9555-routed power/reset, rather than using a + // Pro-style direct-GPIO begin(). ESP32Board::begin(); // ------ Step 6: GPS UART init ------ @@ -105,7 +107,7 @@ for (uint8_t a = 0x6A; a <= 0x6B; a++) { #if HAS_BQ27220 uint16_t voltage = getBattMilliVolts(); MESH_DEBUG_PRINTLN(" Battery voltage: %d mV", voltage); - configureFuelGauge(); // Inherited from TDeckBoard — sets 1500 mAh + configureFuelGauge(); // sets 1500 mAh (MAX design capacity) #endif // ------ Step 11: Early low-voltage protection ------ @@ -358,4 +360,509 @@ void TDeckProMaxBoard::backlightSetBrightness(uint8_t duty) { bool TDeckProMaxBoard::isBacklightOn() const { return _backlightOn; +} + + +// ============================================================================= +// BQ27220 Fuel Gauge +// +// Moved verbatim from TDeckBoard.cpp when the MAX board was decoupled from the +// Pro board class. The BQ27220 is identical hardware on both boards; only the +// class name differs. The three bq27220_* helpers are file-static (one copy +// per translation unit), so this file carries its own. +// ============================================================================= + +uint16_t TDeckProMaxBoard::getBattMilliVolts() { + #if HAS_BQ27220 + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(BQ27220_REG_VOLTAGE); + if (Wire.endTransmission(false) != 0) { + MESH_DEBUG_PRINTLN("BQ27220: I2C error reading voltage"); + return 0; + } + + uint8_t count = Wire.requestFrom((uint8_t)BQ27220_I2C_ADDR, (uint8_t)2); + if (count != 2) { + MESH_DEBUG_PRINTLN("BQ27220: Read error - wrong byte count"); + return 0; + } + + uint16_t voltage = Wire.read(); + voltage |= (Wire.read() << 8); + return voltage; + #else + return 0; + #endif +} + +uint8_t TDeckProMaxBoard::getBatteryPercent() { + #if HAS_BQ27220 + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(BQ27220_REG_SOC); + if (Wire.endTransmission(false) != 0) { + return 0; + } + + uint8_t count = Wire.requestFrom((uint8_t)BQ27220_I2C_ADDR, (uint8_t)2); + if (count != 2) { + return 0; + } + + uint16_t soc = Wire.read(); + soc |= (Wire.read() << 8); + return (uint8_t)min(soc, (uint16_t)100); + #else + return 0; + #endif +} + +// ---- BQ27220 extended register helpers ---- + +#if HAS_BQ27220 +// Read a 16-bit register from BQ27220. Returns 0 on I2C error. +static uint16_t bq27220_read16(uint8_t reg) { + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) return 0; + if (Wire.requestFrom((uint8_t)BQ27220_I2C_ADDR, (uint8_t)2) != 2) return 0; + uint16_t val = Wire.read(); + val |= (Wire.read() << 8); + return val; +} + +// Read a single byte from BQ27220 register. +static uint8_t bq27220_read8(uint8_t reg) { + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) return 0; + if (Wire.requestFrom((uint8_t)BQ27220_I2C_ADDR, (uint8_t)1) != 1) return 0; + return Wire.read(); +} + +// Write a 16-bit subcommand to BQ27220 Control register (0x00). +// Subcommands control unsealing, config mode, sealing, etc. +static bool bq27220_writeControl(uint16_t subcmd) { + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x00); // Control register + Wire.write(subcmd & 0xFF); // LSB first + Wire.write((subcmd >> 8) & 0xFF); // MSB + return Wire.endTransmission() == 0; +} +#endif + +// ---- BQ27220 Design Capacity configuration ---- +// The BQ27220 ships with a 3000 mAh default. The T-Deck Pro uses a 2000 mAh +// cell. This function checks on boot and writes the correct value via the +// MAC Data Memory interface if needed. The value persists in battery-backed +// RAM, so this typically only writes once (or after a full battery disconnect). +// +// Procedure follows TI TRM SLUUBD4A Section 6.1: +// 1. Unseal -> 2. Full Access -> 3. Enter CFG_UPDATE +// 4. Write Design Capacity via MAC -> 5. Exit CFG_UPDATE -> 6. Seal + +bool TDeckProMaxBoard::configureFuelGauge(uint16_t designCapacity_mAh) { +#if HAS_BQ27220 + // Read current design capacity from standard command register + uint16_t currentDC = bq27220_read16(BQ27220_REG_DESIGN_CAP); + Serial.printf("BQ27220: Design Capacity = %d mAh (target %d)\n", currentDC, designCapacity_mAh); + + if (currentDC == designCapacity_mAh) { + // Design Capacity correct, but check if Full Charge Capacity is sane. + uint16_t fcc = bq27220_read16(BQ27220_REG_FULL_CAP); + Serial.printf("BQ27220: Design Capacity already correct, FCC=%d mAh\n", fcc); + // Check if FCC is outside an acceptable band around design capacity. + // Catches both: FCC too high (stale factory 3000mAh) and FCC too low + // (gauge learned on a smaller battery, e.g. 1400mAh on a 2500mAh pack). + uint16_t fccLo = (designCapacity_mAh > 100) ? designCapacity_mAh - 100 : 0; + uint16_t fccHi = designCapacity_mAh + 100; + if (fcc < fccLo || fcc > fccHi) { + // FCC is >=150% of design — stale from factory defaults (typically 3000 mAh). + uint16_t designEnergy = (uint16_t)((uint32_t)designCapacity_mAh * 37 / 10); + Serial.printf("BQ27220: FCC %d outside target band [%d..%d], checking Design Energy (target %d mWh)\n", + fcc, fccLo, fccHi, designEnergy); + + // Unseal to read data memory and issue RESET + bq27220_writeControl(0x0414); delay(2); + bq27220_writeControl(0x3672); delay(2); + // Full Access + bq27220_writeControl(0xFFFF); delay(2); + bq27220_writeControl(0xFFFF); delay(2); + + // Read current Design Energy from data memory to check if it needs writing + // Enter CFG_UPDATE to access data memory + bq27220_writeControl(0x0090); + bool ready = false; + for (int i = 0; i < 50; i++) { + delay(20); + uint16_t opSt = bq27220_read16(BQ27220_REG_OP_STATUS); + if (opSt & 0x0400) { ready = true; break; } + } + if (ready) { + // Read Design Energy at data memory address 0x92A1 + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92); + Wire.endTransmission(); + delay(10); + uint8_t oldMSB = bq27220_read8(0x40); + uint8_t oldLSB = bq27220_read8(0x41); + uint16_t currentDE = (oldMSB << 8) | oldLSB; + + if (currentDE != designEnergy) { + // Design Energy actually needs updating — write it + uint8_t oldChk = bq27220_read8(0x60); + uint8_t dLen = bq27220_read8(0x61); + uint8_t newMSB = (designEnergy >> 8) & 0xFF; + uint8_t newLSB = designEnergy & 0xFF; + uint8_t temp = (255 - oldChk - oldMSB - oldLSB); + uint8_t newChk = 255 - ((temp + newMSB + newLSB) & 0xFF); + + Serial.printf("BQ27220: DE old=%d new=%d mWh, writing\n", currentDE, designEnergy); + + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92); + Wire.write(newMSB); Wire.write(newLSB); + Wire.endTransmission(); + delay(5); + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x60); Wire.write(newChk); Wire.write(dLen); + Wire.endTransmission(); + delay(10); + + // Exit with reinit since we actually changed data + bq27220_writeControl(0x0091); // EXIT_CFG_UPDATE_REINIT + delay(200); + Serial.println("BQ27220: Design Energy written, exited CFG_UPDATE"); + } else { + // DC=2000, DE=7400, Update Status=0x00, but FCC is stuck at 3000. + // Diagnostic scan found the culprits: + // 0x9106 = Qmax Cell 0 (IT Cfg class) — the raw capacity the + // gauge uses for FCC calculation. Factory default 3000. + // 0x929D = Stored FCC reference (Gas Gauging class, 2 bytes + // before Design Capacity). Also stuck at 3000. + // + // Fix: overwrite both with designCapacity_mAh (2000). + Serial.printf("BQ27220: DE correct (%d mWh) — fixing Qmax + stored FCC\n", currentDE); + + // --- Helper lambda for MAC data memory 2-byte write --- + // Reads old value + checksum, computes differential checksum, writes new value. + auto writeDM16 = [](uint16_t addr, uint16_t newVal) -> bool { + // Select address + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); + Wire.write(addr & 0xFF); + Wire.write((addr >> 8) & 0xFF); + Wire.endTransmission(); + delay(10); + + uint8_t oldMSB = bq27220_read8(0x40); + uint8_t oldLSB = bq27220_read8(0x41); + uint8_t oldChk = bq27220_read8(0x60); + uint8_t dLen = bq27220_read8(0x61); + uint16_t oldVal = (oldMSB << 8) | oldLSB; + + if (oldVal == newVal) { + Serial.printf("BQ27220: [0x%04X] already %d, skip\n", addr, newVal); + return true; // already correct + } + + uint8_t newMSB = (newVal >> 8) & 0xFF; + uint8_t newLSB = newVal & 0xFF; + uint8_t temp = (255 - oldChk - oldMSB - oldLSB); + uint8_t newChk = 255 - ((temp + newMSB + newLSB) & 0xFF); + + Serial.printf("BQ27220: [0x%04X] %d -> %d\n", addr, oldVal, newVal); + + // Write new value + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); + Wire.write(addr & 0xFF); + Wire.write((addr >> 8) & 0xFF); + Wire.write(newMSB); + Wire.write(newLSB); + Wire.endTransmission(); + delay(5); + + // Write checksum + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x60); + Wire.write(newChk); + Wire.write(dLen); + Wire.endTransmission(); + delay(10); + return true; + }; + + // Overwrite Qmax Cell 0 (IT Cfg) — this is what FCC is derived from + writeDM16(0x9106, designCapacity_mAh); + + // Overwrite stored FCC reference (Gas Gauging, 2 bytes before DC) + writeDM16(0x929D, designCapacity_mAh); + + // Exit with reinit to apply the new values + bq27220_writeControl(0x0091); // EXIT_CFG_UPDATE_REINIT + delay(200); + Serial.println("BQ27220: Qmax + stored FCC updated, exited CFG_UPDATE"); + } + } else { + Serial.println("BQ27220: Failed to enter CFG_UPDATE for DE check"); + } + + // Seal first, then issue RESET. + // RESET forces the gauge to fully reinitialize its Impedance Track + // algorithm and recalculate FCC from the current DC/DE values. + // This is the actual fix when DC and DE are correct but FCC is stuck. + bq27220_writeControl(0x0030); // SEAL + delay(5); + Serial.println("BQ27220: Issuing RESET to force FCC recalculation..."); + bq27220_writeControl(0x0041); // RESET + delay(2000); // Full reset needs generous settle time + + fcc = bq27220_read16(BQ27220_REG_FULL_CAP); + Serial.printf("BQ27220: FCC after RESET: %d mAh (target <= %d)\n", fcc, designCapacity_mAh); + + if (fcc > designCapacity_mAh) { + // RESET didn't fix FCC — the gauge IT algorithm is stubbornly + // retaining its learned value. This typically resolves after one + // full charge/discharge cycle. Software clamp in + // getFullChargeCapacity() ensures correct display regardless. + Serial.printf("BQ27220: FCC still stale at %d — software clamp active\n", fcc); + } + } + return true; + } + + Serial.printf("BQ27220: Updating Design Capacity from %d to %d mAh\n", currentDC, designCapacity_mAh); + + // Step 1: Unseal (default unseal keys) + bq27220_writeControl(0x0414); + delay(2); + bq27220_writeControl(0x3672); + delay(2); + + // Step 2: Enter Full Access mode + bq27220_writeControl(0xFFFF); + delay(2); + bq27220_writeControl(0xFFFF); + delay(2); + + // Step 3: Enter CFG_UPDATE mode + bq27220_writeControl(0x0090); + + // Wait for CFGUPMODE bit (bit 10) in OperationStatus register + bool cfgReady = false; + for (int i = 0; i < 50; i++) { + delay(20); + uint16_t opStatus = bq27220_read16(BQ27220_REG_OP_STATUS); + Serial.printf("BQ27220: OperationStatus = 0x%04X (attempt %d)\n", opStatus, i); + if (opStatus & 0x0400) { // CFGUPMODE is bit 10 + cfgReady = true; + break; + } + } + if (!cfgReady) { + Serial.println("BQ27220: ERROR - Timeout waiting for CFGUPDATE mode"); + bq27220_writeControl(0x0092); // Try to exit cleanly + bq27220_writeControl(0x0030); // Re-seal + return false; + } + Serial.println("BQ27220: Entered CFGUPDATE mode"); + + // Step 4: Write Design Capacity via MAC Data Memory interface + // Design Capacity mAh lives at data memory address 0x929F + + // 4a. Select the data memory block by writing address to 0x3E-0x3F + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); // MACDataControl register + Wire.write(0x9F); // Address low byte + Wire.write(0x92); // Address high byte + Wire.endTransmission(); + delay(10); + + // 4b. Read old data (MSB, LSB) and checksum for differential update + uint8_t oldMSB = bq27220_read8(0x40); + uint8_t oldLSB = bq27220_read8(0x41); + uint8_t oldChksum = bq27220_read8(0x60); + uint8_t dataLen = bq27220_read8(0x61); + + Serial.printf("BQ27220: Old DC bytes=0x%02X 0x%02X chk=0x%02X len=%d\n", + oldMSB, oldLSB, oldChksum, dataLen); + + // 4c. Compute new values (BQ27220 stores big-endian in data memory) + uint8_t newMSB = (designCapacity_mAh >> 8) & 0xFF; + uint8_t newLSB = designCapacity_mAh & 0xFF; + + // Differential checksum: remove old bytes, add new bytes + uint8_t temp = (255 - oldChksum - oldMSB - oldLSB); + uint8_t newChksum = 255 - ((temp + newMSB + newLSB) & 0xFF); + + Serial.printf("BQ27220: New DC bytes=0x%02X 0x%02X chk=0x%02X\n", + newMSB, newLSB, newChksum); + + // 4d. Write address + new data as a single block transaction + // BQ27220 MAC requires: [0x3E] [addr_lo] [addr_hi] [data...] + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); // Start at MACDataControl + Wire.write(0x9F); // Address low byte + Wire.write(0x92); // Address high byte + Wire.write(newMSB); // Data byte 0 (at 0x40) + Wire.write(newLSB); // Data byte 1 (at 0x41) + uint8_t writeResult = Wire.endTransmission(); + Serial.printf("BQ27220: Write block result = %d\n", writeResult); + + // 4e. Write updated checksum and length + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x60); + Wire.write(newChksum); + Wire.write(dataLen); + writeResult = Wire.endTransmission(); + Serial.printf("BQ27220: Write checksum result = %d\n", writeResult); + delay(10); + + // 4f. Verify the write took effect before exiting config mode + // Re-read the block to confirm + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); + Wire.write(0x9F); + Wire.write(0x92); + Wire.endTransmission(); + delay(10); + uint8_t verMSB = bq27220_read8(0x40); + uint8_t verLSB = bq27220_read8(0x41); + Serial.printf("BQ27220: Verify in CFGUPDATE: DC bytes=0x%02X 0x%02X (%d mAh)\n", + verMSB, verLSB, (verMSB << 8) | verLSB); + + // Step 4g: Also update Design Energy (address 0x92A1) while in CFG_UPDATE. + // Design Energy = capacity x 3.7V (nominal LiPo voltage). + // The gauge uses both DC and DE to compute Full Charge Capacity. + { + uint16_t designEnergy = (uint16_t)((uint32_t)designCapacity_mAh * 37 / 10); + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92); + Wire.endTransmission(); + delay(10); + uint8_t deOldMSB = bq27220_read8(0x40); + uint8_t deOldLSB = bq27220_read8(0x41); + uint8_t deOldChk = bq27220_read8(0x60); + uint8_t deLen = bq27220_read8(0x61); + + uint8_t deNewMSB = (designEnergy >> 8) & 0xFF; + uint8_t deNewLSB = designEnergy & 0xFF; + uint8_t deTemp = (255 - deOldChk - deOldMSB - deOldLSB); + uint8_t deNewChk = 255 - ((deTemp + deNewMSB + deNewLSB) & 0xFF); + + Serial.printf("BQ27220: Design Energy: old=%d new=%d mWh\n", + (deOldMSB << 8) | deOldLSB, designEnergy); + + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92); + Wire.write(deNewMSB); Wire.write(deNewLSB); + Wire.endTransmission(); + delay(5); + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x60); Wire.write(deNewChk); Wire.write(deLen); + Wire.endTransmission(); + delay(10); + } + + // Step 5: Exit CFG_UPDATE (with reinit to apply changes immediately) + bq27220_writeControl(0x0091); // EXIT_CFG_UPDATE_REINIT + Serial.println("BQ27220: Sent EXIT_CFG_UPDATE_REINIT, waiting..."); + delay(200); // Allow gauge to reinitialize + + // Verify + uint16_t verifyDC = bq27220_read16(BQ27220_REG_DESIGN_CAP); + Serial.printf("BQ27220: Design Capacity now reads %d mAh (expected %d)\n", + verifyDC, designCapacity_mAh); + + uint16_t newFCC = bq27220_read16(BQ27220_REG_FULL_CAP); + Serial.printf("BQ27220: Full Charge Capacity: %d mAh\n", newFCC); + + if (verifyDC == designCapacity_mAh) { + Serial.println("BQ27220: Configuration SUCCESS"); + } else { + Serial.println("BQ27220: Configuration FAILED"); + } + + // Step 6: Seal the device + bq27220_writeControl(0x0030); + delay(5); + + // Step 7: Force full gauge RESET to reinitialize FCC from new DC/DE. + // Without this, the Impedance Track algorithm retains the old FCC + // (often 3000 mAh from factory) until a full charge/discharge cycle. + bq27220_writeControl(0x0041); // RESET + delay(1000); // Gauge needs time to fully reinitialize + + // Re-verify after hard reset + verifyDC = bq27220_read16(BQ27220_REG_DESIGN_CAP); + newFCC = bq27220_read16(BQ27220_REG_FULL_CAP); + Serial.printf("BQ27220: Post-RESET DC=%d FCC=%d mAh\n", verifyDC, newFCC); + + return verifyDC == designCapacity_mAh; +#else + return false; +#endif +} + +int16_t TDeckProMaxBoard::getAvgCurrent() { + #if HAS_BQ27220 + return (int16_t)bq27220_read16(BQ27220_REG_AVG_CURRENT); + #else + return 0; + #endif +} + +int16_t TDeckProMaxBoard::getAvgPower() { + #if HAS_BQ27220 + return (int16_t)bq27220_read16(BQ27220_REG_AVG_POWER); + #else + return 0; + #endif +} + +uint16_t TDeckProMaxBoard::getTimeToEmpty() { + #if HAS_BQ27220 + return bq27220_read16(BQ27220_REG_TIME_TO_EMPTY); + #else + return 0xFFFF; + #endif +} + +uint16_t TDeckProMaxBoard::getRemainingCapacity() { + #if HAS_BQ27220 + return bq27220_read16(BQ27220_REG_REMAIN_CAP); + #else + return 0; + #endif +} + +uint16_t TDeckProMaxBoard::getFullChargeCapacity() { + #if HAS_BQ27220 + uint16_t fcc = bq27220_read16(BQ27220_REG_FULL_CAP); + // Clamp to design capacity — the gauge may report a stale factory FCC + // (e.g. 3000 mAh) until it completes a full learning cycle. Never let + // the reported FCC exceed what the actual cell can hold. + if (fcc > BQ27220_DESIGN_CAPACITY_MAH) fcc = BQ27220_DESIGN_CAPACITY_MAH; + return fcc; + #else + return 0; + #endif +} + +uint16_t TDeckProMaxBoard::getDesignCapacity() { + #if HAS_BQ27220 + return bq27220_read16(BQ27220_REG_DESIGN_CAP); + #else + return 0; + #endif +} + +int16_t TDeckProMaxBoard::getBattTemperature() { + #if HAS_BQ27220 + uint16_t raw = bq27220_read16(BQ27220_REG_TEMPERATURE); + // BQ27220 returns 0.1 K, convert to 0.1 C (273.1K = 0 C) + return (int16_t)(raw - 2731); + #else + return 0; + #endif } \ No newline at end of file diff --git a/variants/lilygo_tdeck_max/TDeckProMaxBoard.h b/variants/lilygo_tdeck_max/TDeckProMaxBoard.h index aac7df74..85b64095 100644 --- a/variants/lilygo_tdeck_max/TDeckProMaxBoard.h +++ b/variants/lilygo_tdeck_max/TDeckProMaxBoard.h @@ -3,7 +3,8 @@ // ============================================================================= // TDeckProMaxBoard — Board support for LilyGo T-Deck Pro MAX V0.1 // -// Extends TDeckBoard (which provides all BQ27220 fuel gauge methods) with: +// Standalone board class inheriting ESP32Board directly (decoupled from the +// Pro TDeckBoard). Provides its own BQ27220 fuel gauge methods plus: // - XL9555 I/O expander initialisation and control // - XL9555-routed peripheral power management // - Touch/keyboard reset via XL9555 @@ -17,9 +18,31 @@ // ============================================================================= #include "variant.h" -#include "TDeckBoard.h" // Inherits BQ27220 fuel gauge, deep sleep, power management +#include +#include +#include "helpers/ESP32Board.h" // Direct base -- MAX no longer inherits TDeckBoard (Pro) +#include -class TDeckProMaxBoard : public TDeckBoard { +// BQ27220 Fuel Gauge Registers (moved here from TDeckBoard.h when MAX was +// decoupled from the Pro board class; the BQ27220 hardware is identical). +#define BQ27220_REG_TEMPERATURE 0x06 // Temperature (0.1 K) +#define BQ27220_REG_VOLTAGE 0x08 +#define BQ27220_REG_CURRENT 0x0C // Instantaneous current (mA, signed) +#define BQ27220_REG_SOC 0x2C +#define BQ27220_REG_REMAIN_CAP 0x10 // Remaining capacity (mAh) +#define BQ27220_REG_FULL_CAP 0x12 // Full charge capacity (mAh) +#define BQ27220_REG_AVG_CURRENT 0x14 // Average current (mA, signed) +#define BQ27220_REG_TIME_TO_EMPTY 0x16 // Minutes until empty +#define BQ27220_REG_AVG_POWER 0x24 // Average power (mW, signed) +#define BQ27220_REG_DESIGN_CAP 0x3C // Design capacity (mAh, read-only standard cmd) +#define BQ27220_REG_OP_STATUS 0x3A // Operation status +#define BQ27220_I2C_ADDR 0x55 + +#ifndef BQ27220_DESIGN_CAPACITY_MAH +#define BQ27220_DESIGN_CAPACITY_MAH 1500 +#endif + +class TDeckProMaxBoard : public ESP32Board { public: void begin(); @@ -97,6 +120,76 @@ public: void backlightSetBrightness(uint8_t duty); // 0-255, via LEDC PWM bool isBacklightOn() const; + // ------------------------------------------------------------------------- + // BQ27220 fuel gauge (moved from TDeckBoard when MAX was decoupled). + // Identical hardware to the Pro; these read the same registers. + // ------------------------------------------------------------------------- + uint16_t getBattMilliVolts() override; + uint8_t getBatteryPercent(); + int16_t getAvgCurrent(); + int16_t getAvgPower(); + uint16_t getTimeToEmpty(); + uint16_t getRemainingCapacity(); + uint16_t getFullChargeCapacity(); + uint16_t getDesignCapacity(); + int16_t getBattTemperature(); + bool configureFuelGauge(uint16_t designCapacity_mAh = BQ27220_DESIGN_CAPACITY_MAH); + + // ------------------------------------------------------------------------- + // Sleep / power-off (moved verbatim from TDeckBoard when MAX was decoupled). + // NOTE: powerOff() still references PIN_PERF_POWERON (-1 on MAX) and the + // #ifdef P_LORA_EN block (P_LORA_EN undefined on MAX, so it compiles out). + // Behaviour-preserving lift-and-shift; a MAX-correct XL9555 powerOff() is a + // known follow-up. + // ------------------------------------------------------------------------- + void powerOff() override { + // True hibernate: deep sleep with no software wake sources. + // Only a hardware reset (reset button) or USB power-on wakes the device. + // BLE, WiFi, 4G, GPS, and LoRa are already shut down by UITask + // before this method is called. + + btStop(); // Belt and suspenders -- BLE controller stop + + // Cut power to peripherals (keyboard, BQ27220, sensors) + pinMode(PIN_PERF_POWERON, OUTPUT); + digitalWrite(PIN_PERF_POWERON, LOW); + + // Cut power to LoRa module (radio already in standby from radio_driver.powerOff) + #ifdef P_LORA_EN + digitalWrite(P_LORA_EN, LOW); + #endif + + // Hold LoRa NSS high to prevent SX1262 drawing current from floating CS + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + // Enter deep sleep with no wake sources -- only hardware reset wakes + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF); + esp_deep_sleep_start(); + } + + void enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are held at required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); + } else { + esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1) | (1ULL << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000ULL); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + private: // Shadow registers for XL9555 output ports (avoid I2C read-modify-write) uint8_t _xlPort0 = XL9555_BOOT_PORT0; diff --git a/variants/lilygo_tdeck_max/platformio.ini b/variants/lilygo_tdeck_max/platformio.ini index 8cf89b25..1c5049e4 100644 --- a/variants/lilygo_tdeck_max/platformio.ini +++ b/variants/lilygo_tdeck_max/platformio.ini @@ -115,7 +115,7 @@ build_flags = -D EINK_ROTATION=0 -D EINK_SCALE_X=1.875f -D EINK_SCALE_Y=2.5f - -D EINK_X_OFFSET=2 + -D EINK_X_OFFSET=0 -D EINK_Y_OFFSET=5 ; Legacy display pin aliases (for GxEPDDisplay.cpp) -D PIN_DISPLAY_CS=34 @@ -134,8 +134,6 @@ build_flags = -D ARDUINO_LOOP_STACK_SIZE=32768 -D HAS_MECK_FONTS 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_max> + @@ -218,11 +216,11 @@ 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 MAX_CONTACTS=2000 + -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=1 -D MECK_OTA_UPDATE=1 - -D FIRMWARE_VERSION='"Meck v1.3.MAX.SA"' + -D FIRMWARE_VERSION='"Meck v1.11.MAX.SA"' build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + + diff --git a/variants/lilygo_tdeck_max/variant.h b/variants/lilygo_tdeck_max/variant.h index 80bd9f45..9f952af5 100644 --- a/variants/lilygo_tdeck_max/variant.h +++ b/variants/lilygo_tdeck_max/variant.h @@ -1,5 +1,8 @@ #pragma once +#pragma message(">>> VARIANT DIAG: compiling MAX variant.h (variants/lilygo_tdeck_max)") + + // ============================================================================= // LilyGo T-Deck Pro MAX V0.1 - Pin Definitions // Hardware revision: HD-V3-250911 diff --git a/variants/lilygo_tdeck_pro/variant.h b/variants/lilygo_tdeck_pro/variant.h index c423cf97..aee8ecfd 100644 --- a/variants/lilygo_tdeck_pro/variant.h +++ b/variants/lilygo_tdeck_pro/variant.h @@ -1,5 +1,8 @@ #pragma once +#pragma message(">>> VARIANT DIAG: compiling PRO variant.h (variants/LilyGo_TDeck_Pro)") + + // ============================================================================= // LilyGo T-Deck Pro v1.1 - Pin Definitions // Based on Meshtastic PR #9378 for T-Deck Pro V1.1 compatibility