Skip to content

Add power device (battery + power-off) for lilygo-tdeck-max#553

Open
Crazypedia wants to merge 3 commits into
TactilityProject:mainfrom
Crazypedia:tdeckmax-power
Open

Add power device (battery + power-off) for lilygo-tdeck-max#553
Crazypedia wants to merge 3 commits into
TactilityProject:mainfrom
Crazypedia:tdeckmax-power

Conversation

@Crazypedia

@Crazypedia Crazypedia commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

PR 2 of the incremental plan from #539 (base board → power → radio → GPS/haptics). Stacked on #552 — this branch is tdeckmax-baseboard + one commit (e4dc61a7), so the diff below includes #552's content until that merges. Only the last commit is new here; happy to rebase onto main once #552 lands if that's easier to review standalone.

What this adds

Battery metrics and power-off for the T-Deck Max:

  • BQ27220 fuel gauge (I2C 0x55, existing upstream driver) for battery metrics.
  • SY6970 charger (I2C 0x6A) for charging status/control and power-off (ship mode via REG09 BATFET_DIS). Newer T-Deck Max revisions use the SY6970 instead of the older BQ25896 at 0x6B — confirmed by I2C scan on my unit; the two are register-compatible for everything used here.

TdeckmaxPower::supportsPowerOff() returns true, so the launcher's power-off button is enabled.

Caveat carried over from the vendor's own driver notes: ship mode only fully powers off on battery with USB unplugged — with USB attached the system rail stays up. This matches the chip's documented behavior, not a bug in this driver.

