diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 781e84d3..27fef965 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -98,6 +98,9 @@ class HomeScreen : public UIScreen { #endif #if UI_SENSORS_PAGE == 1 SENSORS, +#endif +#if HAS_BQ27220 + BATTERY, #endif SHUTDOWN, Count // keep as last @@ -292,7 +295,7 @@ public: display.drawTextCentered(display.width() / 2, y, tmp); y += 12; #endif -#if defined(BLE_PIN_CODE) || defined(WIFI_SSID) + #if defined(BLE_PIN_CODE) || defined(WIFI_SSID) if (_task->hasConnection()) { display.setColor(DisplayDriver::GREEN); display.setTextSize(1); @@ -307,7 +310,7 @@ public: y += 18; #endif } -#endif + #endif // Menu shortcuts - tinyfont monospaced grid y += 6; @@ -428,7 +431,7 @@ public: display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; - // NMEA sentence counter — confirms baud rate and data flow + // NMEA sentence counter — confirms baud rate and data flow display.drawTextLeftAlign(0, y, "sentences"); if (gpsDuty.isHardwareOn()) { uint16_t sps = gpsStream.getSentencesPerSec(); @@ -558,6 +561,51 @@ public: } if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; +#endif +#if HAS_BQ27220 + } else if (_page == HomePage::BATTERY) { + char buf[30]; + int y = 18; + + // Title + display.setColor(DisplayDriver::GREEN); + display.drawTextCentered(display.width() / 2, y, "Battery Gauge"); + y += 12; + + display.setColor(DisplayDriver::LIGHT); + + // Time to empty + uint16_t tte = board.getTimeToEmpty(); + display.drawTextLeftAlign(0, y, "remaining"); + if (tte == 0xFFFF || tte == 0) { + strcpy(buf, tte == 0 ? "depleted" : "charging"); + } else if (tte >= 60) { + sprintf(buf, "%dh %dm", tte / 60, tte % 60); + } else { + sprintf(buf, "%d min", tte); + } + display.drawTextRightAlign(display.width()-1, y, buf); + y += 10; + + // Average current + int16_t avgCur = board.getAvgCurrent(); + display.drawTextLeftAlign(0, y, "avg current"); + sprintf(buf, "%d mA", avgCur); + display.drawTextRightAlign(display.width()-1, y, buf); + y += 10; + + // Average power + int16_t avgPow = board.getAvgPower(); + display.drawTextLeftAlign(0, y, "avg power"); + sprintf(buf, "%d mW", avgPow); + display.drawTextRightAlign(display.width()-1, y, buf); + y += 10; + + // Voltage (already available) + uint16_t mv = board.getBattMilliVolts(); + display.drawTextLeftAlign(0, y, "voltage"); + sprintf(buf, "%d.%03d V", mv / 1000, mv % 1000); + display.drawTextRightAlign(display.width()-1, y, buf); #endif } else if (_page == HomePage::SHUTDOWN) { display.setColor(DisplayDriver::GREEN); @@ -1140,13 +1188,13 @@ void UITask::toggleGPS() { if (_sensors != NULL) { if (_node_prefs->gps_enabled) { - // Disable GPS — cut hardware power + // Disable GPS — cut hardware power _sensors->setSettingValue("gps", "0"); _node_prefs->gps_enabled = 0; gpsDuty.disable(); notify(UIEventType::ack); } else { - // Enable GPS — start duty cycle + // Enable GPS — start duty cycle _sensors->setSettingValue("gps", "1"); _node_prefs->gps_enabled = 1; gpsDuty.enable(); diff --git a/variants/lilygo_tdeck_pro/TDeckBoard.cpp b/variants/lilygo_tdeck_pro/TDeckBoard.cpp index b1fd49e2..b0168caa 100644 --- a/variants/lilygo_tdeck_pro/TDeckBoard.cpp +++ b/variants/lilygo_tdeck_pro/TDeckBoard.cpp @@ -72,10 +72,11 @@ void TDeckBoard::begin() { rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); } - // Test BQ27220 communication + // Test BQ27220 communication and configure design capacity #if HAS_BQ27220 uint16_t voltage = getBattMilliVolts(); MESH_DEBUG_PRINTLN("TDeckBoard::begin() - Battery voltage: %d mV", voltage); + configureFuelGauge(); #endif MESH_DEBUG_PRINTLN("TDeckBoard::begin() - complete"); @@ -123,4 +124,233 @@ uint8_t TDeckBoard::getBatteryPercent() { #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 1400 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 TDeckBoard::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) { + Serial.println("BQ27220: Design Capacity already correct, skipping"); + 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 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); + + if (verifyDC == designCapacity_mAh) { + Serial.println("BQ27220: Configuration SUCCESS"); + } else { + Serial.println("BQ27220: Configuration FAILED"); + } + + // Step 7: Seal the device + bq27220_writeControl(0x0030); + delay(5); + + return verifyDC == designCapacity_mAh; +#else + return false; +#endif +} + +int16_t TDeckBoard::getAvgCurrent() { + #if HAS_BQ27220 + return (int16_t)bq27220_read16(BQ27220_REG_AVG_CURRENT); + #else + return 0; + #endif +} + +int16_t TDeckBoard::getAvgPower() { + #if HAS_BQ27220 + return (int16_t)bq27220_read16(BQ27220_REG_AVG_POWER); + #else + return 0; + #endif +} + +uint16_t TDeckBoard::getTimeToEmpty() { + #if HAS_BQ27220 + return bq27220_read16(BQ27220_REG_TIME_TO_EMPTY); + #else + return 0xFFFF; + #endif +} + +uint16_t TDeckBoard::getRemainingCapacity() { + #if HAS_BQ27220 + return bq27220_read16(BQ27220_REG_REMAIN_CAP); + #else + return 0; + #endif +} + +uint16_t TDeckBoard::getFullChargeCapacity() { + #if HAS_BQ27220 + return bq27220_read16(BQ27220_REG_FULL_CAP); + #else + return 0; + #endif +} + +uint16_t TDeckBoard::getDesignCapacity() { + #if HAS_BQ27220 + return bq27220_read16(BQ27220_REG_DESIGN_CAP); + #else + return 0; + #endif } \ No newline at end of file diff --git a/variants/lilygo_tdeck_pro/TDeckBoard.h b/variants/lilygo_tdeck_pro/TDeckBoard.h index ddeb5bb1..06097cab 100644 --- a/variants/lilygo_tdeck_pro/TDeckBoard.h +++ b/variants/lilygo_tdeck_pro/TDeckBoard.h @@ -7,11 +7,23 @@ #include // BQ27220 Fuel Gauge Registers -#define BQ27220_REG_VOLTAGE 0x08 -#define BQ27220_REG_CURRENT 0x0C -#define BQ27220_REG_SOC 0x2C +#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 +// T-Deck Pro battery capacity (all variants use 1400 mAh cell) +#ifndef BQ27220_DESIGN_CAPACITY_MAH +#define BQ27220_DESIGN_CAPACITY_MAH 1400 +#endif + class TDeckBoard : public ESP32Board { public: void begin(); @@ -52,6 +64,27 @@ public: // Read state of charge percentage from BQ27220 uint8_t getBatteryPercent(); + // Read average current in mA (negative = discharging, positive = charging) + int16_t getAvgCurrent(); + + // Read average power in mW (negative = discharging, positive = charging) + int16_t getAvgPower(); + + // Read time-to-empty in minutes (0xFFFF if charging/unavailable) + uint16_t getTimeToEmpty(); + + // Read remaining capacity in mAh + uint16_t getRemainingCapacity(); + + // Read full charge capacity in mAh (learned value, may need cycling to update) + uint16_t getFullChargeCapacity(); + + // Read design capacity in mAh (the configured battery size) + uint16_t getDesignCapacity(); + + // Configure BQ27220 design capacity (checks on boot, writes only if wrong) + bool configureFuelGauge(uint16_t designCapacity_mAh = BQ27220_DESIGN_CAPACITY_MAH); + const char* getManufacturerName() const { return "LilyGo T-Deck Pro"; }