diff --git a/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp b/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp index 98c76b683..bd07a096c 100644 --- a/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp +++ b/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp @@ -1,129 +1,67 @@ #include "Trackball.h" #include -#include +#include #include +#include + constexpr auto* TAG = "Trackball"; namespace trackball { -static TrackballConfig g_config; static lv_indev_t* g_indev = nullptr; -static std::atomic g_initialized{false}; -static std::atomic g_enabled{true}; -static std::atomic g_mode{Mode::Encoder}; +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; -// Interrupt-driven position tracking (atomic for ISR safety) -static std::atomic g_cursorX{160}; -static std::atomic g_cursorY{120}; -static std::atomic g_buttonPressed{false}; +// Pointer mode cursor position (screen-relative) +static int32_t g_cursorX = 160; +static int32_t g_cursorY = 120; -// Encoder mode: accumulated diff since last read -static std::atomic g_encoderDiff{0}; - -// Sensitivity cached for ISR access (atomic for thread safety) -static std::atomic g_encoderSensitivity{1}; // Steps per tick for encoder -static std::atomic 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(reinterpret_cast(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(x); - data->point.y = static_cast(y); - } - return; +// 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 (currentMode == Mode::Encoder) { - // Read and reset accumulated encoder diff - int32_t diff = g_encoderDiff.exchange(0); - data->enc_diff = static_cast(clamp(diff, INT16_MIN, INT16_MAX)); - - if (diff != 0) { + if (g_mode == Mode::Encoder) { + int32_t ticks = (dx + dy) * static_cast(g_encoderSensitivity); + data->enc_diff = static_cast(clamp(ticks, INT16_MIN, INT16_MAX)); + if (ticks != 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(x); - data->point.y = static_cast(y); + g_cursorX = clamp(g_cursorX + dx * static_cast(g_pointerSensitivity), 0, SCREEN_WIDTH - CURSOR_SIZE - 1); + g_cursorY = clamp(g_cursorY + dy * static_cast(g_pointerSensitivity), 0, SCREEN_HEIGHT - CURSOR_SIZE - 1); + data->point.x = static_cast(g_cursorX); + data->point.y = static_cast(g_cursorY); } - // Button state (same for both modes) - bool pressed = g_buttonPressed.load(std::memory_order_relaxed); + 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) { @@ -131,136 +69,31 @@ static void read_cb(lv_indev_t* indev, lv_indev_data_t* data) { } } -lv_indev_t* init(const TrackballConfig& config) { - if (g_initialized.load(std::memory_order_relaxed)) { +lv_indev_t* init() { + if (g_indev != nullptr) { 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(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(static_cast(dirPins[i]))); - if (err != ESP_OK) { - LOG_E(TAG, "Failed to add ISR for GPIO %d: %s", static_cast(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(config.pinClick), esp_err_to_name(err)); - // Cleanup direction handlers - for (int i = 0; i < 4; i++) { - gpio_isr_handler_remove(dirPins[i]); - } + 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; } - 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; - } + g_cursorX = SCREEN_WIDTH / 2; + g_cursorY = SCREEN_HEIGHT / 2; - // 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]); - } + g_device = nullptr; 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(config.pinRight), - static_cast(config.pinUp), - static_cast(config.pinLeft), - static_cast(config.pinDown), - static_cast(config.pinClick)); + LOG_I(TAG, "Initialized"); return g_indev; } @@ -272,8 +105,6 @@ static void createCursor() { 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"); @@ -291,67 +122,41 @@ static void destroyCursor() { } void deinit() { - if (!g_initialized.load(std::memory_order_relaxed)) return; + if (g_indev == nullptr) 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 - }; + lv_indev_delete(g_indev); + g_indev = nullptr; + g_device = nullptr; - 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); + g_mode = Mode::Encoder; + g_enabled = true; 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); + g_encoderSensitivity = sensitivity; 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); + g_pointerSensitivity = sensitivity; LOG_D(TAG, "Pointer sensitivity set to %d", sensitivity); } } void setEnabled(bool enabled) { - g_enabled.store(enabled, std::memory_order_relaxed); + g_enabled = enabled; - 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 (g_cursor != nullptr) { if (enabled) { - lv_obj_clear_flag(cursor, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(g_cursor, LV_OBJ_FLAG_HIDDEN); } else { - lv_obj_add_flag(cursor, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(g_cursor, LV_OBJ_FLAG_HIDDEN); } } @@ -359,40 +164,35 @@ void setEnabled(bool enabled) { } 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) { + if (g_indev == nullptr) { LOG_W(TAG, "Cannot set mode - not initialized"); return; } - if (g_mode.load(std::memory_order_relaxed) == mode) { + if (g_mode == mode) { return; } - g_mode.store(mode, std::memory_order_relaxed); + g_mode = mode; 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) { + if (!g_enabled && 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); + g_cursorX = SCREEN_WIDTH / 2; + g_cursorY = SCREEN_HEIGHT / 2; 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); + return g_mode; } } diff --git a/Devices/lilygo-tdeck/Source/Trackball/Trackball.h b/Devices/lilygo-tdeck/Source/Trackball/Trackball.h index 4f67c9f87..500e21b72 100644 --- a/Devices/lilygo-tdeck/Source/Trackball/Trackball.h +++ b/Devices/lilygo-tdeck/Source/Trackball/Trackball.h @@ -1,6 +1,5 @@ #pragma once -#include #include 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 diff --git a/Devices/lilygo-tdeck/Source/bindings/tdeck_trackball.h b/Devices/lilygo-tdeck/Source/bindings/tdeck_trackball.h new file mode 100644 index 000000000..2eb630b40 --- /dev/null +++ b/Devices/lilygo-tdeck/Source/bindings/tdeck_trackball.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(tdeck_trackball, struct TdeckTrackballConfig) diff --git a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp deleted file mode 100644 index 11c8d5459..000000000 --- a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "TrackballDevice.h" -#include // Driver -#include -#include -#include - -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; -} diff --git a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.h b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.h deleted file mode 100644 index 7a2492f73..000000000 --- a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include - -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; -}; diff --git a/Devices/lilygo-tdeck/Source/drivers/tdeck_trackball.h b/Devices/lilygo-tdeck/Source/drivers/tdeck_trackball.h new file mode 100644 index 000000000..f87b22dbb --- /dev/null +++ b/Devices/lilygo-tdeck/Source/drivers/tdeck_trackball.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +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 diff --git a/Devices/lilygo-tdeck/Source/module.cpp b/Devices/lilygo-tdeck/Source/module.cpp index 5c239157c..8f2741cf2 100644 --- a/Devices/lilygo-tdeck/Source/module.cpp +++ b/Devices/lilygo-tdeck/Source/module.cpp @@ -9,9 +9,11 @@ #include #include #include +#include #include +#include -#include "devices/TrackballDevice.h" +#include #include @@ -26,6 +28,7 @@ extern "C" { extern Driver tdeck_keyboard_driver; extern Driver tdeck_keyboard_backlight_driver; +extern Driver tdeck_trackball_driver; static bool power_on() { gpio_config_t device_power_signal_config = { @@ -66,16 +69,22 @@ 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 trackball = tt::hal::findDevice("Trackball"); - if (trackball != nullptr) { - LOG_I(TAG, "%s starting", trackball->getName().c_str()); - auto tbDevice = std::static_pointer_cast(trackball); - if (tbDevice->start()) { - LOG_I(TAG, "%s started", trackball->getName().c_str()); - } else { - LOG_E(TAG, "%s start failed", trackball->getName().c_str()); + auto tbSettings = tt::settings::trackball::loadOrGetDefault(); + if (tt::lvgl::lock(100)) { + 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); } + tt::lvgl::unlock(); + } else { + LOG_W(TAG, "Failed to acquire LVGL lock for trackball settings"); } }); } @@ -97,6 +106,11 @@ static error_t start() { return ERROR_RESOURCE; } + if (driver_construct_add(&tdeck_trackball_driver) != ERROR_NONE) { + LOG_E(TAG, "Failed to register trackball driver"); + return ERROR_RESOURCE; + } + subscribe_events(); return ERROR_NONE; @@ -112,6 +126,10 @@ static error_t stop() { LOG_E(TAG, "Failed to unregister keyboard backlight driver"); return ERROR_RESOURCE; } + if (driver_remove_destruct(&tdeck_trackball_driver) != ERROR_NONE) { + LOG_E(TAG, "Failed to unregister trackball driver"); + return ERROR_RESOURCE; + } return ERROR_NONE; } diff --git a/Devices/lilygo-tdeck/Source/tdeck_trackball.cpp b/Devices/lilygo-tdeck/Source/tdeck_trackball.cpp new file mode 100644 index 000000000..ec42f240d --- /dev/null +++ b/Devices/lilygo-tdeck/Source/tdeck_trackball.cpp @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#define TAG "tdeck_trackball" +#define GET_CONFIG(device) (static_cast((device)->config)) +#define GET_INTERNAL(device) (static_cast(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 dx {0}; + std::atomic dy {0}; + std::atomic button_pressed {false}; +}; + +// region ISR callbacks + +static void on_right(void* arg) { + static_cast(arg)->dx.fetch_add(1, std::memory_order_relaxed); +} + +static void on_left(void* arg) { + static_cast(arg)->dx.fetch_sub(1, std::memory_order_relaxed); +} + +static void on_down(void* arg) { + static_cast(arg)->dy.fetch_add(1, std::memory_order_relaxed); +} + +static void on_up(void* arg) { + static_cast(arg)->dy.fetch_sub(1, std::memory_order_relaxed); +} + +static void on_click(void* arg) { + auto* internal = static_cast(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_tdeck_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_tdeck_module, + .internal = nullptr +}; + +} diff --git a/Devices/lilygo-tdeck/bindings/lilygo,tdeck-trackball.yaml b/Devices/lilygo-tdeck/bindings/lilygo,tdeck-trackball.yaml new file mode 100644 index 000000000..5def8bba7 --- /dev/null +++ b/Devices/lilygo-tdeck/bindings/lilygo,tdeck-trackball.yaml @@ -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 diff --git a/Devices/lilygo-tdeck/lilygo,tdeck.dts b/Devices/lilygo-tdeck/lilygo,tdeck.dts index e6abdf07c..75fb8bb9e 100644 --- a/Devices/lilygo-tdeck/lilygo,tdeck.dts +++ b/Devices/lilygo-tdeck/lilygo,tdeck.dts @@ -18,6 +18,7 @@ #include #include #include +#include // Reference: https://wiki.lilygo.cc/get_started/en/Wearable/T-Deck-Plus/T-Deck-Plus.html#Pin-Overview / { @@ -53,6 +54,15 @@ 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 = ;