Verification

  • ESP32 build clean, 34% of the app partition free.
  • Flashed to hardware: clean boot, no panics/watchdog resets. Serial log confirms SY6970 configured: 4288 mV / 1024 mA, watchdog off, both BQ27220 and T-Deck Max Power register as kernel devices without error, and the Power app manifest (which drives the launcher's power-off button) registers correctly.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added end-to-end support for the LilyGO T-Deck Max: e-paper display (LVGL integration with refresh/ghost-clearing behavior), CST66xx touch, and TCA8418 keyboard with LVGL input plus backlight control.
    • Added power/charging and battery/charging metrics support for the device.
    • Added device-tree and board metadata, plus support for the XL9555 I/O expander used on the hardware.
  • Documentation
    • Documented the GDEQ031T10 e-paper display driver and the XL9555 I/O expander driver.

Base board bring-up for the LilyGO T-Deck Max (ESP32-S3, 16 MB flash,
8 MB PSRAM), per the plan discussed in TactilityProject#539: display, touch, and
keyboard only — power, LoRa radio, GPS, and haptics follow as separate
PRs.

New reusable drivers:
- Drivers/GDEQ031T10: UC8253-based 240x320 1bpp e-paper panel with
  windowed partial refresh + full-refresh ghost gate, refresh runs on a
  background task.
- Drivers/xl9555-module: XL9555 I2C IO expander as a kernel module with
  gpio_controller bindings (the board routes touch/keyboard resets and
  several power rails through it).

Board specifics:
- CST66xx touch (Hynitron 4-byte register protocol) implemented
  device-locally; vendor docs mislabel the part as CST328.
- TCA8418 keyboard matrix (existing upstream driver) with backlight.
- SD card as an espressif,esp32-sdspi devicetree node on the shared
  SPI2 bus; unused chip-selects (LoRa, SD) are deasserted before the
  EPD touches the bus.
- incubating=true until SD storage is verified on this board.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 74e19516-9d3f-44a6-9fbe-956d116896f4

📥 Commits

Reviewing files that changed from the base of the PR and between e4dc61a and ad2e7a6.

📒 Files selected for processing (13)
  • Devices/lilygo-tdeck-max/CMakeLists.txt
  • Devices/lilygo-tdeck-max/Source/Configuration.cpp
  • Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.cpp
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.cpp
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.h
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.cpp
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.h
  • Devices/lilygo-tdeck-max/device.properties
  • Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts
  • Drivers/GDEQ031T10/README.md
  • Drivers/GDEQ031T10/Source/Gdeq031t10Display.cpp
  • Drivers/GDEQ031T10/Source/Gdeq031t10Display.h
  • Drivers/xl9555-module/source/xl9555.cpp
✅ Files skipped from review due to trivial changes (2)
  • Devices/lilygo-tdeck-max/CMakeLists.txt
  • Drivers/GDEQ031T10/README.md
🚧 Files skipped from review as they are similar to previous changes (11)
  • Devices/lilygo-tdeck-max/device.properties
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.h
  • Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.h
  • Devices/lilygo-tdeck-max/Source/Configuration.cpp
  • Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.cpp
  • Drivers/xl9555-module/source/xl9555.cpp
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.cpp
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.cpp
  • Drivers/GDEQ031T10/Source/Gdeq031t10Display.h
  • Drivers/GDEQ031T10/Source/Gdeq031t10Display.cpp

📝 Walkthrough

Walkthrough

Changes

This pull request adds an xl9555-module I/O expander driver, a GDEQ031T10 SPI e-paper display driver, and a new lilygo-tdeck-max device definition. The board support includes boot-time GPIO setup, touch input, a matrix keyboard with backlight control, and battery/charger management. Supporting build files, devicetree data, module wiring, documentation, and licensing files are included.

Sequence Diagram(s)

Sequence and flow diagrams are included in the hidden review stack artifact above.

Related PRs: None specified.

Suggested labels: enhancement, new-device, driver

Suggested reviewers: Board and HAL maintainers familiar with ESP32 SPI/I2C drivers and LVGL integration

Poem:
A rabbit taps the e-ink screen,
Keys click soft where LEDs gleam,
Bezel buttons, home and back,
Charger hums along its track.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the PR's power-device work, including battery metrics and power-off support for lilygo-tdeck-max.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (1)
Drivers/xl9555-module/include/drivers/xl9555.h (1)

10-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider documenting the valid I2C address range.

address has no documented constraint (XL9555 typically uses 7-bit addresses in the 0x20-0x27 range depending on strap pins). A brief comment would help devicetree authors avoid misconfiguration.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ebd69281-b331-45c1-8886-a47cca880b45

📥 Commits

Reviewing files that changed from the base of the PR and between 66a4eb6 and e4dc61a.

📒 Files selected for processing (28)
  • Devices/lilygo-tdeck-max/CMakeLists.txt
  • Devices/lilygo-tdeck-max/Source/Configuration.cpp
  • Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.cpp
  • Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.h
  • Devices/lilygo-tdeck-max/Source/devices/Display.cpp
  • Devices/lilygo-tdeck-max/Source/devices/Display.h
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.cpp
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.h
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.cpp
  • Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.h
  • Devices/lilygo-tdeck-max/Source/module.cpp
  • Devices/lilygo-tdeck-max/device.properties
  • Devices/lilygo-tdeck-max/devicetree.yaml
  • Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts
  • Drivers/GDEQ031T10/CMakeLists.txt
  • Drivers/GDEQ031T10/README.md
  • Drivers/GDEQ031T10/Source/Gdeq031t10Display.cpp
  • Drivers/GDEQ031T10/Source/Gdeq031t10Display.h
  • Drivers/xl9555-module/CMakeLists.txt
  • Drivers/xl9555-module/LICENSE-Apache-2.0.md
  • Drivers/xl9555-module/README.md
  • Drivers/xl9555-module/bindings/xlsemi,xl9555.yaml
  • Drivers/xl9555-module/devicetree.yaml
  • Drivers/xl9555-module/include/bindings/xl9555.h
  • Drivers/xl9555-module/include/drivers/xl9555.h
  • Drivers/xl9555-module/include/xl9555_module.h
  • Drivers/xl9555-module/source/module.cpp
  • Drivers/xl9555-module/source/xl9555.cpp

Comment thread Devices/lilygo-tdeck-max/device.properties Outdated
Comment thread Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts
Comment thread Devices/lilygo-tdeck-max/Source/devices/TdeckmaxKeyboard.cpp
Comment thread Drivers/GDEQ031T10/README.md Outdated
Comment thread Drivers/GDEQ031T10/Source/Gdeq031t10Display.cpp Outdated
Comment thread Drivers/GDEQ031T10/Source/Gdeq031t10Display.cpp
Comment on lines +507 to +539
bool Gdeq031t10Display::startLvgl() {
if (lvglDisplay != nullptr) {
return true;
}

lvglDisplay = lv_display_create(Gdeq031t10Display::WIDTH, Gdeq031t10Display::HEIGHT);
if (lvglDisplay == nullptr) {
return false;
}

lv_display_set_user_data(lvglDisplay, this);
lv_display_set_color_format(lvglDisplay, LV_COLOR_FORMAT_I1);

// The default colour theme renders accent-coloured text/icons that threshold
// to near-invisible on a 1bpp panel. Apply LVGL's monochrome theme (light
// background, dark foreground) so UI content shows as solid black on white.
lv_theme_t* baseTheme = lv_theme_mono_init(lvglDisplay, false, LV_FONT_DEFAULT);
// Chain a theme on top of the mono theme that disables the textarea cursor
// blink. A blinking cursor invalidates its region ~twice a second, and on
// e-paper every invalidation is a panel refresh, so text-entry screens (e.g.
// the Wi-Fi password field) flash continuously. The mono theme still applies
// first (it's the parent); themeApplyCallback only pins the cursor solid.
static lv_theme_t epaperTheme;
epaperTheme = *baseTheme;
lv_theme_set_parent(&epaperTheme, baseTheme);
lv_theme_set_apply_cb(&epaperTheme, &Gdeq031t10Display::themeApplyCallback);
lv_display_set_theme(lvglDisplay, &epaperTheme);
lv_display_set_render_mode(lvglDisplay, LV_DISPLAY_RENDER_MODE_FULL);
lv_display_set_buffers(lvglDisplay, renderFramebuffer.get(), nullptr, LVGL_I1_PALETTE_SIZE + FRAMEBUFFER_SIZE, LV_DISPLAY_RENDER_MODE_FULL);
lv_display_set_flush_cb(lvglDisplay, flushCallback);

return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant interface and implementation.
git ls-files | rg '(^|/)(DisplayDevice\.h|Gdeq031t10Display\.cpp)$'

echo '--- DisplayDevice.h ---'
cat -n Tactility/Include/Tactility/hal/display/DisplayDevice.h | sed -n '1,260p'

echo '--- Gdeq031t10Display.cpp (relevant sections) ---'
cat -n Drivers/GDEQ031T10/Source/Gdeq031t10Display.cpp | sed -n '1,260p'
echo '---'
cat -n Drivers/GDEQ031T10/Source/Gdeq031t10Display.cpp | sed -n '260,620p'

echo '--- Search for startLvgl/start/queueRefresh/refreshTask/renderFramebuffer/initialized ---'
rg -n "startLvgl|queueRefresh|refreshTask|renderFramebuffer|initialized|lv_display_set_buffers|flushCallback|start\\(" Tactility Drivers -g '!**/build/**' -g '!**/dist/**'

Repository: TactilityProject/Tactility

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Outline: Tactility/Source/lvgl/Lvgl.cpp ---'
ast-grep outline Tactility/Source/lvgl/Lvgl.cpp --view expanded || true

echo '--- Lvgl.cpp relevant sections ---'
cat -n Tactility/Source/lvgl/Lvgl.cpp | sed -n '1,220p'

echo '--- Gdeq031t10Display.h relevant sections ---'
cat -n Drivers/GDEQ031T10/Source/Gdeq031t10Display.h | sed -n '1,260p'

echo '--- Search for display start/startLvgl call sites ---'
rg -n "->startLvgl\\(|->start\\(|display->startLvgl\\(|display->start\\(" Tactility Drivers -g '!**/build/**' -g '!**/dist/**' | sed -n '1,200p'

Repository: TactilityProject/Tactility

Length of output: 19199


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Find Gdeq031t10Display construction / ownership ---'
rg -n "Gdeq031t10Display|DisplayDevice>" Drivers Tactility -g '!**/build/**' -g '!**/dist/**' | sed -n '1,200p'

echo '--- Find direct start() calls on display devices in driver code ---'
rg -n "\.start\(\)|->start\(\)" Drivers/Tactility Tactility/Source Drivers -g '!**/build/**' -g '!**/dist/**' | sed -n '1,250p'

echo '--- Find module/service startup that may call display start ---'
rg -n "findDevices<hal::display::DisplayDevice>|supportsLvgl\(\)|startLvgl\(\)|getLvglDisplay\(" Tactility/Source Drivers -g '!**/build/**' -g '!**/dist/**' | sed -n '1,250p'

Repository: TactilityProject/Tactility

Length of output: 8165


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Find Gdeq031t10Display construction / ownership ---'
rg -n "Gdeq031t10Display|DisplayDevice>" Drivers Tactility -g '!**/build/**' -g '!**/dist/**' | sed -n '1,200p'

echo '--- Find direct start() calls on display devices in driver code ---'
rg -n "\.start\(\)|->start\(\)" Drivers Tactility -g '!**/build/**' -g '!**/dist/**' | sed -n '1,250p'

echo '--- Find module/service startup that may call display start ---'
rg -n "findDevices<hal::display::DisplayDevice>|supportsLvgl\(\)|startLvgl\(\)|getLvglDisplay\(" Tactility/Source Drivers -g '!**/build/**' -g '!**/dist/**' | sed -n '1,250p'

Repository: TactilityProject/Tactility

Length of output: 12701


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Tactility/Source/hal/Hal.cpp ---'
cat -n Tactility/Source/hal/Hal.cpp | sed -n '1,220p'

echo '--- Search for display creation/registration in drivers ---'
rg -n "createDisplay\\(|register.*Display|addDevice\\(|findDevices<display::DisplayDevice>|startLvgl\\(" Drivers Tactility/Source Tactility/Include -g '!**/build/**' -g '!**/dist/**' | sed -n '1,240p'

echo '--- Relevant display start implementations for comparison ---'
cat -n Drivers/ST7796-i8080/Source/St7796i8080Display.cpp | sed -n '200,280p'
echo '---'
cat -n Drivers/EPDiyDisplay/Source/EpdiyDisplay.cpp | sed -n '120,230p'

Repository: TactilityProject/Tactility

Length of output: 15645


Drivers/GDEQ031T10/Source/Gdeq031t10Display.cpp:507-536 — Guard startLvgl() against a failed start()

startLvgl() can still run after start() returns false. In that case renderFramebuffer and refreshTask may be null, so lv_display_set_buffers(...) and the flush path can crash. Bail out when the driver isn’t initialized, or retry start() here like the other display backends.

Comment thread Drivers/GDEQ031T10/Source/Gdeq031t10Display.h
Comment thread Drivers/GDEQ031T10/Source/Gdeq031t10Display.h Outdated
Comment thread Drivers/xl9555-module/source/xl9555.cpp
@KenVanHoeylandt

Copy link
Copy Markdown
Contributor

I'll wait for the other pull request to be merged first, before reviewing this one, as the diff will become smaller that way.

Crazypedia and others added 2 commits July 6, 2026 09:45
…ject#552

GDEQ031T10:
- waitWhileBusy() now times out (5s) instead of spinning forever on a
  stuck BUSY pin, which previously could hang the refresh task and
  make stop() un-recoverable.
- writeCommand()/writeData() and all callers now check the SPI
  transfer result instead of advancing panel/shadow state as if it
  always succeeds.
- start() no longer marks the display powered/initialized before the
  refresh task is confirmed created; on task-creation failure it now
  unwinds the SPI device and allocated buffers instead of leaking them.
- `powered` and `currentRefreshMode` are now atomic (read on the
  refresh task, written from arbitrary caller threads).
- Corrected a stale doc comment on setRefreshMode() (not one-shot).
- README updated to describe the actual windowed partial-refresh
  pipeline instead of the old full-refresh-every-flush description.

TdeckmaxKeyboard:
- Modifier state (shift/sym/caps) moved from function-local statics to
  instance members.
- Keyboard queue send is now non-blocking (drops+logs on a full queue)
  instead of blocking inside the periodic input timer callback.
- initBacklight() now returns false when channel config fails, instead
  of reporting success.

xl9555:
- set_flags() rejects GPIO_FLAG_ACTIVE_LOW combined with
  GPIO_FLAG_DIRECTION_OUTPUT: the polarity register only affects input
  reads, so it silently did nothing for outputs.
- set_level()/set_flags() now serialize their read-modify-write
  register updates with device_lock()/device_unlock(), since the
  shared i2c_controller_register8_{set,reset}_bits() helpers do a
  separate read then write.
- start() uses check() instead of a soft parent-type check, and drops
  a log line the parent already emits.

Configuration.cpp: i2c0 is unconditionally declared in this board's
devicetree, so createDevices() now uses check(i2c) instead of an if,
matching the "trust internal guarantees" convention.

Cst66xxTouch: unbind the Home/Recents bezel keys. Both called
tt::app::start(), which pushes a new app-stack instance on every
press; repeated presses would leak Launcher/AppList instances
indefinitely. Left unbound until the app stack has a way to resume an
existing instance instead of pushing a new one.

device.properties / dts: corrected the product name to "T-Deck Max"
(was "T-Deck Pro Max") to match the rest of the PR, the vendor repo,
and LilyGO's actual naming.

Verified with a full idf.py build for lilygo-tdeck-max (IDF 5.5.2).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-max

Follow-up to TactilityProject#552 (base board). Battery metrics come from the BQ27220
fuel gauge (I2C 0x55); charging status, charge control, and power-off
(ship mode via SY6970 REG09 BATFET_DIS) use the SY6970 charger (0x6A —
newer T-Deck Max revisions use this instead of the older BQ25896 at
0x6B, confirmed by I2C scan; the two are register-compatible for
everything used here). Enables the launcher's power-off button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants