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:
committed by
GitHub
parent
c4406b24ba
commit
50c0a14a93
@@ -11,7 +11,7 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: 'Board select'
|
||||
|
||||
@@ -11,7 +11,7 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: 'Board select'
|
||||
|
||||
@@ -15,7 +15,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: "Checkout repo"
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Linux Dependencies for SDL
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
Linux:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Build"
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
macOS:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Build"
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Build SDK"
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- id: set-matrix
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.GenerateDeviceMatrix.outputs.matrix) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Build Firmware"
|
||||
@@ -62,7 +62,9 @@ jobs:
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Bundle Artifacts"
|
||||
uses: ./.github/actions/bundle-artifacts
|
||||
PublishFirmwareSnapshot:
|
||||
@@ -70,7 +72,7 @@ jobs:
|
||||
needs: [ BundleArtifacts ]
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: "Publish Firmware Snapshot"
|
||||
env:
|
||||
CDN_ID: ${{ secrets.CDN_ID }}
|
||||
@@ -84,7 +86,7 @@ jobs:
|
||||
needs: [ BundleArtifacts ]
|
||||
if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: "Publish Firmware Stable"
|
||||
env:
|
||||
CDN_ID: ${{ secrets.CDN_ID }}
|
||||
@@ -98,7 +100,7 @@ jobs:
|
||||
needs: [ BundleArtifacts ]
|
||||
if: (github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')))
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: "Publish SDKs"
|
||||
env:
|
||||
CDN_ID: ${{ secrets.CDN_ID }}
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout repo"
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: "Configure Project"
|
||||
@@ -30,8 +30,7 @@ jobs:
|
||||
DevicetreeTests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout repo"
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -79,6 +79,11 @@ def property_to_string(property: DeviceProperty, devices: list[Device]) -> str:
|
||||
value_list.append(str(item))
|
||||
return "{ " + ",".join(value_list) + " }"
|
||||
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)
|
||||
elif type == "phandles":
|
||||
value_list = list()
|
||||
|
||||
@@ -13,9 +13,9 @@ $jsonClean = $json.flash_files -replace '[\{\}\@\;]', ''
|
||||
$jsonClean = $jsonClean -replace '[\=]', ' '
|
||||
|
||||
cd Binaries
|
||||
$command = "esptool --port $port erase_flash"
|
||||
$command = "esptool --port $port erase-flash"
|
||||
Invoke-Expression $command
|
||||
$command = "esptool --port $port write_flash $jsonClean"
|
||||
$command = "esptool --port $port write-flash $jsonClean"
|
||||
Invoke-Expression $command
|
||||
cd ..
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ fi
|
||||
# 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=' '`
|
||||
cd Binaries
|
||||
$esptoolPath --port $1 erase_flash
|
||||
$esptoolPath --port $1 write_flash $flash_args
|
||||
$esptoolPath --port $1 erase-flash
|
||||
$esptoolPath --port $1 write-flash $flash_args
|
||||
cd -
|
||||
|
||||
|
||||
@@ -44,6 +44,11 @@ CONFIG_WL_SECTOR_MODE_SAFE=y
|
||||
CONFIG_WL_SECTOR_MODE=1
|
||||
# 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
|
||||
# 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
|
||||
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=3
|
||||
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=5
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <tactility/bindings/esp32_spi.h>
|
||||
#include <tactility/bindings/esp32_sdspi.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>
|
||||
|
||||
/ {
|
||||
@@ -37,7 +37,7 @@
|
||||
};
|
||||
|
||||
touch@1 {
|
||||
compatible = "touch-placeholder";
|
||||
compatible = "pointer-placeholder";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <tactility/bindings/esp32_uart.h>
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/display_placeholder.h>
|
||||
#include <tactility/bindings/touch_placeholder.h>
|
||||
#include <tactility/bindings/pointer_placeholder.h>
|
||||
|
||||
/ {
|
||||
compatible = "root";
|
||||
@@ -46,7 +46,7 @@
|
||||
};
|
||||
|
||||
touch@1 {
|
||||
compatible = "touch-placeholder";
|
||||
compatible = "pointer-placeholder";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <tactility/bindings/esp32_spi.h>
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/display_placeholder.h>
|
||||
#include <tactility/bindings/touch_placeholder.h>
|
||||
#include <tactility/bindings/pointer_placeholder.h>
|
||||
|
||||
/ {
|
||||
compatible = "root";
|
||||
@@ -46,7 +46,7 @@
|
||||
};
|
||||
|
||||
touch@1 {
|
||||
compatible = "touch-placeholder";
|
||||
compatible = "pointer-placeholder";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <tactility/bindings/esp32_spi.h>
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/display_placeholder.h>
|
||||
#include <tactility/bindings/touch_placeholder.h>
|
||||
#include <tactility/bindings/pointer_placeholder.h>
|
||||
|
||||
/ {
|
||||
compatible = "root";
|
||||
@@ -36,7 +36,7 @@
|
||||
};
|
||||
|
||||
touch@1 {
|
||||
compatible = "touch-placeholder";
|
||||
compatible = "pointer-placeholder";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#include <tactility/bindings/esp32_spi.h>
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/display_placeholder.h>
|
||||
#include <tactility/bindings/touch_placeholder.h>
|
||||
#include <tactility/bindings/pointer_placeholder.h>
|
||||
|
||||
/ {
|
||||
compatible = "root";
|
||||
@@ -45,7 +45,7 @@
|
||||
};
|
||||
|
||||
touch@1 {
|
||||
compatible = "touch-placeholder";
|
||||
compatible = "pointer-placeholder";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <tactility/bindings/esp32_uart.h>
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/display_placeholder.h>
|
||||
#include <tactility/bindings/touch_placeholder.h>
|
||||
#include <tactility/bindings/pointer_placeholder.h>
|
||||
|
||||
/ {
|
||||
compatible = "root";
|
||||
@@ -47,7 +47,7 @@
|
||||
};
|
||||
|
||||
touch@1 {
|
||||
compatible = "touch-placeholder";
|
||||
compatible = "pointer-placeholder";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <tactility/bindings/esp32_uart.h>
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/display_placeholder.h>
|
||||
#include <tactility/bindings/touch_placeholder.h>
|
||||
#include <tactility/bindings/pointer_placeholder.h>
|
||||
|
||||
/ {
|
||||
compatible = "root";
|
||||
@@ -47,7 +47,7 @@
|
||||
};
|
||||
|
||||
touch@1 {
|
||||
compatible = "touch-placeholder";
|
||||
compatible = "pointer-placeholder";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
||||
file(GLOB_RECURSE SOURCE_FILES source/*.c*)
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility EspLcdCompat ST7789 GT911 PwmBacklight EstimatedPower driver
|
||||
REQUIRES Tactility driver lilygo-module
|
||||
)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
extern "C" {
|
||||
|
||||
extern void register_device_drivers() {
|
||||
/* NO-OP */
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,5 +1,5 @@
|
||||
general.vendor=LilyGO
|
||||
general.name=T-Deck,T-Deck Plus
|
||||
general.name=T-Deck
|
||||
|
||||
apps.launcherAppId=Launcher
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
dependencies:
|
||||
- Platforms/platform-esp32
|
||||
- Drivers/st7789-module
|
||||
- Drivers/gt911-module
|
||||
- Drivers/lilygo-module
|
||||
dts: lilygo,tdeck.dts
|
||||
|
||||
@@ -1,22 +1,45 @@
|
||||
/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_wifi_pinned.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_spi.h>
|
||||
#include <tactility/bindings/esp32_uart.h>
|
||||
#include <tactility/bindings/esp32_ledc_backlight.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";
|
||||
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 {
|
||||
compatible = "espressif,esp32-wifi-pinned";
|
||||
status = "disabled";
|
||||
@@ -32,12 +55,41 @@
|
||||
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 {
|
||||
@@ -48,6 +100,17 @@
|
||||
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>;
|
||||
@@ -59,20 +122,32 @@
|
||||
pin-sclk = <&gpio0 40 GPIO_FLAG_NONE>;
|
||||
|
||||
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 {
|
||||
compatible = "espressif,esp32-sdspi";
|
||||
status = "disabled"; // Must be started after display
|
||||
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>;
|
||||
grove0 {
|
||||
compatible = "espressif,esp32-grove";
|
||||
defaultMode = <GROVE_MODE_I2C>;
|
||||
pinSdaTx = <&gpio0 43 GPIO_FLAG_NONE>;
|
||||
pinSclRx = <&gpio0 44 GPIO_FLAG_NONE>;
|
||||
uartPort = <UART_NUM_1>;
|
||||
i2cPort = <I2C_NUM_1>;
|
||||
i2cClockFrequency = <400000>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <tactility/bindings/esp32_sdmmc.h>
|
||||
#include <tactility/bindings/esp32_spi.h>
|
||||
#include <tactility/bindings/display_placeholder.h>
|
||||
#include <tactility/bindings/touch_placeholder.h>
|
||||
#include <tactility/bindings/pointer_placeholder.h>
|
||||
|
||||
/ {
|
||||
compatible = "root";
|
||||
@@ -43,7 +43,7 @@
|
||||
};
|
||||
|
||||
touch@1 {
|
||||
compatible = "touch-placeholder";
|
||||
compatible = "pointer-placeholder";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <tactility/bindings/esp32_spi.h>
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/display_placeholder.h>
|
||||
#include <tactility/bindings/touch_placeholder.h>
|
||||
#include <tactility/bindings/pointer_placeholder.h>
|
||||
|
||||
/ {
|
||||
compatible = "root";
|
||||
@@ -53,7 +53,7 @@
|
||||
};
|
||||
|
||||
touch@1 {
|
||||
compatible = "touch-placeholder";
|
||||
compatible = "pointer-placeholder";
|
||||
};
|
||||
|
||||
sdcard@2 {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <tactility/device.h>
|
||||
|
||||
extern const struct DeviceType TOUCH_TYPE;
|
||||
extern struct Module gt911_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
@@ -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 = >911_pointer_api,
|
||||
.device_type = &POINTER_TYPE,
|
||||
.owner = >911_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
@@ -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(>911_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(>911_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module gt911_module = {
|
||||
.name = "gt911",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -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
|
||||
@@ -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
|
||||
+3
-18
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <lvgl.h>
|
||||
|
||||
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 {
|
||||
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);
|
||||
lv_indev_t* init();
|
||||
|
||||
/**
|
||||
* @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
|
||||
@@ -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
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
@@ -102,21 +102,33 @@ endif ()
|
||||
# Devicetree code generation
|
||||
#
|
||||
|
||||
add_custom_target(AlwaysRun
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -f "${GENERATED_DIR}/devicetree.c"
|
||||
)
|
||||
add_custom_command(
|
||||
OUTPUT "${GENERATED_DIR}/devicetree.c"
|
||||
"${GENERATED_DIR}/devicetree.h"
|
||||
# A plain add_custom_command(OUTPUT ...) only reruns when its explicit DEPENDS (devicetree.yaml)
|
||||
# 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
|
||||
# DEPENDS can't enumerate, so this used to be forced to always-rerun via a separate "AlwaysRun"
|
||||
# custom target that deleted devicetree.c first (DEPENDS on a *target* only creates an order-only
|
||||
# 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"
|
||||
"${DEVICETREE_LOCATION}" "${GENERATED_DIR}"
|
||||
BYPRODUCTS "${GENERATED_DIR}/devicetree.c" "${GENERATED_DIR}/devicetree.h"
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
DEPENDS AlwaysRun "${DEVICETREE_LOCATION}/devicetree.yaml" # AlwaysRun ensures it always gets built
|
||||
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.h" PROPERTIES GENERATED TRUE)
|
||||
# Update target for generated code
|
||||
target_sources(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}/devicetree.c")
|
||||
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>
|
||||
|
||||
extern struct LvglModuleConfig lvgl_module_config;
|
||||
extern void lvgl_devices_attach();
|
||||
extern void lvgl_devices_detach();
|
||||
|
||||
static bool initialized = false;
|
||||
|
||||
@@ -44,6 +46,8 @@ error_t lvgl_arch_start() {
|
||||
// devices and services. The latter might start adding widgets immediately.
|
||||
initialized = true;
|
||||
|
||||
lvgl_devices_attach();
|
||||
|
||||
if (lvgl_module_config.on_start) lvgl_module_config.on_start();
|
||||
|
||||
return ERROR_NONE;
|
||||
@@ -52,6 +56,8 @@ error_t lvgl_arch_start() {
|
||||
error_t lvgl_arch_stop() {
|
||||
if (lvgl_module_config.on_stop) lvgl_module_config.on_stop();
|
||||
|
||||
lvgl_devices_detach();
|
||||
|
||||
if (lvgl_port_deinit() != ESP_OK) {
|
||||
// Call on_start again to recover
|
||||
if (lvgl_module_config.on_start) lvgl_module_config.on_start();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ idf_component_register(
|
||||
SRCS ${SOURCES}
|
||||
INCLUDE_DIRS "include/"
|
||||
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)
|
||||
|
||||
@@ -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
-2
@@ -2,13 +2,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <tactility/drivers/touch_placeholder.h>
|
||||
#include <tactility/drivers/esp32_adc_oneshot.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(touch_placeholder, struct TouchPlaceholderConfig)
|
||||
DEFINE_DEVICETREE(esp32_adc_oneshot, struct Esp32AdcOneshotConfig)
|
||||
|
||||
#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
Reference in New Issue
Block a user