From b2967fc1a72aa604c54a36b86c4a5ed2ff1f27a9 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sun, 15 Feb 2026 09:13:41 +1100 Subject: [PATCH] battery gauge implementation --- examples/companion_radio/MyMesh.h | 4 +- examples/companion_radio/ui-new/UITask.cpp | 64 +++++- variants/lilygo_tdeck_pro/TDeckBoard.cpp | 232 ++++++++++++++++++++- variants/lilygo_tdeck_pro/TDeckBoard.h | 39 +++- variants/lilygo_tdeck_pro/platformio.ini | 2 +- 5 files changed, 331 insertions(+), 10 deletions(-) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f949c7ab..411ff286 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,11 +8,11 @@ #define FIRMWARE_VER_CODE 8 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "14 Feb 2026" +#define FIRMWARE_BUILD_DATE "15 Feb 2026" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "Meck v0.8.8" +#define FIRMWARE_VERSION "Meck v0.8.9" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index f28a2647..f7ea68c3 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -88,13 +88,18 @@ class HomeScreen : public UIScreen { FIRST, RECENT, RADIO, +#ifdef BLE_PIN_CODE BLUETOOTH, +#endif ADVERT, #if ENV_INCLUDE_GPS == 1 GPS, #endif #if UI_SENSORS_PAGE == 1 SENSORS, +#endif +#if HAS_BQ27220 + BATTERY, #endif SHUTDOWN, Count // keep as last @@ -207,12 +212,12 @@ public: int render(DisplayDriver& display) override { char tmp[80]; - // node name - display.setTextSize(1); + // node name (tinyfont to avoid overlapping clock) + display.setTextSize(0); display.setColor(DisplayDriver::GREEN); char filtered_name[sizeof(_node_prefs->node_name)]; display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name)); - display.setCursor(0, 0); + display.setCursor(0, -3); display.print(filtered_name); // battery voltage @@ -267,18 +272,22 @@ public: display.drawTextCentered(display.width() / 2, y, tmp); y += 12; #endif + #if defined(BLE_PIN_CODE) || defined(WIFI_SSID) if (_task->hasConnection()) { display.setColor(DisplayDriver::GREEN); display.setTextSize(1); display.drawTextCentered(display.width() / 2, y, "< Connected >"); y += 12; +#ifdef BLE_PIN_CODE } else if (_task->isSerialEnabled() && the_mesh.getBLEPin() != 0) { display.setColor(DisplayDriver::RED); display.setTextSize(2); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); display.drawTextCentered(display.width() / 2, y, tmp); y += 18; +#endif } + #endif // Menu shortcuts - tinyfont monospaced grid y += 6; @@ -341,6 +350,7 @@ public: display.setCursor(0, 53); sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor()); display.print(tmp); +#ifdef BLE_PIN_CODE } else if (_page == HomePage::BLUETOOTH) { display.setColor(DisplayDriver::GREEN); display.drawXbm((display.width() - 32) / 2, 18, @@ -348,6 +358,7 @@ public: 32, 32); display.setTextSize(1); display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL); +#endif } else if (_page == HomePage::ADVERT) { display.setColor(DisplayDriver::GREEN); display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32); @@ -527,6 +538,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); @@ -587,6 +643,7 @@ public: } return true; } +#ifdef BLE_PIN_CODE if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { if (_task->isSerialEnabled()) { // toggle Bluetooth on/off _task->disableSerial(); @@ -595,6 +652,7 @@ public: } return true; } +#endif if (c == KEY_ENTER && _page == HomePage::ADVERT) { _task->notify(UIEventType::ack); if (the_mesh.advert()) { 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"; } diff --git a/variants/lilygo_tdeck_pro/platformio.ini b/variants/lilygo_tdeck_pro/platformio.ini index cd40730f..06420f9f 100644 --- a/variants/lilygo_tdeck_pro/platformio.ini +++ b/variants/lilygo_tdeck_pro/platformio.ini @@ -80,7 +80,7 @@ build_flags = -D PIN_DISPLAY_BL=45 -D PIN_USER_BTN=0 -D CST328_PIN_RST=38 - -D FIRMWARE_VERSION='"Meck v0.8.8"' + -D FIRMWARE_VERSION='"Meck v0.8.9"' build_src_filter = ${esp32_base.build_src_filter} +<../variants/LilyGo_TDeck_Pro> +