New kernel drivers and more (#560)

- Added kernel base drivers for: display, pointer (touch, mouse, etc.), keyboard, adc, power supply and backlight
- Implement new kernel driver modules: `st7789-module` and `gt911-module`
- Implement ESP32 ADC "oneshot" kernel driver
- Implement ESP32  backlight kernel driver with ledc API
- Implemented `battery-sense` driver that allows for voltage measurement and creates a power supply child device
- Updated github actions
- Updated flash scripts
- Fix for esp32 legacy driver conflict with ADC
- Created separate `lilygo-tdeck` and `lilygo-tdeck-plus` devices
- Created `lilygo-module` with LilyGO kernel drivers
- Fix for intermittent errors in build related to code generation of `devicetree.c`
- `lvgl-module` can now map kernel drivers onto LVGL devices
- Created `KernelDisplayApp` as a mirror of `HalDisplayApp` (formerly `DisplayApp`)
- Removed `struct` and `enum` prefix in a lot of kernel driver cpp source files
- `lilygo-tdeck` and `lilygo-tdeck-plus` are now fully relying on kernel drivers and don't use any of the old HAL
This commit is contained in:
Ken Van Hoeylandt
2026-07-12 00:29:12 +02:00
committed by GitHub
parent c4406b24ba
commit 50c0a14a93
149 changed files with 5274 additions and 1266 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ inputs:
runs: runs:
using: "composite" using: "composite"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
submodules: recursive submodules: recursive
- name: 'Board select' - name: 'Board select'
+1 -1
View File
@@ -11,7 +11,7 @@ inputs:
runs: runs:
using: "composite" using: "composite"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
submodules: recursive submodules: recursive
- name: 'Board select' - name: 'Board select'
+1 -1
View File
@@ -15,7 +15,7 @@ runs:
using: "composite" using: "composite"
steps: steps:
- name: "Checkout repo" - name: "Checkout repo"
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
submodules: recursive submodules: recursive
- name: Install Linux Dependencies for SDL - name: Install Linux Dependencies for SDL
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
Linux: Linux:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- name: "Build" - name: "Build"
@@ -22,7 +22,7 @@ jobs:
macOS: macOS:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- name: "Build" - name: "Build"
+9 -7
View File
@@ -22,7 +22,7 @@ jobs:
] ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- name: "Build SDK" - name: "Build SDK"
@@ -36,7 +36,7 @@ jobs:
outputs: outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }} matrix: ${{ steps.set-matrix.outputs.matrix }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- id: set-matrix - id: set-matrix
@@ -47,7 +47,7 @@ jobs:
strategy: strategy:
matrix: ${{ fromJSON(needs.GenerateDeviceMatrix.outputs.matrix) }} matrix: ${{ fromJSON(needs.GenerateDeviceMatrix.outputs.matrix) }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- name: "Build Firmware" - name: "Build Firmware"
@@ -62,7 +62,9 @@ jobs:
(github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'push' && github.ref == 'refs/heads/main') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with:
persist-credentials: false
- name: "Bundle Artifacts" - name: "Bundle Artifacts"
uses: ./.github/actions/bundle-artifacts uses: ./.github/actions/bundle-artifacts
PublishFirmwareSnapshot: PublishFirmwareSnapshot:
@@ -70,7 +72,7 @@ jobs:
needs: [ BundleArtifacts ] needs: [ BundleArtifacts ]
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') if: (github.event_name == 'push' && github.ref == 'refs/heads/main')
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: "Publish Firmware Snapshot" - name: "Publish Firmware Snapshot"
env: env:
CDN_ID: ${{ secrets.CDN_ID }} CDN_ID: ${{ secrets.CDN_ID }}
@@ -84,7 +86,7 @@ jobs:
needs: [ BundleArtifacts ] needs: [ BundleArtifacts ]
if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: "Publish Firmware Stable" - name: "Publish Firmware Stable"
env: env:
CDN_ID: ${{ secrets.CDN_ID }} CDN_ID: ${{ secrets.CDN_ID }}
@@ -98,7 +100,7 @@ jobs:
needs: [ BundleArtifacts ] needs: [ BundleArtifacts ]
if: (github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))) if: (github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')))
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: "Publish SDKs" - name: "Publish SDKs"
env: env:
CDN_ID: ${{ secrets.CDN_ID }} CDN_ID: ${{ secrets.CDN_ID }}
+2 -3
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: "Checkout repo" - name: "Checkout repo"
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
submodules: recursive submodules: recursive
- name: "Configure Project" - name: "Configure Project"
@@ -30,8 +30,7 @@ jobs:
DevicetreeTests: DevicetreeTests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: "Checkout repo" - uses: actions/checkout@v6
- uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
submodules: recursive submodules: recursive
@@ -79,6 +79,11 @@ def property_to_string(property: DeviceProperty, devices: list[Device]) -> str:
value_list.append(str(item)) value_list.append(str(item))
return "{ " + ",".join(value_list) + " }" return "{ " + ",".join(value_list) + " }"
elif type == "phandle": elif type == "phandle":
# Mirrors the string-passthrough convention "phandles" already uses for sentinel defaults
# like GPIO_PIN_SPEC_NONE: a binding default of "NULL" represents "no device referenced",
# not an actual node name to resolve.
if isinstance(property.value, str) and property.value == "NULL":
return "NULL"
return find_phandle(devices, property.value) return find_phandle(devices, property.value)
elif type == "phandles": elif type == "phandles":
value_list = list() value_list = list()
+2 -2
View File
@@ -13,9 +13,9 @@ $jsonClean = $json.flash_files -replace '[\{\}\@\;]', ''
$jsonClean = $jsonClean -replace '[\=]', ' ' $jsonClean = $jsonClean -replace '[\=]', ' '
cd Binaries cd Binaries
$command = "esptool --port $port erase_flash" $command = "esptool --port $port erase-flash"
Invoke-Expression $command Invoke-Expression $command
$command = "esptool --port $port write_flash $jsonClean" $command = "esptool --port $port write-flash $jsonClean"
Invoke-Expression $command Invoke-Expression $command
cd .. cd ..
+2 -2
View File
@@ -49,7 +49,7 @@ fi
# Take the flash_arg file contents and join each line in the file into a single line # Take the flash_arg file contents and join each line in the file into a single line
flash_args=`grep \n Binaries/flash_args | awk '{print}' ORS=' '` flash_args=`grep \n Binaries/flash_args | awk '{print}' ORS=' '`
cd Binaries cd Binaries
$esptoolPath --port $1 erase_flash $esptoolPath --port $1 erase-flash
$esptoolPath --port $1 write_flash $flash_args $esptoolPath --port $1 write-flash $flash_args
cd - cd -
@@ -44,6 +44,11 @@ CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1 CONFIG_WL_SECTOR_MODE=1
# Allow new i2c_master API (used by Tab5Keyboard for LP_I2C_NUM_0) to coexist with legacy i2c driver # Allow new i2c_master API (used by Tab5Keyboard for LP_I2C_NUM_0) to coexist with legacy i2c driver
CONFIG_I2C_SKIP_LEGACY_CONFLICT_CHECK=y CONFIG_I2C_SKIP_LEGACY_CONFLICT_CHECK=y
# Allow new adc_oneshot API (esp32_adc_oneshot kernel driver, linked into every device via
# platform-esp32) to coexist with boards still reading battery voltage via the legacy ADC driver
# (e.g. m5stack-cardputer, m5stack-cardputer-adv, lilygo-thmi). Only one API is ever actually used
# per board at runtime, so this just bypasses ESP-IDF's link-time-only conflict abort.
CONFIG_ADC_SKIP_LEGACY_CONFLICT_CHECK=y
# Wi-Fi memory optimizations # Wi-Fi memory optimizations
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=3 CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=3
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=5 CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=5
+2 -2
View File
@@ -6,7 +6,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/display_placeholder.h>
#include <tactility/bindings/touch_placeholder.h> #include <tactility/bindings/pointer_placeholder.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
/ { / {
@@ -37,7 +37,7 @@
}; };
touch@1 { touch@1 {
compatible = "touch-placeholder"; compatible = "pointer-placeholder";
}; };
}; };
+2 -2
View File
@@ -8,7 +8,7 @@
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/display_placeholder.h>
#include <tactility/bindings/touch_placeholder.h> #include <tactility/bindings/pointer_placeholder.h>
/ { / {
compatible = "root"; compatible = "root";
@@ -46,7 +46,7 @@
}; };
touch@1 { touch@1 {
compatible = "touch-placeholder"; compatible = "pointer-placeholder";
}; };
}; };
+2 -2
View File
@@ -8,7 +8,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/display_placeholder.h>
#include <tactility/bindings/touch_placeholder.h> #include <tactility/bindings/pointer_placeholder.h>
/ { / {
compatible = "root"; compatible = "root";
@@ -46,7 +46,7 @@
}; };
touch@1 { touch@1 {
compatible = "touch-placeholder"; compatible = "pointer-placeholder";
}; };
}; };
+2 -2
View File
@@ -6,7 +6,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/display_placeholder.h>
#include <tactility/bindings/touch_placeholder.h> #include <tactility/bindings/pointer_placeholder.h>
/ { / {
compatible = "root"; compatible = "root";
@@ -36,7 +36,7 @@
}; };
touch@1 { touch@1 {
compatible = "touch-placeholder"; compatible = "pointer-placeholder";
}; };
}; };
+2 -2
View File
@@ -7,7 +7,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/display_placeholder.h>
#include <tactility/bindings/touch_placeholder.h> #include <tactility/bindings/pointer_placeholder.h>
/ { / {
compatible = "root"; compatible = "root";
@@ -45,7 +45,7 @@
}; };
touch@1 { touch@1 {
compatible = "touch-placeholder"; compatible = "pointer-placeholder";
}; };
}; };
@@ -8,7 +8,7 @@
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/display_placeholder.h>
#include <tactility/bindings/touch_placeholder.h> #include <tactility/bindings/pointer_placeholder.h>
/ { / {
compatible = "root"; compatible = "root";
@@ -47,7 +47,7 @@
}; };
touch@1 { touch@1 {
compatible = "touch-placeholder"; compatible = "pointer-placeholder";
}; };
}; };
@@ -8,7 +8,7 @@
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/display_placeholder.h>
#include <tactility/bindings/touch_placeholder.h> #include <tactility/bindings/pointer_placeholder.h>
/ { / {
compatible = "root"; compatible = "root";
@@ -47,7 +47,7 @@
}; };
touch@1 { touch@1 {
compatible = "touch-placeholder"; compatible = "pointer-placeholder";
}; };
}; };
+6
View File
@@ -0,0 +1,6 @@
file(GLOB_RECURSE SOURCE_FILES source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
REQUIRES Tactility driver lilygo-module
)
@@ -0,0 +1,23 @@
general.vendor=LilyGO
general.name=T-Deck Plus
apps.launcherAppId=Launcher
hardware.target=ESP32S3
hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
storage.userDataLocation=SD
display.size=2.8"
display.shape=rectangle
display.dpi=143
cdn.infoMessage=To put the device into bootloader mode: <br/>1. Press the trackball and then the reset button at the same time,<br/>2. Let go of the reset button, then the trackball.<br/><br/>When this website reports that flashing is finished, you likely have to press the reset button.
lvgl.colorDepth=16
@@ -0,0 +1,6 @@
dependencies:
- Platforms/platform-esp32
- Drivers/st7789-module
- Drivers/gt911-module
- Drivers/lilygo-module
dts: lilygo,tdeck-plus.dts
@@ -0,0 +1,150 @@
/dts-v1/;
#include <tactility/bindings/battery_sense.h>
#include <tactility/bindings/root.h>
#include <tactility/bindings/esp32_adc_oneshot.h>
#include <tactility/bindings/esp32_ble.h>
#include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_grove.h>
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_ledc_backlight.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/esp32_uart.h>
#include <bindings/gt911.h>
#include <bindings/st7789.h>
#include <lilygo/bindings/tdeck_keyboard.h>
#include <lilygo/bindings/tdeck_keyboard_backlight.h>
#include <lilygo/bindings/tdeck_trackball.h>
// Reference: https://wiki.lilygo.cc/products/t-deck-series/t-deck-plus/
/ {
compatible = "root";
model = "LilyGO T-Deck Plus";
adc0 {
compatible = "espressif,esp32-adc-oneshot";
unit-id = <ADC_UNIT_1>;
clk-src = <ADC_RTC_CLK_SRC_DEFAULT>;
channels = <ADC_CHANNEL_3 ADC_ATTEN_DB_12 ADC_BITWIDTH_DEFAULT>;
};
battery-sense {
compatible = "battery-sense";
io-channel = <&adc0 0>;
reference-voltage-mv = <3300>;
multiplier = <2110>;
};
wifi0 {
compatible = "espressif,esp32-wifi-pinned";
status = "disabled";
};
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
compatible = "espressif,esp32-gpio";
gpio-count = <49>;
};
trackball {
compatible = "lilygo,tdeck-trackball";
pin-right = <&gpio0 2 GPIO_FLAG_NONE>;
pin-up = <&gpio0 3 GPIO_FLAG_NONE>;
pin-left = <&gpio0 1 GPIO_FLAG_NONE>;
pin-down = <&gpio0 15 GPIO_FLAG_NONE>;
pin-click = <&gpio0 0 GPIO_FLAG_NONE>;
};
i2c_internal: i2c0 {
compatible = "espressif,esp32-i2c";
port = <I2C_NUM_0>;
clock-frequency = <100000>;
pin-sda = <&gpio0 18 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 8 GPIO_FLAG_NONE>;
touch {
compatible = "goodix,gt911";
reg = <0x5D>;
x-max = <240>;
y-max = <320>;
swap-xy;
mirror-x;
pin-interrupt = <&gpio0 16 GPIO_FLAG_NONE>;
};
keyboard {
compatible = "lilygo,tdeck-keyboard";
reg = <0x55>;
};
keyboard_backlight {
compatible = "lilygo,tdeck-keyboard-backlight";
reg = <0x55>;
};
};
i2s0 {
compatible = "espressif,esp32-i2s";
port = <I2S_NUM_0>;
pin-bclk = <&gpio0 7 GPIO_FLAG_NONE>;
pin-ws = <&gpio0 5 GPIO_FLAG_NONE>;
pin-data-out = <&gpio0 6 GPIO_FLAG_NONE>;
};
display_backlight {
compatible = "espressif,esp32-ledc-backlight";
// Off by default so dispaly power-on won't show the screen from before the last power loss.
// The display backlight is turned on during the boot process.
status = "disabled";
pin-backlight = <&gpio0 42 GPIO_FLAG_NONE>;
// 32 KHz and higher causes the screen to start dimming again above 80% brightness
// when moving the brightness slider rapidly from a lower setting to 100% (debug-traced, not a slider bug).
frequency-hz = <30000>;
};
spi0 {
compatible = "espressif,esp32-spi";
host = <SPI2_HOST>;
cs-gpios = <&gpio0 12 GPIO_FLAG_NONE>, // Display
<&gpio0 39 GPIO_FLAG_NONE>, // SD card
<&gpio0 9 GPIO_FLAG_NONE>; // Radio
pin-mosi = <&gpio0 41 GPIO_FLAG_NONE>;
pin-miso = <&gpio0 38 GPIO_FLAG_NONE>;
pin-sclk = <&gpio0 40 GPIO_FLAG_NONE>;
display@0 {
compatible = "sitronix,st7789";
horizontal-resolution = <320>;
vertical-resolution = <240>;
swap-xy;
mirror-x;
invert-color;
pixel-clock-hz = <62500000>;
pin-dc = <&gpio0 11 GPIO_FLAG_NONE>;
backlight = <&display_backlight>;
};
// Must be started after display
sdcard@1 {
compatible = "espressif,esp32-sdspi";
status = "disabled";
frequency-khz = <20000>;
};
};
uart0 {
compatible = "espressif,esp32-uart";
port = <UART_NUM_1>;
pin-tx = <&gpio0 43 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 44 GPIO_FLAG_NONE>;
};
};
@@ -0,0 +1,87 @@
#include <tactility/module.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/lvgl_module.h>
#include <Tactility/SystemEvents.h>
#include <Tactility/LogMessages.h>
#include <Tactility/hal/Configuration.h>
#include <Tactility/hal/gps/GpsConfiguration.h>
#include <Tactility/kernel/Kernel.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/gps/GpsService.h>
#include <Tactility/settings/TrackballSettings.h>
#include <lilygo/drivers/trackball.h>
#include <lilygo/drivers/tdeck_power_on.h>
#include <driver/gpio.h>
constexpr auto* TAG = "tdeck-plus";
// Legacy placeholder (required until legacy HAL is cleaned up everywhere)
extern const tt::hal::Configuration hardwareConfiguration = {};
extern "C" {
void subscribe_events() {
tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) {
auto gps_service = tt::service::gps::findGpsService();
if (gps_service != nullptr) {
std::vector<tt::hal::gps::GpsConfiguration> gps_configurations;
gps_service->getGpsConfigurations(gps_configurations);
if (gps_configurations.empty()) {
if (gps_service->addGpsConfiguration(tt::hal::gps::GpsConfiguration {.uartName = "uart0", .baudRate = 38400, .model = tt::hal::gps::GpsModel::UBLOX10})) {
LOG_I(TAG, "Configured internal GPS");
} else {
LOG_E(TAG, "Failed to configure internal GPS");
}
}
}
});
// The kernel trackball device is already started by kernel_init(); this just registers it as an
// LVGL input device and applies persisted settings, both of which require LVGL to be up first.
tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) {
auto tbSettings = tt::settings::trackball::loadOrGetDefault();
lvgl_lock();
if (trackball::init() != nullptr) {
trackball::setMode(tbSettings.trackballMode == tt::settings::trackball::TrackballMode::Pointer
? trackball::Mode::Pointer
: trackball::Mode::Encoder);
trackball::setEncoderSensitivity(tbSettings.encoderSensitivity);
trackball::setPointerSensitivity(tbSettings.pointerSensitivity);
trackball::setEnabled(tbSettings.trackballEnabled);
}
lvgl_unlock();
});
}
static error_t start() {
LOG_I(TAG, LOG_MESSAGE_POWER_ON_START);
if (!tdeck_power_on()) {
LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED);
return ERROR_RESOURCE;
}
// Avoids crash when no SD card is inserted. It's unknown why, but likely is related to power draw.
tt::kernel::delayMillis(100);
subscribe_events();
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
Module lilygo_tdeck_plus_module = {
.name = "lilygo-tdeck-plus",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
}
+2 -3
View File
@@ -1,7 +1,6 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*) file(GLOB_RECURSE SOURCE_FILES source/*.c*)
idf_component_register( idf_component_register(
SRCS ${SOURCE_FILES} SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source" REQUIRES Tactility driver lilygo-module
REQUIRES Tactility EspLcdCompat ST7789 GT911 PwmBacklight EstimatedPower driver
) )
@@ -1,30 +0,0 @@
#include "devices/Display.h"
#include "devices/KeyboardBacklight.h"
#include "devices/Power.h"
#include "devices/TdeckKeyboard.h"
#include "devices/TrackballDevice.h"
#include <Tactility/hal/Configuration.h>
#include <tactility/check.h>
#include <tactility/device.h>
bool initBoot();
using namespace tt::hal;
static std::vector<std::shared_ptr<tt::hal::Device>> createDevices() {
auto* i2c_internal = device_find_by_name("i2c0");
check(i2c_internal);
return {
createPower(),
createDisplay(),
std::make_shared<TdeckKeyboard>(i2c_internal),
std::make_shared<KeyboardBacklightDevice>(),
std::make_shared<TrackballDevice>(),
};
}
extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};
-7
View File
@@ -1,7 +0,0 @@
extern "C" {
extern void register_device_drivers() {
/* NO-OP */
}
}
-94
View File
@@ -1,94 +0,0 @@
#include "PwmBacklight.h"
#include "devices/KeyboardBacklight.h"
#include "devices/TrackballDevice.h"
#include <Tactility/SystemEvents.h>
#include <Tactility/LogMessages.h>
#include <Tactility/hal/gps/GpsConfiguration.h>
#include <Tactility/kernel/Kernel.h>
#include <Tactility/service/gps/GpsService.h>
#include <tactility/log.h>
constexpr auto* TAG = "T-Deck";
constexpr auto TDECK_POWERON_GPIO = GPIO_NUM_10;
static bool powerOn() {
gpio_config_t device_power_signal_config = {
.pin_bit_mask = BIT64(TDECK_POWERON_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
if (gpio_config(&device_power_signal_config) != ESP_OK) {
return false;
}
if (gpio_set_level(TDECK_POWERON_GPIO, 1) != ESP_OK) {
return false;
}
// Avoids crash when no SD card is inserted. It's unknown why, but likely is related to power draw.
tt::kernel::delayMillis(100);
return true;
}
bool initBoot() {
LOG_I(TAG, LOG_MESSAGE_POWER_ON_START);
if (!powerOn()) {
LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED);
return false;
}
/* 32 Khz and higher gives an issue where the screen starts dimming again above 80% brightness
* when moving the brightness slider rapidly from a lower setting to 100%.
* This is not a slider bug (data was debug-traced) */
if (!driver::pwmbacklight::init(GPIO_NUM_42, 30000)) {
LOG_E(TAG, "Backlight init failed");
return false;
}
tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) {
auto gps_service = tt::service::gps::findGpsService();
if (gps_service != nullptr) {
std::vector<tt::hal::gps::GpsConfiguration> gps_configurations;
gps_service->getGpsConfigurations(gps_configurations);
if (gps_configurations.empty()) {
if (gps_service->addGpsConfiguration(tt::hal::gps::GpsConfiguration {.uartName = "uart0", .baudRate = 38400, .model = tt::hal::gps::GpsModel::UBLOX10})) {
LOG_I(TAG, "Configured internal GPS");
} else {
LOG_E(TAG, "Failed to configure internal GPS");
}
}
}
});
tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) {
auto kbBacklight = tt::hal::findDevice("Keyboard Backlight");
if (kbBacklight != nullptr) {
LOG_I(TAG, "%s starting", kbBacklight->getName().c_str());
auto kbDevice = std::static_pointer_cast<KeyboardBacklightDevice>(kbBacklight);
if (kbDevice->start()) {
LOG_I(TAG, "%s started", kbBacklight->getName().c_str());
} else {
LOG_E(TAG, "%s start failed", kbBacklight->getName().c_str());
}
}
auto trackball = tt::hal::findDevice("Trackball");
if (trackball != nullptr) {
LOG_I(TAG, "%s starting", trackball->getName().c_str());
auto tbDevice = std::static_pointer_cast<TrackballDevice>(trackball);
if (tbDevice->start()) {
LOG_I(TAG, "%s started", trackball->getName().c_str());
} else {
LOG_E(TAG, "%s start failed", trackball->getName().c_str());
}
}
});
return true;
}
@@ -1,111 +0,0 @@
#include "KeyboardBacklight.h"
#include <cstring>
#include <esp_log.h>
#include <tactility/log.h>
constexpr auto* TAG = "KeyboardBacklight";
namespace keyboardbacklight {
static const uint8_t CMD_BRIGHTNESS = 0x01;
static const uint8_t CMD_DEFAULT_BRIGHTNESS = 0x02;
static i2c_port_t g_i2cPort = I2C_NUM_MAX;
static uint8_t g_slaveAddress = 0x55;
static uint8_t g_currentBrightness = 127;
// TODO: Umm...something. Calls xxxBrightness, ignores return values.
bool init(i2c_port_t i2cPort, uint8_t slaveAddress) {
g_i2cPort = i2cPort;
g_slaveAddress = slaveAddress;
LOG_I(TAG, "Initialized on I2C port %d, address 0x%02X", static_cast<int>(g_i2cPort), (unsigned)g_slaveAddress);
// Set a reasonable default brightness
if (!setDefaultBrightness(127)) {
LOG_E(TAG, "Failed to set default brightness");
return false;
}
if (!setBrightness(127)) {
LOG_E(TAG, "Failed to set brightness");
return false;
}
return true;
}
bool setBrightness(uint8_t brightness) {
if (g_i2cPort >= I2C_NUM_MAX) {
LOG_E(TAG, "Not initialized");
return false;
}
// Skip if brightness is already at target value (avoid I2C spam on every keypress)
if (brightness == g_currentBrightness) {
return true;
}
LOG_I(TAG, "Setting brightness to %d on I2C port %d, address 0x%02X", brightness, static_cast<int>(g_i2cPort), (unsigned)g_slaveAddress);
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (g_slaveAddress << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, CMD_BRIGHTNESS, true);
i2c_master_write_byte(cmd, brightness, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(g_i2cPort, cmd, pdMS_TO_TICKS(100));
i2c_cmd_link_delete(cmd);
if (ret == ESP_OK) {
g_currentBrightness = brightness;
LOG_I(TAG, "Successfully set brightness to %d", brightness);
return true;
} else {
LOG_E(TAG, "Failed to set brightness: %s (0x%02X)", esp_err_to_name(ret), (unsigned)ret);
return false;
}
}
bool setDefaultBrightness(uint8_t brightness) {
if (g_i2cPort >= I2C_NUM_MAX) {
LOG_E(TAG, "Not initialized");
return false;
}
// Clamp to valid range for default brightness
if (brightness < 30) {
brightness = 30;
}
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (g_slaveAddress << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, CMD_DEFAULT_BRIGHTNESS, true);
i2c_master_write_byte(cmd, brightness, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(g_i2cPort, cmd, pdMS_TO_TICKS(100));
i2c_cmd_link_delete(cmd);
if (ret == ESP_OK) {
LOG_D(TAG, "Set default brightness to %d", brightness);
return true;
} else {
LOG_E(TAG, "Failed to set default brightness: %s", esp_err_to_name(ret));
return false;
}
}
uint8_t getBrightness() {
if (g_i2cPort >= I2C_NUM_MAX) {
LOG_E(TAG, "Not initialized");
return 0;
}
return g_currentBrightness;
}
}
@@ -1,36 +0,0 @@
#pragma once
#include <driver/i2c.h>
#include <cstdint>
namespace keyboardbacklight {
/**
* @brief Initialize keyboard backlight control
* @param i2cPort I2C port number (I2C_NUM_0 or I2C_NUM_1)
* @param slaveAddress I2C slave address (default 0x55 for T-Deck keyboard)
* @return true if initialization succeeded
*/
bool init(i2c_port_t i2cPort, uint8_t slaveAddress = 0x55);
/**
* @brief Set keyboard backlight brightness
* @param brightness Brightness level (0-255, 0=off, 255=max)
* @return true if command succeeded
*/
bool setBrightness(uint8_t brightness);
/**
* @brief Set default keyboard backlight brightness for ALT+B toggle
* @param brightness Default brightness level (30-255)
* @return true if command succeeded
*/
bool setDefaultBrightness(uint8_t brightness);
/**
* @brief Get current keyboard backlight brightness
* @return Current brightness level (0-255)
*/
uint8_t getBrightness();
}
@@ -1,398 +0,0 @@
#include "Trackball.h"
#include <Tactility/Assets.h>
#include <atomic>
#include <tactility/log.h>
constexpr auto* TAG = "Trackball";
namespace trackball {
static TrackballConfig g_config;
static lv_indev_t* g_indev = nullptr;
static std::atomic<bool> g_initialized{false};
static std::atomic<bool> g_enabled{true};
static std::atomic<Mode> g_mode{Mode::Encoder};
// Interrupt-driven position tracking (atomic for ISR safety)
static std::atomic<int32_t> g_cursorX{160};
static std::atomic<int32_t> g_cursorY{120};
static std::atomic<bool> g_buttonPressed{false};
// Encoder mode: accumulated diff since last read
static std::atomic<int32_t> g_encoderDiff{0};
// Sensitivity cached for ISR access (atomic for thread safety)
static std::atomic<int32_t> g_encoderSensitivity{1}; // Steps per tick for encoder
static std::atomic<int32_t> g_pointerSensitivity{10}; // Pixels per tick for pointer
// Cursor object for pointer mode
static lv_obj_t* g_cursor = nullptr;
// Screen dimensions (T-Deck: 320x240)
static constexpr int32_t SCREEN_WIDTH = 320;
static constexpr int32_t SCREEN_HEIGHT = 240;
static constexpr int32_t CURSOR_SIZE = 16;
// ISR handler for trackball directions
static void IRAM_ATTR trackball_isr_handler(void* arg) {
// Skip accumulating movement when disabled
if (!g_enabled.load(std::memory_order_relaxed)) {
return;
}
gpio_num_t pin = static_cast<gpio_num_t>(reinterpret_cast<intptr_t>(arg));
if (g_mode.load(std::memory_order_relaxed) == Mode::Pointer) {
// Pointer mode: update absolute position using atomic fetch_add/sub
// Clamping is done in read_cb to avoid race conditions
int32_t step = g_pointerSensitivity.load(std::memory_order_relaxed);
if (pin == g_config.pinRight) {
g_cursorX.fetch_add(step, std::memory_order_relaxed);
} else if (pin == g_config.pinLeft) {
g_cursorX.fetch_sub(step, std::memory_order_relaxed);
} else if (pin == g_config.pinUp) {
g_cursorY.fetch_sub(step, std::memory_order_relaxed);
} else if (pin == g_config.pinDown) {
g_cursorY.fetch_add(step, std::memory_order_relaxed);
}
} else {
// Encoder mode: accumulate diff
int32_t step = g_encoderSensitivity.load(std::memory_order_relaxed);
if (pin == g_config.pinRight || pin == g_config.pinDown) {
g_encoderDiff.fetch_add(step, std::memory_order_relaxed);
} else if (pin == g_config.pinLeft || pin == g_config.pinUp) {
g_encoderDiff.fetch_sub(step, std::memory_order_relaxed);
}
}
}
// ISR handler for button (any edge)
static void IRAM_ATTR button_isr_handler(void* arg) {
// Read current button state (active low)
bool pressed = gpio_get_level(g_config.pinClick) == 0;
g_buttonPressed.store(pressed, std::memory_order_relaxed);
}
// Helper to clamp value to range
static inline int32_t clamp(int32_t val, int32_t minVal, int32_t maxVal) {
if (val < minVal) return minVal;
if (val > maxVal) return maxVal;
return val;
}
static void read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
Mode currentMode = g_mode.load(std::memory_order_relaxed);
if (!g_initialized.load(std::memory_order_relaxed) || !g_enabled.load(std::memory_order_relaxed)) {
data->state = LV_INDEV_STATE_RELEASED;
if (currentMode == Mode::Encoder) {
data->enc_diff = 0;
} else {
// Clamp cursor position to screen bounds
int32_t x = clamp(g_cursorX.load(std::memory_order_relaxed), 0, SCREEN_WIDTH - CURSOR_SIZE - 1);
int32_t y = clamp(g_cursorY.load(std::memory_order_relaxed), 0, SCREEN_HEIGHT - CURSOR_SIZE - 1);
g_cursorX.store(x, std::memory_order_relaxed);
g_cursorY.store(y, std::memory_order_relaxed);
data->point.x = static_cast<int16_t>(x);
data->point.y = static_cast<int16_t>(y);
}
return;
}
if (currentMode == Mode::Encoder) {
// Read and reset accumulated encoder diff
int32_t diff = g_encoderDiff.exchange(0);
data->enc_diff = static_cast<int16_t>(clamp(diff, INT16_MIN, INT16_MAX));
if (diff != 0) {
lv_display_trigger_activity(nullptr);
}
} else {
// Pointer mode: read and clamp cursor position
int32_t x = clamp(g_cursorX.load(std::memory_order_relaxed), 0, SCREEN_WIDTH - CURSOR_SIZE - 1);
int32_t y = clamp(g_cursorY.load(std::memory_order_relaxed), 0, SCREEN_HEIGHT - CURSOR_SIZE - 1);
// Store clamped values back to prevent unbounded growth
g_cursorX.store(x, std::memory_order_relaxed);
g_cursorY.store(y, std::memory_order_relaxed);
data->point.x = static_cast<int16_t>(x);
data->point.y = static_cast<int16_t>(y);
}
// Button state (same for both modes)
bool pressed = g_buttonPressed.load(std::memory_order_relaxed);
data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
if (pressed) {
lv_display_trigger_activity(nullptr);
}
}
lv_indev_t* init(const TrackballConfig& config) {
if (g_initialized.load(std::memory_order_relaxed)) {
LOG_W(TAG, "Already initialized");
return g_indev;
}
g_config = config;
// Set default sensitivities if not specified
if (g_config.encoderSensitivity == 0) {
g_config.encoderSensitivity = 1;
}
if (g_config.pointerSensitivity == 0) {
g_config.pointerSensitivity = 10;
}
g_encoderSensitivity.store(g_config.encoderSensitivity, std::memory_order_relaxed);
g_pointerSensitivity.store(g_config.pointerSensitivity, std::memory_order_relaxed);
// Initialize cursor position to center
g_cursorX.store(SCREEN_WIDTH / 2, std::memory_order_relaxed);
g_cursorY.store(SCREEN_HEIGHT / 2, std::memory_order_relaxed);
g_encoderDiff.store(0, std::memory_order_relaxed);
g_buttonPressed.store(false, std::memory_order_relaxed);
// Configure direction pins as interrupt inputs (falling edge)
const gpio_num_t dirPins[4] = {
config.pinRight,
config.pinUp,
config.pinLeft,
config.pinDown
};
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_NEGEDGE; // Falling edge (active low)
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
// Install GPIO ISR service (if not already installed)
static bool isr_service_installed = false;
if (!isr_service_installed) {
esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
if (err == ESP_OK || err == ESP_ERR_INVALID_STATE) {
// ESP_ERR_INVALID_STATE means already installed, which is fine
isr_service_installed = true;
} else {
LOG_E(TAG, "Failed to install GPIO ISR service: %s", esp_err_to_name(err));
return nullptr;
}
}
// Track added handlers for cleanup on failure
int handlersAdded = 0;
// Configure and attach ISR for direction pins
for (int i = 0; i < 4; i++) {
io_conf.pin_bit_mask = (1ULL << dirPins[i]);
esp_err_t err = gpio_config(&io_conf);
if (err != ESP_OK) {
LOG_E(TAG, "Failed to configure GPIO %d: %s", static_cast<int>(dirPins[i]), esp_err_to_name(err));
// Cleanup previously added handlers
for (int j = 0; j < handlersAdded; j++) {
gpio_isr_handler_remove(dirPins[j]);
}
return nullptr;
}
err = gpio_isr_handler_add(dirPins[i], trackball_isr_handler, reinterpret_cast<void*>(static_cast<intptr_t>(dirPins[i])));
if (err != ESP_OK) {
LOG_E(TAG, "Failed to add ISR for GPIO %d: %s", static_cast<int>(dirPins[i]), esp_err_to_name(err));
// Cleanup previously added handlers
for (int j = 0; j < handlersAdded; j++) {
gpio_isr_handler_remove(dirPins[j]);
}
return nullptr;
}
handlersAdded++;
}
// Configure button pin (any edge for press/release detection)
io_conf.intr_type = GPIO_INTR_ANYEDGE;
io_conf.pin_bit_mask = (1ULL << config.pinClick);
esp_err_t err = gpio_config(&io_conf);
if (err != ESP_OK) {
LOG_E(TAG, "Failed to configure button GPIO %d: %s", static_cast<int>(config.pinClick), esp_err_to_name(err));
// Cleanup direction handlers
for (int i = 0; i < 4; i++) {
gpio_isr_handler_remove(dirPins[i]);
}
return nullptr;
}
err = gpio_isr_handler_add(config.pinClick, button_isr_handler, nullptr);
if (err != ESP_OK) {
LOG_E(TAG, "Failed to add button ISR: %s", esp_err_to_name(err));
// Cleanup direction handlers
for (int i = 0; i < 4; i++) {
gpio_isr_handler_remove(dirPins[i]);
}
return nullptr;
}
// Read initial button state
g_buttonPressed.store(gpio_get_level(config.pinClick) == 0);
// Register as LVGL encoder input device for group navigation (default mode)
g_indev = lv_indev_create();
if (g_indev == nullptr) {
LOG_E(TAG, "Failed to register LVGL input device");
// Cleanup ISR handlers on failure
const gpio_num_t pins[5] = {
config.pinRight, config.pinUp, config.pinLeft,
config.pinDown, config.pinClick
};
for (int i = 0; i < 5; i++) {
gpio_intr_disable(pins[i]);
gpio_isr_handler_remove(pins[i]);
}
return nullptr;
}
lv_indev_set_type(g_indev, LV_INDEV_TYPE_ENCODER);
lv_indev_set_read_cb(g_indev, read_cb);
g_initialized.store(true, std::memory_order_relaxed);
LOG_I(TAG, "Initialized with interrupts (R:%d U:%d L:%d D:%d Click:%d)",
static_cast<int>(config.pinRight),
static_cast<int>(config.pinUp),
static_cast<int>(config.pinLeft),
static_cast<int>(config.pinDown),
static_cast<int>(config.pinClick));
return g_indev;
}
// Create cursor for pointer mode
static void createCursor() {
if (g_cursor != nullptr || g_indev == nullptr) return;
g_cursor = lv_image_create(lv_layer_sys());
if (g_cursor != nullptr) {
lv_obj_remove_flag(g_cursor, LV_OBJ_FLAG_CLICKABLE);
// Set cursor image
lv_image_set_src(g_cursor, TT_ASSETS_UI_CURSOR);
lv_indev_set_cursor(g_indev, g_cursor);
LOG_D(TAG, "Cursor created");
}
}
// Destroy cursor when switching back to encoder mode
static void destroyCursor() {
if (g_cursor == nullptr) return;
// Delete the cursor object - this automatically detaches it from the indev
lv_obj_delete(g_cursor);
g_cursor = nullptr;
LOG_D(TAG, "Cursor destroyed");
}
void deinit() {
if (!g_initialized.load(std::memory_order_relaxed)) return;
destroyCursor();
// Disable interrupts and remove ISR handlers
const gpio_num_t pins[5] = {
g_config.pinRight,
g_config.pinUp,
g_config.pinLeft,
g_config.pinDown,
g_config.pinClick
};
for (int i = 0; i < 5; i++) {
gpio_intr_disable(pins[i]);
gpio_isr_handler_remove(pins[i]);
}
if (g_indev) {
lv_indev_delete(g_indev);
g_indev = nullptr;
}
g_initialized.store(false, std::memory_order_relaxed);
g_mode.store(Mode::Encoder, std::memory_order_relaxed);
g_enabled.store(true, std::memory_order_relaxed);
LOG_I(TAG, "Deinitialized");
}
void setEncoderSensitivity(uint8_t sensitivity) {
if (sensitivity > 0) {
// Only update the atomic - ISR reads from atomic, not g_config
g_encoderSensitivity.store(sensitivity, std::memory_order_relaxed);
LOG_D(TAG, "Encoder sensitivity set to %d", sensitivity);
}
}
void setPointerSensitivity(uint8_t sensitivity) {
if (sensitivity > 0) {
// Only update the atomic - ISR reads from atomic, not g_config
g_pointerSensitivity.store(sensitivity, std::memory_order_relaxed);
LOG_D(TAG, "Pointer sensitivity set to %d", sensitivity);
}
}
void setEnabled(bool enabled) {
g_enabled.store(enabled, std::memory_order_relaxed);
if (!enabled) {
// Clear accumulated state to prevent jumps on re-enable
g_encoderDiff.store(0, std::memory_order_relaxed);
}
// Hide/show cursor based on enabled state when in pointer mode
// Note: Must be called from LVGL thread (main thread) for thread safety
lv_obj_t* cursor = g_cursor; // Local copy to avoid race with setMode
if (cursor != nullptr) {
if (enabled) {
lv_obj_clear_flag(cursor, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(cursor, LV_OBJ_FLAG_HIDDEN);
}
}
LOG_I(TAG, "%s", enabled ? "Enabled" : "Disabled");
}
void setMode(Mode mode) {
// Note: Must be called from LVGL thread (main thread) for thread safety
if (!g_initialized.load(std::memory_order_relaxed) || g_indev == nullptr) {
LOG_W(TAG, "Cannot set mode - not initialized");
return;
}
if (g_mode.load(std::memory_order_relaxed) == mode) {
return;
}
g_mode.store(mode, std::memory_order_relaxed);
if (mode == Mode::Pointer) {
// Switch to pointer mode
lv_indev_set_type(g_indev, LV_INDEV_TYPE_POINTER);
createCursor();
if (!g_enabled.load(std::memory_order_relaxed) && g_cursor != nullptr) {
lv_obj_add_flag(g_cursor, LV_OBJ_FLAG_HIDDEN);
}
// Reset cursor to center when switching modes
g_cursorX.store(SCREEN_WIDTH / 2, std::memory_order_relaxed);
g_cursorY.store(SCREEN_HEIGHT / 2, std::memory_order_relaxed);
LOG_I(TAG, "Switched to Pointer mode");
} else {
// Switch to encoder mode
destroyCursor();
lv_indev_set_type(g_indev, LV_INDEV_TYPE_ENCODER);
g_encoderDiff.store(0, std::memory_order_relaxed); // Reset encoder diff
LOG_I(TAG, "Switched to Encoder mode");
}
}
Mode getMode() {
return g_mode.load(std::memory_order_relaxed);
}
}
@@ -1,53 +0,0 @@
#include "Display.h"
#include <Gt911Touch.h>
#include <PwmBacklight.h>
#include <St7789Display.h>
#include <tactility/check.h>
#include <tactility/device.h>
static std::shared_ptr<tt::hal::touch::TouchDevice> createTouch() {
auto* i2c = device_find_by_name("i2c0");
check(i2c);
auto configuration = std::make_unique<Gt911Touch::Configuration>(
i2c,
240,
320,
true,
true,
false,
GPIO_NUM_NC,
GPIO_NUM_16
);
return std::make_shared<Gt911Touch>(std::move(configuration));
}
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
St7789Display::Configuration panel_configuration = {
.horizontalResolution = 320,
.verticalResolution = 240,
.gapX = 0,
.gapY = 0,
.swapXY = true,
.mirrorX = true,
.mirrorY = false,
.invertColor = true,
.bufferSize = LCD_BUFFER_SIZE,
.touch = createTouch(),
.backlightDutyFunction = driver::pwmbacklight::setBacklightDuty,
.resetPin = GPIO_NUM_NC,
.lvglSwapBytes = false,
.buffSpiram = false // Enabling leads to crashes when refreshing App Hub list
};
auto spi_configuration = std::make_shared<St7789Display::SpiConfiguration>(St7789Display::SpiConfiguration {
.spiHostDevice = LCD_SPI_HOST,
.csPin = LCD_PIN_CS,
.dcPin = LCD_PIN_DC,
.pixelClockFrequency = 62'500'000,
.transactionQueueDepth = 10
});
return std::make_shared<St7789Display>(panel_configuration, spi_configuration);
}
@@ -1,16 +0,0 @@
#pragma once
#include <Tactility/hal/display/DisplayDevice.h>
#include <driver/gpio.h>
#include <driver/spi_common.h>
constexpr auto LCD_SPI_HOST = SPI2_HOST;
constexpr auto LCD_PIN_CS = GPIO_NUM_12;
constexpr auto LCD_PIN_DC = GPIO_NUM_11; // RS
constexpr auto LCD_HORIZONTAL_RESOLUTION = 320;
constexpr auto LCD_VERTICAL_RESOLUTION = 240;
constexpr auto LCD_BUFFER_HEIGHT = (LCD_VERTICAL_RESOLUTION / 10);
constexpr auto LCD_BUFFER_SIZE = (LCD_HORIZONTAL_RESOLUTION * LCD_BUFFER_HEIGHT);
constexpr auto LCD_SPI_TRANSFER_SIZE_LIMIT = LCD_BUFFER_SIZE * LV_COLOR_DEPTH / 8;
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
@@ -1,51 +0,0 @@
#include "KeyboardBacklight.h"
#include <KeyboardBacklight/KeyboardBacklight.h> // Driver
#include <Tactility/settings/KeyboardSettings.h>
// TODO: Add Mutex and consider refactoring into a class
bool KeyboardBacklightDevice::start() {
if (initialized) {
return true;
}
// T-Deck uses I2C_NUM_0 for internal peripherals
initialized = keyboardbacklight::init(I2C_NUM_0);
if (!initialized) {
return false;
}
// Backlight doesn't seem to turn on until toggled on and off from keyboard settings...
// Or let the display and backlight sleep then wake it up.
// Then it works fine...until reboot, then you need to toggle again.
// The current keyboard firmware sets backlight duty to 0 on boot.
// https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/firmware/T-Keyboard_Keyboard_ESP32C3_250620.bin
// https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/Keyboard_ESP32C3/Keyboard_ESP32C3.ino#L25
// https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/Keyboard_ESP32C3/Keyboard_ESP32C3.ino#L217
auto kbSettings = tt::settings::keyboard::loadOrGetDefault();
keyboardbacklight::setBrightness(kbSettings.backlightEnabled ? kbSettings.backlightBrightness : 0);
return true;
}
bool KeyboardBacklightDevice::stop() {
if (initialized) {
// Turn off backlight on shutdown
keyboardbacklight::setBrightness(0);
initialized = false;
}
return true;
}
bool KeyboardBacklightDevice::setBrightness(uint8_t brightness) {
if (!initialized) {
return false;
}
return keyboardbacklight::setBrightness(brightness);
}
uint8_t KeyboardBacklightDevice::getBrightness() const {
if (!initialized) {
return 0;
}
return keyboardbacklight::getBrightness();
}
@@ -1,32 +0,0 @@
#pragma once
#include <tactility/hal/Device.h>
#include <Tactility/TactilityCore.h>
class KeyboardBacklightDevice final : public tt::hal::Device {
bool initialized = false;
public:
tt::hal::Device::Type getType() const override { return tt::hal::Device::Type::I2c; }
std::string getName() const override { return "Keyboard Backlight"; }
std::string getDescription() const override { return "T-Deck keyboard backlight control"; }
bool start();
bool stop();
bool isAttached() const { return initialized; }
/**
* Set keyboard backlight brightness
* @param brightness 0-255 (0=off, 255=max)
*/
bool setBrightness(uint8_t brightness);
/**
* Get current brightness
* @return 0-255
*/
uint8_t getBrightness() const;
};
@@ -1,10 +0,0 @@
#include <ChargeFromAdcVoltage.h>
#include <EstimatedPower.h>
std::shared_ptr<PowerDevice> createPower() {
ChargeFromAdcVoltage::Configuration configuration;
// 2.0 ratio, but +.11 added as display voltage sag compensation.
configuration.adcMultiplier = 2.11;
return std::make_shared<EstimatedPower>(configuration);
}
@@ -1,5 +0,0 @@
#pragma once
#include <Tactility/hal/power/PowerDevice.h>
std::shared_ptr<tt::hal::power::PowerDevice> createPower();
@@ -1,86 +0,0 @@
#include "TdeckKeyboard.h"
#include <KeyboardBacklight/KeyboardBacklight.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/settings/DisplaySettings.h>
#include <Tactility/settings/KeyboardSettings.h>
#include <lvgl.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
using tt::hal::findFirstDevice;
constexpr auto* TAG = "TdeckKeyboard";
constexpr uint8_t TDECK_KEYBOARD_SLAVE_ADDRESS = 0x55;
/**
* The callback simulates press and release events, because the T-Deck
* keyboard only publishes press events on I2C.
* LVGL currently works without those extra release events, but they
* are implemented for correctness and future compatibility.
*
* @param indev_drv
* @param data
*/
static void keyboard_read_callback(lv_indev_t* indev, lv_indev_data_t* data) {
static uint8_t last_buffer = 0x00;
uint8_t read_buffer = 0x00;
// Defaults
data->key = 0;
data->state = LV_INDEV_STATE_RELEASED;
auto* keyboard = static_cast<TdeckKeyboard*>(lv_indev_get_user_data(indev));
if (i2c_controller_read(keyboard->getI2cController(), TDECK_KEYBOARD_SLAVE_ADDRESS, &read_buffer, 1, 100 / portTICK_PERIOD_MS) == ERROR_NONE) {
if (read_buffer == 0 && read_buffer != last_buffer) {
LOG_D(TAG, "Released %d", last_buffer);
data->key = last_buffer;
data->state = LV_INDEV_STATE_RELEASED;
} else if (read_buffer != 0) {
LOG_D(TAG, "Pressed %d", read_buffer);
data->key = read_buffer;
data->state = LV_INDEV_STATE_PRESSED;
// TODO: Avoid performance hit by calling loadOrGetDefault() on each key press
// Ensure LVGL activity is triggered so idle services can wake the display
lv_display_trigger_activity(nullptr);
// Actively wake display/backlights immediately on key press (independent of idle tick)
// Restore display backlight if off (we assume duty 0 means dimmed)
auto display = findFirstDevice<tt::hal::display::DisplayDevice>(tt::hal::Device::Type::Display);
if (display && display->supportsBacklightDuty()) {
// Load display settings for target duty
auto dsettings = tt::settings::display::loadOrGetDefault();
// Always set duty, harmless if already on
display->setBacklightDuty(dsettings.backlightDuty);
}
// Restore keyboard backlight if enabled in settings
auto ksettings = tt::settings::keyboard::loadOrGetDefault();
if (ksettings.backlightEnabled) {
keyboardbacklight::setBrightness(ksettings.backlightBrightness);
}
}
}
last_buffer = read_buffer;
}
bool TdeckKeyboard::startLvgl(lv_display_t* display) {
deviceHandle = lv_indev_create();
lv_indev_set_type(deviceHandle, LV_INDEV_TYPE_KEYPAD);
lv_indev_set_read_cb(deviceHandle, &keyboard_read_callback);
lv_indev_set_display(deviceHandle, display);
lv_indev_set_user_data(deviceHandle, this);
return true;
}
bool TdeckKeyboard::stopLvgl() {
lv_indev_delete(deviceHandle);
deviceHandle = nullptr;
return true;
}
bool TdeckKeyboard::isAttached() const {
return i2c_controller_has_device_at_address(i2cController, TDECK_KEYBOARD_SLAVE_ADDRESS, 100) == ERROR_NONE;
}
@@ -1,25 +0,0 @@
#pragma once
#include <Tactility/hal/keyboard/KeyboardDevice.h>
struct Device;
class TdeckKeyboard final : public tt::hal::keyboard::KeyboardDevice {
::Device* i2cController;
lv_indev_t* deviceHandle = nullptr;
public:
explicit TdeckKeyboard(::Device* i2cController) : i2cController(i2cController) {}
std::string getName() const override { return "T-Deck Keyboard"; }
std::string getDescription() const override { return "I2C keyboard"; }
::Device* getI2cController() const { return i2cController; }
bool startLvgl(lv_display_t* display) override;
bool stopLvgl() override;
bool isAttached() const override;
lv_indev_t* getLvglIndev() override { return deviceHandle; }
};
@@ -1,57 +0,0 @@
#include "TrackballDevice.h"
#include <Trackball/Trackball.h> // Driver
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/settings/TrackballSettings.h>
#include <tactility/log.h>
constexpr auto* TAG = "TrackballDevice";
bool TrackballDevice::start() {
if (initialized) {
return true;
}
// T-Deck trackball GPIO configuration from LilyGo reference
trackball::TrackballConfig config = {
.pinRight = GPIO_NUM_2, // BOARD_TBOX_G02
.pinUp = GPIO_NUM_3, // BOARD_TBOX_G01
.pinLeft = GPIO_NUM_1, // BOARD_TBOX_G04
.pinDown = GPIO_NUM_15, // BOARD_TBOX_G03
.pinClick = GPIO_NUM_0, // BOARD_BOOT_PIN
.encoderSensitivity = 1, // 1 step per tick for menu navigation
.pointerSensitivity = 10 // 10 pixels per tick for cursor movement
};
indev = trackball::init(config);
if (indev == nullptr) {
return false;
}
initialized = true;
// Apply persisted trackball settings (requires LVGL lock for cursor manipulation)
auto tbSettings = tt::settings::trackball::loadOrGetDefault();
if (tt::lvgl::lock(100)) {
trackball::setMode(tbSettings.trackballMode == tt::settings::trackball::TrackballMode::Pointer
? trackball::Mode::Pointer
: trackball::Mode::Encoder);
trackball::setEncoderSensitivity(tbSettings.encoderSensitivity);
trackball::setPointerSensitivity(tbSettings.pointerSensitivity);
trackball::setEnabled(tbSettings.trackballEnabled);
tt::lvgl::unlock();
} else {
LOG_W(TAG, "Failed to acquire LVGL lock for trackball settings");
}
return true;
}
bool TrackballDevice::stop() {
if (initialized) {
// LVGL will handle indev cleanup
trackball::deinit();
indev = nullptr;
initialized = false;
}
return true;
}
@@ -1,21 +0,0 @@
#pragma once
#include <tactility/hal/Device.h>
#include <lvgl.h>
class TrackballDevice : public tt::hal::Device {
public:
tt::hal::Device::Type getType() const override { return tt::hal::Device::Type::Other; }
std::string getName() const override { return "Trackball"; }
std::string getDescription() const override { return "5-way GPIO trackball navigation"; }
bool start();
bool stop();
bool isAttached() const { return initialized; }
lv_indev_t* getLvglIndev() const { return indev; }
private:
lv_indev_t* indev = nullptr;
bool initialized = false;
};
-23
View File
@@ -1,23 +0,0 @@
#include <tactility/module.h>
extern "C" {
static error_t start() {
// Empty for now
return ERROR_NONE;
}
static error_t stop() {
// Empty for now
return ERROR_NONE;
}
struct Module lilygo_tdeck_module = {
.name = "lilygo-tdeck",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
}
+1 -1
View File
@@ -1,5 +1,5 @@
general.vendor=LilyGO general.vendor=LilyGO
general.name=T-Deck,T-Deck Plus general.name=T-Deck
apps.launcherAppId=Launcher apps.launcherAppId=Launcher
+3
View File
@@ -1,3 +1,6 @@
dependencies: dependencies:
- Platforms/platform-esp32 - Platforms/platform-esp32
- Drivers/st7789-module
- Drivers/gt911-module
- Drivers/lilygo-module
dts: lilygo,tdeck.dts dts: lilygo,tdeck.dts
+87 -12
View File
@@ -1,22 +1,45 @@
/dts-v1/; /dts-v1/;
#include <tactility/bindings/battery_sense.h>
#include <tactility/bindings/root.h> #include <tactility/bindings/root.h>
#include <tactility/bindings/esp32_adc_oneshot.h>
#include <tactility/bindings/esp32_ble.h> #include <tactility/bindings/esp32_ble.h>
#include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/esp32_gpio.h> #include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_grove.h> #include <tactility/bindings/esp32_grove.h>
#include <tactility/bindings/esp32_i2c.h> #include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_i2s.h> #include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_ledc_backlight.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/esp32_uart.h>
// Reference: https://wiki.lilygo.cc/get_started/en/Wearable/T-Deck-Plus/T-Deck-Plus.html#Pin-Overview #include <bindings/gt911.h>
#include <bindings/st7789.h>
#include <lilygo/bindings/tdeck_keyboard.h>
#include <lilygo/bindings/tdeck_keyboard_backlight.h>
#include <lilygo/bindings/tdeck_trackball.h>
// Reference: https://wiki.lilygo.cc/products/t-deck-series/t-deck/
/ { / {
compatible = "root"; compatible = "root";
model = "LilyGO T-Deck"; model = "LilyGO T-Deck";
adc0 {
compatible = "espressif,esp32-adc-oneshot";
unit-id = <ADC_UNIT_1>;
clk-src = <ADC_RTC_CLK_SRC_DEFAULT>;
channels = <ADC_CHANNEL_3 ADC_ATTEN_DB_12 ADC_BITWIDTH_DEFAULT>;
};
battery-sense {
compatible = "battery-sense";
io-channel = <&adc0 0>;
reference-voltage-mv = <3300>;
multiplier = <2110>;
};
wifi0 { wifi0 {
compatible = "espressif,esp32-wifi-pinned"; compatible = "espressif,esp32-wifi-pinned";
status = "disabled"; status = "disabled";
@@ -32,12 +55,41 @@
gpio-count = <49>; gpio-count = <49>;
}; };
trackball {
compatible = "lilygo,tdeck-trackball";
pin-right = <&gpio0 2 GPIO_FLAG_NONE>;
pin-up = <&gpio0 3 GPIO_FLAG_NONE>;
pin-left = <&gpio0 1 GPIO_FLAG_NONE>;
pin-down = <&gpio0 15 GPIO_FLAG_NONE>;
pin-click = <&gpio0 0 GPIO_FLAG_NONE>;
};
i2c_internal: i2c0 { i2c_internal: i2c0 {
compatible = "espressif,esp32-i2c"; compatible = "espressif,esp32-i2c";
port = <I2C_NUM_0>; port = <I2C_NUM_0>;
clock-frequency = <100000>; clock-frequency = <100000>;
pin-sda = <&gpio0 18 GPIO_FLAG_NONE>; pin-sda = <&gpio0 18 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 8 GPIO_FLAG_NONE>; pin-scl = <&gpio0 8 GPIO_FLAG_NONE>;
touch {
compatible = "goodix,gt911";
reg = <0x5D>;
x-max = <240>;
y-max = <320>;
swap-xy;
mirror-x;
pin-interrupt = <&gpio0 16 GPIO_FLAG_NONE>;
};
keyboard {
compatible = "lilygo,tdeck-keyboard";
reg = <0x55>;
};
keyboard_backlight {
compatible = "lilygo,tdeck-keyboard-backlight";
reg = <0x55>;
};
}; };
i2s0 { i2s0 {
@@ -48,6 +100,17 @@
pin-data-out = <&gpio0 6 GPIO_FLAG_NONE>; pin-data-out = <&gpio0 6 GPIO_FLAG_NONE>;
}; };
display_backlight {
compatible = "espressif,esp32-ledc-backlight";
// Off by default so dispaly power-on won't show the screen from before the last power loss.
// The display backlight is turned on during the boot process.
status = "disabled";
pin-backlight = <&gpio0 42 GPIO_FLAG_NONE>;
// 32 KHz and higher causes the screen to start dimming again above 80% brightness
// when moving the brightness slider rapidly from a lower setting to 100% (debug-traced, not a slider bug).
frequency-hz = <30000>;
};
spi0 { spi0 {
compatible = "espressif,esp32-spi"; compatible = "espressif,esp32-spi";
host = <SPI2_HOST>; host = <SPI2_HOST>;
@@ -59,20 +122,32 @@
pin-sclk = <&gpio0 40 GPIO_FLAG_NONE>; pin-sclk = <&gpio0 40 GPIO_FLAG_NONE>;
display@0 { display@0 {
compatible = "display-placeholder"; compatible = "sitronix,st7789";
horizontal-resolution = <320>;
vertical-resolution = <240>;
swap-xy;
mirror-x;
invert-color;
pixel-clock-hz = <62500000>;
pin-dc = <&gpio0 11 GPIO_FLAG_NONE>;
backlight = <&display_backlight>;
}; };
// Must be started after display
sdcard@1 { sdcard@1 {
compatible = "espressif,esp32-sdspi"; compatible = "espressif,esp32-sdspi";
status = "disabled"; // Must be started after display status = "disabled";
frequency-khz = <20000>; frequency-khz = <20000>;
}; };
}; };
uart0 { grove0 {
compatible = "espressif,esp32-uart"; compatible = "espressif,esp32-grove";
port = <UART_NUM_1>; defaultMode = <GROVE_MODE_I2C>;
pin-tx = <&gpio0 43 GPIO_FLAG_NONE>; pinSdaTx = <&gpio0 43 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 44 GPIO_FLAG_NONE>; pinSclRx = <&gpio0 44 GPIO_FLAG_NONE>;
uartPort = <UART_NUM_1>;
i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <400000>;
}; };
}; };
+71
View File
@@ -0,0 +1,71 @@
#include <tactility/module.h>
#include "lilygo/drivers/tdeck_power_on.h"
#include "tactility/lvgl_module.h"
#include <tactility/error.h>
#include <tactility/log.h>
#include <Tactility/SystemEvents.h>
#include <Tactility/LogMessages.h>
#include <Tactility/hal/Configuration.h>
#include <Tactility/kernel/Kernel.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/gps/GpsService.h>
#include <Tactility/settings/TrackballSettings.h>
#include <lilygo/drivers/trackball.h>
constexpr auto* TAG = "tdeck";
// Legacy placeholder (required until legacy HAL is cleaned up everywhere)
extern const tt::hal::Configuration hardwareConfiguration = {};
extern "C" {
void subscribe_events() {
// The kernel trackball device is already started by kernel_init(); this just registers it as an
// LVGL input device and applies persisted settings, both of which require LVGL to be up first.
tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) {
auto tbSettings = tt::settings::trackball::loadOrGetDefault();
lvgl_lock();
if (trackball::init() != nullptr) {
trackball::setMode(tbSettings.trackballMode == tt::settings::trackball::TrackballMode::Pointer
? trackball::Mode::Pointer
: trackball::Mode::Encoder);
trackball::setEncoderSensitivity(tbSettings.encoderSensitivity);
trackball::setPointerSensitivity(tbSettings.pointerSensitivity);
trackball::setEnabled(tbSettings.trackballEnabled);
}
lvgl_unlock();
});
}
static error_t start() {
LOG_I(TAG, LOG_MESSAGE_POWER_ON_START);
if (!tdeck_power_on()) {
LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED);
return ERROR_RESOURCE;
}
// Avoids crash when no SD card is inserted. It's unknown why, but likely is related to power draw.
tt::kernel::delayMillis(100);
subscribe_events();
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
Module lilygo_tdeck_module = {
.name = "lilygo-tdeck",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
}
+2 -2
View File
@@ -8,7 +8,7 @@
#include <tactility/bindings/esp32_sdmmc.h> #include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/display_placeholder.h>
#include <tactility/bindings/touch_placeholder.h> #include <tactility/bindings/pointer_placeholder.h>
/ { / {
compatible = "root"; compatible = "root";
@@ -43,7 +43,7 @@
}; };
touch@1 { touch@1 {
compatible = "touch-placeholder"; compatible = "pointer-placeholder";
}; };
}; };
+2 -2
View File
@@ -8,7 +8,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/display_placeholder.h> #include <tactility/bindings/display_placeholder.h>
#include <tactility/bindings/touch_placeholder.h> #include <tactility/bindings/pointer_placeholder.h>
/ { / {
compatible = "root"; compatible = "root";
@@ -53,7 +53,7 @@
}; };
touch@1 { touch@1 {
compatible = "touch-placeholder"; compatible = "pointer-placeholder";
}; };
sdcard@2 { sdcard@2 {
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(gt911-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel platform-esp32 esp_lcd_touch_gt911 esp_lcd driver
)
@@ -0,0 +1,45 @@
description: Goodix GT911 capacitive touch controller
include: ["i2c-device.yaml"]
compatible: "goodix,gt911"
bus: i2c
properties:
x-max:
type: int
required: true
description: Maximum X coordinate reported by the controller (typically the panel's horizontal resolution)
y-max:
type: int
required: true
description: Maximum Y coordinate reported by the controller (typically the panel's vertical resolution)
swap-xy:
type: boolean
default: false
description: Swap the X and Y axes
mirror-x:
type: boolean
default: false
description: Mirror the X axis
mirror-y:
type: boolean
default: false
description: Mirror the Y axis
pin-reset:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Reset GPIO pin
pin-interrupt:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Interrupt GPIO pin
reset-active-high:
type: boolean
default: false
description: Whether the reset pin is active high
interrupt-active-high:
type: boolean
default: false
description: Whether the interrupt pin is active high
+3
View File
@@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/gt911.h>
DEFINE_DEVICETREE(gt911, struct Gt911Config)
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include <tactility/drivers/gpio.h>
struct Gt911Config {
// Devicetree address hint. The driver auto-probes both known GT911 addresses
// (0x5D primary, 0x14 backup) regardless, since the effective address depends on
// the controller's INT pin strapping level at power-up, not a fixed board wiring choice.
uint8_t address;
uint16_t x_max;
uint16_t y_max;
bool swap_xy;
bool mirror_x;
bool mirror_y;
struct GpioPinSpec pin_reset;
struct GpioPinSpec pin_interrupt;
bool reset_active_high;
bool interrupt_active_high;
};
#ifdef __cplusplus
}
#endif
@@ -1,13 +1,13 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#pragma once #pragma once
#include <tactility/module.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
#include <tactility/device.h> extern struct Module gt911_module;
extern const struct DeviceType TOUCH_TYPE;
#ifdef __cplusplus #ifdef __cplusplus
} }
+214
View File
@@ -0,0 +1,214 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/gt911.h>
#include <gt911_module.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_i2c.h>
#include <tactility/drivers/esp32_i2c_master.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/pointer.h>
#include <tactility/log.h>
#include <esp_err.h>
#include <esp_lcd_io_i2c.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_touch.h>
#include <esp_lcd_touch_gt911.h>
#include <cstdlib>
#define TAG "GT911"
#define GET_CONFIG(device) (static_cast<const Gt911Config*>((device)->config))
struct Gt911Internal {
esp_lcd_panel_io_handle_t io_handle;
esp_lcd_touch_handle_t touch_handle;
};
static inline gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) {
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
}
// region Driver lifecycle
// GT911's I2C address depends on the controller's INT pin level at power-up (board-strapped, not
// devicetree-fixed), so both known addresses are probed regardless of the devicetree "reg" hint.
static esp_err_t create_io_handle(Device* parent, esp_lcd_panel_io_handle_t* out_handle) {
esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG();
if (i2c_controller_has_device_at_address(parent, ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS, pdMS_TO_TICKS(10)) == ERROR_NONE) {
io_config.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS;
} else if (i2c_controller_has_device_at_address(parent, ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP, pdMS_TO_TICKS(10)) == ERROR_NONE) {
io_config.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP;
} else {
LOG_E(TAG, "No GT911 found on I2C bus");
return ESP_ERR_NOT_FOUND;
}
auto* parent_driver = device_get_driver(parent);
if (driver_is_compatible(parent_driver, "espressif,esp32-i2c")) {
auto port = static_cast<const Esp32I2cConfig*>(parent->config)->port;
return esp_lcd_new_panel_io_i2c_v1(port, &io_config, out_handle);
}
if (driver_is_compatible(parent_driver, "espressif,esp32-i2c-master")) {
auto bus = esp32_i2c_master_get_bus_handle(parent);
io_config.scl_speed_hz = esp32_i2c_master_get_clock_frequency(parent);
return esp_lcd_new_panel_io_i2c_v2(bus, &io_config, out_handle);
}
LOG_E(TAG, "Unsupported I2C driver");
return ESP_ERR_NOT_SUPPORTED;
}
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<Gt911Internal*>(malloc(sizeof(Gt911Internal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
esp_err_t ret = create_io_handle(parent, &internal->io_handle);
if (ret != ESP_OK) {
free(internal);
return ERROR_RESOURCE;
}
esp_lcd_touch_config_t touch_config = {
.x_max = config->x_max,
.y_max = config->y_max,
.rst_gpio_num = pin_or_nc(config->pin_reset),
.int_gpio_num = pin_or_nc(config->pin_interrupt),
.levels = {
.reset = config->reset_active_high ? 1u : 0u,
.interrupt = config->interrupt_active_high ? 1u : 0u,
},
.flags = {
.swap_xy = config->swap_xy ? 1u : 0u,
.mirror_x = config->mirror_x ? 1u : 0u,
.mirror_y = config->mirror_y ? 1u : 0u,
},
.process_coordinates = nullptr,
.interrupt_callback = nullptr,
.user_data = nullptr,
.driver_data = nullptr,
};
ret = esp_lcd_touch_new_i2c_gt911(internal->io_handle, &touch_config, &internal->touch_handle);
if (ret != ESP_OK) {
LOG_E(TAG, "Failed to create touch handle: %s", esp_err_to_name(ret));
esp_lcd_panel_io_del(internal->io_handle);
free(internal);
return ERROR_RESOURCE;
}
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<Gt911Internal*>(device_get_driver_data(device));
bool ok = true;
// esp_lcd_touch_del() only releases the touch-side resources; the panel IO handle is owned
// separately and needs its own deletion.
if (esp_lcd_touch_del(internal->touch_handle) != ESP_OK) {
LOG_E(TAG, "Failed to delete touch handle");
ok = false;
}
if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
LOG_E(TAG, "Failed to delete panel IO handle");
ok = false;
}
free(internal);
return ok ? ERROR_NONE : ERROR_RESOURCE;
}
// endregion
// region PointerApi
static error_t gt911_enter_sleep(Device* device) {
auto* internal = static_cast<Gt911Internal*>(device_get_driver_data(device));
return esp_lcd_touch_enter_sleep(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t gt911_exit_sleep(Device* device) {
auto* internal = static_cast<Gt911Internal*>(device_get_driver_data(device));
return esp_lcd_touch_exit_sleep(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t gt911_read_data(Device* device, TickType_t timeout) {
(void)timeout; // esp_lcd_touch_read_data() has no timeout parameter
auto* internal = static_cast<Gt911Internal*>(device_get_driver_data(device));
return esp_lcd_touch_read_data(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static bool gt911_get_touched_points(Device* device, uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* point_count, uint8_t max_point_count) {
auto* internal = static_cast<Gt911Internal*>(device_get_driver_data(device));
return esp_lcd_touch_get_coordinates(internal->touch_handle, x, y, strength, point_count, max_point_count);
}
static error_t gt911_set_swap_xy(Device* device, bool swap) {
auto* internal = static_cast<Gt911Internal*>(device_get_driver_data(device));
return esp_lcd_touch_set_swap_xy(internal->touch_handle, swap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t gt911_get_swap_xy(Device* device, bool* swap) {
auto* internal = static_cast<Gt911Internal*>(device_get_driver_data(device));
return esp_lcd_touch_get_swap_xy(internal->touch_handle, swap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t gt911_set_mirror_x(Device* device, bool mirror) {
auto* internal = static_cast<Gt911Internal*>(device_get_driver_data(device));
return esp_lcd_touch_set_mirror_x(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t gt911_get_mirror_x(Device* device, bool* mirror) {
auto* internal = static_cast<Gt911Internal*>(device_get_driver_data(device));
return esp_lcd_touch_get_mirror_x(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t gt911_set_mirror_y(Device* device, bool mirror) {
auto* internal = static_cast<Gt911Internal*>(device_get_driver_data(device));
return esp_lcd_touch_set_mirror_y(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t gt911_get_mirror_y(Device* device, bool* mirror) {
auto* internal = static_cast<Gt911Internal*>(device_get_driver_data(device));
return esp_lcd_touch_get_mirror_y(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
// endregion
static const PointerApi gt911_pointer_api = {
.enter_sleep = gt911_enter_sleep,
.exit_sleep = gt911_exit_sleep,
.read_data = gt911_read_data,
.get_touched_points = gt911_get_touched_points,
.set_swap_xy = gt911_set_swap_xy,
.get_swap_xy = gt911_get_swap_xy,
.set_mirror_x = gt911_set_mirror_x,
.get_mirror_x = gt911_get_mirror_x,
.set_mirror_y = gt911_set_mirror_y,
.get_mirror_y = gt911_get_mirror_y,
};
Driver gt911_driver = {
.name = "gt911",
.compatible = (const char*[]) { "goodix,gt911", nullptr },
.start_device = start,
.stop_device = stop,
.api = &gt911_pointer_api,
.device_type = &POINTER_TYPE,
.owner = &gt911_module,
.internal = nullptr
};
+32
View File
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/check.h>
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver gt911_driver;
static error_t start() {
/* We crash when construct fails, because if a single driver fails to construct,
* there is no guarantee that the previously constructed drivers can be destroyed */
check(driver_construct_add(&gt911_driver) == ERROR_NONE);
return ERROR_NONE;
}
static error_t stop() {
/* We crash when destruct fails, because if a single driver fails to destruct,
* there is no guarantee that the previously destroyed drivers can be recovered */
check(driver_remove_destruct(&gt911_driver) == ERROR_NONE);
return ERROR_NONE;
}
Module gt911_module = {
.name = "gt911",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
} // extern "C"
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(lilygo-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel Tactility lvgl
)
@@ -0,0 +1,13 @@
description: LilyGO T-Deck onboard keyboard backlight controller
include: ["i2c-device.yaml"]
compatible: "lilygo,tdeck-keyboard-backlight"
bus: i2c
properties:
brightness-default:
type: int
default: 127
description: Default brightness level, applied on start and by set_brightness_default(). Range 0-255.
@@ -0,0 +1,7 @@
description: LilyGO T-Deck onboard keyboard controller
include: ["i2c-device.yaml"]
compatible: "lilygo,tdeck-keyboard"
bus: i2c
@@ -0,0 +1,25 @@
description: LilyGO T-Deck 5-way GPIO trackball (4 directions + click button)
compatible: "lilygo,tdeck-trackball"
properties:
pin-right:
type: phandles
required: true
description: Right-direction GPIO pin
pin-up:
type: phandles
required: true
description: Up-direction GPIO pin
pin-left:
type: phandles
required: true
description: Left-direction GPIO pin
pin-down:
type: phandles
required: true
description: Down-direction GPIO pin
pin-click:
type: phandles
required: true
description: Click button GPIO pin
+3
View File
@@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <lilygo/drivers/tdeck_keyboard.h>
DEFINE_DEVICETREE(tdeck_keyboard, struct TdeckKeyboardConfig)
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <lilygo/drivers/tdeck_keyboard_backlight.h>
DEFINE_DEVICETREE(tdeck_keyboard_backlight, struct TdeckKeyboardBacklightConfig)
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <lilygo/drivers/tdeck_trackball.h>
DEFINE_DEVICETREE(tdeck_trackball, struct TdeckTrackballConfig)
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
struct TdeckKeyboardConfig {
uint8_t address;
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
struct TdeckKeyboardBacklightConfig {
uint8_t address;
uint8_t brightness_default;
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
bool tdeck_power_on();
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <tactility/drivers/gpio.h>
#include <tactility/error.h>
#include <stdbool.h>
#include <stdint.h>
struct Device;
struct DeviceType;
struct TdeckTrackballConfig {
struct GpioPinSpec pin_right;
struct GpioPinSpec pin_up;
struct GpioPinSpec pin_left;
struct GpioPinSpec pin_down;
struct GpioPinSpec pin_click;
};
/**
* @brief API for the T-Deck 5-way trackball driver.
* Reports raw, unscaled movement: sensitivity and mode (encoder vs. pointer) are UI concerns
* layered on top by the consumer, not something this driver knows about.
*/
struct TdeckTrackballApi {
/**
* @brief Reads the accumulated movement since the last read, then resets it to zero.
* @param[in] device the trackball device
* @param[out] out_dx horizontal movement (right positive), in raw pulses
* @param[out] out_dy vertical movement (down positive), in raw pulses
* @retval ERROR_NONE when the operation was successful
*/
error_t (*read_delta)(struct Device* device, int32_t* out_dx, int32_t* out_dy);
/**
* @brief Gets whether the click button is currently pressed.
* @param[in] device the trackball device
* @param[out] out_pressed true when pressed
* @retval ERROR_NONE when the operation was successful
*/
error_t (*get_button_pressed)(struct Device* device, bool* out_pressed);
};
/**
* @brief Reads the accumulated movement using the specified trackball device.
*/
error_t tdeck_trackball_read_delta(struct Device* device, int32_t* out_dx, int32_t* out_dy);
/**
* @brief Gets whether the click button is currently pressed on the specified trackball device.
*/
error_t tdeck_trackball_get_button_pressed(struct Device* device, bool* out_pressed);
extern const struct DeviceType TDECK_TRACKBALL_TYPE;
#ifdef __cplusplus
}
#endif
@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <driver/gpio.h>
#include <lvgl.h> #include <lvgl.h>
namespace trackball { namespace trackball {
@@ -14,24 +13,10 @@ enum class Mode {
}; };
/** /**
* @brief Trackball configuration structure * @brief Initialize trackball as an LVGL input device, backed by the kernel tdeck_trackball driver.
* @return LVGL input device pointer, or nullptr if the kernel device isn't found/started
*/ */
struct TrackballConfig { lv_indev_t* init();
gpio_num_t pinRight; // Right direction GPIO
gpio_num_t pinUp; // Up direction GPIO
gpio_num_t pinLeft; // Left direction GPIO
gpio_num_t pinDown; // Down direction GPIO
gpio_num_t pinClick; // Click/select button GPIO
uint8_t encoderSensitivity = 1; // Encoder mode: steps per tick
uint8_t pointerSensitivity = 10; // Pointer mode: pixels per tick
};
/**
* @brief Initialize trackball as LVGL input device
* @param config Trackball GPIO configuration
* @return LVGL input device pointer, or nullptr on failure
*/
lv_indev_t* init(const TrackballConfig& config);
/** /**
* @brief Deinitialize trackball * @brief Deinitialize trackball
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module lilygo_module;
#ifdef __cplusplus
}
#endif
+38
View File
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/check.h>
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver tdeck_keyboard_driver;
extern Driver tdeck_keyboard_backlight_driver;
extern Driver tdeck_trackball_driver;
static error_t start() {
/* We crash when construct fails, because if a single driver fails to construct,
* there is no guarantee that the previously constructed drivers can be destroyed */
check(driver_construct_add(&tdeck_keyboard_driver) == ERROR_NONE);
check(driver_construct_add(&tdeck_keyboard_backlight_driver) == ERROR_NONE);
check(driver_construct_add(&tdeck_trackball_driver) == ERROR_NONE);
return ERROR_NONE;
}
static error_t stop() {
/* We crash when destruct fails, because if a single driver fails to destruct,
* there is no guarantee that the previously destroyed drivers can be recovered */
check(driver_remove_destruct(&tdeck_keyboard_driver) == ERROR_NONE);
check(driver_remove_destruct(&tdeck_keyboard_backlight_driver) == ERROR_NONE);
check(driver_remove_destruct(&tdeck_trackball_driver) == ERROR_NONE);
return ERROR_NONE;
}
Module lilygo_module = {
.name = "lilygo",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
} // extern "C"
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0
#include <lilygo/drivers/tdeck_keyboard.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/log.h>
#include <cstdlib>
#define TAG "TdeckKeyboard"
#define GET_CONFIG(device) (static_cast<const TdeckKeyboardConfig*>((device)->config))
static constexpr TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(100);
struct TdeckKeyboardInternal {
// The T-Deck keyboard controller only ever reports the currently pressed key (0x00 = none) and
// never a release event, so release events are synthesized by comparing against this.
uint8_t last_key;
};
// region Driver lifecycle
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
auto address = GET_CONFIG(device)->address;
if (i2c_controller_has_device_at_address(parent, address, I2C_TIMEOUT) != ERROR_NONE) {
LOG_E(TAG, "No device found on I2C bus at address 0x%02X", address);
return ERROR_RESOURCE;
}
auto* internal = static_cast<TdeckKeyboardInternal*>(malloc(sizeof(TdeckKeyboardInternal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->last_key = 0;
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<TdeckKeyboardInternal*>(device_get_driver_data(device));
free(internal);
return ERROR_NONE;
}
// endregion
// region KeyboardApi
static error_t tdeck_keyboard_read_key(Device* device, KeyboardKeyData* data) {
auto* parent = device_get_parent(device);
auto address = GET_CONFIG(device)->address;
auto* internal = static_cast<TdeckKeyboardInternal*>(device_get_driver_data(device));
uint8_t read_buffer = 0;
if (i2c_controller_read(parent, address, &read_buffer, 1, I2C_TIMEOUT) != ERROR_NONE) {
return ERROR_RESOURCE;
}
// This controller never buffers more than one key event.
data->continue_reading = false;
if (read_buffer == 0 && internal->last_key != 0) {
data->key = internal->last_key;
data->pressed = false;
} else if (read_buffer != 0) {
data->key = read_buffer;
data->pressed = true;
} else {
data->key = 0;
data->pressed = false;
}
internal->last_key = read_buffer;
return ERROR_NONE;
}
// endregion
static const KeyboardApi tdeck_keyboard_api = {
.read_key = tdeck_keyboard_read_key,
};
extern Module lilygo_module;
Driver tdeck_keyboard_driver = {
.name = "tdeck_keyboard",
.compatible = (const char*[]) { "lilygo,tdeck-keyboard", nullptr },
.start_device = start,
.stop_device = stop,
.api = &tdeck_keyboard_api,
.device_type = &KEYBOARD_TYPE,
.owner = &lilygo_module,
.internal = nullptr
};
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: Apache-2.0
#include <lilygo/drivers/tdeck_keyboard_backlight.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/backlight.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#include <cstdlib>
#define TAG "TdeckKeyboardBacklight"
#define GET_CONFIG(device) (static_cast<const TdeckKeyboardBacklightConfig*>((device)->config))
static constexpr TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(100);
static constexpr uint8_t CMD_BRIGHTNESS = 0x01;
static constexpr uint8_t CMD_DEFAULT_BRIGHTNESS = 0x02;
struct TdeckKeyboardBacklightInternal {
// The controller is write-only for brightness, so the current value is cached here.
uint8_t current_brightness;
};
// region Driver lifecycle
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
auto address = GET_CONFIG(device)->address;
if (i2c_controller_has_device_at_address(parent, address, I2C_TIMEOUT) != ERROR_NONE) {
LOG_E(TAG, "No device found on I2C bus at address 0x%02X", address);
return ERROR_RESOURCE;
}
auto* internal = static_cast<TdeckKeyboardBacklightInternal*>(malloc(sizeof(TdeckKeyboardBacklightInternal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->current_brightness = 0;
device_set_driver_data(device, internal);
auto brightness_default = GET_CONFIG(device)->brightness_default;
// Configures the keyboard controller's own persisted default, used by its onboard ALT+B toggle.
if (i2c_controller_write_register(parent, address, CMD_DEFAULT_BRIGHTNESS, &brightness_default, 1, I2C_TIMEOUT) != ERROR_NONE) {
LOG_E(TAG, "Failed to set default brightness");
}
if (i2c_controller_write_register(parent, address, CMD_BRIGHTNESS, &brightness_default, 1, I2C_TIMEOUT) != ERROR_NONE) {
LOG_E(TAG, "Failed to set initial brightness");
} else {
internal->current_brightness = brightness_default;
}
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<TdeckKeyboardBacklightInternal*>(device_get_driver_data(device));
free(internal);
return ERROR_NONE;
}
// endregion
// region BacklightApi
static error_t tdeck_keyboard_backlight_set_brightness(Device* device, uint8_t brightness) {
auto* parent = device_get_parent(device);
auto address = GET_CONFIG(device)->address;
auto* internal = static_cast<TdeckKeyboardBacklightInternal*>(device_get_driver_data(device));
if (i2c_controller_write_register(parent, address, CMD_BRIGHTNESS, &brightness, 1, I2C_TIMEOUT) != ERROR_NONE) {
return ERROR_RESOURCE;
}
internal->current_brightness = brightness;
return ERROR_NONE;
}
static error_t tdeck_keyboard_backlight_set_brightness_default(Device* device) {
return tdeck_keyboard_backlight_set_brightness(device, GET_CONFIG(device)->brightness_default);
}
static error_t tdeck_keyboard_backlight_get_brightness(Device* device, uint8_t* out_brightness) {
auto* internal = static_cast<TdeckKeyboardBacklightInternal*>(device_get_driver_data(device));
*out_brightness = internal->current_brightness;
return ERROR_NONE;
}
static uint8_t tdeck_keyboard_backlight_get_min_brightness(Device*) {
return 0;
}
static uint8_t tdeck_keyboard_backlight_get_max_brightness(Device*) {
return 255;
}
// endregion
static const BacklightApi tdeck_keyboard_backlight_api = {
.set_brightness = tdeck_keyboard_backlight_set_brightness,
.set_brightness_default = tdeck_keyboard_backlight_set_brightness_default,
.get_brightness = tdeck_keyboard_backlight_get_brightness,
.get_min_brightness = tdeck_keyboard_backlight_get_min_brightness,
.get_max_brightness = tdeck_keyboard_backlight_get_max_brightness,
};
extern struct Module lilygo_module;
Driver tdeck_keyboard_backlight_driver = {
.name = "tdeck_keyboard_backlight",
.compatible = (const char*[]) { "lilygo,tdeck-keyboard-backlight", nullptr },
.start_device = start,
.stop_device = stop,
.api = &tdeck_keyboard_backlight_api,
.device_type = &BACKLIGHT_TYPE,
.owner = &lilygo_module,
.internal = nullptr
};
@@ -0,0 +1,23 @@
#include <driver/gpio.h>
#define TDECK_POWERON_GPIO GPIO_NUM_10
bool tdeck_power_on() {
gpio_config_t device_power_signal_config = {
.pin_bit_mask = BIT64(TDECK_POWERON_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
if (gpio_config(&device_power_signal_config) != ESP_OK) {
return false;
}
if (gpio_set_level(TDECK_POWERON_GPIO, 1) != ESP_OK) {
return false;
}
return true;
}
@@ -0,0 +1,196 @@
// SPDX-License-Identifier: Apache-2.0
#include <lilygo/drivers/tdeck_trackball.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/gpio_descriptor.h>
#include <tactility/log.h>
#include <atomic>
#include <new>
#define TAG "tdeck_trackball"
#define GET_CONFIG(device) (static_cast<const TdeckTrackballConfig*>((device)->config))
#define GET_INTERNAL(device) (static_cast<TdeckTrackballInternal*>(device_get_driver_data(device)))
struct TdeckTrackballInternal {
GpioDescriptor* pin_right = nullptr;
GpioDescriptor* pin_up = nullptr;
GpioDescriptor* pin_left = nullptr;
GpioDescriptor* pin_down = nullptr;
GpioDescriptor* pin_click = nullptr;
std::atomic<int32_t> dx {0};
std::atomic<int32_t> dy {0};
std::atomic<bool> button_pressed {false};
};
// region ISR callbacks
static void on_right(void* arg) {
static_cast<TdeckTrackballInternal*>(arg)->dx.fetch_add(1, std::memory_order_relaxed);
}
static void on_left(void* arg) {
static_cast<TdeckTrackballInternal*>(arg)->dx.fetch_sub(1, std::memory_order_relaxed);
}
static void on_down(void* arg) {
static_cast<TdeckTrackballInternal*>(arg)->dy.fetch_add(1, std::memory_order_relaxed);
}
static void on_up(void* arg) {
static_cast<TdeckTrackballInternal*>(arg)->dy.fetch_sub(1, std::memory_order_relaxed);
}
static void on_click(void* arg) {
auto* internal = static_cast<TdeckTrackballInternal*>(arg);
bool high = true;
gpio_descriptor_get_level(internal->pin_click, &high);
// Active low: pressed when level is low
internal->button_pressed.store(!high, std::memory_order_relaxed);
}
// endregion
// region Pin acquisition
static error_t acquire_pin(const GpioPinSpec& spec, GpioInterruptType interrupt_type, void (*callback)(void*), void* arg, GpioDescriptor** out_descriptor) {
auto* descriptor = gpio_descriptor_acquire(spec.gpio_controller, spec.pin, GPIO_OWNER_GPIO);
if (descriptor == nullptr) {
return ERROR_RESOURCE;
}
gpio_flags_t flags = GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_PULL_UP;
flags = GPIO_FLAG_INTERRUPT_TO_OPTIONS(flags, interrupt_type);
error_t error = gpio_descriptor_set_flags(descriptor, flags);
if (error == ERROR_NONE) {
error = gpio_descriptor_add_callback(descriptor, callback, arg);
}
if (error == ERROR_NONE) {
error = gpio_descriptor_enable_interrupt(descriptor);
}
if (error != ERROR_NONE) {
gpio_descriptor_remove_callback(descriptor);
gpio_descriptor_release(descriptor);
return error;
}
*out_descriptor = descriptor;
return ERROR_NONE;
}
static void release_pin(GpioDescriptor*& descriptor) {
if (descriptor == nullptr) {
return;
}
gpio_descriptor_disable_interrupt(descriptor);
gpio_descriptor_remove_callback(descriptor);
gpio_descriptor_release(descriptor);
descriptor = nullptr;
}
static void release_all_pins(TdeckTrackballInternal* internal) {
release_pin(internal->pin_right);
release_pin(internal->pin_up);
release_pin(internal->pin_left);
release_pin(internal->pin_down);
release_pin(internal->pin_click);
}
// endregion
extern "C" {
static error_t read_delta(Device* device, int32_t* out_dx, int32_t* out_dy) {
auto* internal = GET_INTERNAL(device);
*out_dx = internal->dx.exchange(0, std::memory_order_relaxed);
*out_dy = internal->dy.exchange(0, std::memory_order_relaxed);
return ERROR_NONE;
}
static error_t get_button_pressed(Device* device, bool* out_pressed) {
*out_pressed = GET_INTERNAL(device)->button_pressed.load(std::memory_order_relaxed);
return ERROR_NONE;
}
error_t tdeck_trackball_read_delta(Device* device, int32_t* out_dx, int32_t* out_dy) {
return read_delta(device, out_dx, out_dy);
}
error_t tdeck_trackball_get_button_pressed(Device* device, bool* out_pressed) {
return get_button_pressed(device, out_pressed);
}
static error_t start(Device* device) {
LOG_I(TAG, "start %s", device->name);
auto* config = GET_CONFIG(device);
auto* internal = new(std::nothrow) TdeckTrackballInternal();
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
error_t error = acquire_pin(config->pin_right, GPIO_INTERRUPT_NEG_EDGE, on_right, internal, &internal->pin_right);
if (error == ERROR_NONE) {
error = acquire_pin(config->pin_up, GPIO_INTERRUPT_NEG_EDGE, on_up, internal, &internal->pin_up);
}
if (error == ERROR_NONE) {
error = acquire_pin(config->pin_left, GPIO_INTERRUPT_NEG_EDGE, on_left, internal, &internal->pin_left);
}
if (error == ERROR_NONE) {
error = acquire_pin(config->pin_down, GPIO_INTERRUPT_NEG_EDGE, on_down, internal, &internal->pin_down);
}
if (error == ERROR_NONE) {
error = acquire_pin(config->pin_click, GPIO_INTERRUPT_ANY_EDGE, on_click, internal, &internal->pin_click);
}
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to acquire trackball pins: %s", error_to_string(error));
release_all_pins(internal);
delete internal;
return error;
}
// Read the click pin's initial level now that the descriptor is acquired.
on_click(internal);
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
LOG_I(TAG, "stop %s", device->name);
auto* internal = GET_INTERNAL(device);
release_all_pins(internal);
device_set_driver_data(device, nullptr);
delete internal;
return ERROR_NONE;
}
static constexpr TdeckTrackballApi TDECK_TRACKBALL_API = {
.read_delta = read_delta,
.get_button_pressed = get_button_pressed,
};
const struct DeviceType TDECK_TRACKBALL_TYPE {
.name = "tdeck-trackball"
};
extern Module lilygo_module;
Driver tdeck_trackball_driver = {
.name = "tdeck_trackball",
.compatible = (const char*[]) { "lilygo,tdeck-trackball", nullptr },
.start_device = start,
.stop_device = stop,
.api = &TDECK_TRACKBALL_API,
.device_type = &TDECK_TRACKBALL_TYPE,
.owner = &lilygo_module,
.internal = nullptr
};
}
+199
View File
@@ -0,0 +1,199 @@
#include <lilygo/drivers/trackball.h>
#include <lilygo/drivers/tdeck_trackball.h>
#include <Tactility/Assets.h>
#include <tactility/device.h>
#include <tactility/log.h>
constexpr auto* TAG = "Trackball";
namespace trackball {
static lv_indev_t* g_indev = nullptr;
static Device* g_device = nullptr;
static bool g_enabled = true;
static Mode g_mode = Mode::Encoder;
static uint8_t g_encoderSensitivity = 1;
static uint8_t g_pointerSensitivity = 10;
// Pointer mode cursor position (screen-relative)
static int32_t g_cursorX = 160;
static int32_t g_cursorY = 120;
static lv_obj_t* g_cursor = nullptr;
// Screen dimensions (T-Deck: 320x240)
static constexpr int32_t SCREEN_WIDTH = 320;
static constexpr int32_t SCREEN_HEIGHT = 240;
static constexpr int32_t CURSOR_SIZE = 16;
static inline int32_t clamp(int32_t val, int32_t minVal, int32_t maxVal) {
if (val < minVal) return minVal;
if (val > maxVal) return maxVal;
return val;
}
// Note: must be called from the LVGL thread (main thread), same as the setters below.
static void read_cb(lv_indev_t* /*indev*/, lv_indev_data_t* data) {
// Always drain accumulated movement so it doesn't jump on re-enable, but discard it while disabled.
int32_t dx = 0;
int32_t dy = 0;
tdeck_trackball_read_delta(g_device, &dx, &dy);
if (!g_enabled) {
dx = 0;
dy = 0;
}
if (g_mode == Mode::Encoder) {
int32_t ticks = (dx + dy) * static_cast<int32_t>(g_encoderSensitivity);
data->enc_diff = static_cast<int16_t>(clamp(ticks, INT16_MIN, INT16_MAX));
if (ticks != 0) {
lv_display_trigger_activity(nullptr);
}
} else {
g_cursorX = clamp(g_cursorX + dx * static_cast<int32_t>(g_pointerSensitivity), 0, SCREEN_WIDTH - CURSOR_SIZE - 1);
g_cursorY = clamp(g_cursorY + dy * static_cast<int32_t>(g_pointerSensitivity), 0, SCREEN_HEIGHT - CURSOR_SIZE - 1);
data->point.x = static_cast<int16_t>(g_cursorX);
data->point.y = static_cast<int16_t>(g_cursorY);
}
bool pressed = false;
if (g_enabled) {
tdeck_trackball_get_button_pressed(g_device, &pressed);
}
data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
if (pressed) {
lv_display_trigger_activity(nullptr);
}
}
lv_indev_t* init() {
if (g_indev != nullptr) {
LOG_W(TAG, "Already initialized");
return g_indev;
}
g_device = device_find_first_active_by_type(&TDECK_TRACKBALL_TYPE);
if (g_device == nullptr) {
LOG_E(TAG, "tdeck_trackball kernel device not found or not started");
return nullptr;
}
g_cursorX = SCREEN_WIDTH / 2;
g_cursorY = SCREEN_HEIGHT / 2;
g_indev = lv_indev_create();
if (g_indev == nullptr) {
LOG_E(TAG, "Failed to register LVGL input device");
g_device = nullptr;
return nullptr;
}
lv_indev_set_type(g_indev, LV_INDEV_TYPE_ENCODER);
lv_indev_set_read_cb(g_indev, read_cb);
LOG_I(TAG, "Initialized");
return g_indev;
}
// Create cursor for pointer mode
static void createCursor() {
if (g_cursor != nullptr || g_indev == nullptr) return;
g_cursor = lv_image_create(lv_layer_sys());
if (g_cursor != nullptr) {
lv_obj_remove_flag(g_cursor, LV_OBJ_FLAG_CLICKABLE);
lv_image_set_src(g_cursor, TT_ASSETS_UI_CURSOR);
lv_indev_set_cursor(g_indev, g_cursor);
LOG_D(TAG, "Cursor created");
}
}
// Destroy cursor when switching back to encoder mode
static void destroyCursor() {
if (g_cursor == nullptr) return;
// Delete the cursor object - this automatically detaches it from the indev
lv_obj_delete(g_cursor);
g_cursor = nullptr;
LOG_D(TAG, "Cursor destroyed");
}
void deinit() {
if (g_indev == nullptr) return;
destroyCursor();
lv_indev_delete(g_indev);
g_indev = nullptr;
g_device = nullptr;
g_mode = Mode::Encoder;
g_enabled = true;
LOG_I(TAG, "Deinitialized");
}
void setEncoderSensitivity(uint8_t sensitivity) {
if (sensitivity > 0) {
g_encoderSensitivity = sensitivity;
LOG_D(TAG, "Encoder sensitivity set to %d", sensitivity);
}
}
void setPointerSensitivity(uint8_t sensitivity) {
if (sensitivity > 0) {
g_pointerSensitivity = sensitivity;
LOG_D(TAG, "Pointer sensitivity set to %d", sensitivity);
}
}
void setEnabled(bool enabled) {
g_enabled = enabled;
if (g_cursor != nullptr) {
if (enabled) {
lv_obj_clear_flag(g_cursor, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(g_cursor, LV_OBJ_FLAG_HIDDEN);
}
}
LOG_I(TAG, "%s", enabled ? "Enabled" : "Disabled");
}
void setMode(Mode mode) {
if (g_indev == nullptr) {
LOG_W(TAG, "Cannot set mode - not initialized");
return;
}
if (g_mode == mode) {
return;
}
g_mode = mode;
if (mode == Mode::Pointer) {
lv_indev_set_type(g_indev, LV_INDEV_TYPE_POINTER);
createCursor();
if (!g_enabled && g_cursor != nullptr) {
lv_obj_add_flag(g_cursor, LV_OBJ_FLAG_HIDDEN);
}
g_cursorX = SCREEN_WIDTH / 2;
g_cursorY = SCREEN_HEIGHT / 2;
LOG_I(TAG, "Switched to Pointer mode");
} else {
destroyCursor();
lv_indev_set_type(g_indev, LV_INDEV_TYPE_ENCODER);
LOG_I(TAG, "Switched to Encoder mode");
}
}
Mode getMode() {
return g_mode;
}
}
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(st7789-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel platform-esp32 esp_lcd driver
)
@@ -0,0 +1,71 @@
description: Sitronix ST7789 display panel
compatible: "sitronix,st7789"
bus: spi
properties:
horizontal-resolution:
type: int
required: true
description: Horizontal resolution in pixels
vertical-resolution:
type: int
required: true
description: Vertical resolution in pixels
gap-x:
type: int
default: 0
description: X offset applied to all draw operations
gap-y:
type: int
default: 0
description: Y offset applied to all draw operations
swap-xy:
type: boolean
default: false
description: Swap the X and Y axes
mirror-x:
type: boolean
default: false
description: Mirror the X axis
mirror-y:
type: boolean
default: false
description: Mirror the Y axis
invert-color:
type: boolean
default: false
description: Invert the panel's color output
bgr-order:
type: boolean
default: false
description: Use BGR element order instead of RGB
bits-per-pixel:
type: int
default: 16
description: Color depth in bits per pixel
pixel-clock-hz:
type: int
default: 80000000
description: SPI pixel clock frequency in Hz
transaction-queue-depth:
type: int
default: 10
description: Size of the internal SPI transaction queue
pin-dc:
type: phandles
required: true
description: Data/Command GPIO pin
pin-reset:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Reset GPIO pin
reset-active-high:
type: boolean
default: false
description: Whether the reset pin is active high
backlight:
type: phandle
default: NULL
description: Optional reference to this display's backlight device
+3
View File
@@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/st7789.h>
DEFINE_DEVICETREE(st7789, struct St7789Config)
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include <tactility/device.h>
#include <tactility/drivers/gpio.h>
struct St7789Config {
uint16_t horizontal_resolution;
uint16_t vertical_resolution;
int32_t gap_x;
int32_t gap_y;
bool swap_xy;
bool mirror_x;
bool mirror_y;
bool invert_color;
bool bgr_order;
uint32_t bits_per_pixel;
uint32_t pixel_clock_hz;
uint8_t transaction_queue_depth;
struct GpioPinSpec pin_dc;
struct GpioPinSpec pin_reset;
bool reset_active_high;
// Optional reference to this display's backlight device, NULL if none.
struct Device* backlight;
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module st7789_module;
#ifdef __cplusplus
}
#endif
+32
View File
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/check.h>
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver st7789_driver;
static error_t start() {
/* We crash when construct fails, because if a single driver fails to construct,
* there is no guarantee that the previously constructed drivers can be destroyed */
check(driver_construct_add(&st7789_driver) == ERROR_NONE);
return ERROR_NONE;
}
static error_t stop() {
/* We crash when destruct fails, because if a single driver fails to destruct,
* there is no guarantee that the previously destroyed drivers can be recovered */
check(driver_remove_destruct(&st7789_driver) == ERROR_NONE);
return ERROR_NONE;
}
Module st7789_module = {
.name = "st7789",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
} // extern "C"
+278
View File
@@ -0,0 +1,278 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/st7789.h>
#include <st7789_module.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/esp32_spi.h>
#include <tactility/drivers/spi_controller.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <esp_err.h>
#include <esp_lcd_io_spi.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_st7789.h>
#include <cstdlib>
#define TAG "ST7789"
#define GET_CONFIG(device) (static_cast<const St7789Config*>((device)->config))
struct St7789Internal {
esp_lcd_panel_io_handle_t io_handle;
esp_lcd_panel_handle_t panel_handle;
};
static int pin_or_unused(const struct GpioPinSpec& pin) {
return pin.gpio_controller == nullptr ? -1 : static_cast<int>(pin.pin);
}
// region Driver lifecycle
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
const auto* spi_config = static_cast<const Esp32SpiConfig*>(parent->config);
const auto* config = GET_CONFIG(device);
struct GpioPinSpec cs_pin;
if (esp32_spi_get_cs_pin(device, &cs_pin) != ERROR_NONE) {
LOG_E(TAG, "Failed to resolve CS pin");
return ERROR_RESOURCE;
}
auto* internal = static_cast<St7789Internal*>(malloc(sizeof(St7789Internal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
esp_lcd_panel_io_spi_config_t io_config = {
.cs_gpio_num = pin_or_unused(cs_pin),
.dc_gpio_num = pin_or_unused(config->pin_dc),
.spi_mode = 0,
.pclk_hz = config->pixel_clock_hz,
.trans_queue_depth = config->transaction_queue_depth,
.on_color_trans_done = nullptr,
.user_ctx = nullptr,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.flags = {
.dc_high_on_cmd = 0,
.dc_low_on_data = 0,
.dc_low_on_param = 0,
.octal_mode = 0,
.quad_mode = 0,
.sio_mode = 1,
.lsb_first = 0,
.cs_high_active = 0,
},
};
esp_err_t ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)spi_config->host, &io_config, &internal->io_handle);
if (ret != ESP_OK) {
LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(ret));
free(internal);
return ERROR_RESOURCE;
}
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = pin_or_unused(config->pin_reset),
.rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB,
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
.bits_per_pixel = config->bits_per_pixel,
.flags = { .reset_active_high = config->reset_active_high },
.vendor_config = nullptr,
};
ret = esp_lcd_new_panel_st7789(internal->io_handle, &panel_config, &internal->panel_handle);
if (ret != ESP_OK) {
LOG_E(TAG, "Failed to create panel: %s", esp_err_to_name(ret));
esp_lcd_panel_io_del(internal->io_handle);
free(internal);
return ERROR_RESOURCE;
}
// Bring-up sequence, order matches EspLcdDisplayV2::applyConfiguration (proven correct on real ST7789 panels).
// Every failure path below must clean up fully: unlike stop_device, this is never retried by the kernel
// if start_device fails (see device_start() in TactilityKernel), so a partial failure here would leak.
bool ok =
esp_lcd_panel_reset(internal->panel_handle) == ESP_OK &&
esp_lcd_panel_init(internal->panel_handle) == ESP_OK &&
(!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK);
if (ok) {
int gap_x = config->swap_xy ? config->gap_y : config->gap_x;
int gap_y = config->swap_xy ? config->gap_x : config->gap_y;
ok = (gap_x == 0 && gap_y == 0) || esp_lcd_panel_set_gap(internal->panel_handle, gap_x, gap_y) == ESP_OK;
}
ok = ok && (!config->swap_xy || esp_lcd_panel_swap_xy(internal->panel_handle, true) == ESP_OK);
ok = ok && ((!config->mirror_x && !config->mirror_y) || esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK);
ok = ok && (!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK);
ok = ok && esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK;
if (!ok) {
LOG_E(TAG, "Failed to bring up panel");
esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle);
free(internal);
return ERROR_RESOURCE;
}
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<St7789Internal*>(device_get_driver_data(device));
if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) {
LOG_E(TAG, "Failed to delete panel");
return ERROR_RESOURCE;
}
if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
LOG_E(TAG, "Failed to delete panel IO");
return ERROR_RESOURCE;
}
free(internal);
return ERROR_NONE;
}
// endregion
// region DisplayApi
static error_t st7789_reset(Device* device) {
auto* internal = static_cast<St7789Internal*>(device_get_driver_data(device));
return esp_lcd_panel_reset(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7789_init(Device* device) {
auto* internal = static_cast<St7789Internal*>(device_get_driver_data(device));
return esp_lcd_panel_init(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7789_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
auto* internal = static_cast<St7789Internal*>(device_get_driver_data(device));
return esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7789_mirror(Device* device, bool x_axis, bool y_axis) {
auto* internal = static_cast<St7789Internal*>(device_get_driver_data(device));
return esp_lcd_panel_mirror(internal->panel_handle, x_axis, y_axis) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7789_swap_xy(Device* device, bool swap_axes) {
auto* internal = static_cast<St7789Internal*>(device_get_driver_data(device));
return esp_lcd_panel_swap_xy(internal->panel_handle, swap_axes) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
// Reads the devicetree-configured baseline, not live hardware state: swap_xy()/mirror() calls made after
// start_device() (e.g. by an LVGL rotation binding) intentionally don't change what "rotation 0" means here.
static bool st7789_get_swap_xy(Device* device) {
return GET_CONFIG(device)->swap_xy;
}
static bool st7789_get_mirror_x(Device* device) {
return GET_CONFIG(device)->mirror_x;
}
static bool st7789_get_mirror_y(Device* device) {
return GET_CONFIG(device)->mirror_y;
}
static error_t st7789_set_gap(Device* device, int32_t x_gap, int32_t y_gap) {
auto* internal = static_cast<St7789Internal*>(device_get_driver_data(device));
return esp_lcd_panel_set_gap(internal->panel_handle, x_gap, y_gap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7789_invert_color(Device* device, bool invert_color_data) {
auto* internal = static_cast<St7789Internal*>(device_get_driver_data(device));
return esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7789_disp_on_off(Device* device, bool on_off) {
auto* internal = static_cast<St7789Internal*>(device_get_driver_data(device));
return esp_lcd_panel_disp_on_off(internal->panel_handle, on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7789_disp_sleep(Device* device, bool sleep) {
auto* internal = static_cast<St7789Internal*>(device_get_driver_data(device));
return esp_lcd_panel_disp_sleep(internal->panel_handle, sleep) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
// SPI panels have no direct-mapped frame buffer; every draw is an SPI transaction, so the color
// format here is derived purely from the devicetree config (bgr_order), not queried from hardware.
// Only 16bpp (RGB565/BGR565) is representable in DisplayColorFormat; other bit depths are not
// distinguishable and fall back to the 16bpp mapping.
static enum DisplayColorFormat st7789_get_color_format(Device* device) {
const auto* config = GET_CONFIG(device);
return config->bgr_order ? DISPLAY_COLOR_FORMAT_BGR565 : DISPLAY_COLOR_FORMAT_RGB565;
}
static uint16_t st7789_get_resolution_x(Device* device) {
return GET_CONFIG(device)->horizontal_resolution;
}
static uint16_t st7789_get_resolution_y(Device* device) {
return GET_CONFIG(device)->vertical_resolution;
}
static void st7789_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
*out_buffer = nullptr;
}
static uint8_t st7789_get_frame_buffer_count(Device*) {
return 0;
}
static error_t st7789_get_backlight(Device* device, Device** backlight) {
auto* configured_backlight = GET_CONFIG(device)->backlight;
if (configured_backlight == nullptr) {
return ERROR_NOT_SUPPORTED;
}
*backlight = configured_backlight;
return ERROR_NONE;
}
// endregion
static const DisplayApi st7789_display_api = {
.reset = st7789_reset,
.init = st7789_init,
.draw_bitmap = st7789_draw_bitmap,
.mirror = st7789_mirror,
.swap_xy = st7789_swap_xy,
.get_swap_xy = st7789_get_swap_xy,
.get_mirror_x = st7789_get_mirror_x,
.get_mirror_y = st7789_get_mirror_y,
.set_gap = st7789_set_gap,
.invert_color = st7789_invert_color,
.disp_on_off = st7789_disp_on_off,
.disp_sleep = st7789_disp_sleep,
.get_color_format = st7789_get_color_format,
.get_resolution_x = st7789_get_resolution_x,
.get_resolution_y = st7789_get_resolution_y,
.get_frame_buffer = st7789_get_frame_buffer,
.get_frame_buffer_count = st7789_get_frame_buffer_count,
.get_backlight = st7789_get_backlight,
};
Driver st7789_driver = {
.name = "st7789",
.compatible = (const char*[]) { "sitronix,st7789", nullptr },
.start_device = start,
.stop_device = stop,
.api = &st7789_display_api,
.device_type = &DISPLAY_TYPE,
.owner = &st7789_module,
.internal = nullptr
};
+20 -8
View File
@@ -102,21 +102,33 @@ endif ()
# Devicetree code generation # Devicetree code generation
# #
add_custom_target(AlwaysRun # A plain add_custom_command(OUTPUT ...) only reruns when its explicit DEPENDS (devicetree.yaml)
COMMAND ${CMAKE_COMMAND} -E rm -f "${GENERATED_DIR}/devicetree.c" # changes, since ninja computes dirtiness up front, before any command in this invocation runs.
) # The devicetree's real inputs span many files (dts, bindings yaml, driver headers) that a single
add_custom_command( # DEPENDS can't enumerate, so this used to be forced to always-rerun via a separate "AlwaysRun"
OUTPUT "${GENERATED_DIR}/devicetree.c" # custom target that deleted devicetree.c first (DEPENDS on a *target* only creates an order-only
"${GENERATED_DIR}/devicetree.h" # ninja edge). That delete was invisible to ninja's DAG: on an up-to-date build, ninja would still
# run AlwaysRun (custom targets with no real output are always considered dirty) and delete the
# file, but *not* rerun the generation edge (its own explicit input hadn't changed) or recompile
# devicetree.c.obj (also considered clean) - silently leaving devicetree.c missing on disk until
# some later, unrelated build finally noticed and regenerated it. In between, any build that did
# need to (re)compile devicetree.c.obj hit "No such file or directory".
#
# add_custom_target(... COMMAND ...) has no such problem: unlike add_custom_command(OUTPUT ...),
# a custom target with a COMMAND always reruns on every ninja invocation, so generation and the
# stale artifact removal happen atomically in one edge. BYPRODUCTS tells ninja which files this
# target produces, so it still wires a proper (non-order-only) dependency for the sources that
# consume them.
add_custom_target(Generated ALL
COMMAND python "${CMAKE_SOURCE_DIR}/Buildscripts/DevicetreeCompiler/compile.py" COMMAND python "${CMAKE_SOURCE_DIR}/Buildscripts/DevicetreeCompiler/compile.py"
"${DEVICETREE_LOCATION}" "${GENERATED_DIR}" "${DEVICETREE_LOCATION}" "${GENERATED_DIR}"
BYPRODUCTS "${GENERATED_DIR}/devicetree.c" "${GENERATED_DIR}/devicetree.h"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
DEPENDS AlwaysRun "${DEVICETREE_LOCATION}/devicetree.yaml" # AlwaysRun ensures it always gets built
COMMENT "Generating devicetree source files..." COMMENT "Generating devicetree source files..."
) )
add_custom_target(Generated DEPENDS "${GENERATED_DIR}/devicetree.c")
set_source_files_properties("${GENERATED_DIR}/devicetree.c" PROPERTIES GENERATED TRUE) set_source_files_properties("${GENERATED_DIR}/devicetree.c" PROPERTIES GENERATED TRUE)
set_source_files_properties("${GENERATED_DIR}/devicetree.h" PROPERTIES GENERATED TRUE) set_source_files_properties("${GENERATED_DIR}/devicetree.h" PROPERTIES GENERATED TRUE)
# Update target for generated code # Update target for generated code
target_sources(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}/devicetree.c") target_sources(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}/devicetree.c")
target_include_directories(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}") target_include_directories(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}")
add_dependencies(${COMPONENT_LIB} Generated)
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include <lvgl.h>
#include <tactility/device.h>
#include <tactility/error.h>
/**
* @brief Configuration for binding a kernel DisplayApi device to an lv_display_t.
*/
struct LvglDisplayConfig {
/**
* Number of horizontal lines per draw buffer. 0 means the full vertical resolution.
* Ignored when the device exposes its own frame buffer(s) (display_get_frame_buffer_count() > 0).
*/
uint16_t buffer_height;
/**
* Allocates a second draw buffer for double buffering.
* Ignored when the device exposes its own frame buffer(s).
*/
bool double_buffer;
};
/**
* @brief Creates an lv_display_t bound to the given DISPLAY_TYPE device and registers a flush callback
* that draws through the device's DisplayApi.
*
* The device's swap_xy/mirror_x/mirror_y state at the time of this call (via display_get_swap_xy() etc.)
* is treated as the LV_DISPLAY_ROTATION_0 baseline; LVGL-driven rotation changes are applied relative to it.
*
* @warning Caller must hold the LVGL lock (see lvgl_lock() in lvgl_module.h) — call this from
* LvglModuleConfig.on_start, or after calling lvgl_lock() explicitly.
*
* @param[in] device a device of type DISPLAY_TYPE
* @param[in] config binding configuration
* @param[out] out_display the created display, valid only when ERROR_NONE is returned
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT if device, config or out_display is NULL, or device is not of type DISPLAY_TYPE
* @retval ERROR_NOT_SUPPORTED if the device's color format has no LVGL equivalent
* @retval ERROR_OUT_OF_MEMORY if buffer or lv_display_t allocation failed
*/
error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* config, lv_display_t** out_display);
/**
* @brief Removes a display previously created with lvgl_display_add(), freeing any buffers it owns.
* @warning Caller must hold the LVGL lock.
*/
void lvgl_display_remove(lv_display_t* display);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <lvgl.h>
#include <tactility/device.h>
#include <tactility/error.h>
/**
* @brief Creates an lv_indev_t bound to the given KEYBOARD_TYPE device and registers a read callback
* that polls the device through its KeyboardApi.
*
* @warning Caller must hold the LVGL lock (see lvgl_lock() in lvgl_module.h) — call this from
* LvglModuleConfig.on_start, or after calling lvgl_lock() explicitly.
*
* @param[in] device a device of type KEYBOARD_TYPE
* @param[in] display the display this indev should be associated with, or NULL to leave it unset
* @param[out] out_indev the created indev, valid only when ERROR_NONE is returned
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT if device or out_indev is NULL, or device is not of type KEYBOARD_TYPE
* @retval ERROR_OUT_OF_MEMORY if allocation failed
*/
error_t lvgl_keyboard_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev);
/**
* @brief Removes an indev previously created with lvgl_keyboard_add().
* @warning Caller must hold the LVGL lock.
*/
void lvgl_keyboard_remove(lv_indev_t* indev);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <lvgl.h>
#include <tactility/device.h>
#include <tactility/error.h>
/**
* @brief Creates an lv_indev_t bound to the given POINTER_TYPE device and registers a read callback
* that polls the device through its PointerApi.
*
* @warning Caller must hold the LVGL lock (see lvgl_lock() in lvgl_module.h) — call this from
* LvglModuleConfig.on_start, or after calling lvgl_lock() explicitly.
*
* @param[in] device a device of type POINTER_TYPE
* @param[in] display the display this indev should be associated with, or NULL to leave it unset
* @param[out] out_indev the created indev, valid only when ERROR_NONE is returned
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT if device or out_indev is NULL, or device is not of type POINTER_TYPE
* @retval ERROR_OUT_OF_MEMORY if allocation failed
*/
error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev);
/**
* @brief Removes an indev previously created with lvgl_pointer_add().
* @warning Caller must hold the LVGL lock.
*/
void lvgl_pointer_remove(lv_indev_t* indev);
#ifdef __cplusplus
}
#endif
@@ -8,6 +8,8 @@
#include <tactility/lvgl_module.h> #include <tactility/lvgl_module.h>
extern struct LvglModuleConfig lvgl_module_config; extern struct LvglModuleConfig lvgl_module_config;
extern void lvgl_devices_attach();
extern void lvgl_devices_detach();
static bool initialized = false; static bool initialized = false;
@@ -44,6 +46,8 @@ error_t lvgl_arch_start() {
// devices and services. The latter might start adding widgets immediately. // devices and services. The latter might start adding widgets immediately.
initialized = true; initialized = true;
lvgl_devices_attach();
if (lvgl_module_config.on_start) lvgl_module_config.on_start(); if (lvgl_module_config.on_start) lvgl_module_config.on_start();
return ERROR_NONE; return ERROR_NONE;
@@ -52,6 +56,8 @@ error_t lvgl_arch_start() {
error_t lvgl_arch_stop() { error_t lvgl_arch_stop() {
if (lvgl_module_config.on_stop) lvgl_module_config.on_stop(); if (lvgl_module_config.on_stop) lvgl_module_config.on_stop();
lvgl_devices_detach();
if (lvgl_port_deinit() != ESP_OK) { if (lvgl_port_deinit() != ESP_OK) {
// Call on_start again to recover // Call on_start again to recover
if (lvgl_module_config.on_start) lvgl_module_config.on_start(); if (lvgl_module_config.on_start) lvgl_module_config.on_start();
+87
View File
@@ -0,0 +1,87 @@
#include "tactility/lvgl_module.h"
#include <tactility/device.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/drivers/pointer.h>
#include <tactility/log.h>
#include <tactility/lvgl_display.h>
#include <tactility/lvgl_keyboard.h>
#include <tactility/lvgl_pointer.h>
#include <lvgl.h>
#define TAG "lvgl"
void lvgl_devices_attach() {
lvgl_lock();
lv_disp_t* lvgl_display = NULL;
struct Device* kernel_display_device = device_find_first_by_type(&DISPLAY_TYPE);
// Placeholder drivers (boards not yet migrated to the kernel display driver) register with a
// NULL api: they exist so the devicetree node resolves, but have nothing for LVGL to bind to.
if (kernel_display_device != NULL && device_get_driver(kernel_display_device)->api == NULL) {
kernel_display_device = NULL;
}
if (kernel_display_device != NULL) {
struct LvglDisplayConfig lvgl_display_config = {};
if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &lvgl_display) == ERROR_NONE) {
LOG_I(TAG, "Bound %s to LVGL", kernel_display_device->name);
} else {
LOG_E(TAG, "Failed to bind %s to LVGL", kernel_display_device->name);
}
}
struct Device* kernel_pointer_device = device_find_first_by_type(&POINTER_TYPE);
// Same placeholder situation as the display above.
if (kernel_pointer_device != NULL && device_get_driver(kernel_pointer_device)->api == NULL) {
kernel_pointer_device = NULL;
}
lv_indev_t* lvgl_pointer_device;
if (kernel_pointer_device != NULL) {
if (lvgl_pointer_add(kernel_pointer_device, lvgl_display, &lvgl_pointer_device) == ERROR_NONE) {
LOG_I(TAG, "Bound %s to LVGL", kernel_pointer_device->name);
} else {
LOG_E(TAG, "Failed to bind %s to LVG", kernel_pointer_device->name);
}
}
struct Device* kernel_keyboard_device = device_find_first_by_type(&KEYBOARD_TYPE);
lv_indev_t* lvgl_keyboard_device;
if (kernel_keyboard_device != NULL) {
if (lvgl_keyboard_add(kernel_keyboard_device, lvgl_display, &lvgl_keyboard_device) == ERROR_NONE) {
LOG_I(TAG, "Bound %s to LVGL", kernel_keyboard_device->name);
} else {
LOG_E(TAG, "Failed to bind %s to LVGL", kernel_keyboard_device->name);
}
}
lvgl_unlock();
}
void lvgl_devices_detach() {
lvgl_lock();
lv_indev_t* device = lv_indev_get_next(NULL);
while (device != NULL) {
lv_indev_type_t type = lv_indev_get_type(device);
if (type == LV_INDEV_TYPE_POINTER) {
lvgl_pointer_remove(device);
} else if (type == LV_INDEV_TYPE_KEYPAD) {
lvgl_keyboard_remove(device);
} else {
lv_indev_delete(device);
}
// Always get the first item, because getting the next one doesn't work as the current pointer just became corrupt
device = lv_indev_get_next(NULL);
}
lv_disp_t* display = lv_disp_get_next(NULL);
while (display != NULL) {
lv_display_delete(display);
display = lv_disp_get_next(NULL);
}
lvgl_lock();
}
+215
View File
@@ -0,0 +1,215 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/lvgl_display.h>
#include <tactility/drivers/display.h>
#include <tactility/log.h>
#include <stdlib.h>
#ifdef ESP_PLATFORM
#include <esp_heap_caps.h>
#endif
#define TAG "lvgl_display"
struct LvglDisplayCtx {
struct Device* device;
void* buf1;
void* buf2;
bool owns_buffers; // false when buf1/buf2 point at the device's own frame buffer(s)
// The device's swap_xy/mirror_x/mirror_y at bind time, queried once and treated as the LV_DISPLAY_ROTATION_0 baseline.
bool base_swap_xy;
bool base_mirror_x;
bool base_mirror_y;
};
static void* lvgl_display_alloc_buffer(size_t size_bytes) {
#ifdef ESP_PLATFORM
void* buf = heap_caps_aligned_alloc(4, size_bytes, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
if (buf == NULL) {
buf = heap_caps_aligned_alloc(4, size_bytes, MALLOC_CAP_DEFAULT);
}
return buf;
#else
return malloc(size_bytes);
#endif
}
static void lvgl_display_free_buffer(void* buf) {
#ifdef ESP_PLATFORM
heap_caps_free(buf);
#else
free(buf);
#endif
}
static bool lvgl_display_map_color_format(enum DisplayColorFormat in, lv_color_format_t* out) {
switch (in) {
case DISPLAY_COLOR_FORMAT_RGB565:
*out = LV_COLOR_FORMAT_RGB565;
return true;
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
*out = LV_COLOR_FORMAT_RGB565_SWAPPED;
return true;
case DISPLAY_COLOR_FORMAT_RGB888:
*out = LV_COLOR_FORMAT_RGB888;
return true;
default:
// DISPLAY_COLOR_FORMAT_BGR565/_SWAPPED: no LVGL equivalent (channel order, not byte order).
// DISPLAY_COLOR_FORMAT_MONOCHROME: unsupported for now, no 1bpp conversion buffer implemented.
return false;
}
}
static void lvgl_display_apply_rotation(struct LvglDisplayCtx* ctx, lv_display_rotation_t rotation) {
bool swap_xy = ctx->base_swap_xy;
bool mirror_x = ctx->base_mirror_x;
bool mirror_y = ctx->base_mirror_y;
switch (rotation) {
case LV_DISPLAY_ROTATION_0:
break;
case LV_DISPLAY_ROTATION_90:
swap_xy = !ctx->base_swap_xy;
if (ctx->base_swap_xy) {
mirror_x = !ctx->base_mirror_x;
} else {
mirror_y = !ctx->base_mirror_y;
}
break;
case LV_DISPLAY_ROTATION_180:
mirror_x = !ctx->base_mirror_x;
mirror_y = !ctx->base_mirror_y;
break;
case LV_DISPLAY_ROTATION_270:
swap_xy = !ctx->base_swap_xy;
if (ctx->base_swap_xy) {
mirror_y = !ctx->base_mirror_y;
} else {
mirror_x = !ctx->base_mirror_x;
}
break;
}
display_swap_xy(ctx->device, swap_xy);
display_mirror(ctx->device, mirror_x, mirror_y);
}
static void lvgl_display_rotation_event_cb(lv_event_t* e) {
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_event_get_user_data(e);
lv_display_t* disp = (lv_display_t*)lv_event_get_current_target(e);
lvgl_display_apply_rotation(ctx, lv_display_get_rotation(disp));
}
static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* color_map) {
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_display_get_driver_data(disp);
// LVGL's area is inclusive; DisplayApi's draw_bitmap wants an exclusive end.
display_draw_bitmap(ctx->device, area->x1, area->y1, area->x2 + 1, area->y2 + 1, color_map);
// DisplayApi has no async completion callback, so draw_bitmap is synchronous.
lv_display_flush_ready(disp);
}
error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* config, lv_display_t** out_display) {
if (device == NULL || config == NULL || out_display == NULL) {
return ERROR_INVALID_ARGUMENT;
}
if (device_get_type(device) != &DISPLAY_TYPE) {
return ERROR_INVALID_ARGUMENT;
}
lv_color_format_t lv_color_format;
enum DisplayColorFormat kernel_color_format = display_get_color_format(device);
if (!lvgl_display_map_color_format(kernel_color_format, &lv_color_format)) {
LOG_E(TAG, "Unsupported color format %d (no LVGL equivalent)", (int)kernel_color_format);
return ERROR_NOT_SUPPORTED;
}
uint16_t hres = display_get_resolution_x(device);
uint16_t vres = display_get_resolution_y(device);
uint8_t fb_count = display_get_frame_buffer_count(device);
uint8_t bpp = lv_color_format_get_size(lv_color_format);
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)calloc(1, sizeof(struct LvglDisplayCtx));
if (ctx == NULL) {
return ERROR_OUT_OF_MEMORY;
}
ctx->device = device;
ctx->base_swap_xy = display_get_swap_xy(device);
ctx->base_mirror_x = display_get_mirror_x(device);
ctx->base_mirror_y = display_get_mirror_y(device);
lv_display_render_mode_t render_mode;
size_t buf_size_bytes;
if (fb_count > 0) {
display_get_frame_buffer(device, 0, &ctx->buf1);
if (fb_count > 1) {
display_get_frame_buffer(device, 1, &ctx->buf2);
}
ctx->owns_buffers = false;
render_mode = LV_DISPLAY_RENDER_MODE_FULL;
buf_size_bytes = (size_t)hres * vres * bpp;
} else {
uint16_t buf_height = config->buffer_height > 0 ? config->buffer_height : vres;
buf_size_bytes = (size_t)hres * buf_height * bpp;
ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes);
if (ctx->buf1 == NULL) {
free(ctx);
return ERROR_OUT_OF_MEMORY;
}
if (config->double_buffer) {
ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes);
if (ctx->buf2 == NULL) {
lvgl_display_free_buffer(ctx->buf1);
free(ctx);
return ERROR_OUT_OF_MEMORY;
}
}
ctx->owns_buffers = true;
render_mode = LV_DISPLAY_RENDER_MODE_PARTIAL;
}
lv_display_t* disp = lv_display_create(hres, vres);
if (disp == NULL) {
if (ctx->owns_buffers) {
lvgl_display_free_buffer(ctx->buf1);
lvgl_display_free_buffer(ctx->buf2);
}
free(ctx);
return ERROR_OUT_OF_MEMORY;
}
lv_display_set_color_format(disp, lv_color_format);
lv_display_set_buffers(disp, ctx->buf1, ctx->buf2, buf_size_bytes, render_mode);
lv_display_set_flush_cb(disp, lvgl_display_flush_cb);
lv_display_set_driver_data(disp, ctx);
lv_display_add_event_cb(disp, lvgl_display_rotation_event_cb, LV_EVENT_RESOLUTION_CHANGED, ctx);
// Apply once explicitly, independent of whether LV_EVENT_RESOLUTION_CHANGED fires on creation.
lvgl_display_apply_rotation(ctx, lv_display_get_rotation(disp));
*out_display = disp;
return ERROR_NONE;
}
void lvgl_display_remove(lv_display_t* display) {
if (display == NULL) {
return;
}
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_display_get_driver_data(display);
lv_display_delete(display);
if (ctx != NULL) {
if (ctx->owns_buffers) {
if (ctx->buf1 != NULL) {
lvgl_display_free_buffer(ctx->buf1);
}
if (ctx->buf2 != NULL) {
lvgl_display_free_buffer(ctx->buf2);
}
}
free(ctx);
}
}
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/lvgl_keyboard.h>
#include <tactility/drivers/keyboard.h>
#include <stdlib.h>
struct LvglKeyboardCtx {
struct Device* device;
};
static void lvgl_keyboard_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
struct LvglKeyboardCtx* ctx = (struct LvglKeyboardCtx*)lv_indev_get_driver_data(indev);
struct KeyboardKeyData key_data = {0};
if (keyboard_read_key(ctx->device, &key_data) != ERROR_NONE) {
data->state = LV_INDEV_STATE_RELEASED;
data->continue_reading = false;
return;
}
// KeyboardKeyData deliberately mirrors lv_indev_data_t's key/continue_reading fields, so no translation is needed.
data->key = key_data.key;
data->state = key_data.pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
data->continue_reading = key_data.continue_reading;
}
error_t lvgl_keyboard_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev) {
if (device == NULL || out_indev == NULL) {
return ERROR_INVALID_ARGUMENT;
}
if (device_get_type(device) != &KEYBOARD_TYPE) {
return ERROR_INVALID_ARGUMENT;
}
struct LvglKeyboardCtx* ctx = (struct LvglKeyboardCtx*)malloc(sizeof(struct LvglKeyboardCtx));
if (ctx == NULL) {
return ERROR_OUT_OF_MEMORY;
}
ctx->device = device;
lv_indev_t* indev = lv_indev_create();
if (indev == NULL) {
free(ctx);
return ERROR_OUT_OF_MEMORY;
}
lv_indev_set_type(indev, LV_INDEV_TYPE_KEYPAD);
lv_indev_set_read_cb(indev, lvgl_keyboard_read_cb);
lv_indev_set_driver_data(indev, ctx);
if (display != NULL) {
lv_indev_set_display(indev, display);
}
*out_indev = indev;
return ERROR_NONE;
}
void lvgl_keyboard_remove(lv_indev_t* indev) {
if (indev == NULL) {
return;
}
struct LvglKeyboardCtx* ctx = (struct LvglKeyboardCtx*)lv_indev_get_driver_data(indev);
lv_indev_delete(indev);
free(ctx);
}
+76
View File
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/lvgl_pointer.h>
#include <tactility/drivers/pointer.h>
#include <stdlib.h>
struct LvglPointerCtx {
struct Device* device;
};
// Bus reads are expected to complete quickly; bound the wait so a stalled controller can't block the LVGL indev poll.
static const TickType_t LVGL_POINTER_READ_TIMEOUT = pdMS_TO_TICKS(10);
static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)lv_indev_get_driver_data(indev);
if (pointer_read_data(ctx->device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) {
data->state = LV_INDEV_STATE_RELEASED;
return;
}
uint16_t x = 0;
uint16_t y = 0;
uint8_t point_count = 0;
bool touched = pointer_get_touched_points(ctx->device, &x, &y, NULL, &point_count, 1);
if (touched && point_count > 0) {
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev) {
if (device == NULL || out_indev == NULL) {
return ERROR_INVALID_ARGUMENT;
}
if (device_get_type(device) != &POINTER_TYPE) {
return ERROR_INVALID_ARGUMENT;
}
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)malloc(sizeof(struct LvglPointerCtx));
if (ctx == NULL) {
return ERROR_OUT_OF_MEMORY;
}
ctx->device = device;
lv_indev_t* indev = lv_indev_create();
if (indev == NULL) {
free(ctx);
return ERROR_OUT_OF_MEMORY;
}
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, lvgl_pointer_read_cb);
lv_indev_set_driver_data(indev, ctx);
if (display != NULL) {
lv_indev_set_display(indev, display);
}
*out_indev = indev;
return ERROR_NONE;
}
void lvgl_pointer_remove(lv_indev_t* indev) {
if (indev == NULL) {
return;
}
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)lv_indev_get_driver_data(indev);
lv_indev_delete(indev);
free(ctx);
}
+1 -1
View File
@@ -6,7 +6,7 @@ idf_component_register(
SRCS ${SOURCES} SRCS ${SOURCES}
INCLUDE_DIRS "include/" INCLUDE_DIRS "include/"
PRIV_INCLUDE_DIRS "private/" PRIV_INCLUDE_DIRS "private/"
REQUIRES TactilityKernel driver esp_driver_i2c vfs fatfs esp_wifi esp_netif esp_event REQUIRES TactilityKernel driver esp_adc esp_driver_i2c vfs fatfs esp_wifi esp_netif esp_event
) )
idf_component_optional_requires(PRIVATE bt usb espressif__usb_host_hid espressif__usb_host_msc) idf_component_optional_requires(PRIVATE bt usb espressif__usb_host_hid espressif__usb_host_msc)
@@ -0,0 +1,30 @@
description: ESP32 ADC Controller (oneshot mode)
include: ["adc-controller.yaml"]
compatible: "espressif,esp32-adc-oneshot"
properties:
unit-id:
type: int
required: true
description: |
The ADC unit, defined by adc_unit_t (e.g. ADC_UNIT_1, ADC_UNIT_2).
clk-src:
type: int
default: 0
description: |
Clock source, defined by adc_oneshot_clk_src_t. 0 selects the default clock source.
ulp-mode:
type: int
default: 0
description: |
ULP control mode, defined by adc_ulp_mode_t. 0 means ADC_ULP_MODE_DISABLE.
channels:
type: phandle-array
element-type: "struct Esp32AdcOneshotChannelConfig"
default: "{ }"
description: |
Per-channel configuration. Each entry is written as <channel atten bitwidth>,
e.g. <ADC_CHANNEL_3 ADC_ATTEN_DB_12 ADC_BITWIDTH_DEFAULT>.
A consumer device refers to a channel with a phandle and channel number, e.g. `<&adc0 3>`.
@@ -0,0 +1,29 @@
description: ESP32 LEDC-backed PWM backlight
compatible: "espressif,esp32-ledc-backlight"
properties:
pin-backlight:
type: phandles
required: true
description: Backlight PWM output pin
frequency-hz:
type: int
default: 30000
description: PWM frequency in Hz
brightness-level-range:
type: values
default: [0, 255]
description: Inclusive [min,max] brightness range. The minimum value turns the backlight off.
brightness-default:
type: int
default: 200
description: Default brightness level, applied by set_brightness_default(). Should fall within brightness-level-range.
ledc-timer:
type: int
default: 0
description: LEDC timer index, defined by ledc_timer_t
ledc-channel:
type: int
default: 0
description: LEDC channel index, defined by ledc_channel_t
@@ -2,13 +2,13 @@
#pragma once #pragma once
#include <tactility/bindings/bindings.h> #include <tactility/bindings/bindings.h>
#include <tactility/drivers/touch_placeholder.h> #include <tactility/drivers/esp32_adc_oneshot.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
DEFINE_DEVICETREE(touch_placeholder, struct TouchPlaceholderConfig) DEFINE_DEVICETREE(esp32_adc_oneshot, struct Esp32AdcOneshotConfig)
#ifdef __cplusplus #ifdef __cplusplus
} }
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <tactility/drivers/esp32_ledc_backlight.h>
#ifdef __cplusplus
extern "C" {
#endif
DEFINE_DEVICETREE(esp32_ledc_backlight, struct Esp32LedcBacklightConfig)
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/device.h>
#include <esp_adc/adc_oneshot.h>
#include <hal/adc_types.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Esp32AdcOneshotChannelConfig {
adc_channel_t channel;
adc_atten_t atten;
adc_bitwidth_t bitwidth;
};
struct Esp32AdcOneshotConfig {
adc_unit_t unit_id;
adc_oneshot_clk_src_t clk_src;
adc_ulp_mode_t ulp_mode;
/** Per-channel configuration */
const struct Esp32AdcOneshotChannelConfig* channels;
/** The item count of channels */
size_t channel_count;
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <driver/ledc.h>
#include <tactility/drivers/backlight.h>
#include <tactility/drivers/gpio.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Esp32LedcBacklightConfig {
struct GpioPinSpec pin_backlight;
uint32_t frequency_hz;
struct BrightnessLevelRange brightness_range;
uint8_t brightness_default;
ledc_timer_t ledc_timer;
ledc_channel_t ledc_channel;
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: Apache-2.0
#include <esp_adc/adc_oneshot.h>
#include <tactility/error_esp32.h>
#include <tactility/driver.h>
#include <tactility/drivers/adc_controller.h>
#include <tactility/drivers/esp32_adc_oneshot.h>
#include <tactility/log.h>
#include <tactility/time.h>
#define TAG "esp32_adc_oneshot"
#define GET_CONFIG(device) ((Esp32AdcOneshotConfig*)device->config)
#define GET_HANDLE(device) ((adc_oneshot_unit_handle_t)device_get_driver_data(device))
static const Esp32AdcOneshotChannelConfig* find_channel_config(const Esp32AdcOneshotConfig* dts_config, uint8_t channel_index) {
if (channel_index >= dts_config->channel_count) {
return nullptr;
}
return &dts_config->channels[channel_index];
}
extern "C" {
static error_t read_raw(Device* device, uint8_t channel, int* out_raw, TickType_t timeout) {
if (xPortInIsrContext()) return ERROR_ISR_STATUS;
auto* dts_config = GET_CONFIG(device);
auto* channel_config = find_channel_config(dts_config, channel);
if (channel_config == nullptr) {
return ERROR_OUT_OF_RANGE;
}
esp_err_t esp_error = adc_oneshot_read(GET_HANDLE(device), channel_config->channel, out_raw);
if (esp_error != ESP_OK) {
LOG_E(TAG, "read(channel=%u) failed: %s", channel, esp_err_to_name(esp_error));
}
return esp_err_to_error(esp_error);
}
static error_t start(Device* device) {
LOG_I(TAG, "start %s", device->name);
auto* dts_config = GET_CONFIG(device);
adc_oneshot_unit_init_cfg_t init_config = {
.unit_id = dts_config->unit_id,
.clk_src = dts_config->clk_src,
.ulp_mode = dts_config->ulp_mode,
};
adc_oneshot_unit_handle_t handle;
esp_err_t esp_error = adc_oneshot_new_unit(&init_config, &handle);
if (esp_error != ESP_OK) {
LOG_E(TAG, "Failed to create ADC unit %d: %s", (int)dts_config->unit_id, esp_err_to_name(esp_error));
return ERROR_RESOURCE;
}
for (size_t i = 0; i < dts_config->channel_count; i++) {
const auto& channel_config = dts_config->channels[i];
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = channel_config.atten,
.bitwidth = channel_config.bitwidth,
};
esp_error = adc_oneshot_config_channel(handle, channel_config.channel, &chan_cfg);
if (esp_error != ESP_OK) {
LOG_E(TAG, "Failed to configure channel %d: %s", (int)channel_config.channel, esp_err_to_name(esp_error));
adc_oneshot_del_unit(handle);
return ERROR_RESOURCE;
}
}
device_set_driver_data(device, handle);
return ERROR_NONE;
}
static error_t stop(Device* device) {
LOG_I(TAG, "stop %s", device->name);
esp_err_t esp_error = adc_oneshot_del_unit(GET_HANDLE(device));
if (esp_error != ESP_OK) {
LOG_E(TAG, "Deleting ADC unit failed: %s", esp_err_to_name(esp_error));
}
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
static constexpr AdcControllerApi ESP32_ADC_ONESHOT_API = {
.read_raw = read_raw
};
extern Module platform_esp32_module;
Driver esp32_adc_oneshot_driver = {
.name = "esp32_adc_oneshot",
.compatible = (const char*[]) { "espressif,esp32-adc-oneshot", nullptr },
.start_device = start,
.stop_device = stop,
.api = &ESP32_ADC_ONESHOT_API,
.device_type = &ADC_CONTROLLER_TYPE,
.owner = &platform_esp32_module,
.internal = nullptr
};
} // extern "C"
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/drivers/esp32_ledc_backlight.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/backlight.h>
#include <tactility/log.h>
#include <driver/ledc.h>
#include <esp_err.h>
#include <cstdlib>
#define TAG "Esp32LedcBacklight"
#define GET_CONFIG(device) (static_cast<const Esp32LedcBacklightConfig*>((device)->config))
struct Esp32LedcBacklightInternal {
uint8_t brightness;
};
// region Driver lifecycle
static error_t start(Device* device) {
const auto* config = GET_CONFIG(device);
ledc_timer_config_t timer_config = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_8_BIT,
.timer_num = config->ledc_timer,
.freq_hz = config->frequency_hz,
.clk_cfg = LEDC_AUTO_CLK,
.deconfigure = false,
};
if (ledc_timer_config(&timer_config) != ESP_OK) {
LOG_E(TAG, "Failed to configure LEDC timer");
return ERROR_RESOURCE;
}
ledc_channel_config_t channel_config = {
.gpio_num = (int)config->pin_backlight.pin,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = config->ledc_channel,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = config->ledc_timer,
.duty = config->brightness_range.min,
.hpoint = 0,
.sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD,
.flags = {
.output_invert = 0,
},
};
if (ledc_channel_config(&channel_config) != ESP_OK) {
LOG_E(TAG, "Failed to configure LEDC channel");
return ERROR_RESOURCE;
}
auto* internal = static_cast<Esp32LedcBacklightInternal*>(malloc(sizeof(Esp32LedcBacklightInternal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->brightness = config->brightness_range.min;
device_set_driver_data(device, internal);
backlight_set_brightness_default(device); // Allowed to fail, we don't care about the result
return ERROR_NONE;
}
static error_t stop(Device* device) {
backlight_set_brightness(device, 0); // Allowed to fail, we don't care about the result
auto* internal = static_cast<Esp32LedcBacklightInternal*>(device_get_driver_data(device));
free(internal);
return ERROR_NONE;
}
// endregion
// region BacklightApi
static error_t esp32_ledc_backlight_set_brightness(Device* device, uint8_t brightness) {
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<Esp32LedcBacklightInternal*>(device_get_driver_data(device));
esp_err_t ret = ledc_set_duty(LEDC_LOW_SPEED_MODE, config->ledc_channel, brightness);
if (ret == ESP_OK) {
ret = ledc_update_duty(LEDC_LOW_SPEED_MODE, config->ledc_channel);
}
if (ret != ESP_OK) {
LOG_E(TAG, "Failed to set brightness: %s", esp_err_to_name(ret));
return ERROR_RESOURCE;
}
internal->brightness = brightness;
return ERROR_NONE;
}
static error_t esp32_ledc_backlight_set_brightness_default(Device* device) {
return esp32_ledc_backlight_set_brightness(device, GET_CONFIG(device)->brightness_default);
}
static error_t esp32_ledc_backlight_get_brightness(Device* device, uint8_t* out_brightness) {
auto* internal = static_cast<Esp32LedcBacklightInternal*>(device_get_driver_data(device));
*out_brightness = internal->brightness;
return ERROR_NONE;
}
static uint8_t esp32_ledc_backlight_get_min_brightness(Device* device) {
return GET_CONFIG(device)->brightness_range.min;
}
static uint8_t esp32_ledc_backlight_get_max_brightness(Device* device) {
return GET_CONFIG(device)->brightness_range.max;
}
// endregion
static const BacklightApi esp32_ledc_backlight_api = {
.set_brightness = esp32_ledc_backlight_set_brightness,
.set_brightness_default = esp32_ledc_backlight_set_brightness_default,
.get_brightness = esp32_ledc_backlight_get_brightness,
.get_min_brightness = esp32_ledc_backlight_get_min_brightness,
.get_max_brightness = esp32_ledc_backlight_get_max_brightness,
};
extern Module platform_esp32_module;
Driver esp32_ledc_backlight_driver = {
.name = "esp32_ledc_backlight",
.compatible = (const char*[]) { "espressif,esp32-ledc-backlight", nullptr },
.start_device = start,
.stop_device = stop,
.api = &esp32_ledc_backlight_api,
.device_type = &BACKLIGHT_TYPE,
.owner = &platform_esp32_module,
.internal = nullptr
};

Some files were not shown because too many files have changed in this diff Show More