Fix: Battery stability and USB detection for T-Display/S3
What changed, and why it matters
This commit fixes how two hardware variants of the Blockstream Jade crypto wallet (T-Display and T-Display S3) read battery levels and detect whether they are plugged into USB. It replaces an unreliable voltage-guess method with proper ADC hardware reading and, for the S3, uses the actual USB state. It also stops falsely showing a 'charging' icon. There is no obvious security vulnerability here; it is a hardware-support and user-experience fix.
No security action required. Treat as normal hardware-support fix. If reviewing for supply-chain or hardware integrity, verify the ADC calibration fallback and voltage divider constants match the T-Display/T-Display S3 schematics, but this is a hardware-accuracy concern, not a security flaw.
Security signals we found
No memory-unsafe operations, buffer handling, or cryptographic code changed.
No privilege escalation, authentication bypass, or key-handling logic touched.
Changes are confined to power/battery ADC measurement and USB detection heuristics.
Previous voltage-threshold heuristic could mislead UI state; patch removes that heuristic.
Evidence from the diff
The patch adds a new power-management include for the TTGO T-Display board and updates the T-Display S3 include. Key changes: (1) both boards now use the ESP-IDF ADC oneshot API with optional calibration and an exponential moving average to report battery voltage; (2) the S3 uses TinyUSB’s tud_mounted()/tud_suspended() plus a low-voltage fallback to determine USB power; (3) both boards now return false for charging status because the charging IC status pin is not wired to a GPIO; (4) power_shutdown() now enters deep sleep. The previous code inferred charging and USB state from voltage thresholds, which could be wrong. The commit is a correctness/stability improvement, not a security patch.
Changed components
main/power.cmain/power/tdisplay.incmain/power/tdisplays3.incInspect captured patch +170 / −24
diff --git a/main/power.c b/main/power.c
index 95474b6..f965c6e 100644
--- a/main/power.c
+++ b/main/power.c
@@ -27,9 +27,12 @@
#include "power/ip5306.inc"
#elif defined(CONFIG_BOARD_TYPE_WS_TOUCH_LCD2)
#include "power/wslcdtouch2.inc"
-#elif defined(CONFIG_BOARD_TYPE_TTGO_TDISPLAYS3) && defined(CONFIG_HAS_BATTERY)
-// ttgo-tdisplays3 can read battery level and charging status if a battery is connected
+#elif defined(CONFIG_BOARD_TYPE_TTGO_TDISPLAYS3)
+// T-Display S3 can read battery level but lacks hardware to read charging status
#include "power/tdisplays3.inc"
+#elif defined(CONFIG_BOARD_TYPE_TTGO_TDISPLAY)
+// T-Display can read battery level but lacks hardware to read charging status
+#include "power/tdisplay.inc"
#else
// Stubs for other hw boards (ie. no power management)
#include "power/minimal.inc"
diff --git a/main/power/tdisplay.inc b/main/power/tdisplay.inc
new file mode 100644
index 0000000..c29ec4e
--- /dev/null
+++ b/main/power/tdisplay.inc
@@ -0,0 +1,116 @@
+// T-Display (ESP32) implementation without PMIC (ADC-based)
+//
+
+#include <driver/gpio.h>
+#include <esp_adc/adc_cali.h>
+#include <esp_adc/adc_cali_scheme.h>
+#include <esp_adc/adc_oneshot.h>
+#include <esp_sleep.h>
+
+#define BATTERY_ADC_CHANNEL ADC_CHANNEL_6
+#define BATTERY_ADC_ATTEN ADC_ATTEN_DB_12
+#define BATTERY_EMA_ALPHA 0.3f // estimated moving average smoothing factor
+
+static adc_oneshot_unit_handle_t adc1_handle = NULL;
+static adc_cali_handle_t adc1_cali_chan0_handle = NULL;
+static int ema_voltage = 0;
+
+esp_err_t power_init(void)
+{
+ // Initialise the ADC to measure battery level
+ adc_oneshot_unit_init_cfg_t init_config1 = {
+ .unit_id = ADC_UNIT_1,
+ };
+ ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle));
+ JADE_ASSERT(adc1_handle);
+ // ADC Config
+ adc_oneshot_chan_cfg_t config = {
+ .atten = BATTERY_ADC_ATTEN,
+ .bitwidth = ADC_BITWIDTH_DEFAULT,
+ };
+ ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, BATTERY_ADC_CHANNEL, &config));
+ // ESP32 (original) uses Line Fitting, not Curve Fitting like S3
+ adc_cali_line_fitting_config_t cali_config = {
+ .unit_id = ADC_UNIT_1,
+ .atten = BATTERY_ADC_ATTEN,
+ .bitwidth = ADC_BITWIDTH_DEFAULT,
+ };
+ if (adc_cali_create_scheme_line_fitting(&cali_config, &adc1_cali_chan0_handle) != ESP_OK) {
+ JADE_LOGW("ADC calibration not available, continuing without it");
+ adc1_cali_chan0_handle = NULL;
+ }
+ return ESP_OK;
+}
+
+esp_err_t power_shutdown(void)
+{
+ esp_deep_sleep_start();
+ return ESP_OK;
+}
+esp_err_t power_screen_on(void) { return ESP_OK; }
+esp_err_t power_screen_off(void) { return ESP_OK; }
+esp_err_t power_backlight_on(const uint8_t brightness) { return ESP_OK; }
+esp_err_t power_backlight_off(void) { return ESP_OK; }
+esp_err_t power_camera_on(void) { return ESP_OK; }
+esp_err_t power_camera_off(void) { return ESP_OK; }
+
+uint16_t power_get_vbat(void)
+{
+ JADE_ASSERT(adc1_handle);
+ int raw_adc, voltage;
+ ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, BATTERY_ADC_CHANNEL, &raw_adc));
+ if (adc1_cali_chan0_handle) {
+ ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_chan0_handle, raw_adc, &voltage));
+ } else {
+ // Fallback for ESP32: 3.3V ref approx (less accurate without cali)
+ voltage = (raw_adc * 3300) / 4095;
+ }
+ // Apply Estimated Moving Average (EMA)
+ const int ema_v = ema_voltage ? ema_voltage : voltage;
+ ema_voltage = (BATTERY_EMA_ALPHA * voltage) + ((1 - BATTERY_EMA_ALPHA) * ema_v);
+ // T-Display voltage divider is 2 (100k/100k)
+ return (uint16_t)(ema_voltage * 2);
+}
+
+uint8_t power_get_battery_status(void)
+{
+ const uint16_t vbat = power_get_vbat();
+
+ if (vbat > 4000) {
+ return 5;
+ } else if (vbat > 3800) {
+ return 4;
+ } else if (vbat > 3600) {
+ return 3;
+ } else if (vbat > 3400) {
+ return 2;
+ } else if (vbat > 3200) {
+ return 1;
+ }
+ return 0;
+}
+
+bool power_get_battery_charging(void)
+{
+ // The charging IC STAT pin is not connected to GPIO.
+ // Return false to avoid showing "Charging" icon permanently when on USB.
+ return false;
+}
+
+uint16_t power_get_ibat_charge(void) { return 0; }
+uint16_t power_get_ibat_discharge(void) { return 0; }
+uint16_t power_get_vusb(void) { return 0; }
+uint16_t power_get_iusb(void) { return 0; }
+uint16_t power_get_temp(void) { return 0; }
+
+void disable_usb_host(void) {}
+void enable_usb_host(void) {}
+
+bool usb_is_powered(void)
+{
+ // The T-Display (ESP32) does not have native USB (uses CP2104/CH9102 bridge).
+ // We cannot reliably detect if we are on USB or Battery because the charging IC
+ // applies voltage to the battery rails even without a battery.
+ const uint16_t vbat = power_get_vbat();
+ return (vbat < 1000) || (vbat > 4150);
+}
\ No newline at end of file
diff --git a/main/power/tdisplays3.inc b/main/power/tdisplays3.inc
index dd28a46..739b46f 100644
--- a/main/power/tdisplays3.inc
+++ b/main/power/tdisplays3.inc
@@ -1,14 +1,21 @@
-// tdisplays3 and devices with no power-management but it can measure voltage level of the battery and
-// detect charging status when a battery is connected through an analog input
+// T-Display S3 implementation without PMIC (ADC-based)
//
+
#include <driver/gpio.h>
#include <esp_adc/adc_cali.h>
+#include <esp_adc/adc_cali_scheme.h>
#include <esp_adc/adc_oneshot.h>
+#include <esp_sleep.h>
+
+#include "tusb.h"
#define BATTERY_ADC_CHANNEL ADC_CHANNEL_3
+#define BATTERY_ADC_ATTEN ADC_ATTEN_DB_12
+#define BATTERY_EMA_ALPHA 0.3f // estimated moving average smoothing factor
static adc_oneshot_unit_handle_t adc1_handle = NULL;
-static adc_cali_handle_t adc1_cali_handle = NULL;
+static adc_cali_handle_t adc1_cali_chan0_handle = NULL;
+static int ema_voltage = 0;
esp_err_t power_init(void)
{
@@ -20,22 +27,30 @@ esp_err_t power_init(void)
JADE_ASSERT(adc1_handle);
// ADC Config
adc_oneshot_chan_cfg_t config = {
- .atten = ADC_ATTEN_DB_12,
+ .atten = BATTERY_ADC_ATTEN,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, BATTERY_ADC_CHANNEL, &config));
- // Curve fitting calibration
+ // Use voltage calibration
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = ADC_UNIT_1,
- .atten = ADC_ATTEN_DB_12,
+ .atten = BATTERY_ADC_ATTEN,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
- ESP_ERROR_CHECK(adc_cali_create_scheme_curve_fitting(&cali_config, &adc1_cali_handle));
+ if (adc_cali_create_scheme_curve_fitting(&cali_config, &adc1_cali_chan0_handle) != ESP_OK) {
+ JADE_LOGW("ADC calibration not available, continuing without it");
+ adc1_cali_chan0_handle = NULL;
+ }
return ESP_OK;
}
-esp_err_t power_shutdown(void) { return ESP_OK; }
+esp_err_t power_shutdown(void)
+{
+ esp_deep_sleep_start();
+ return ESP_OK;
+}
esp_err_t power_screen_on(void) { return ESP_OK; }
+esp_err_t power_screen_off(void) { return ESP_OK; }
esp_err_t power_backlight_on(const uint8_t brightness) { return ESP_OK; }
esp_err_t power_backlight_off(void) { return ESP_OK; }
esp_err_t power_camera_on(void) { return ESP_OK; }
@@ -44,12 +59,20 @@ esp_err_t power_camera_off(void) { return ESP_OK; }
uint16_t power_get_vbat(void)
{
JADE_ASSERT(adc1_handle);
- int cal_vbat = 0;
- int raw_vbat = 0;
- ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, BATTERY_ADC_CHANNEL, &raw_vbat));
- ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_handle, raw_vbat, &cal_vbat));
- return (uint16_t)(cal_vbat * 2);
+ int raw_adc, voltage;
+ ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, BATTERY_ADC_CHANNEL, &raw_adc));
+ if (adc1_cali_chan0_handle) {
+ ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_chan0_handle, raw_adc, &voltage));
+ } else {
+ voltage = (raw_adc * 3300) / 4095;
+ }
+ // Apply Estimated Moving Average (EMA)
+ const int ema_v = ema_voltage ? ema_voltage : voltage;
+ ema_voltage = (BATTERY_EMA_ALPHA * voltage) + ((1 - BATTERY_EMA_ALPHA) * ema_v);
+ // T-Display S3 voltage divider is 2 (100k/100k)
+ return (uint16_t)(ema_voltage * 2);
}
+
uint8_t power_get_battery_status(void)
{
const uint16_t vbat = power_get_vbat();
@@ -70,11 +93,8 @@ uint8_t power_get_battery_status(void)
bool power_get_battery_charging(void)
{
- uint16_t vbat = power_get_vbat();
- // If the voltage is greater than 4500 mV and less than 4750 it means its charging
- if (vbat > 4500 && vbat < 4750) {
- return true;
- }
+ // The charging IC STAT pin is not connected to GPIO, so we can't know for sure.
+ // Return false to avoid showing "Charging" icon permanently when on USB.
return false;
}
@@ -84,12 +104,19 @@ uint16_t power_get_vusb(void) { return 0; }
uint16_t power_get_iusb(void) { return 0; }
uint16_t power_get_temp(void) { return 0; }
+void disable_usb_host(void) {}
+void enable_usb_host(void) {}
+
bool usb_is_powered(void)
{
- // If the voltage is greater than 4500 mV it means USB is connected
- uint16_t vbat = power_get_vbat();
- if (vbat > 4500) {
+ // Check if USB is mounted and not suspended (active connection)
+ if (tud_mounted() && !tud_suspended()) {
+ return true;
+ }
+ // Fallback: If voltage is near zero (<1V) but chip is on,
+ // we must be running on USB power without a battery.
+ if (power_get_vbat() < 1000) {
return true;
}
return false;
-}
+}
\ No newline at end of file
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.