From c729e8340fe2d09f0641476852599d3a04f5f13b Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Thu, 30 Jul 2026 13:38:19 +0200 Subject: [PATCH] Trackball rewrite (#600) - Implement generic trackball settings - Refactor T-Deck trackball driver into generic driver at `Drivers/gpio-trackball-module` - Automatically bind/unbind trackball devices with LVGL - Automatically load settings for trackball devices on boot --- Devices/lilygo-tdeck-plus/CMakeLists.txt | 2 +- Devices/lilygo-tdeck-plus/devicetree.yaml | 1 + .../lilygo-tdeck-plus/lilygo,tdeck-plus.dts | 4 +- Devices/lilygo-tdeck-plus/source/module.cpp | 36 +-- Devices/lilygo-tdeck/CMakeLists.txt | 2 +- Devices/lilygo-tdeck/devicetree.yaml | 1 + Devices/lilygo-tdeck/lilygo,tdeck.dts | 4 +- Devices/lilygo-tdeck/source/module.cpp | 41 +-- Drivers/gpio-trackball-module/CMakeLists.txt | 11 + .../LICENSE-Apache-2.0.md | 195 ++++++++++++++ Drivers/gpio-trackball-module/README.md | 27 ++ .../bindings/tactility,gpio-trackball.yaml} | 4 +- Drivers/gpio-trackball-module/devicetree.yaml | 3 + .../include/bindings/gpio_trackball.h | 7 + .../include/drivers/gpio_trackball.h | 25 ++ .../include/gpio_trackball_module.h | 14 + .../source/gpio_trackball.cpp} | 54 ++-- .../gpio-trackball-module/source/module.cpp | 19 ++ .../include/lilygo/bindings/tdeck_trackball.h | 7 - .../include/lilygo/drivers/trackball.h | 56 ---- Drivers/lilygo-module/source/module.cpp | 2 - Drivers/lilygo-module/source/trackball.cpp | 201 -------------- .../include/lvgl/devices/device_context.h | 26 ++ .../include/lvgl/devices/trackball.h | 107 ++++++++ Modules/lvgl-module/source/devices/devices.c | 119 -------- .../lvgl-module/source/devices/devices.cpp | 185 +++++++++++++ .../source/devices/{display.c => display.cpp} | 63 +++-- .../lvgl-module/source/devices/keyboard.cpp | 35 ++- .../source/devices/{pointer.c => pointer.cpp} | 40 +-- .../lvgl-module/source/devices/trackball.cpp | 255 ++++++++++++++++++ .../Tactility/settings/TrackballSettings.h | 22 +- .../Private/Tactility/lvgl/TrackballInit.h | 10 + Tactility/Source/Tactility.cpp | 18 +- .../app/trackball/TrackballSettings.cpp | 126 +++++---- Tactility/Source/lvgl/TrackballInit.cpp | 45 ++++ .../Source/settings/TrackballSettings.cpp | 38 ++- .../include/tactility/drivers/trackball.h | 26 +- TactilityKernel/source/drivers/trackball.cpp | 24 ++ TactilityKernel/source/symbols.c | 5 + 39 files changed, 1194 insertions(+), 666 deletions(-) create mode 100644 Drivers/gpio-trackball-module/CMakeLists.txt create mode 100644 Drivers/gpio-trackball-module/LICENSE-Apache-2.0.md create mode 100644 Drivers/gpio-trackball-module/README.md rename Drivers/{lilygo-module/bindings/lilygo,tdeck-trackball.yaml => gpio-trackball-module/bindings/tactility,gpio-trackball.yaml} (80%) create mode 100644 Drivers/gpio-trackball-module/devicetree.yaml create mode 100644 Drivers/gpio-trackball-module/include/bindings/gpio_trackball.h create mode 100644 Drivers/gpio-trackball-module/include/drivers/gpio_trackball.h create mode 100644 Drivers/gpio-trackball-module/include/gpio_trackball_module.h rename Drivers/{lilygo-module/source/tdeck_trackball.cpp => gpio-trackball-module/source/gpio_trackball.cpp} (74%) create mode 100644 Drivers/gpio-trackball-module/source/module.cpp delete mode 100644 Drivers/lilygo-module/include/lilygo/bindings/tdeck_trackball.h delete mode 100644 Drivers/lilygo-module/include/lilygo/drivers/trackball.h delete mode 100644 Drivers/lilygo-module/source/trackball.cpp create mode 100644 Modules/lvgl-module/include/lvgl/devices/device_context.h create mode 100644 Modules/lvgl-module/include/lvgl/devices/trackball.h delete mode 100644 Modules/lvgl-module/source/devices/devices.c create mode 100644 Modules/lvgl-module/source/devices/devices.cpp rename Modules/lvgl-module/source/devices/{display.c => display.cpp} (92%) rename Modules/lvgl-module/source/devices/{pointer.c => pointer.cpp} (79%) create mode 100644 Modules/lvgl-module/source/devices/trackball.cpp create mode 100644 Tactility/Private/Tactility/lvgl/TrackballInit.h create mode 100644 Tactility/Source/lvgl/TrackballInit.cpp rename Drivers/lilygo-module/include/lilygo/drivers/tdeck_trackball.h => TactilityKernel/include/tactility/drivers/trackball.h (65%) create mode 100644 TactilityKernel/source/drivers/trackball.cpp diff --git a/Devices/lilygo-tdeck-plus/CMakeLists.txt b/Devices/lilygo-tdeck-plus/CMakeLists.txt index 34acc969a..37631092a 100644 --- a/Devices/lilygo-tdeck-plus/CMakeLists.txt +++ b/Devices/lilygo-tdeck-plus/CMakeLists.txt @@ -2,5 +2,5 @@ file(GLOB_RECURSE SOURCE_FILES source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} - REQUIRES Tactility driver lilygo-module + REQUIRES TactilityKernel lilygo-module ) diff --git a/Devices/lilygo-tdeck-plus/devicetree.yaml b/Devices/lilygo-tdeck-plus/devicetree.yaml index 8634f22b4..c1eb7204a 100644 --- a/Devices/lilygo-tdeck-plus/devicetree.yaml +++ b/Devices/lilygo-tdeck-plus/devicetree.yaml @@ -4,6 +4,7 @@ dependencies: - Drivers/st7789-module - Drivers/gt911-module - Drivers/lilygo-module + - Drivers/gpio-trackball-module - Drivers/es7210-module - Drivers/dummy-i2s-amp-module - Drivers/audio-stream-module diff --git a/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts b/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts index f9bd97d01..961b9d851 100644 --- a/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts +++ b/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts @@ -19,12 +19,12 @@ #include #include #include +#include #include #include #include -#include // Reference: https://wiki.lilygo.cc/products/t-deck-series/t-deck-plus/ / { @@ -61,7 +61,7 @@ }; trackball { - compatible = "lilygo,tdeck-trackball"; + compatible = "tactility,gpio-trackball"; pin-right = <&gpio0 2 GPIO_FLAG_NONE>; pin-up = <&gpio0 3 GPIO_FLAG_NONE>; pin-left = <&gpio0 1 GPIO_FLAG_NONE>; diff --git a/Devices/lilygo-tdeck-plus/source/module.cpp b/Devices/lilygo-tdeck-plus/source/module.cpp index 1c5d29aeb..2ae197226 100644 --- a/Devices/lilygo-tdeck-plus/source/module.cpp +++ b/Devices/lilygo-tdeck-plus/source/module.cpp @@ -1,57 +1,25 @@ -#include #include #include #include -#include -#include -#include - -#include - -#include #include constexpr auto* TAG = "tdeck-plus"; extern "C" { -void init_trackball() { - 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 void on_boot_completed(struct SystemEvent* /*event*/, void* /*context*/) { - init_trackball(); -} - static error_t start() { - LOG_I(TAG, LOG_MESSAGE_POWER_ON_START); + LOG_I(TAG, "Power on start"); if (!tdeck_power_on()) { - LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED); + LOG_E(TAG, "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. - delay_millis(100); - - system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr); - return ERROR_NONE; } static error_t stop() { - system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed); return ERROR_NONE; } diff --git a/Devices/lilygo-tdeck/CMakeLists.txt b/Devices/lilygo-tdeck/CMakeLists.txt index 34acc969a..37631092a 100644 --- a/Devices/lilygo-tdeck/CMakeLists.txt +++ b/Devices/lilygo-tdeck/CMakeLists.txt @@ -2,5 +2,5 @@ file(GLOB_RECURSE SOURCE_FILES source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} - REQUIRES Tactility driver lilygo-module + REQUIRES TactilityKernel lilygo-module ) diff --git a/Devices/lilygo-tdeck/devicetree.yaml b/Devices/lilygo-tdeck/devicetree.yaml index 59fbd4fc9..d4fac1fc6 100644 --- a/Devices/lilygo-tdeck/devicetree.yaml +++ b/Devices/lilygo-tdeck/devicetree.yaml @@ -3,6 +3,7 @@ dependencies: - Drivers/st7789-module - Drivers/gt911-module - Drivers/lilygo-module + - Drivers/gpio-trackball-module - Drivers/es7210-module - Drivers/dummy-i2s-amp-module - Drivers/audio-stream-module diff --git a/Devices/lilygo-tdeck/lilygo,tdeck.dts b/Devices/lilygo-tdeck/lilygo,tdeck.dts index 62ede4f77..d915f1247 100644 --- a/Devices/lilygo-tdeck/lilygo,tdeck.dts +++ b/Devices/lilygo-tdeck/lilygo,tdeck.dts @@ -19,10 +19,10 @@ #include #include #include +#include #include #include -#include // Reference: https://wiki.lilygo.cc/products/t-deck-series/t-deck/ / { @@ -59,7 +59,7 @@ }; trackball { - compatible = "lilygo,tdeck-trackball"; + compatible = "tactility,gpio-trackball"; pin-right = <&gpio0 2 GPIO_FLAG_NONE>; pin-up = <&gpio0 3 GPIO_FLAG_NONE>; pin-left = <&gpio0 1 GPIO_FLAG_NONE>; diff --git a/Devices/lilygo-tdeck/source/module.cpp b/Devices/lilygo-tdeck/source/module.cpp index e9863403f..a5590c11f 100644 --- a/Devices/lilygo-tdeck/source/module.cpp +++ b/Devices/lilygo-tdeck/source/module.cpp @@ -1,55 +1,20 @@ -#include -#include #include #include +#include -#include - -#include - -#include -#include -#include - -#include #include constexpr auto* TAG = "tdeck"; extern "C" { -static void on_boot_completed(struct SystemEvent* /*event*/, void* /*context*/) { - 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(); -} - -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. - system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr); -} - static error_t start() { - LOG_I(TAG, LOG_MESSAGE_POWER_ON_START); + LOG_I(TAG, "Power on start"); if (!tdeck_power_on()) { - LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED); + LOG_E(TAG, "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. - delay_millis(100); - - subscribe_events(); - return ERROR_NONE; } diff --git a/Drivers/gpio-trackball-module/CMakeLists.txt b/Drivers/gpio-trackball-module/CMakeLists.txt new file mode 100644 index 000000000..5aa4fb108 --- /dev/null +++ b/Drivers/gpio-trackball-module/CMakeLists.txt @@ -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(gpio-trackball-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/gpio-trackball-module/LICENSE-Apache-2.0.md b/Drivers/gpio-trackball-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/gpio-trackball-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/gpio-trackball-module/README.md b/Drivers/gpio-trackball-module/README.md new file mode 100644 index 000000000..97dec2075 --- /dev/null +++ b/Drivers/gpio-trackball-module/README.md @@ -0,0 +1,27 @@ +# GPIO Trackball + +Kernel driver for a 5-way GPIO trackball (4 direction pins + a click button), exposing a +`TRACKBALL_TYPE` device (`tactility/drivers/trackball.h`). + +Devicetree binding: `tactility,gpio-trackball` (see `bindings/tactility,gpio-trackball.yaml`). + +Each direction pin is wired to a falling-edge interrupt that accumulates a signed delta; +the click pin is active-low and read on any edge. `read_delta()` drains and resets the +accumulated delta on each call. + +## Example + +```dts +trackball { + compatible = "tactility,gpio-trackball"; + pin-right = <&gpio0 1 GPIO_FLAG_NONE>; + pin-up = <&gpio0 2 GPIO_FLAG_NONE>; + pin-left = <&gpio0 3 GPIO_FLAG_NONE>; + pin-down = <&gpio0 4 GPIO_FLAG_NONE>; + pin-click = <&gpio0 5 GPIO_FLAG_NONE>; +}; +``` + +## License + +[Apache License Version 2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/lilygo-module/bindings/lilygo,tdeck-trackball.yaml b/Drivers/gpio-trackball-module/bindings/tactility,gpio-trackball.yaml similarity index 80% rename from Drivers/lilygo-module/bindings/lilygo,tdeck-trackball.yaml rename to Drivers/gpio-trackball-module/bindings/tactility,gpio-trackball.yaml index 5def8bba7..5c480c582 100644 --- a/Drivers/lilygo-module/bindings/lilygo,tdeck-trackball.yaml +++ b/Drivers/gpio-trackball-module/bindings/tactility,gpio-trackball.yaml @@ -1,6 +1,6 @@ -description: LilyGO T-Deck 5-way GPIO trackball (4 directions + click button) +description: 5-way GPIO trackball (4 directions + click button) -compatible: "lilygo,tdeck-trackball" +compatible: "tactility,gpio-trackball" properties: pin-right: diff --git a/Drivers/gpio-trackball-module/devicetree.yaml b/Drivers/gpio-trackball-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/gpio-trackball-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/gpio-trackball-module/include/bindings/gpio_trackball.h b/Drivers/gpio-trackball-module/include/bindings/gpio_trackball.h new file mode 100644 index 000000000..ad4c5cfb7 --- /dev/null +++ b/Drivers/gpio-trackball-module/include/bindings/gpio_trackball.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +DEFINE_DEVICETREE(gpio_trackball, struct GpioTrackballConfig) diff --git a/Drivers/gpio-trackball-module/include/drivers/gpio_trackball.h b/Drivers/gpio-trackball-module/include/drivers/gpio_trackball.h new file mode 100644 index 000000000..d1a233aad --- /dev/null +++ b/Drivers/gpio-trackball-module/include/drivers/gpio_trackball.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +struct Device; + +struct GpioTrackballConfig { + struct GpioPinSpec pin_right; + struct GpioPinSpec pin_up; + struct GpioPinSpec pin_left; + struct GpioPinSpec pin_down; + struct GpioPinSpec pin_click; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/gpio-trackball-module/include/gpio_trackball_module.h b/Drivers/gpio-trackball-module/include/gpio_trackball_module.h new file mode 100644 index 000000000..2c51b4e74 --- /dev/null +++ b/Drivers/gpio-trackball-module/include/gpio_trackball_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module gpio_trackball_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/lilygo-module/source/tdeck_trackball.cpp b/Drivers/gpio-trackball-module/source/gpio_trackball.cpp similarity index 74% rename from Drivers/lilygo-module/source/tdeck_trackball.cpp rename to Drivers/gpio-trackball-module/source/gpio_trackball.cpp index 9ec7881ee..895ecdee9 100644 --- a/Drivers/lilygo-module/source/tdeck_trackball.cpp +++ b/Drivers/gpio-trackball-module/source/gpio_trackball.cpp @@ -1,20 +1,22 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#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))) +constexpr auto* TAG = "gpio_trackball"; -struct TdeckTrackballInternal { +#define GET_CONFIG(device) (static_cast((device)->config)) +#define GET_INTERNAL(device) (static_cast(device_get_driver_data(device))) + +struct GpioTrackballInternal { GpioDescriptor* pin_right = nullptr; GpioDescriptor* pin_up = nullptr; GpioDescriptor* pin_left = nullptr; @@ -29,23 +31,23 @@ struct TdeckTrackballInternal { // region ISR callbacks static void on_right(void* arg) { - static_cast(arg)->dx.fetch_add(1, std::memory_order_relaxed); + 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_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_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_cast(arg)->dy.fetch_sub(1, std::memory_order_relaxed); } static void on_click(void* arg) { - auto* internal = static_cast(arg); + auto* internal = static_cast(arg); bool high = true; gpio_descriptor_get_level(internal->pin_click, &high); // Active low: pressed when level is low @@ -90,7 +92,7 @@ static void release_pin(GpioDescriptor*& descriptor) { descriptor = nullptr; } -static void release_all_pins(TdeckTrackballInternal* internal) { +static void release_all_pins(GpioTrackballInternal* internal) { release_pin(internal->pin_right); release_pin(internal->pin_up); release_pin(internal->pin_left); @@ -114,19 +116,11 @@ static error_t get_button_pressed(Device* device, bool* out_pressed) { 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(); + auto* internal = new(std::nothrow) GpioTrackballInternal(); if (internal == nullptr) { return ERROR_OUT_OF_MEMORY; } @@ -168,25 +162,21 @@ static error_t stop(Device* device) { return ERROR_NONE; } -static constexpr TdeckTrackballApi TDECK_TRACKBALL_API = { +static constexpr TrackballApi GPIO_TRACKBALL_API = { .read_delta = read_delta, .get_button_pressed = get_button_pressed, }; -const struct DeviceType TDECK_TRACKBALL_TYPE { - .name = "tdeck-trackball" -}; +extern Module gpio_trackball_module; -extern Module lilygo_module; - -Driver tdeck_trackball_driver = { - .name = "tdeck_trackball", - .compatible = (const char*[]) { "lilygo,tdeck-trackball", nullptr }, +Driver gpio_trackball_driver = { + .name = "gpio_trackball", + .compatible = (const char*[]) { "tactility,gpio-trackball", nullptr }, .start_device = start, .stop_device = stop, - .api = &TDECK_TRACKBALL_API, - .device_type = &TDECK_TRACKBALL_TYPE, - .owner = &lilygo_module, + .api = &GPIO_TRACKBALL_API, + .device_type = &TRACKBALL_TYPE, + .owner = &gpio_trackball_module, .internal = nullptr }; diff --git a/Drivers/gpio-trackball-module/source/module.cpp b/Drivers/gpio-trackball-module/source/module.cpp new file mode 100644 index 000000000..0b463f445 --- /dev/null +++ b/Drivers/gpio-trackball-module/source/module.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +extern "C" { + +extern Driver gpio_trackball_driver; + +static Driver* const gpio_trackball_drivers[] = { + &gpio_trackball_driver, + nullptr +}; + +Module gpio_trackball_module = { + .name = "gpio-trackball", + .drivers = gpio_trackball_drivers +}; + +} // extern "C" diff --git a/Drivers/lilygo-module/include/lilygo/bindings/tdeck_trackball.h b/Drivers/lilygo-module/include/lilygo/bindings/tdeck_trackball.h deleted file mode 100644 index 244f78c89..000000000 --- a/Drivers/lilygo-module/include/lilygo/bindings/tdeck_trackball.h +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -DEFINE_DEVICETREE(tdeck_trackball, struct TdeckTrackballConfig) diff --git a/Drivers/lilygo-module/include/lilygo/drivers/trackball.h b/Drivers/lilygo-module/include/lilygo/drivers/trackball.h deleted file mode 100644 index 500e21b72..000000000 --- a/Drivers/lilygo-module/include/lilygo/drivers/trackball.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once - -#include - -namespace trackball { - -/** - * @brief Trackball operating mode - */ -enum class Mode { - Encoder, // Navigation via enc_diff (scroll wheel behavior) - Pointer // Mouse cursor via point.x/y -}; - -/** - * @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 - */ -lv_indev_t* init(); - -/** - * @brief Deinitialize trackball - */ -void deinit(); - -/** - * @brief Set encoder mode sensitivity - * @param sensitivity Steps per trackball tick (1-10, default: 1) - */ -void setEncoderSensitivity(uint8_t sensitivity); - -/** - * @brief Set pointer mode sensitivity - * @param sensitivity Pixels per trackball tick (1-10, default: 10) - */ -void setPointerSensitivity(uint8_t sensitivity); - -/** - * @brief Enable or disable trackball input processing - * @param enabled Boolean value to enable or disable - */ -void setEnabled(bool enabled); - -/** - * @brief Set trackball operating mode - * @param mode Encoder or Pointer mode - */ -void setMode(Mode mode); - -/** - * @brief Get current trackball operating mode - * @return Current mode - */ -Mode getMode(); - -} diff --git a/Drivers/lilygo-module/source/module.cpp b/Drivers/lilygo-module/source/module.cpp index c68103fd8..87b301abf 100644 --- a/Drivers/lilygo-module/source/module.cpp +++ b/Drivers/lilygo-module/source/module.cpp @@ -6,13 +6,11 @@ extern "C" { extern Driver tdeck_keyboard_driver; extern Driver tdeck_keyboard_backlight_driver; -extern Driver tdeck_trackball_driver; extern Driver tpager_encoder_driver; static Driver* const lilygo_drivers[] = { &tdeck_keyboard_driver, &tdeck_keyboard_backlight_driver, - &tdeck_trackball_driver, &tpager_encoder_driver, nullptr }; diff --git a/Drivers/lilygo-module/source/trackball.cpp b/Drivers/lilygo-module/source/trackball.cpp deleted file mode 100644 index 4f6a0484a..000000000 --- a/Drivers/lilygo-module/source/trackball.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include -#include - -#include - -#include -#include - - -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(g_encoderSensitivity); - data->enc_diff = static_cast(clamp(ticks, INT16_MIN, INT16_MAX)); - if (ticks != 0) { - lv_display_trigger_activity(nullptr); - } - } else { - 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); - } - - 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; - } - - if (device_get_first_active_by_type(&TDECK_TRACKBALL_TYPE, &g_device) != ERROR_NONE) { - 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"); - device_put(g_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; - - device_put(g_device); - 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; -} - -} diff --git a/Modules/lvgl-module/include/lvgl/devices/device_context.h b/Modules/lvgl-module/include/lvgl/devices/device_context.h new file mode 100644 index 000000000..98159b9ae --- /dev/null +++ b/Modules/lvgl-module/include/lvgl/devices/device_context.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +struct Device; + +/** + * Common driver-data wrapper attached to every LVGL display/indev created by this module: bundles + * the kernel Device* with an optional device-type-specific context blob (LvglDisplayCtx, + * LvglPointerCtx, LvglTrackballCtx, ...), so each device type doesn't have to store its own Device* + * redundantly. Owns context: any resources referenced through it (buffers, cursor objects, etc.) + * must be released by the caller before the wrapper is deleted, since context is trivially + * destructible and only its own memory is freed here. + */ +struct LvglDeviceContext { + struct Device* device = nullptr; + void* context; + + explicit LvglDeviceContext(void* context) : context(context) {} + + ~LvglDeviceContext() { + if (context != nullptr) { + ::operator delete(context); + } + } +}; diff --git a/Modules/lvgl-module/include/lvgl/devices/trackball.h b/Modules/lvgl-module/include/lvgl/devices/trackball.h new file mode 100644 index 000000000..15e37d1d8 --- /dev/null +++ b/Modules/lvgl-module/include/lvgl/devices/trackball.h @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Trackball operating mode. + */ +enum LvglTrackballMode { + /** Navigation via enc_diff (scroll wheel behavior). No cursor is shown. */ + LVGL_TRACKBALL_MODE_ENCODER, + /** Mouse cursor via point.x/point.y. A cursor is shown if an image was set via + * lvgl_trackball_set_cursor_image(). */ + LVGL_TRACKBALL_MODE_POINTER +}; + +/** + * @brief Runtime-adjustable trackball behavior. See lvgl_trackball_set_settings()/ + * lvgl_trackball_get_settings(). + */ +struct LvglTrackballSettings { + enum LvglTrackballMode mode; + /** When false, movement and button presses are drained from the device and discarded: + * the indev reports no movement and stays released. */ + bool enabled; + /** Encoder mode: steps per raw trackball tick. Must be >= 1. */ + uint8_t encoder_sensitivity; + /** Pointer mode: pixels per raw trackball tick. Must be >= 1. */ + uint8_t pointer_sensitivity; +}; + +/** + * @brief Creates an lv_indev_t bound to the given TRACKBALL_TYPE device and registers a read + * callback that polls the device through its TrackballApi. + * + * The indev starts in LVGL_TRACKBALL_MODE_ENCODER, enabled, with default sensitivities (see + * lvgl_trackball_settings_get_default()). Call lvgl_trackball_set_settings() to change this, + * e.g. to switch to pointer mode - each device added this way can independently run with or + * without pointer support. + * + * @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 TRACKBALL_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 TRACKBALL_TYPE + * @retval ERROR_OUT_OF_MEMORY if allocation failed + */ +error_t lvgl_trackball_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev); + +/** + * @brief Removes an indev previously created with lvgl_trackball_add(), including its cursor + * object (if one was showing). + * @warning Caller must hold the LVGL lock. + */ +void lvgl_trackball_remove(lv_indev_t* indev); + +/** + * @return the default (all fields explicitly set) LvglTrackballSettings: encoder mode, enabled, + * encoder_sensitivity 1, pointer_sensitivity 10. + */ +struct LvglTrackballSettings lvgl_trackball_settings_get_default(void); + +/** + * @brief Applies settings to an indev previously created with lvgl_trackball_add(). Switching + * `mode` shows/hides the cursor (pointer mode) and re-centers it on the indev's display. + * @warning Caller must hold the LVGL lock. + * @param[in] indev an indev previously created by lvgl_trackball_add() + * @param[in] settings the settings to apply + * @retval ERROR_NONE on success + * @retval ERROR_INVALID_ARGUMENT if indev or settings is NULL, or either sensitivity is 0 + */ +error_t lvgl_trackball_set_settings(lv_indev_t* indev, const struct LvglTrackballSettings* settings); + +/** + * @brief Retrieves the settings currently active on indev. + * @warning Caller must hold the LVGL lock. + * @return true when indev is a trackball indev (out_settings is filled), false otherwise + */ +bool lvgl_trackball_get_settings(lv_indev_t* indev, struct LvglTrackballSettings* out_settings); + +/** + * @brief Sets the image shown as the mouse cursor while indev is in LVGL_TRACKBALL_MODE_POINTER. + * lvgl-module has no bundled cursor asset (it doesn't depend on any particular asset layout), so + * pointer mode has no visible cursor until a caller supplies one here; the indev still moves and + * reports point.x/point.y either way. Pass NULL to remove the cursor. + * @warning Caller must hold the LVGL lock. + * @param[in] indev an indev previously created by lvgl_trackball_add() + * @param[in] image_src an LVGL image source (as accepted by lv_image_set_src()), or NULL + */ +void lvgl_trackball_set_cursor_image(lv_indev_t* indev, const void* image_src); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/lvgl-module/source/devices/devices.c b/Modules/lvgl-module/source/devices/devices.c deleted file mode 100644 index a119b61e4..000000000 --- a/Modules/lvgl-module/source/devices/devices.c +++ /dev/null @@ -1,119 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#define TAG "lvgl" - -void lvgl_devices_attach() { - lvgl_lock(); - - lv_disp_t* lvgl_display = NULL; - bool display_updates_slowly = false; - struct Device* kernel_display_device = NULL; - device_get_first_by_type(&DISPLAY_TYPE, &kernel_display_device); - - // 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) { - device_put(kernel_display_device); - kernel_display_device = NULL; - } - - if (kernel_display_device != NULL) { - uint16_t vres = display_get_resolution_y(kernel_display_device); - enum DisplayColorFormat color_format = display_get_color_format(kernel_display_device); - bool swap_bytes = color_format == DISPLAY_COLOR_FORMAT_RGB565_SWAPPED || - color_format == DISPLAY_COLOR_FORMAT_BGR565_SWAPPED || - color_format == DISPLAY_COLOR_FORMAT_BGR565; - bool display_requires_full_frame = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME); - display_updates_slowly = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_SLOW_REFRESH); - // Without CAP_SWAP_XY the driver can't rotate 90/270 in hardware (display_swap_xy() is - // null and silently skipped by lvgl_display_apply_rotation()) - LVGL would still switch - // its own logical w/h for those rotations, mismatching the panel's fixed physical - // orientation (e.g. RGB/DPI panels, whose video timing is fixed at panel-init time). - // sw_rotate makes LVGL rotate the rendered pixels in software instead, so the driver - // itself is never asked to do something it can't. - bool can_hw_rotate = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_SWAP_XY) && - display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_MIRROR); - struct LvglDisplayConfig lvgl_display_config = { - .buffer_height = vres > 10 ? vres / 10 : vres, - .sw_rotate = !can_hw_rotate, - .swap_bytes = swap_bytes, - .force_full_frame = display_requires_full_frame - }; - 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); - } - device_put(kernel_display_device); - } - - struct Device* kernel_pointer_device = NULL; - device_get_first_by_type(&POINTER_TYPE, &kernel_pointer_device); - if (kernel_pointer_device != NULL && device_get_driver(kernel_pointer_device)->api == NULL) { - device_put(kernel_pointer_device); - kernel_pointer_device = NULL; - } - if (kernel_pointer_device != NULL) { - lv_indev_t* lvgl_pointer_device; - 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); - // Slow panels cause taps to be missed due to the long update time, prevent that - if (display_updates_slowly ) { - lv_indev_set_long_press_time(lvgl_pointer_device, 2000); - } - } else { - LOG_E(TAG, "Failed to bind %s to LVG", kernel_pointer_device->name); - } - device_put(kernel_pointer_device); - } - - struct Device* kernel_keyboard_device = NULL; - device_get_first_by_type(&KEYBOARD_TYPE, &kernel_keyboard_device); - 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); - } - device_put(kernel_keyboard_device); - } - - 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) { - lvgl_display_remove(display); - display = lv_disp_get_next(NULL); - } - - lvgl_unlock(); -} diff --git a/Modules/lvgl-module/source/devices/devices.cpp b/Modules/lvgl-module/source/devices/devices.cpp new file mode 100644 index 000000000..6b70b80ed --- /dev/null +++ b/Modules/lvgl-module/source/devices/devices.cpp @@ -0,0 +1,185 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +constexpr auto* TAG = "lvgl"; + +// Boards are not expected to expose more devices of a single type than this; device_for_each_of_type() +// callbacks run under the device ledger lock, so devices are collected here and processed afterwards. +constexpr auto LVGL_DEVICES_MAX_PER_TYPE = 8; + +struct LvglDeviceList { + struct Device* devices[LVGL_DEVICES_MAX_PER_TYPE]; + size_t count; +}; + +extern "C" { + +static bool lvgl_device_list_collect(struct Device* device, void* context) { + struct LvglDeviceList* list = (struct LvglDeviceList*)context; + if (list->count < LVGL_DEVICES_MAX_PER_TYPE) { + list->devices[list->count++] = device; + } + return true; +} + +void lvgl_devices_attach() { + lvgl_lock(); + + lv_disp_t* lvgl_display = NULL; + bool display_updates_slowly = false; + + struct LvglDeviceList display_devices = {0}; + device_for_each_of_type(&DISPLAY_TYPE, &display_devices, lvgl_device_list_collect); + for (size_t i = 0; i < display_devices.count; i++) { + struct Device* kernel_display_device = display_devices.devices[i]; + + // 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 (device_get_driver(kernel_display_device)->api == NULL) { + continue; + } + + uint16_t vres = display_get_resolution_y(kernel_display_device); + enum DisplayColorFormat color_format = display_get_color_format(kernel_display_device); + bool swap_bytes = color_format == DISPLAY_COLOR_FORMAT_RGB565_SWAPPED || + color_format == DISPLAY_COLOR_FORMAT_BGR565_SWAPPED || + color_format == DISPLAY_COLOR_FORMAT_BGR565; + bool display_requires_full_frame = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME); + // Without CAP_SWAP_XY the driver can't rotate 90/270 in hardware (display_swap_xy() is + // null and silently skipped by lvgl_display_apply_rotation()) - LVGL would still switch + // its own logical w/h for those rotations, mismatching the panel's fixed physical + // orientation (e.g. RGB/DPI panels, whose video timing is fixed at panel-init time). + // sw_rotate makes LVGL rotate the rendered pixels in software instead, so the driver + // itself is never asked to do something it can't. + bool can_hw_rotate = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_SWAP_XY) && + display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_MIRROR); + struct LvglDisplayConfig lvgl_display_config = { + .buffer_height = (uint16_t)(vres > 10 ? vres / 10 : vres), + .sw_rotate = !can_hw_rotate, + .swap_bytes = swap_bytes, + .force_full_frame = display_requires_full_frame + }; + lv_disp_t* added_display = NULL; + if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &added_display) == ERROR_NONE) { + LOG_I(TAG, "Bound %s to LVGL", kernel_display_device->name); + // Pointers/keyboards below bind to the first display bound here, matching that display's + // refresh behavior. + if (lvgl_display == NULL) { + lvgl_display = added_display; + display_updates_slowly = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_SLOW_REFRESH); + } + } else { + LOG_E(TAG, "Failed to bind %s to LVGL", kernel_display_device->name); + } + } + + struct LvglDeviceList pointer_devices = {0}; + device_for_each_of_type(&POINTER_TYPE, &pointer_devices, lvgl_device_list_collect); + for (size_t i = 0; i < pointer_devices.count; i++) { + struct Device* kernel_pointer_device = pointer_devices.devices[i]; + if (device_get_driver(kernel_pointer_device)->api == NULL) { + continue; + } + + lv_indev_t* lvgl_pointer_device; + 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); + // Slow panels cause taps to be missed due to the long update time, prevent that + if (display_updates_slowly ) { + lv_indev_set_long_press_time(lvgl_pointer_device, 2000); + } + } else { + LOG_E(TAG, "Failed to bind %s to LVG", kernel_pointer_device->name); + } + } + + struct LvglDeviceList keyboard_devices = {0}; + device_for_each_of_type(&KEYBOARD_TYPE, &keyboard_devices, lvgl_device_list_collect); + for (size_t i = 0; i < keyboard_devices.count; i++) { + struct Device* kernel_keyboard_device = keyboard_devices.devices[i]; + lv_indev_t* lvgl_keyboard_device; + 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); + } + } + + struct Device* kernel_trackball_device = NULL; + device_get_first_by_type(&TRACKBALL_TYPE, &kernel_trackball_device); + if (kernel_trackball_device != NULL) { + lv_indev_t* lvgl_trackball_device; + if (lvgl_trackball_add(kernel_trackball_device, lvgl_display, &lvgl_trackball_device) == ERROR_NONE) { + LOG_I(TAG, "Bound %s to LVGL", kernel_trackball_device->name); + } else { + LOG_E(TAG, "Failed to bind %s to LVGL", kernel_trackball_device->name); + } + device_put(kernel_trackball_device); + } + + lvgl_unlock(); +} + +void lvgl_devices_detach() { + lvgl_lock(); + + lv_indev_t* indev = lv_indev_get_next(NULL); + while (indev != NULL) { + lv_indev_type_t type = lv_indev_get_type(indev); + if (type == LV_INDEV_TYPE_POINTER || type == LV_INDEV_TYPE_ENCODER) { + bool handled = false; + void* driver_data = lv_indev_get_driver_data(indev); + if (driver_data != nullptr) { + // Trackball is a special pointer type as it can operate as a mouse or as a device that changes widget focus. + LvglDeviceContext* context = static_cast(driver_data); + if (context->device != nullptr) { + const DeviceType* device_type = device_get_type(context->device); + if (device_type == &TRACKBALL_TYPE) { + lvgl_trackball_remove(indev); + handled = true; + } else if (device_type == &POINTER_TYPE) { + lvgl_pointer_remove(indev); + handled = true; + } else { + LOG_E(TAG, "Unknown pointer device with possible memory leak of driver data"); + } + } else { + LOG_W(TAG, "Unknown pointer device (no data attached)"); + } + } + if (!handled) { + lv_indev_delete(indev); + } + } else if (type == LV_INDEV_TYPE_KEYPAD) { + lvgl_keyboard_remove(indev); + } else { + lv_indev_delete(indev); + } + // Always get the first item, because getting the next one doesn't work as the current pointer just became corrupt + indev = lv_indev_get_next(NULL); + } + + lv_disp_t* display = lv_disp_get_next(NULL); + while (display != NULL) { + lvgl_display_remove(display); + display = lv_disp_get_next(NULL); + } + + lvgl_unlock(); +} + +} // extern C diff --git a/Modules/lvgl-module/source/devices/display.c b/Modules/lvgl-module/source/devices/display.cpp similarity index 92% rename from Modules/lvgl-module/source/devices/display.c rename to Modules/lvgl-module/source/devices/display.cpp index 9f523a5b3..b43a7927e 100644 --- a/Modules/lvgl-module/source/devices/display.c +++ b/Modules/lvgl-module/source/devices/display.cpp @@ -8,16 +8,17 @@ #include #include +#include + #include #ifdef ESP_PLATFORM #include #endif -#define TAG "lvgl_display" +constexpr auto* 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) @@ -108,7 +109,9 @@ static bool lvgl_display_map_color_format(enum DisplayColorFormat in, lv_color_f } } -static void lvgl_display_apply_rotation(struct LvglDisplayCtx* ctx, lv_display_rotation_t rotation) { +static void lvgl_display_apply_rotation(struct LvglDeviceContext* wrapper, lv_display_rotation_t rotation) { + struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)wrapper->context; + // SW-rotated displays stay in their base orientation; rotation is applied per-flush instead. if (ctx->sw_rotate) { return; @@ -144,10 +147,10 @@ static void lvgl_display_apply_rotation(struct LvglDisplayCtx* ctx, lv_display_r } if (ctx->has_swap_xy_cap) { - display_swap_xy(ctx->device, swap_xy); + display_swap_xy(wrapper->device, swap_xy); } if (ctx->has_mirror_cap) { - display_mirror(ctx->device, mirror_x, mirror_y); + display_mirror(wrapper->device, mirror_x, mirror_y); } if (ctx->has_set_gap_cap) { // set_gap() takes its (x,y) in whatever axes the panel is currently drawn with, not the @@ -155,14 +158,14 @@ static void lvgl_display_apply_rotation(struct LvglDisplayCtx* ctx, lv_display_r bool gap_axes_swapped = swap_xy != ctx->base_swap_xy; int32_t gap_x = gap_axes_swapped ? ctx->base_gap_y : ctx->base_gap_x; int32_t gap_y = gap_axes_swapped ? ctx->base_gap_x : ctx->base_gap_y; - display_set_gap(ctx->device, gap_x, gap_y); + display_set_gap(wrapper->device, gap_x, gap_y); } } static void lvgl_display_rotation_event_cb(lv_event_t* e) { - struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_event_get_user_data(e); + struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)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)); + lvgl_display_apply_rotation(wrapper, lv_display_get_rotation(disp)); } // Returns which of buf1/buf2 (the real frame buffers, when !owns_buffers) color_map falls inside. @@ -206,7 +209,8 @@ static void* lvgl_display_try_ppa_rotate(struct LvglDisplayCtx* ctx, const uint8 } 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); + struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_display_get_driver_data(disp); + struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)wrapper->context; bool is_i1 = lv_display_get_color_format(disp) == LV_COLOR_FORMAT_I1; int32_t x1 = area->x1; @@ -280,8 +284,8 @@ static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uin } else { fb_base = (uint8_t*)lvgl_display_fb_base(ctx, color_map); } - uint16_t hres = display_get_resolution_x(ctx->device); - uint16_t vres = display_get_resolution_y(ctx->device); + uint16_t hres = display_get_resolution_x(wrapper->device); + uint16_t vres = display_get_resolution_y(wrapper->device); if (rotating) { // fb_base now holds the whole completed frame, but still in LVGL's *logical* @@ -303,13 +307,13 @@ static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uin } } - display_draw_bitmap(ctx->device, 0, 0, hres, vres, fb_base); + display_draw_bitmap(wrapper->device, 0, 0, hres, vres, fb_base); } } else if (ctx->owns_buffers) { // PARTIAL mode: each flush_cb call is one independent, complete tile into a buffer that // gets reused for the next tile, so present it immediately rather than waiting. // LVGL's area is inclusive; DisplayApi's draw_bitmap wants an exclusive end. - display_draw_bitmap(ctx->device, x1, y1, x2 + 1, y2 + 1, color_map); + display_draw_bitmap(wrapper->device, x1, y1, x2 + 1, y2 + 1, color_map); } // DisplayApi has no async completion callback, so draw_bitmap is synchronous. lv_display_flush_ready(disp); @@ -335,11 +339,17 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* 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)); + struct LvglDisplayCtx* ctx = new(std::nothrow) LvglDisplayCtx(); if (ctx == NULL) { return ERROR_OUT_OF_MEMORY; } - ctx->device = device; + struct LvglDeviceContext* wrapper = new(std::nothrow) LvglDeviceContext(ctx); + if (wrapper == NULL) { + delete ctx; + return ERROR_OUT_OF_MEMORY; + } + wrapper->device = device; + ctx->byte_swap = config->swap_bytes; ctx->sw_rotate = config->sw_rotate; // Only relevant when sw_rotate is set - lvgl_display_try_ppa_rotate() also checks @@ -388,7 +398,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* buf_size_bytes = (size_t)((hres + 7) / 8) * vres + 8; ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes); if (ctx->buf1 == NULL) { - free(ctx); + delete wrapper; return ERROR_OUT_OF_MEMORY; } ctx->owns_buffers = true; @@ -408,14 +418,14 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes); if (ctx->buf1 == NULL) { - free(ctx); + delete wrapper; 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); + delete wrapper; return ERROR_OUT_OF_MEMORY; } } @@ -432,7 +442,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* lvgl_display_free_buffer(ctx->buf1); lvgl_display_free_buffer(ctx->buf2); } - free(ctx); + delete wrapper; return ERROR_OUT_OF_MEMORY; } } @@ -444,7 +454,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* lvgl_display_free_buffer(ctx->buf2); } lvgl_display_free_buffer(ctx->rotate_buf); - free(ctx); + delete wrapper; return ERROR_OUT_OF_MEMORY; } @@ -452,11 +462,11 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* 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); + lv_display_set_driver_data(disp, wrapper); + lv_display_add_event_cb(disp, lvgl_display_rotation_event_cb, LV_EVENT_RESOLUTION_CHANGED, wrapper); // Apply once explicitly, independent of whether LV_EVENT_RESOLUTION_CHANGED fires on creation. - lvgl_display_apply_rotation(ctx, lv_display_get_rotation(disp)); + lvgl_display_apply_rotation(wrapper, lv_display_get_rotation(disp)); *out_display = disp; return ERROR_NONE; @@ -467,10 +477,11 @@ void lvgl_display_remove(lv_display_t* display) { return; } - struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_display_get_driver_data(display); + struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_display_get_driver_data(display); lv_display_delete(display); - if (ctx != NULL) { + if (wrapper != NULL) { + struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)wrapper->context; if (ctx->owns_buffers) { if (ctx->buf1 != NULL) { lvgl_display_free_buffer(ctx->buf1); @@ -485,6 +496,6 @@ void lvgl_display_remove(lv_display_t* display) { if (ctx->ppa_handle != NULL) { lvgl_ppa_delete(ctx->ppa_handle); } - free(ctx); + delete wrapper; } } diff --git a/Modules/lvgl-module/source/devices/keyboard.cpp b/Modules/lvgl-module/source/devices/keyboard.cpp index 15efa33f4..24ad2d29d 100644 --- a/Modules/lvgl-module/source/devices/keyboard.cpp +++ b/Modules/lvgl-module/source/devices/keyboard.cpp @@ -1,16 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include -#include #include -struct LvglKeyboardCtx { - Device* device; -}; - static LvglSoftwareKeyboard last_software_keyboard = { .object = nullptr }; @@ -39,10 +35,10 @@ void lvgl_keyboard_on_stop_lvgl() { } static void lvgl_keyboard_read_cb(lv_indev_t* indev, lv_indev_data_t* data) { - LvglKeyboardCtx* ctx = static_cast(lv_indev_get_driver_data(indev)); + auto* wrapper = static_cast(lv_indev_get_driver_data(indev)); KeyboardKeyData key_data = {}; - if (keyboard_read_key(ctx->device, &key_data) != ERROR_NONE) { + if (keyboard_read_key(wrapper->device, &key_data) != ERROR_NONE) { data->state = LV_INDEV_STATE_RELEASED; data->continue_reading = false; return; @@ -62,21 +58,21 @@ error_t lvgl_keyboard_add(struct Device* device, lv_display_t* display, lv_indev return ERROR_INVALID_ARGUMENT; } - LvglKeyboardCtx* ctx = static_cast(malloc(sizeof(struct LvglKeyboardCtx))); - if (ctx == NULL) { + auto* wrapper = new(std::nothrow) LvglDeviceContext(nullptr); + if (wrapper == NULL) { return ERROR_OUT_OF_MEMORY; } - ctx->device = device; + wrapper->device = device; lv_indev_t* indev = lv_indev_create(); if (indev == NULL) { - free(ctx); + delete wrapper; 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); + lv_indev_set_driver_data(indev, wrapper); if (display != NULL) { lv_indev_set_display(indev, display); } @@ -90,9 +86,9 @@ void lvgl_keyboard_remove(lv_indev_t* indev) { return; } - LvglKeyboardCtx* ctx = (struct LvglKeyboardCtx*)lv_indev_get_driver_data(indev); + auto* wrapper = static_cast(lv_indev_get_driver_data(indev)); lv_indev_delete(indev); - free(ctx); + delete wrapper; } void lvgl_keyboard_enable(lv_indev_t* indev) { @@ -115,22 +111,21 @@ bool lvgl_hardware_keyboard_is_available() { } void lvgl_hardware_keyboard_add_custom(lv_indev_t* indev) { - LvglKeyboardCtx* ctx = static_cast(malloc(sizeof(struct LvglKeyboardCtx))); - if (ctx == nullptr) { + auto* wrapper = new(std::nothrow) LvglDeviceContext(nullptr); + if (wrapper == nullptr) { return; } - ctx->device = nullptr; - lv_indev_set_driver_data(indev, ctx); + lv_indev_set_driver_data(indev, wrapper); lvgl_keyboard_enable(indev); } void lvgl_hardware_keyboard_remove_custom(lv_indev_t* indev) { lvgl_keyboard_disable(indev); - auto* data = lv_indev_get_driver_data(indev); + auto* wrapper = static_cast(lv_indev_get_driver_data(indev)); lv_indev_set_driver_data(indev, nullptr); - free(data); // LvglKeyboardCtx* + delete wrapper; } static void textarea_show_keyboard(lv_event_t* event) { diff --git a/Modules/lvgl-module/source/devices/pointer.c b/Modules/lvgl-module/source/devices/pointer.cpp similarity index 79% rename from Modules/lvgl-module/source/devices/pointer.c rename to Modules/lvgl-module/source/devices/pointer.cpp index 9acbc9fde..a8237bcd7 100644 --- a/Modules/lvgl-module/source/devices/pointer.c +++ b/Modules/lvgl-module/source/devices/pointer.cpp @@ -1,14 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include -#include - -#define TAG "lvgl_pointer" +constexpr auto* TAG = "lvgl_pointer"; struct LvglPointerCtx { - struct Device* device; bool calibration_enabled; struct LvglPointerCalibration calibration; }; @@ -60,13 +58,13 @@ static void lvgl_pointer_calibration_apply( // fixed pixel dimensions, not a rotation. This function has no notion of LVGL rotation at all: // calibration corrects the raw sensor's fixed physical mapping, which never changes with on-screen // orientation, so it doesn't belong anywhere near rotation math. -static bool lvgl_pointer_read_calibrated(struct LvglPointerCtx* ctx, int32_t native_x_max, int32_t native_y_max, uint16_t* x, uint16_t* y) { - if (pointer_read_data(ctx->device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) { +static bool lvgl_pointer_read_calibrated(struct Device* device, struct LvglPointerCtx* ctx, int32_t native_x_max, int32_t native_y_max, uint16_t* x, uint16_t* y) { + if (pointer_read_data(device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) { return false; } uint8_t point_count = 0; - if (!pointer_get_touched_points(ctx->device, x, y, NULL, &point_count, 1) || point_count == 0) { + if (!pointer_get_touched_points(device, x, y, NULL, &point_count, 1) || point_count == 0) { return false; } @@ -82,7 +80,8 @@ static bool lvgl_pointer_read_calibrated(struct LvglPointerCtx* ctx, int32_t nat // logical space - unconditionally, since native-space coordinates always need this regardless of // whether calibration is enabled. 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); + struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev); + struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context; lv_display_t* display = lv_indev_get_display(indev); // lv_display_get_original_*_resolution() is the native (LV_DISPLAY_ROTATION_0) size, @@ -92,7 +91,7 @@ static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) { uint16_t x = 0; uint16_t y = 0; - if (!lvgl_pointer_read_calibrated(ctx, native_x_max, native_y_max, &x, &y)) { + if (!lvgl_pointer_read_calibrated(wrapper->device, ctx, native_x_max, native_y_max, &x, &y)) { data->state = LV_INDEV_STATE_RELEASED; return; } @@ -110,21 +109,26 @@ error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_ return ERROR_INVALID_ARGUMENT; } - struct LvglPointerCtx* ctx = calloc(1, sizeof(struct LvglPointerCtx)); + struct LvglPointerCtx* ctx = new(std::nothrow) LvglPointerCtx(); if (ctx == NULL) { return ERROR_OUT_OF_MEMORY; } - ctx->device = device; + struct LvglDeviceContext* wrapper = new(std::nothrow) LvglDeviceContext(ctx); + if (wrapper == NULL) { + delete ctx; + return ERROR_OUT_OF_MEMORY; + } + wrapper->device = device; lv_indev_t* indev = lv_indev_create(); if (indev == NULL) { - free(ctx); + delete wrapper; 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); + lv_indev_set_driver_data(indev, wrapper); if (display != NULL) { lv_indev_set_display(indev, display); } @@ -145,7 +149,8 @@ error_t lvgl_pointer_set_calibration(lv_indev_t* indev, const struct LvglPointer if (indev == NULL) { return ERROR_INVALID_ARGUMENT; } - struct LvglPointerCtx* ctx = lv_indev_get_driver_data(indev); + struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev); + struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context; if (calibration == NULL) { ctx->calibration_enabled = false; @@ -164,7 +169,8 @@ bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibrati if (indev == NULL || out_calibration == NULL) { return false; } - struct LvglPointerCtx* ctx = lv_indev_get_driver_data(indev); + struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev); + struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context; if (!ctx->calibration_enabled) { return false; } @@ -177,10 +183,10 @@ void lvgl_pointer_remove(lv_indev_t* indev) { return; } - struct LvglPointerCtx* ctx = lv_indev_get_driver_data(indev); + struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev); if (default_pointer_indev == indev) { default_pointer_indev = NULL; } lv_indev_delete(indev); - free(ctx); + delete wrapper; } diff --git a/Modules/lvgl-module/source/devices/trackball.cpp b/Modules/lvgl-module/source/devices/trackball.cpp new file mode 100644 index 000000000..266697f8d --- /dev/null +++ b/Modules/lvgl-module/source/devices/trackball.cpp @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include + +#include + +constexpr auto* TAG = "lvgl_trackball"; + +struct LvglTrackballCtx { + LvglTrackballSettings settings; + int32_t cursor_x; + int32_t cursor_y; + lv_obj_t* cursor; + const void* cursor_image_src; +}; + +static inline int32_t clamp(int32_t value, int32_t min_value, int32_t max_value) { + if (value < min_value) return min_value; + if (value > max_value) return max_value; + return value; +} + +static void recenter_cursor(LvglTrackballCtx* ctx, lv_indev_t* indev) { + lv_display_t* display = lv_indev_get_display(indev); + // lv_display_get_original_*_resolution() is the native (LV_DISPLAY_ROTATION_0) size, + // unaffected by the display's current rotation, matching lvgl/devices/pointer.c's approach. + ctx->cursor_x = display != nullptr ? lv_display_get_original_horizontal_resolution(display) / 2 : 0; + ctx->cursor_y = display != nullptr ? lv_display_get_original_vertical_resolution(display) / 2 : 0; +} + +// Creates the cursor object on first use and binds it to indev via lv_indev_set_cursor() exactly +// once - LVGL's lv_indev_set_cursor() unconditionally reparents whatever it's given (even NULL: +// it does not special-case that, see hide_cursor() below), so it must only ever be called here, +// with a freshly-created, non-null object whose parent already matches (lv_layer_sys()), making +// lv_obj_set_parent()'s "parent == obj->parent" check a safe no-op. Once bound, later image/ +// enabled changes update the existing object in place instead of re-binding. +static void show_cursor(LvglTrackballCtx* ctx, lv_indev_t* indev) { + if (ctx->cursor_image_src == nullptr) { + return; + } + + if (ctx->cursor == nullptr) { + ctx->cursor = lv_image_create(lv_layer_sys()); + if (ctx->cursor == nullptr) { + return; + } + lv_obj_remove_flag(ctx->cursor, LV_OBJ_FLAG_CLICKABLE); + lv_indev_set_cursor(indev, ctx->cursor); + } + + lv_image_set_src(ctx->cursor, ctx->cursor_image_src); + if (ctx->settings.enabled) { + lv_obj_remove_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN); + } +} + +// Only hides the cursor object - never deletes it +static void hide_cursor(LvglTrackballCtx* ctx) { + if (ctx->cursor == nullptr) { + return; + } + lv_obj_add_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN); +} + +static void lvgl_trackball_read_cb(lv_indev_t* indev, lv_indev_data_t* data) { + auto* wrapper = static_cast(lv_indev_get_driver_data(indev)); + auto* ctx = static_cast(wrapper->context); + + // 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; + trackball_read_delta(wrapper->device, &dx, &dy); + if (!ctx->settings.enabled) { + dx = 0; + dy = 0; + } + + lv_display_t* display = lv_indev_get_display(indev); + + if (ctx->settings.mode == LVGL_TRACKBALL_MODE_ENCODER) { + int32_t ticks = (dx + dy) * static_cast(ctx->settings.encoder_sensitivity); + data->enc_diff = static_cast(clamp(ticks, INT16_MIN, INT16_MAX)); + if (ticks != 0) { + lv_display_trigger_activity(display); + } + } else { + int32_t max_x = display != nullptr ? lv_display_get_original_horizontal_resolution(display) - 1 : 0; + int32_t max_y = display != nullptr ? lv_display_get_original_vertical_resolution(display) - 1 : 0; + ctx->cursor_x = clamp(ctx->cursor_x + dx * static_cast(ctx->settings.pointer_sensitivity), 0, max_x); + ctx->cursor_y = clamp(ctx->cursor_y + dy * static_cast(ctx->settings.pointer_sensitivity), 0, max_y); + data->point.x = static_cast(ctx->cursor_x); + data->point.y = static_cast(ctx->cursor_y); + } + + bool pressed = false; + if (ctx->settings.enabled) { + trackball_get_button_pressed(wrapper->device, &pressed); + } + data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + + if (pressed) { + lv_display_trigger_activity(display); + } +} + +extern "C" { + +LvglTrackballSettings lvgl_trackball_settings_get_default() { + return LvglTrackballSettings { + .mode = LVGL_TRACKBALL_MODE_ENCODER, + .enabled = true, + .encoder_sensitivity = 1, + .pointer_sensitivity = 10, + }; +} + +error_t lvgl_trackball_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev) { + if (device == nullptr || out_indev == nullptr) { + return ERROR_INVALID_ARGUMENT; + } + if (device_get_type(device) != &TRACKBALL_TYPE) { + return ERROR_INVALID_ARGUMENT; + } + + auto* ctx = new(std::nothrow) LvglTrackballCtx(); + if (ctx == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + ctx->settings = lvgl_trackball_settings_get_default(); + + auto* wrapper = new(std::nothrow) LvglDeviceContext(ctx); + if (wrapper == nullptr) { + delete ctx; + return ERROR_OUT_OF_MEMORY; + } + wrapper->device = device; + + lv_indev_t* indev = lv_indev_create(); + if (indev == nullptr) { + delete wrapper; + return ERROR_OUT_OF_MEMORY; + } + + lv_indev_set_type(indev, LV_INDEV_TYPE_ENCODER); + lv_indev_set_read_cb(indev, lvgl_trackball_read_cb); + lv_indev_set_driver_data(indev, wrapper); + if (display != nullptr) { + lv_indev_set_display(indev, display); + } + recenter_cursor(ctx, indev); + + *out_indev = indev; + return ERROR_NONE; +} + +void lvgl_trackball_remove(lv_indev_t* indev) { + if (indev == nullptr) { + return; + } + + auto* wrapper = static_cast(lv_indev_get_driver_data(indev)); + check(wrapper); + auto* ctx = static_cast(wrapper->context); + check(ctx); + if (ctx->cursor != nullptr) { + lv_obj_delete(ctx->cursor); + } + lv_indev_delete(indev); + delete wrapper; +} + +error_t lvgl_trackball_set_settings(lv_indev_t* indev, const struct LvglTrackballSettings* settings) { + if (indev == nullptr || settings == nullptr) { + return ERROR_INVALID_ARGUMENT; + } + + if ( + (settings->mode != LVGL_TRACKBALL_MODE_ENCODER && settings->mode != LVGL_TRACKBALL_MODE_POINTER) || + settings->encoder_sensitivity == 0 || + settings->pointer_sensitivity == 0 + ) { + return ERROR_INVALID_ARGUMENT; + } + + auto* wrapper = static_cast(lv_indev_get_driver_data(indev)); + check(wrapper); + auto* ctx = static_cast(wrapper->context); + check(ctx); + bool mode_changed = ctx->settings.mode != settings->mode; + ctx->settings = *settings; + + if (mode_changed) { + if (settings->mode == LVGL_TRACKBALL_MODE_POINTER) { + lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); + recenter_cursor(ctx, indev); + show_cursor(ctx, indev); + } else { + hide_cursor(ctx); + lv_indev_set_type(indev, LV_INDEV_TYPE_ENCODER); + } + } + + // Cursor visibility only tracks the enabled toggle in pointer mode - in encoder mode it must + // stay hidden regardless of enabled, otherwise this unconditionally un-hides the cursor + // hide_cursor() just hid above (enabled is independent of mode, and defaults to true). + if (ctx->cursor != nullptr && ctx->settings.mode == LVGL_TRACKBALL_MODE_POINTER) { + if (ctx->settings.enabled) { + lv_obj_remove_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN); + } + } + + return ERROR_NONE; +} + +bool lvgl_trackball_get_settings(lv_indev_t* indev, struct LvglTrackballSettings* out_settings) { + if (indev == nullptr || out_settings == nullptr) { + return false; + } + + auto* wrapper = static_cast(lv_indev_get_driver_data(indev)); + check(wrapper); + auto* ctx = static_cast(wrapper->context); + check(ctx); + *out_settings = ctx->settings; + return true; +} + +void lvgl_trackball_set_cursor_image(lv_indev_t* indev, const void* image_src) { + if (indev == nullptr) { + return; + } + + auto* wrapper = static_cast(lv_indev_get_driver_data(indev)); + check(wrapper); + auto* ctx = static_cast(wrapper->context); + check(ctx); + ctx->cursor_image_src = image_src; + + if (ctx->settings.mode == LVGL_TRACKBALL_MODE_POINTER) { + if (image_src == nullptr) { + hide_cursor(ctx); + } else { + show_cursor(ctx, indev); + } + } +} + +} // extern "C" diff --git a/Tactility/Include/Tactility/settings/TrackballSettings.h b/Tactility/Include/Tactility/settings/TrackballSettings.h index ff769a502..bc8f7302a 100644 --- a/Tactility/Include/Tactility/settings/TrackballSettings.h +++ b/Tactility/Include/Tactility/settings/TrackballSettings.h @@ -1,27 +1,15 @@ #pragma once -#include +#include namespace tt::settings::trackball { -enum class TrackballMode : uint8_t { - Encoder = 0, // Scroll wheel navigation (default) - Pointer = 1 // Mouse cursor mode -}; +bool load(LvglTrackballSettings& settings); -struct TrackballSettings { - bool trackballEnabled = false; - TrackballMode trackballMode = TrackballMode::Encoder; - uint8_t encoderSensitivity = 1; // Steps per tick (1-10) - uint8_t pointerSensitivity = 10; // Pixels per tick (1-10) -}; +LvglTrackballSettings loadOrGetDefault(); -bool load(TrackballSettings& settings); +LvglTrackballSettings getDefault(); -TrackballSettings loadOrGetDefault(); - -TrackballSettings getDefault(); - -bool save(const TrackballSettings& settings); +bool save(const LvglTrackballSettings& settings); } diff --git a/Tactility/Private/Tactility/lvgl/TrackballInit.h b/Tactility/Private/Tactility/lvgl/TrackballInit.h new file mode 100644 index 000000000..7ccaa52c8 --- /dev/null +++ b/Tactility/Private/Tactility/lvgl/TrackballInit.h @@ -0,0 +1,10 @@ +#pragma once + +namespace tt::lvgl { + +/** + * Applies trackball settings (either stored or default ones) to all trackball devices. + */ +void initTrackball(); + +} \ No newline at end of file diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index ae7321c98..be2b474db 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -12,8 +12,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -34,11 +34,13 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -154,9 +156,10 @@ namespace app { namespace webserversettings { extern const AppManifest manifest; } #if CONFIG_TT_TDECK_WORKAROUND == 1 namespace keyboardsettings { extern const AppManifest manifest; } // T-Deck only for now +#endif +#endif + namespace trackballsettings { extern const AppManifest manifest; } // T-Deck only for now -#endif -#endif #if TT_FEATURE_SCREENSHOT_ENABLED namespace screenshot { extern const AppManifest manifest; } @@ -216,9 +219,12 @@ static void registerInternalApps() { addAppManifest(app::development::manifest); #if defined(CONFIG_TT_TDECK_WORKAROUND) addAppManifest(app::keyboardsettings::manifest); +#endif +#endif + + if (device_exists_of_type(&TRACKBALL_TYPE)) { addAppManifest(app::trackballsettings::manifest); -#endif -#endif + } #if defined(CONFIG_TINYUSB_MSC_ENABLED) && CONFIG_TINYUSB_MSC_ENABLED addAppManifest(app::usbsettings::manifest); @@ -373,6 +379,8 @@ static void onLvglStarted() { addService(service::screenshot::manifest); #endif + lvgl::initTrackball(); + memory_print_stats(); } diff --git a/Tactility/Source/app/trackball/TrackballSettings.cpp b/Tactility/Source/app/trackball/TrackballSettings.cpp index 0caa4d039..165ef19e9 100644 --- a/Tactility/Source/app/trackball/TrackballSettings.cpp +++ b/Tactility/Source/app/trackball/TrackballSettings.cpp @@ -1,59 +1,61 @@ -#ifdef ESP_PLATFORM - #include +#include +#include #include #include -#include +#include + +#include #include #include - -// Forward declare driver functions -namespace trackball { - void setEnabled(bool enabled); - enum class Mode { Encoder, Pointer }; - void setMode(Mode mode); - void setEncoderSensitivity(uint8_t sensitivity); - void setPointerSensitivity(uint8_t sensitivity); -} +#include namespace tt::app::trackballsettings { constexpr auto* TAG = "TrackballSettings"; -static trackball::Mode toDriverMode(settings::trackball::TrackballMode mode) { +// Convert mode to dropdown index (dropdown order: Encoder=0, Pointer=1) +static uint32_t modeToDropdownIndex(LvglTrackballMode mode) { switch (mode) { - case settings::trackball::TrackballMode::Encoder: return trackball::Mode::Encoder; - case settings::trackball::TrackballMode::Pointer: return trackball::Mode::Pointer; - } - return trackball::Mode::Encoder; // default -} - -// Convert settings enum to dropdown index (dropdown order: Encoder=0, Pointer=1) -static uint32_t modeToDropdownIndex(settings::trackball::TrackballMode mode) { - switch (mode) { - case settings::trackball::TrackballMode::Encoder: return 0; - case settings::trackball::TrackballMode::Pointer: return 1; + case LVGL_TRACKBALL_MODE_ENCODER: return 0; + case LVGL_TRACKBALL_MODE_POINTER: return 1; } return 0; // default to Encoder } class TrackballSettingsApp final : public App { - settings::trackball::TrackballSettings tbSettings; + LvglTrackballSettings tbSettings; bool updated = false; + // The trackball indev currently bound by lvgl_devices_attach() at LVGL startup, if any - + // there's at most one at a time (see devices.c), so "first active trackball device" reduces + // to whatever is already attached. + lv_indev_t* trackballIndev = nullptr; lv_obj_t* switchTrackball = nullptr; lv_obj_t* trackballModeDropdown = nullptr; lv_obj_t* encoderSensitivitySlider = nullptr; lv_obj_t* pointerSensitivitySlider = nullptr; + void applyLive() { + if (trackballIndev == nullptr) { + return; + } + lvgl_lock(); + lvgl_trackball_set_settings(trackballIndev, &tbSettings); + if (tbSettings.mode == LVGL_TRACKBALL_MODE_POINTER) { + lvgl_trackball_set_cursor_image(trackballIndev, TT_ASSETS_UI_CURSOR); + } + lvgl_unlock(); + } + static void onTrackballSwitch(lv_event_t* e) { auto* app = static_cast(lv_event_get_user_data(e)); bool enabled = lv_obj_has_state(app->switchTrackball, LV_STATE_CHECKED); - app->tbSettings.trackballEnabled = enabled; + app->tbSettings.enabled = enabled; app->updated = true; - trackball::setEnabled(enabled); + app->applyLive(); // Enable/disable controls based on trackball state if (enabled) { @@ -72,38 +74,57 @@ class TrackballSettingsApp final : public App { uint32_t selected = lv_dropdown_get_selected(app->trackballModeDropdown); // Validate selection matches expected enum values (dropdown order: Encoder=0, Pointer=1) - settings::trackball::TrackballMode mode; + LvglTrackballMode mode; switch (selected) { - case 0: mode = settings::trackball::TrackballMode::Encoder; break; - case 1: mode = settings::trackball::TrackballMode::Pointer; break; + case 0: mode = LVGL_TRACKBALL_MODE_ENCODER; break; + case 1: mode = LVGL_TRACKBALL_MODE_POINTER; break; default: return; // Invalid selection, ignore } - app->tbSettings.trackballMode = mode; + app->tbSettings.mode = mode; app->updated = true; // Apply mode change immediately - trackball::setMode(toDriverMode(mode)); + app->applyLive(); } static void onEncoderSensitivityChanged(lv_event_t* e) { auto* app = static_cast(lv_event_get_user_data(e)); int32_t value = lv_slider_get_value(app->encoderSensitivitySlider); - app->tbSettings.encoderSensitivity = static_cast(value); + app->tbSettings.encoder_sensitivity = static_cast(value); app->updated = true; // Apply immediately - trackball::setEncoderSensitivity(static_cast(value)); + app->applyLive(); } static void onPointerSensitivityChanged(lv_event_t* e) { auto* app = static_cast(lv_event_get_user_data(e)); int32_t value = lv_slider_get_value(app->pointerSensitivitySlider); - app->tbSettings.pointerSensitivity = static_cast(value); + app->tbSettings.pointer_sensitivity = static_cast(value); app->updated = true; // Apply immediately - trackball::setPointerSensitivity(static_cast(value)); + app->applyLive(); + } + + static lv_indev_t* findFirstTrackballIndev() { + lv_indev_t* indev = lv_indev_get_next(nullptr); + while (indev != nullptr) { + void* driver_data = lv_indev_get_driver_data(indev); + if (driver_data) { + LvglDeviceContext* context = static_cast(driver_data); + if (context->device) { + const DeviceType* device_type = device_get_type(context->device); + if (device_type == &TRACKBALL_TYPE) { + return indev; + } + } + } + + indev = lv_indev_get_next(indev); + } + return nullptr; } public: @@ -111,15 +132,32 @@ public: tbSettings = settings::trackball::loadOrGetDefault(); auto ui_density = lvgl_get_ui_density(); updated = false; + trackballIndev = findFirstTrackballIndev(); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lv_obj_t* toolbar = lvgl::toolbar_create(parent, app); + if (trackballIndev == nullptr) { + auto* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + auto* label = lv_label_create(wrapper); + lv_label_set_text(label, "No trackball device found"); + return; + } + + // The live indev may still be running with lvgl_trackball_settings_get_default() (it's + // bound at LVGL startup before persisted settings are known) - bring it in line with what + // this screen is about to display. + applyLive(); + switchTrackball = lvgl_toolbar_add_switch_action(toolbar); lv_obj_add_event_cb(switchTrackball, onTrackballSwitch, LV_EVENT_VALUE_CHANGED, this); - if (tbSettings.trackballEnabled) lv_obj_add_state(switchTrackball, LV_STATE_CHECKED); + if (tbSettings.enabled) lv_obj_add_state(switchTrackball, LV_STATE_CHECKED); auto* main_wrapper = lv_obj_create(parent); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); @@ -139,11 +177,11 @@ public: trackballModeDropdown = lv_dropdown_create(tb_mode_wrapper); lv_dropdown_set_options(trackballModeDropdown, "Encoder\nPointer"); lv_obj_align(trackballModeDropdown, LV_ALIGN_RIGHT_MID, 0, 0); - lv_dropdown_set_selected(trackballModeDropdown, modeToDropdownIndex(tbSettings.trackballMode)); + lv_dropdown_set_selected(trackballModeDropdown, modeToDropdownIndex(tbSettings.mode)); lv_obj_add_event_cb(trackballModeDropdown, onTrackballModeChanged, LV_EVENT_VALUE_CHANGED, this); // Disable dropdown if trackball is disabled - if (!tbSettings.trackballEnabled) { + if (!tbSettings.enabled) { lv_obj_add_state(trackballModeDropdown, LV_STATE_DISABLED); } @@ -162,12 +200,12 @@ public: encoderSensitivitySlider = lv_slider_create(enc_sens_wrapper); lv_slider_set_range(encoderSensitivitySlider, 1, 10); - lv_slider_set_value(encoderSensitivitySlider, tbSettings.encoderSensitivity, LV_ANIM_OFF); + lv_slider_set_value(encoderSensitivitySlider, tbSettings.encoder_sensitivity, LV_ANIM_OFF); lv_obj_set_width(encoderSensitivitySlider, LV_PCT(50)); lv_obj_align(encoderSensitivitySlider, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(encoderSensitivitySlider, onEncoderSensitivityChanged, LV_EVENT_VALUE_CHANGED, this); - if (!tbSettings.trackballEnabled) { + if (!tbSettings.enabled) { lv_obj_add_state(encoderSensitivitySlider, LV_STATE_DISABLED); } @@ -186,12 +224,12 @@ public: pointerSensitivitySlider = lv_slider_create(ptr_sens_wrapper); lv_slider_set_range(pointerSensitivitySlider, 1, 10); - lv_slider_set_value(pointerSensitivitySlider, tbSettings.pointerSensitivity, LV_ANIM_OFF); + lv_slider_set_value(pointerSensitivitySlider, tbSettings.pointer_sensitivity, LV_ANIM_OFF); lv_obj_set_width(pointerSensitivitySlider, LV_PCT(50)); lv_obj_align(pointerSensitivitySlider, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(pointerSensitivitySlider, onPointerSensitivityChanged, LV_EVENT_VALUE_CHANGED, this); - if (!tbSettings.trackballEnabled) { + if (!tbSettings.enabled) { lv_obj_add_state(pointerSensitivitySlider, LV_STATE_DISABLED); } } @@ -214,5 +252,3 @@ extern const AppManifest manifest = { }; } - -#endif diff --git a/Tactility/Source/lvgl/TrackballInit.cpp b/Tactility/Source/lvgl/TrackballInit.cpp new file mode 100644 index 000000000..09789773a --- /dev/null +++ b/Tactility/Source/lvgl/TrackballInit.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include +#include + +#include +#include + +namespace tt::lvgl { + +static void initTrackball(lv_indev_t* indev, LvglTrackballSettings& settings) { + lv_indev_type_t type = lv_indev_get_type(indev); + + void* driver_data = lv_indev_get_driver_data(indev); + if (!driver_data) { + return; + } + + LvglDeviceContext* context = static_cast(driver_data); + if (!context->device) { + return; + } + + const DeviceType* device_type = device_get_type(context->device); + if (device_type != &TRACKBALL_TYPE) { + return; + } + + lvgl_trackball_set_settings(indev, &settings); + if (settings.mode == LVGL_TRACKBALL_MODE_POINTER) { + lvgl_trackball_set_cursor_image(indev, TT_ASSETS_UI_CURSOR); + } +} + +void initTrackball() { + auto trackball_settings = settings::trackball::loadOrGetDefault(); + lv_indev_t* indev = lv_indev_get_next(nullptr); + while (indev != nullptr) { + initTrackball(indev, trackball_settings); + indev = lv_indev_get_next(indev); + } +} + +} \ No newline at end of file diff --git a/Tactility/Source/settings/TrackballSettings.cpp b/Tactility/Source/settings/TrackballSettings.cpp index fa3518a6d..8e6b617f0 100644 --- a/Tactility/Source/settings/TrackballSettings.cpp +++ b/Tactility/Source/settings/TrackballSettings.cpp @@ -23,7 +23,7 @@ constexpr uint8_t MAX_ENCODER_SENSITIVITY = 10; constexpr uint8_t MIN_POINTER_SENSITIVITY = 1; constexpr uint8_t MAX_POINTER_SENSITIVITY = 10; -bool load(TrackballSettings& settings) { +bool load(LvglTrackballSettings& settings) { auto settings_path = getSettingsFilePath(); if (!file::isFile(settings_path)) { return false; @@ -55,41 +55,37 @@ bool load(TrackballSettings& settings) { auto isTrueValue = [](const std::string& s) { return s == "1" || s == "true" || s == "True" || s == "TRUE"; }; - settings.trackballEnabled = (tb_enabled != map.end()) ? isTrueValue(tb_enabled->second) : true; - settings.trackballMode = (tb_mode != map.end() && tb_mode->second == "1") ? TrackballMode::Pointer : TrackballMode::Encoder; - settings.encoderSensitivity = (enc_sens != map.end()) ? safeParseUint8(enc_sens->second, MIN_ENCODER_SENSITIVITY) : MIN_ENCODER_SENSITIVITY; - settings.pointerSensitivity = (ptr_sens != map.end()) ? safeParseUint8(ptr_sens->second, MAX_POINTER_SENSITIVITY) : MAX_POINTER_SENSITIVITY; + settings.enabled = (tb_enabled != map.end()) ? isTrueValue(tb_enabled->second) : true; + settings.mode = (tb_mode != map.end() && tb_mode->second == "1") ? LVGL_TRACKBALL_MODE_POINTER : LVGL_TRACKBALL_MODE_ENCODER; + auto default_settings = lvgl_trackball_settings_get_default(); + settings.encoder_sensitivity = (enc_sens != map.end()) ? safeParseUint8(enc_sens->second, default_settings.encoder_sensitivity) : default_settings.encoder_sensitivity; + settings.pointer_sensitivity = (ptr_sens != map.end()) ? safeParseUint8(ptr_sens->second, default_settings.pointer_sensitivity) : default_settings.pointer_sensitivity; // Clamp values to valid ranges - settings.encoderSensitivity = std::clamp(settings.encoderSensitivity, MIN_ENCODER_SENSITIVITY, MAX_ENCODER_SENSITIVITY); - settings.pointerSensitivity = std::clamp(settings.pointerSensitivity, MIN_POINTER_SENSITIVITY, MAX_POINTER_SENSITIVITY); + settings.encoder_sensitivity = std::clamp(settings.encoder_sensitivity, MIN_ENCODER_SENSITIVITY, MAX_ENCODER_SENSITIVITY); + settings.pointer_sensitivity = std::clamp(settings.pointer_sensitivity, MIN_POINTER_SENSITIVITY, MAX_POINTER_SENSITIVITY); return true; } -TrackballSettings getDefault() { - return TrackballSettings{ - .trackballEnabled = true, - .trackballMode = TrackballMode::Encoder, - .encoderSensitivity = MIN_ENCODER_SENSITIVITY, - .pointerSensitivity = MAX_POINTER_SENSITIVITY - }; +LvglTrackballSettings getDefault() { + return lvgl_trackball_settings_get_default(); } -TrackballSettings loadOrGetDefault() { - TrackballSettings s; +LvglTrackballSettings loadOrGetDefault() { + LvglTrackballSettings s; if (!load(s)) { s = getDefault(); } return s; } -bool save(const TrackballSettings& settings) { +bool save(const LvglTrackballSettings& settings) { std::map map; - map[KEY_TRACKBALL_ENABLED] = settings.trackballEnabled ? "1" : "0"; - map[KEY_TRACKBALL_MODE] = (settings.trackballMode == TrackballMode::Pointer) ? "1" : "0"; - map[KEY_ENCODER_SENSITIVITY] = std::to_string(std::clamp(settings.encoderSensitivity, MIN_ENCODER_SENSITIVITY, MAX_ENCODER_SENSITIVITY)); - map[KEY_POINTER_SENSITIVITY] = std::to_string(std::clamp(settings.pointerSensitivity, MIN_POINTER_SENSITIVITY, MAX_POINTER_SENSITIVITY)); + map[KEY_TRACKBALL_ENABLED] = settings.enabled ? "1" : "0"; + map[KEY_TRACKBALL_MODE] = (settings.mode == LVGL_TRACKBALL_MODE_POINTER) ? "1" : "0"; + map[KEY_ENCODER_SENSITIVITY] = std::to_string(std::clamp(settings.encoder_sensitivity, MIN_ENCODER_SENSITIVITY, MAX_ENCODER_SENSITIVITY)); + map[KEY_POINTER_SENSITIVITY] = std::to_string(std::clamp(settings.pointer_sensitivity, MIN_POINTER_SENSITIVITY, MAX_POINTER_SENSITIVITY)); auto settings_path = getSettingsFilePath(); if (!file::findOrCreateParentDirectory(settings_path, 0755)) { return false; diff --git a/Drivers/lilygo-module/include/lilygo/drivers/tdeck_trackball.h b/TactilityKernel/include/tactility/drivers/trackball.h similarity index 65% rename from Drivers/lilygo-module/include/lilygo/drivers/tdeck_trackball.h rename to TactilityKernel/include/tactility/drivers/trackball.h index f87b22dbb..2ace46bce 100644 --- a/Drivers/lilygo-module/include/lilygo/drivers/tdeck_trackball.h +++ b/TactilityKernel/include/tactility/drivers/trackball.h @@ -5,28 +5,18 @@ 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; -}; +#include +#include /** - * @brief API for the T-Deck 5-way trackball driver. + * @brief API for trackball drivers. * 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. + * layered on top by the consumer (see lvgl/devices/trackball.h), not something this driver knows about. */ -struct TdeckTrackballApi { +struct TrackballApi { /** * @brief Reads the accumulated movement since the last read, then resets it to zero. * @param[in] device the trackball device @@ -48,14 +38,14 @@ struct TdeckTrackballApi { /** * @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); +error_t 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); +error_t trackball_get_button_pressed(struct Device* device, bool* out_pressed); -extern const struct DeviceType TDECK_TRACKBALL_TYPE; +extern const struct DeviceType TRACKBALL_TYPE; #ifdef __cplusplus } diff --git a/TactilityKernel/source/drivers/trackball.cpp b/TactilityKernel/source/drivers/trackball.cpp new file mode 100644 index 000000000..77383fc2e --- /dev/null +++ b/TactilityKernel/source/drivers/trackball.cpp @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#define TRACKBALL_DRIVER_API(driver) ((struct TrackballApi*)driver->api) + +extern "C" { + +error_t trackball_read_delta(Device* device, int32_t* out_dx, int32_t* out_dy) { + const auto* driver = device_get_driver(device); + return TRACKBALL_DRIVER_API(driver)->read_delta(device, out_dx, out_dy); +} + +error_t trackball_get_button_pressed(Device* device, bool* out_pressed) { + const auto* driver = device_get_driver(device); + return TRACKBALL_DRIVER_API(driver)->get_button_pressed(device, out_pressed); +} + +const DeviceType TRACKBALL_TYPE { + .name = "trackball" +}; + +} diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 64fe89bce..d398d192b 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -294,6 +295,10 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(spi_controller_try_lock), DEFINE_MODULE_SYMBOL(spi_controller_unlock), DEFINE_MODULE_SYMBOL(SPI_CONTROLLER_TYPE), + // drivers/trackball + DEFINE_MODULE_SYMBOL(trackball_read_delta), + DEFINE_MODULE_SYMBOL(trackball_get_button_pressed), + DEFINE_MODULE_SYMBOL(TRACKBALL_TYPE), // drivers/uart_controller DEFINE_MODULE_SYMBOL(uart_controller_open), DEFINE_MODULE_SYMBOL(uart_controller_close),