mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-08-20 17:05:06 +00:00
Compare commits
14 Commits
f61a30726f
...
a96fd77986
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a96fd77986 | ||
|
|
e1fb827548 | ||
|
|
87f4985037 | ||
|
|
994aa31f6e | ||
|
|
fc98bb6131 | ||
|
|
ca7cb9fd02 | ||
|
|
e5d05a3a6a | ||
|
|
a2056e8e96 | ||
|
|
ba59437f44 | ||
|
|
1ae6e77083 | ||
|
|
dc3f6104b8 | ||
|
|
564d8af64c | ||
|
|
c628668e75 | ||
|
|
c6373b79e9 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -16,3 +16,6 @@
|
||||
[submodule "Libraries/cJSON/cJSON"]
|
||||
path = Libraries/cJSON/cJSON
|
||||
url = https://github.com/DaveGamble/cJSON.git
|
||||
[submodule "Libraries/esp_epaper"]
|
||||
path = Libraries/esp_epaper
|
||||
url = https://github.com/NellowTCS/esp_epaper.git
|
||||
|
||||
@ -2,12 +2,13 @@ idf_component_register(
|
||||
INCLUDE_DIRS
|
||||
"Libraries/TactilityC/include"
|
||||
"Libraries/TactilityKernel/include"
|
||||
"Libraries/TactilityFreeRtos/include"
|
||||
"Libraries/TactilityFreeRtos/Include"
|
||||
"Libraries/lvgl/include"
|
||||
"Libraries/minmea/include"
|
||||
"Libraries/minitar/include"
|
||||
"Modules/lvgl-module/include"
|
||||
# DRIVER_INCLUDE_DIRS_PLACEHOLDER
|
||||
REQUIRES esp_timer
|
||||
REQUIRES esp_timer app-module crypt-module gps-module lvgl-module lvgl-window-manager-module service-module
|
||||
)
|
||||
|
||||
# Regular and core features
|
||||
@ -15,8 +16,10 @@ add_prebuilt_library(TactilityC Libraries/TactilityC/binary/libTactilityC.a)
|
||||
add_prebuilt_library(TactilityKernel Libraries/TactilityKernel/binary/libTactilityKernel.a)
|
||||
add_prebuilt_library(lvgl Libraries/lvgl/binary/liblvgl.a)
|
||||
add_prebuilt_library(minmea Libraries/minmea/binary/libminmea.a)
|
||||
add_prebuilt_library(minitar Libraries/minitar/binary/libminitar.a)
|
||||
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE TactilityC)
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE TactilityKernel)
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE lvgl)
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE minmea)
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE minitar)
|
||||
|
||||
@ -18,13 +18,19 @@ macro(tactility_project project_name)
|
||||
endif()
|
||||
|
||||
set(EXTRA_COMPONENT_DIRS
|
||||
"Libraries/TactilityFreeRtos"
|
||||
"Modules"
|
||||
"Drivers"
|
||||
"${TACTILITY_SDK_PATH}/Libraries/TactilityFreeRtos"
|
||||
"${TACTILITY_SDK_PATH}/Modules"
|
||||
"${TACTILITY_SDK_PATH}/Drivers"
|
||||
)
|
||||
|
||||
set(COMPONENTS
|
||||
TactilityFreeRtos
|
||||
app-module
|
||||
crypt-module
|
||||
gps-module
|
||||
lvgl-module
|
||||
lvgl-window-manager-module
|
||||
service-module
|
||||
# DRIVER_COMPONENTS_PLACEHOLDER
|
||||
)
|
||||
|
||||
|
||||
@ -167,7 +167,7 @@ def main():
|
||||
{'src': 'TactilityC/CMakeLists.txt', 'dst': 'Libraries/TactilityC/'},
|
||||
{'src': 'TactilityC/LICENSE*.*', 'dst': 'Libraries/TactilityC/'},
|
||||
# TactilityFreeRtos
|
||||
{'src': 'TactilityFreeRtos/Include/**', 'dst': 'Libraries/TactilityFreeRtos/include/'},
|
||||
{'src': 'TactilityFreeRtos/Include/**', 'dst': 'Libraries/TactilityFreeRtos/Include/'},
|
||||
{'src': 'TactilityFreeRtos/CMakeLists.txt', 'dst': 'Libraries/TactilityFreeRtos/'},
|
||||
{'src': 'TactilityFreeRtos/LICENSE*.*', 'dst': 'Libraries/TactilityFreeRtos/'},
|
||||
# TactilityKernel
|
||||
@ -185,6 +185,10 @@ def main():
|
||||
# elf_loader
|
||||
{'src': 'Libraries/elf_loader/elf_loader.cmake', 'dst': 'Libraries/elf_loader/'},
|
||||
{'src': 'Libraries/elf_loader/license.txt', 'dst': 'Libraries/elf_loader/'},
|
||||
# minitar
|
||||
{'src': 'build/esp-idf/minitar/libminitar.a', 'dst': 'Libraries/minitar/binary/'},
|
||||
{'src': 'Libraries/minitar/minitar/minitar.h', 'dst': 'Libraries/minitar/include/'},
|
||||
{'src': 'Libraries/minitar/minitar/LICENSE*', 'dst': 'Libraries/minitar/'},
|
||||
# minmea
|
||||
{'src': 'build/esp-idf/minmea/libminmea.a', 'dst': 'Libraries/minmea/binary/'},
|
||||
{'src': 'Libraries/minmea/Include/**', 'dst': 'Libraries/minmea/include/'},
|
||||
@ -197,9 +201,11 @@ def main():
|
||||
map_copy(mappings, target_path)
|
||||
|
||||
# Modules
|
||||
add_module(target_path, "lvgl-module")
|
||||
add_module(target_path, "app-module")
|
||||
add_module(target_path, "crypt-module")
|
||||
add_module(target_path, "gps-module")
|
||||
add_module(target_path, "lvgl-module")
|
||||
add_module(target_path, "lvgl-window-manager-module")
|
||||
add_module(target_path, "service-module")
|
||||
|
||||
# Drivers - only ones actually built for this target (chip-restricted drivers like
|
||||
|
||||
@ -45,6 +45,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
"TactilityC"
|
||||
"TactilityFreeRtos"
|
||||
"Libraries/elf_loader"
|
||||
"Libraries/esp_epaper"
|
||||
"Libraries/lv_screenshot"
|
||||
"Libraries/minitar"
|
||||
"Libraries/minmea"
|
||||
@ -99,6 +100,8 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
|
||||
add_subdirectory(Modules/crypt-module)
|
||||
add_subdirectory(Modules/gps-module)
|
||||
add_subdirectory(Modules/service-module)
|
||||
add_subdirectory(Modules/app-module)
|
||||
add_subdirectory(Modules/lvgl-window-manager-module)
|
||||
add_subdirectory(Drivers/gps-generic-module)
|
||||
add_subdirectory(Drivers/gps-meshtastic-module)
|
||||
|
||||
|
||||
7
Devices/cl32/CMakeLists.txt
Normal file
7
Devices/cl32/CMakeLists.txt
Normal file
@ -0,0 +1,7 @@
|
||||
file(GLOB_RECURSE SOURCE_FILES source/*.c*)
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS "source"
|
||||
REQUIRES TactilityKernel
|
||||
)
|
||||
195
Devices/cl32/LICENSE-Apache-2.0.md
Normal file
195
Devices/cl32/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### 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.
|
||||
|
||||
121
Devices/cl32/cl32.dts
Normal file
121
Devices/cl32/cl32.dts
Normal file
@ -0,0 +1,121 @@
|
||||
/dts-v1/;
|
||||
|
||||
#include <tactility/bindings/root.h>
|
||||
#include <tactility/bindings/esp32_gpio.h>
|
||||
#include <tactility/bindings/esp32_grove.h>
|
||||
#include <tactility/bindings/esp32_i2c.h>
|
||||
#include <tactility/bindings/esp32_spi.h>
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/esp32_wifi.h>
|
||||
|
||||
#include <bindings/tca8418.h>
|
||||
#include <bindings/bm8563.h>
|
||||
#include <bindings/esp_epaper.h>
|
||||
|
||||
/ {
|
||||
compatible = "root";
|
||||
model = "CL-32";
|
||||
|
||||
gpio0 {
|
||||
compatible = "espressif,esp32-gpio";
|
||||
gpio-count = <49>;
|
||||
};
|
||||
|
||||
wifi0 {
|
||||
compatible = "espressif,esp32-wifi";
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
// Top Stemma/Qwiic port 1 shares this bus with the keyboard and RTC (SDA: pin 1 <-> SCL: pin 2).
|
||||
// External I2C devices on that port are reachable via i2c0.
|
||||
i2c0 {
|
||||
compatible = "espressif,esp32-i2c";
|
||||
port = <I2C_NUM_0>;
|
||||
clock-frequency = <100000>;
|
||||
pin-sda = <&gpio0 1 GPIO_FLAG_PULL_UP>;
|
||||
pin-scl = <&gpio0 2 GPIO_FLAG_PULL_UP>;
|
||||
|
||||
keyboard {
|
||||
compatible = "ti,tca8418";
|
||||
reg = <0x34>;
|
||||
rows = <8>;
|
||||
columns = <10>;
|
||||
keymap-lc = [
|
||||
37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8
|
||||
57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT
|
||||
9 113 119 101 114 116 121 117 105 0 // TAB q w e r t y u i
|
||||
111 112 10 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP
|
||||
0 97 115 100 102 103 104 106 107 0 // a s d f g h j k
|
||||
108 17 35 123 125 42 44 46 2 0 // l UP # { } * , . MENU
|
||||
122 120 99 118 98 32 32 110 109 0 // z x c v b n m
|
||||
20 18 19 60 62 47 92 61 10 0 // LEFT DOWN RIGHT < > / \ = RUN
|
||||
];
|
||||
keymap-uc = [
|
||||
37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8
|
||||
57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT
|
||||
9 81 87 69 82 84 89 85 73 0 // TAB Q W E R T Y U I
|
||||
79 80 10 40 41 45 59 58 3 0 // O P ENTER ( ) - ; : STOP
|
||||
0 65 83 68 70 71 72 74 75 0 // A S D F G H J K
|
||||
76 17 35 123 125 42 44 46 2 0 // L UP # { } * , . MENU
|
||||
90 88 67 86 66 32 32 78 77 0 // Z X C V B N M
|
||||
20 18 19 60 62 47 92 61 10 0 // LEFT DOWN RIGHT < > / \ = RUN
|
||||
];
|
||||
keymap-sy = [
|
||||
37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8
|
||||
57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT
|
||||
9 113 119 101 114 116 121 117 105 0 // TAB q w e r t y u i
|
||||
111 112 10 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP
|
||||
0 97 115 100 102 103 104 106 107 0 // a s d f g h j k
|
||||
108 17 35 123 125 42 44 46 2 0 // l UP # { } * , . MENU
|
||||
122 120 99 118 98 32 32 110 109 0 // z x c v b n m
|
||||
20 18 19 60 62 47 92 61 10 0 // LEFT DOWN RIGHT < > / \ = RUN
|
||||
];
|
||||
shift-row = <4>;
|
||||
shift-col = <0>;
|
||||
sym-row = <5>;
|
||||
sym-col = <8>;
|
||||
};
|
||||
|
||||
rtc: bm8563 {
|
||||
compatible = "belling,bm8563";
|
||||
reg = <0x51>;
|
||||
};
|
||||
};
|
||||
|
||||
port_b: grove1 {
|
||||
compatible = "espressif,esp32-grove";
|
||||
defaultMode = <GROVE_MODE_I2C>;
|
||||
pinSdaTx = <&gpio0 47 GPIO_FLAG_NONE>;
|
||||
pinSclRx = <&gpio0 48 GPIO_FLAG_NONE>;
|
||||
uartPort = <UART_NUM_1>;
|
||||
i2cPort = <I2C_NUM_1>;
|
||||
i2cClockFrequency = <100000>;
|
||||
};
|
||||
|
||||
spi1 {
|
||||
compatible = "espressif,esp32-spi";
|
||||
host = <SPI2_HOST>;
|
||||
cs-gpios = <&gpio0 7 GPIO_FLAG_NONE>;
|
||||
pin-mosi = <&gpio0 10 GPIO_FLAG_NONE>;
|
||||
pin-miso = <&gpio0 11 GPIO_FLAG_NONE>;
|
||||
pin-sclk = <&gpio0 9 GPIO_FLAG_NONE>;
|
||||
max-transfer-size = <4096>;
|
||||
|
||||
sdcard@0 {
|
||||
compatible = "espressif,esp32-sdspi";
|
||||
frequency-khz = <20000>;
|
||||
};
|
||||
|
||||
epd@1 {
|
||||
compatible = "tuanpmt,esp-epaper";
|
||||
pin-dc = <&gpio0 13 GPIO_FLAG_NONE>;
|
||||
pin-reset = <&gpio0 12 GPIO_FLAG_NONE>;
|
||||
pin-busy = <&gpio0 14 GPIO_FLAG_NONE>;
|
||||
pin-cs = <&gpio0 6 GPIO_FLAG_NONE>;
|
||||
clock-speed-hz = <4000000>;
|
||||
panel-type = "gdey029t71h";
|
||||
update-mode = <2>;
|
||||
rotation = <1>;
|
||||
};
|
||||
};
|
||||
};
|
||||
26
Devices/cl32/device.properties
Normal file
26
Devices/cl32/device.properties
Normal file
@ -0,0 +1,26 @@
|
||||
general.vendor=CL-32
|
||||
general.name=CL-32
|
||||
|
||||
apps.launcherAppId=Launcher
|
||||
|
||||
hardware.target=ESP32S3
|
||||
hardware.flashSize=8MB
|
||||
hardware.spiRam=true
|
||||
hardware.spiRamMode=QUAD
|
||||
hardware.spiRamSpeed=80M
|
||||
hardware.esptoolFlashFreq=80M
|
||||
hardware.bluetooth=true
|
||||
|
||||
storage.userDataLocation=SD
|
||||
|
||||
display.size=2.9"
|
||||
display.shape=rectangle
|
||||
display.dpi=139
|
||||
|
||||
lvgl.colorDepth=8
|
||||
lvgl.theme=Mono
|
||||
|
||||
# Fix error "PSRAM space not enough for the Flash instructions" on boot:
|
||||
sdkconfig.CONFIG_SPIRAM_FETCH_INSTRUCTIONS=n
|
||||
sdkconfig.CONFIG_SPIRAM_RODATA=n
|
||||
sdkconfig.CONFIG_SPIRAM_XIP_FROM_PSRAM=n
|
||||
6
Devices/cl32/devicetree.yaml
Normal file
6
Devices/cl32/devicetree.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
dependencies:
|
||||
- Platforms/platform-esp32
|
||||
- Drivers/tca8418-module
|
||||
- Drivers/esp-epaper-module
|
||||
- Drivers/bm8563-module
|
||||
dts: cl32.dts
|
||||
9
Devices/cl32/source/module.cpp
Normal file
9
Devices/cl32/source/module.cpp
Normal file
@ -0,0 +1,9 @@
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
struct Module cl32_module = {
|
||||
.name = "cl32"
|
||||
};
|
||||
|
||||
}
|
||||
@ -1,10 +1,3 @@
|
||||
dependencies:
|
||||
- Platforms/platform-esp32
|
||||
# Add all driver modules because the generic devices are used to build the SDK
|
||||
- Drivers/bm8563-module
|
||||
- Drivers/bmi270-module
|
||||
- Drivers/mpu6886-module
|
||||
- Drivers/pi4ioe5v6408-module
|
||||
- Drivers/qmi8658-module
|
||||
- Drivers/rx8130ce-module
|
||||
dts: generic,esp32.dts
|
||||
|
||||
@ -1,10 +1,3 @@
|
||||
dependencies:
|
||||
- Platforms/platform-esp32
|
||||
# Add all driver modules because the generic devices are used to build the SDK
|
||||
- Drivers/bm8563-module
|
||||
- Drivers/bmi270-module
|
||||
- Drivers/mpu6886-module
|
||||
- Drivers/pi4ioe5v6408-module
|
||||
- Drivers/qmi8658-module
|
||||
- Drivers/rx8130ce-module
|
||||
dts: generic,esp32c6.dts
|
||||
|
||||
@ -1,11 +1,3 @@
|
||||
dependencies:
|
||||
- Platforms/platform-esp32
|
||||
# Add all driver modules because the generic devices are used to build the SDK
|
||||
- Drivers/bm8563-module
|
||||
- Drivers/bmi270-module
|
||||
- Drivers/mpu6886-module
|
||||
- Drivers/pi4ioe5v6408-module
|
||||
- Drivers/qmi8658-module
|
||||
- Drivers/rx8130ce-module
|
||||
- Drivers/sc2356-module
|
||||
dts: generic,esp32p4.dts
|
||||
|
||||
@ -1,10 +1,3 @@
|
||||
dependencies:
|
||||
- Platforms/platform-esp32
|
||||
# Add all driver modules because the generic devices are used to build the SDK
|
||||
- Drivers/bm8563-module
|
||||
- Drivers/bmi270-module
|
||||
- Drivers/mpu6886-module
|
||||
- Drivers/pi4ioe5v6408-module
|
||||
- Drivers/qmi8658-module
|
||||
- Drivers/rx8130ce-module
|
||||
dts: generic,esp32s3.dts
|
||||
|
||||
@ -23,3 +23,8 @@ cdn.infoMessage=To put the device into bootloader mode: <br/>1. Press the trackb
|
||||
lvgl.colorDepth=16
|
||||
|
||||
sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y
|
||||
|
||||
# Fix error "PSRAM space not enough for the Flash instructions" on boot:
|
||||
sdkconfig.CONFIG_SPIRAM_FETCH_INSTRUCTIONS=n
|
||||
sdkconfig.CONFIG_SPIRAM_RODATA=n
|
||||
sdkconfig.CONFIG_SPIRAM_XIP_FROM_PSRAM=n
|
||||
|
||||
@ -11,46 +11,27 @@
|
||||
#include <epd_board.h>
|
||||
#include <epdiy.h>
|
||||
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#define TAG "Papers3Display"
|
||||
#define GET_CONFIG(device) (static_cast<const Papers3DisplayConfig*>((device)->config))
|
||||
|
||||
// Maps each src byte (8px, MSB-first, bit=1 -> white/0x0F) to the 4 packed dst bytes
|
||||
// (2px/byte, EPDiy MODE_PACKING_2PPB nibble order) it produces, replacing a per-pixel
|
||||
// branch loop with a table lookup.
|
||||
static uint32_t s_unpack_lut[256];
|
||||
|
||||
static void init_unpack_lut() {
|
||||
for (uint32_t byte = 0; byte < 256; byte++) {
|
||||
uint8_t dst[4];
|
||||
for (int32_t pair = 0; pair < 4; pair++) {
|
||||
const uint8_t bit0 = (byte >> (7 - pair * 2)) & 0x01U;
|
||||
const uint8_t bit1 = (byte >> (7 - pair * 2 - 1)) & 0x01U;
|
||||
const uint8_t p0 = bit0 ? 0x0FU : 0x00U;
|
||||
const uint8_t p1 = bit1 ? 0x0FU : 0x00U;
|
||||
dst[pair] = static_cast<uint8_t>((p1 << 4U) | p0);
|
||||
}
|
||||
memcpy(&s_unpack_lut[byte], dst, sizeof(dst));
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Module m5stack_papers3_module;
|
||||
|
||||
// epd_hl_init() sets an internal already_initialized flag and has no matching deinit, so the
|
||||
// highlevel state (and the framebuffer it owns) must persist across stop()/start() cycles and be
|
||||
// reused rather than recreated - ported from the old deprecated-HAL EpdiyDisplay's identical
|
||||
// s_hlInitialized/s_hlState statics.
|
||||
// epd_hl_init() has no matching deinit and sets an internal already_initialized flag, so the
|
||||
// highlevel state must persist across stop()/start() cycles and be reused rather than recreated.
|
||||
static bool s_hl_initialized = false;
|
||||
static EpdiyHighlevelState s_hl_state = {};
|
||||
|
||||
struct Papers3DisplayInternal {
|
||||
EpdiyHighlevelState hl_state;
|
||||
uint8_t* framebuffer;
|
||||
// Scratch buffer for the I1(1bpp)->EPDiy(4bpp packed, 2px/byte) conversion in draw_bitmap().
|
||||
// Scratch buffer for the grayscale8->EPDiy(4bpp packed, 2px/byte) conversion in draw_bitmap().
|
||||
uint8_t* packed_buffer;
|
||||
bool powered;
|
||||
};
|
||||
@ -76,16 +57,19 @@ static error_t papers3_display_reset(Device* device) {
|
||||
}
|
||||
|
||||
static error_t papers3_display_init(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
auto* internal = static_cast<Papers3DisplayInternal*>(device_get_driver_data(device));
|
||||
power_on(internal);
|
||||
epd_clear();
|
||||
epd_hl_set_all_white(&internal->hl_state);
|
||||
// The bootloader/boot-logo splash draws via partial refreshes that never get a real quality
|
||||
// pass, leaving a faint ghost. Run a full clear now, before LVGL's first flush ever reaches
|
||||
// draw_bitmap(), so it never has to undo content LVGL already put on screen.
|
||||
epd_fullclear(&internal->hl_state, config->temperature_celsius);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// LVGL only ever calls this with the full frame: DISPLAY_COLOR_FORMAT_MONOCHROME forces
|
||||
// LV_DISPLAY_RENDER_MODE_FULL in the generic kernel LVGL bridge (lvgl_display.c), and FULL mode
|
||||
// only presents (calls draw_bitmap) once per render cycle, with the complete 0,0..hres,vres rect.
|
||||
// Reports GRAYSCALE8 (not MONOCHROME) so LVGL uses partial/tile updates instead of forcing
|
||||
// full-frame - the bridge hardcodes full-frame for MONOCHROME/I1 regardless of capability flags.
|
||||
// So draw_bitmap is called once per changed tile, not necessarily the whole panel.
|
||||
static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
|
||||
auto* internal = static_cast<Papers3DisplayInternal*>(device_get_driver_data(device));
|
||||
const auto* config = GET_CONFIG(device);
|
||||
@ -93,32 +77,26 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3
|
||||
const int32_t width = x_end - x_start;
|
||||
const int32_t height = y_end - y_start;
|
||||
|
||||
// color_data is DISPLAY_COLOR_FORMAT_MONOCHROME: row-major, MSB-first 1bpp (LVGL's LV_COLOR_FORMAT_I1
|
||||
// with the palette header already stripped by the caller). Bit 1 = white/lit (LVGL's I1 blend
|
||||
// sets a bit when the source luminance is above its threshold), bit 0 = black.
|
||||
// color_data is DISPLAY_COLOR_FORMAT_GRAYSCALE8: row-major, 1 byte/pixel luminance
|
||||
// (0x00=black..0xFF=white, matching LVGL's L8). EPDiy wants 4bpp packed (2px/byte, 0x0=black,
|
||||
// 0xF=white) - a plain >>4 truncation preserves all 16 real gray levels the panel supports
|
||||
// (this panel is not B/W-only; see MODE_GC16/GL16 in papers3-display.yaml's draw-mode doc).
|
||||
const auto* src = static_cast<const uint8_t*>(color_data);
|
||||
const size_t src_stride = static_cast<size_t>(width + 7) / 8;
|
||||
const size_t src_stride = static_cast<size_t>(width);
|
||||
const size_t packed_stride = static_cast<size_t>(width + 1) / 2;
|
||||
|
||||
for (int32_t row = 0; row < height; row++) {
|
||||
const uint8_t* src_row = src + static_cast<size_t>(row) * src_stride;
|
||||
uint8_t* dst_row = internal->packed_buffer + static_cast<size_t>(row) * packed_stride;
|
||||
int32_t col = 0;
|
||||
// Bulk path: one LUT lookup + 4-byte copy per 8 source pixels.
|
||||
for (; col + 8 <= width; col += 8) {
|
||||
memcpy(dst_row + col / 2, &s_unpack_lut[src_row[col / 8]], 4);
|
||||
}
|
||||
// Tail: fewer than 8 pixels left (width not a multiple of 8).
|
||||
for (; col < width; col += 2) {
|
||||
const uint8_t bit0 = (src_row[col / 8] >> (7 - (col % 8))) & 0x01U;
|
||||
const uint8_t p0 = bit0 ? 0x0FU : 0x00U;
|
||||
uint8_t p1 = 0;
|
||||
if (col + 1 < width) {
|
||||
const uint8_t bit1 = (src_row[(col + 1) / 8] >> (7 - ((col + 1) % 8))) & 0x01U;
|
||||
p1 = bit1 ? 0x0FU : 0x00U;
|
||||
}
|
||||
for (; col + 2 <= width; col += 2) {
|
||||
const uint8_t p0 = src_row[col] >> 4U;
|
||||
const uint8_t p1 = src_row[col + 1] >> 4U;
|
||||
dst_row[col / 2] = static_cast<uint8_t>((p1 << 4U) | p0);
|
||||
}
|
||||
if (col < width) { // odd width: last column has no pair, low nibble unused
|
||||
dst_row[col / 2] = static_cast<uint8_t>(src_row[col] >> 4U);
|
||||
}
|
||||
}
|
||||
|
||||
const EpdRect update_area = {
|
||||
@ -152,7 +130,7 @@ static error_t papers3_display_disp_on_off(Device* device, bool on_off) {
|
||||
}
|
||||
|
||||
static DisplayColorFormat papers3_display_get_color_format(Device*) {
|
||||
return DISPLAY_COLOR_FORMAT_MONOCHROME;
|
||||
return DISPLAY_COLOR_FORMAT_GRAYSCALE8;
|
||||
}
|
||||
|
||||
// epd_width()/epd_height() are the panel's native, unrotated dimensions (display->width/height in
|
||||
@ -172,7 +150,7 @@ static uint16_t papers3_display_get_resolution_y(Device*) {
|
||||
|
||||
static void papers3_display_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
|
||||
// Not exposed via the generic fb-direct path: EPDiy's framebuffer is its own 4bpp packed
|
||||
// format, not the DISPLAY_COLOR_FORMAT_MONOCHROME (1bpp) this driver reports - see
|
||||
// format, not the DISPLAY_COLOR_FORMAT_GRAYSCALE8 (1 byte/pixel) this driver reports - see
|
||||
// get_frame_buffer_count() and draw_bitmap()'s conversion.
|
||||
*out_buffer = nullptr;
|
||||
}
|
||||
@ -184,7 +162,9 @@ static uint8_t papers3_display_get_frame_buffer_count(Device*) {
|
||||
// endregion
|
||||
|
||||
static const DisplayApi papers3_display_api = {
|
||||
.capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME | DISPLAY_CAPABILITY_SLOW_REFRESH,
|
||||
// PREFER_EXTERNAL_RAM: draw_bitmap() converts into packed_buffer before touching hardware,
|
||||
// never DMAs from LVGL's pointer directly - frees LVGL's draw buffers from forced internal RAM.
|
||||
.capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH | DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM,
|
||||
.reset = papers3_display_reset,
|
||||
.init = papers3_display_init,
|
||||
.draw_bitmap = papers3_display_draw_bitmap,
|
||||
@ -213,12 +193,6 @@ static const DisplayApi papers3_display_api = {
|
||||
static error_t start(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
static bool s_lut_initialized = false;
|
||||
if (!s_lut_initialized) {
|
||||
init_unpack_lut();
|
||||
s_lut_initialized = true;
|
||||
}
|
||||
|
||||
auto* internal = static_cast<Papers3DisplayInternal*>(malloc(sizeof(Papers3DisplayInternal)));
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
@ -245,8 +219,12 @@ static error_t start(Device* device) {
|
||||
internal->framebuffer = epd_hl_get_framebuffer(&internal->hl_state);
|
||||
|
||||
// Sized for the rotated (LVGL-facing) resolution - see get_resolution_x()/y()'s comment.
|
||||
// ~260KB for this panel - a plain malloc() would land in scarce internal RAM. This buffer is
|
||||
// only ever read once per draw_bitmap() call by epd_draw_rotated_image() (into epdiy's own
|
||||
// SPIRAM-backed framebuffers, see highlevel.c), so it has no internal-RAM/DMA requirement and
|
||||
// belongs in PSRAM instead, matching epdiy's own front_fb/back_fb/difference_fb allocations.
|
||||
const size_t packed_buffer_size = static_cast<size_t>((epd_rotated_display_width() + 1) / 2) * static_cast<size_t>(epd_rotated_display_height());
|
||||
internal->packed_buffer = static_cast<uint8_t*>(malloc(packed_buffer_size));
|
||||
internal->packed_buffer = static_cast<uint8_t*>(heap_caps_malloc(packed_buffer_size, MALLOC_CAP_SPIRAM));
|
||||
if (internal->packed_buffer == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate packed pixel buffer");
|
||||
epd_deinit();
|
||||
|
||||
@ -35,6 +35,12 @@ static constexpr uint32_t REPEAT_RATE_MS = 80;
|
||||
// REG_INT_STAT polling (when no IRQ pin) and software key-repeat ticking.
|
||||
static constexpr uint32_t POLL_INTERVAL_MS = 20;
|
||||
|
||||
// Upper bound on events consumed per drain_events() call. Since the loop re-reads REG_EVENT_NUM
|
||||
// each iteration rather than counting down a latched value, this caps the damage if the device
|
||||
// ever reports a non-zero count that never drains - without it, that would spin forever holding
|
||||
// the I2C bus. The device's own queue is far smaller than this, so it never limits normal bursts.
|
||||
static constexpr uint8_t MAX_EVENTS_PER_DRAIN = 32;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register addresses
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -117,6 +123,12 @@ static constexpr HidMapping KEY_MATRIX_HID_SYM[70] = {
|
||||
// Covers all codes present in the Tab5 matrix tables above. LV_KEY_* are plain uint32_t
|
||||
// constants - matching KeyboardKeyData::key's driver-defined contract and the same convention
|
||||
// m5stack-module's cardputer_keyboard.cpp kernel driver already uses.
|
||||
//
|
||||
// `ctrl` only selects the LVGL focus-navigation aliases for the arrow keys. Ctrl chords on
|
||||
// ordinary keys are NOT folded into the returned value - the C0 control codes a terminal wants
|
||||
// (Ctrl+C = 0x03, Ctrl+K = 0x0B, ...) collide with the LVGL constants returned here (LV_KEY_END = 3,
|
||||
// LV_KEY_PREV = 11, ...), so Ctrl is reported out-of-band via KeyboardKeyData::ctrl instead and
|
||||
// consumers that want control codes derive them themselves.
|
||||
// ---------------------------------------------------------------------------
|
||||
static uint32_t tab5_translate_key(uint8_t keycode, uint8_t modifier, bool ctrl) {
|
||||
const bool shift = (modifier & 0x22U) != 0U;
|
||||
@ -172,6 +184,15 @@ static uint32_t now_ms() {
|
||||
return static_cast<uint32_t>(esp_timer_get_time() / 1000);
|
||||
}
|
||||
|
||||
// Queued key event. Modifier state is captured here at enqueue time rather than read back from
|
||||
// Tab5KeyboardInternal at dequeue time, since the user can release Ctrl before read_key() drains
|
||||
// the event - and software key-repeat replays this same struct, so a held chord keeps its modifiers.
|
||||
struct Tab5KeyEvent {
|
||||
uint32_t key;
|
||||
bool ctrl;
|
||||
bool alt;
|
||||
};
|
||||
|
||||
struct Tab5KeyboardInternal {
|
||||
QueueHandle_t queue;
|
||||
|
||||
@ -181,6 +202,7 @@ struct Tab5KeyboardInternal {
|
||||
bool aa_held;
|
||||
bool aa_tapped;
|
||||
bool ctrl_held;
|
||||
bool alt_held;
|
||||
|
||||
// IRQ-driven event gating
|
||||
volatile bool irq_pending;
|
||||
@ -193,7 +215,7 @@ struct Tab5KeyboardInternal {
|
||||
uint32_t last_poll_ms;
|
||||
|
||||
// Software key-repeat state (tracked by position to survive modifier changes)
|
||||
uint32_t repeat_key;
|
||||
Tab5KeyEvent repeat_event;
|
||||
uint8_t repeat_row;
|
||||
uint8_t repeat_col;
|
||||
uint32_t repeat_start_ms;
|
||||
@ -289,16 +311,22 @@ static void remove_irq_pin(Tab5KeyboardInternal* internal) {
|
||||
// drain_events - reads all pending events from the device queue
|
||||
// ---------------------------------------------------------------------------
|
||||
static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
|
||||
uint8_t count = 0;
|
||||
if (!read_reg(device, REG_EVENT_NUM, &count) || count == 0) {
|
||||
return;
|
||||
}
|
||||
// REG_EVENT_NUM is re-read every iteration rather than latched once and counted down, matching
|
||||
// M5's own UnitTab5Keyboard::drain_events(). Each REG_KEY_EVENT read consumes one event from the
|
||||
// device queue, so a count latched up front can go stale mid-drain; re-reading makes the loop
|
||||
// self-correcting and lets it stop as soon as the device says the queue is actually empty.
|
||||
uint8_t drained = 0;
|
||||
while (drained < MAX_EVENTS_PER_DRAIN) {
|
||||
uint8_t count = 0;
|
||||
if (!read_reg(device, REG_EVENT_NUM, &count) || count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
while (count > 0) {
|
||||
uint8_t raw = 0;
|
||||
if (!read_reg(device, REG_KEY_EVENT, &raw) || raw == KEY_EVENT_EMPTY) {
|
||||
break;
|
||||
}
|
||||
drained++;
|
||||
|
||||
const bool pressed = (raw & 0x80U) != 0U;
|
||||
const uint8_t row = (raw >> 4U) & 0x07U;
|
||||
@ -308,7 +336,6 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
|
||||
if (row == MOD_ROW_SYM && col == MOD_COL_SYM) {
|
||||
internal->sym_active = pressed;
|
||||
update_leds(device, internal);
|
||||
count--;
|
||||
continue;
|
||||
}
|
||||
if (row == MOD_ROW_AA && col == MOD_COL_AA) {
|
||||
@ -324,16 +351,14 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
|
||||
internal->aa_tapped = false;
|
||||
}
|
||||
update_leds(device, internal);
|
||||
count--;
|
||||
continue;
|
||||
}
|
||||
if (row == MOD_ROW_CTRL && col == MOD_COL_CTRL) {
|
||||
internal->ctrl_held = pressed;
|
||||
count--;
|
||||
continue;
|
||||
}
|
||||
if (row == MOD_ROW_ALT && col == MOD_COL_ALT) {
|
||||
count--;
|
||||
internal->alt_held = pressed;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -354,10 +379,11 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
|
||||
// no business reaching into, so ESC is now just queued as a normal key
|
||||
// like everything else (LVGL/app code already handles ESC via focus/group
|
||||
// navigation the same way a dedicated ESC key on any other keyboard would).
|
||||
xQueueSend(internal->queue, &lv_key, 0);
|
||||
const Tab5KeyEvent event = { lv_key, internal->ctrl_held, internal->alt_held };
|
||||
xQueueSend(internal->queue, &event, 0);
|
||||
// Arm software repeat tracking by row/col to survive modifier changes
|
||||
const uint32_t now = now_ms();
|
||||
internal->repeat_key = lv_key;
|
||||
internal->repeat_event = event;
|
||||
internal->repeat_row = row;
|
||||
internal->repeat_col = col;
|
||||
internal->repeat_start_ms = now;
|
||||
@ -370,12 +396,11 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
|
||||
}
|
||||
} else if (row == internal->repeat_row && col == internal->repeat_col) {
|
||||
// Match release by position, not translated value — survives sticky Aa clear
|
||||
internal->repeat_key = 0;
|
||||
internal->repeat_event.key = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
count--;
|
||||
}
|
||||
|
||||
// Clear INT status after draining so the line de-asserts
|
||||
@ -434,13 +459,19 @@ static void poll_if_due(Device* device, Tab5KeyboardInternal* internal) {
|
||||
drain_events(device, internal);
|
||||
}
|
||||
|
||||
// Software key-repeat (runs every tick regardless of IRQ)
|
||||
if (internal->repeat_key != 0U) {
|
||||
if ((now - internal->repeat_start_ms) >= REPEAT_INITIAL_MS) {
|
||||
// Software key-repeat (runs every tick regardless of IRQ).
|
||||
//
|
||||
// The clock is re-read here rather than reusing `now` from the top of the function: a press
|
||||
// handled by the drain above sets repeat_start_ms to a timestamp taken *during* the drain, which
|
||||
// is later than `now`. The unsigned subtraction below would then wrap to a huge value and clear
|
||||
// the REPEAT_INITIAL_MS gate immediately, emitting one spurious repeat ~1ms after every press.
|
||||
const uint32_t repeat_now = now_ms();
|
||||
if (internal->repeat_event.key != 0U) {
|
||||
if ((repeat_now - internal->repeat_start_ms) >= REPEAT_INITIAL_MS) {
|
||||
const uint32_t last = internal->repeat_last_ms;
|
||||
if (last == 0 || (now - last) >= REPEAT_RATE_MS) {
|
||||
internal->repeat_last_ms = now;
|
||||
xQueueSend(internal->queue, &internal->repeat_key, 0);
|
||||
if (last == 0 || (repeat_now - last) >= REPEAT_RATE_MS) {
|
||||
internal->repeat_last_ms = repeat_now;
|
||||
xQueueSend(internal->queue, &internal->repeat_event, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -464,7 +495,7 @@ static error_t start(Device* device) {
|
||||
}
|
||||
memset(internal, 0, sizeof(Tab5KeyboardInternal));
|
||||
|
||||
internal->queue = xQueueCreate(20, sizeof(uint32_t));
|
||||
internal->queue = xQueueCreate(20, sizeof(Tab5KeyEvent));
|
||||
if (internal->queue == nullptr) {
|
||||
free(internal);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
@ -522,15 +553,19 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) {
|
||||
|
||||
poll_if_due(device, internal);
|
||||
|
||||
uint32_t lv_key = 0;
|
||||
if (xQueueReceive(internal->queue, &lv_key, 0) == pdTRUE) {
|
||||
data->key = lv_key;
|
||||
Tab5KeyEvent event = {};
|
||||
if (xQueueReceive(internal->queue, &event, 0) == pdTRUE) {
|
||||
data->key = event.key;
|
||||
data->pressed = true;
|
||||
data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0;
|
||||
data->ctrl = event.ctrl;
|
||||
data->alt = event.alt;
|
||||
} else {
|
||||
data->key = 0;
|
||||
data->pressed = false;
|
||||
data->continue_reading = false;
|
||||
data->ctrl = false;
|
||||
data->alt = false;
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
|
||||
7
Devices/tulip-4r11/CMakeLists.txt
Normal file
7
Devices/tulip-4r11/CMakeLists.txt
Normal file
@ -0,0 +1,7 @@
|
||||
file(GLOB_RECURSE SOURCE_FILES source/*.c*)
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS "source"
|
||||
REQUIRES TactilityKernel
|
||||
)
|
||||
195
Devices/tulip-4r11/LICENSE-Apache-2.0.md
Normal file
195
Devices/tulip-4r11/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### 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.
|
||||
|
||||
27
Devices/tulip-4r11/device.properties
Normal file
27
Devices/tulip-4r11/device.properties
Normal file
@ -0,0 +1,27 @@
|
||||
general.vendor=Tulip
|
||||
general.name=Tulip Creative Computer (4r11)
|
||||
|
||||
apps.launcherAppId=Launcher
|
||||
|
||||
hardware.target=ESP32S3
|
||||
hardware.flashSize=32MB
|
||||
hardware.spiRam=true
|
||||
hardware.spiRamMode=OCT
|
||||
hardware.spiRamSpeed=120M
|
||||
hardware.esptoolFlashFreq=120M
|
||||
hardware.usbHostEnabled=true
|
||||
hardware.bluetooth=true
|
||||
|
||||
storage.userDataLocation=Internal
|
||||
|
||||
touch.calibrationSupported=true
|
||||
touch.calibrationRequired=false
|
||||
|
||||
display.size=7"
|
||||
display.shape=rectangle
|
||||
display.dpi=117
|
||||
|
||||
lvgl.colorDepth=16
|
||||
lvgl.fontSize=20
|
||||
|
||||
sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y
|
||||
7
Devices/tulip-4r11/devicetree.yaml
Normal file
7
Devices/tulip-4r11/devicetree.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
dependencies:
|
||||
- Platforms/platform-esp32
|
||||
- Drivers/rgb-display-module
|
||||
- Drivers/gt911-module
|
||||
- Drivers/dummy-i2s-amp-module
|
||||
- Drivers/audio-stream-module
|
||||
dts: tulip-4r11.dts
|
||||
9
Devices/tulip-4r11/source/module.cpp
Normal file
9
Devices/tulip-4r11/source/module.cpp
Normal file
@ -0,0 +1,9 @@
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
Module tulip_4r11_module = {
|
||||
.name = "tulip-4r11"
|
||||
};
|
||||
|
||||
}
|
||||
195
Devices/tulip-4r11/tulip-4r11.dts
Normal file
195
Devices/tulip-4r11/tulip-4r11.dts
Normal file
@ -0,0 +1,195 @@
|
||||
/dts-v1/;
|
||||
|
||||
#include <tactility/bindings/battery_sense.h>
|
||||
#include <tactility/bindings/root.h>
|
||||
#include <tactility/bindings/esp32_adc_oneshot.h>
|
||||
#include <tactility/bindings/esp32_ble.h>
|
||||
#include <tactility/bindings/esp32_wifi_pinned.h>
|
||||
#include <tactility/bindings/esp32_gpio.h>
|
||||
#include <tactility/bindings/esp32_i2c.h>
|
||||
#include <tactility/bindings/esp32_i2s.h>
|
||||
#include <tactility/bindings/esp32_usbhost.h>
|
||||
#include <tactility/bindings/esp32_pwm_ledc.h>
|
||||
#include <tactility/bindings/gpio_hog.h>
|
||||
#include <tactility/bindings/pwm_backlight.h>
|
||||
|
||||
#include <bindings/rgb_display.h>
|
||||
#include <bindings/gt911.h>
|
||||
#include <bindings/dummy_i2s_amp.h>
|
||||
|
||||
// Reference: https: //github.com/TulipCC/tulipcc/
|
||||
/ {
|
||||
compatible = "root";
|
||||
model = "Tulip 4 (R11)";
|
||||
|
||||
wifi0 {
|
||||
compatible = "espressif,esp32-wifi-pinned";
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
ble0 {
|
||||
compatible = "espressif,esp32-ble";
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
gpio0 {
|
||||
compatible = "espressif,esp32-gpio";
|
||||
gpio-count = <49>;
|
||||
};
|
||||
|
||||
// Battery voltage is sensed through the 470K/470K divider (R34/R38) into GPIO3
|
||||
// (ADC1_CH2), i.e. a straight 2: 1 divider; the RY3730 charger IC has no I2C
|
||||
// interface to drive. Reference: tulipcc tulip/shared/py/tulip.py battery().
|
||||
adc0 {
|
||||
compatible = "espressif,esp32-adc-oneshot";
|
||||
unit-id = <ADC_UNIT_1>;
|
||||
channels = <ADC_CHANNEL_2 ADC_ATTEN_DB_12 ADC_BITWIDTH_DEFAULT>;
|
||||
};
|
||||
|
||||
battery-sense {
|
||||
compatible = "battery-sense";
|
||||
io-channel = <&adc0 0>;
|
||||
reference-voltage-mv = <3300>;
|
||||
multiplier = <2000>;
|
||||
};
|
||||
|
||||
// The Grove connector shares this I2C bus with the touch controller
|
||||
i2c0 {
|
||||
compatible = "espressif,esp32-i2c";
|
||||
port = <I2C_NUM_0>;
|
||||
clock-frequency = <100000>;
|
||||
pin-sda = <&gpio0 17 GPIO_FLAG_PULL_UP>;
|
||||
pin-scl = <&gpio0 18 GPIO_FLAG_PULL_UP>;
|
||||
|
||||
touch0 {
|
||||
compatible = "goodix,gt911";
|
||||
reg = <0x5D>;
|
||||
x-max = <1024>;
|
||||
y-max = <600>;
|
||||
// R11 inverts the reset line (active-high) and needs two toggle cycles before it
|
||||
// starts responding on I2C.
|
||||
pin-reset = <&gpio0 48 GPIO_FLAG_ACTIVE_HIGH>;
|
||||
reset-pulses = <2>;
|
||||
// The controller's programmed Y range (~750) exceeds the panel's 600 rows and the
|
||||
// sensor sits slightly offset from the panel; calibrate per the reference firmware
|
||||
x-offset = <-2>;
|
||||
y-offset = <-10>;
|
||||
y-scale = <800>;
|
||||
};
|
||||
};
|
||||
|
||||
// The unused panel data lines (G2 and B3 are unconnected on R11) are driven low exactly
|
||||
// like the reference firmware does, so the panel never sees floating inputs on them.
|
||||
panel_data_low_b4 {
|
||||
compatible = "gpio-hog";
|
||||
pin = <&gpio0 38 GPIO_FLAG_NONE>;
|
||||
mode = <GPIO_HOG_MODE_OUTPUT_LOW>;
|
||||
};
|
||||
|
||||
panel_data_low_b5 {
|
||||
compatible = "gpio-hog";
|
||||
pin = <&gpio0 46 GPIO_FLAG_NONE>;
|
||||
mode = <GPIO_HOG_MODE_OUTPUT_LOW>;
|
||||
};
|
||||
|
||||
panel_data_low_g3 {
|
||||
compatible = "gpio-hog";
|
||||
pin = <&gpio0 6 GPIO_FLAG_NONE>;
|
||||
mode = <GPIO_HOG_MODE_OUTPUT_LOW>;
|
||||
};
|
||||
|
||||
panel_data_low_g4 {
|
||||
compatible = "gpio-hog";
|
||||
pin = <&gpio0 7 GPIO_FLAG_NONE>;
|
||||
mode = <GPIO_HOG_MODE_OUTPUT_LOW>;
|
||||
};
|
||||
|
||||
panel_data_low_r3 {
|
||||
compatible = "gpio-hog";
|
||||
pin = <&gpio0 45 GPIO_FLAG_NONE>;
|
||||
mode = <GPIO_HOG_MODE_OUTPUT_LOW>;
|
||||
};
|
||||
|
||||
panel_data_low_r4 {
|
||||
compatible = "gpio-hog";
|
||||
pin = <&gpio0 13 GPIO_FLAG_NONE>;
|
||||
mode = <GPIO_HOG_MODE_OUTPUT_LOW>;
|
||||
};
|
||||
|
||||
// Backlight is active-low: the reference firmware drives duty 0 at max brightness.
|
||||
display_backlight_pwm {
|
||||
compatible = "espressif,esp32-pwm-ledc";
|
||||
pin = <&gpio0 47 GPIO_FLAG_NONE>;
|
||||
period-ns = <3333333>;
|
||||
duty-resolution = <13>;
|
||||
ledc-timer = <1>;
|
||||
ledc-channel = <1>;
|
||||
inverted;
|
||||
};
|
||||
|
||||
display_backlight {
|
||||
compatible = "pwm-backlight";
|
||||
pwm = <&display_backlight_pwm>;
|
||||
brightness-level-range = <0 255>;
|
||||
brightness-default = <200>;
|
||||
};
|
||||
|
||||
display0 {
|
||||
compatible = "espressif,esp32-rgb-display";
|
||||
horizontal-resolution = <1024>;
|
||||
vertical-resolution = <600>;
|
||||
pixel-clock-hz = <28000000>;
|
||||
hsync-pulse-width = <30>;
|
||||
hsync-back-porch = <16>;
|
||||
hsync-front-porch = <210>;
|
||||
vsync-pulse-width = <13>;
|
||||
vsync-back-porch = <10>;
|
||||
vsync-front-porch = <22>;
|
||||
data-width = <8>;
|
||||
bits-per-pixel = <8>;
|
||||
custom-pixel-format = <RGB_DISPLAY_PIXEL_FORMAT_RGB332>;
|
||||
num-fbs = <1>;
|
||||
pin-hsync = <&gpio0 39 GPIO_FLAG_NONE>;
|
||||
pin-vsync = <&gpio0 41 GPIO_FLAG_NONE>;
|
||||
pin-de = <&gpio0 40 GPIO_FLAG_NONE>;
|
||||
pin-pclk = <&gpio0 42 GPIO_FLAG_NONE>;
|
||||
pin-data0 = <&gpio0 9 GPIO_FLAG_NONE>; // B6
|
||||
pin-data1 = <&gpio0 1 GPIO_FLAG_NONE>; // B7
|
||||
pin-data2 = <&gpio0 15 GPIO_FLAG_NONE>; // G5
|
||||
pin-data3 = <&gpio0 16 GPIO_FLAG_NONE>; // G6
|
||||
pin-data4 = <&gpio0 4 GPIO_FLAG_NONE>; // G7
|
||||
pin-data5 = <&gpio0 10 GPIO_FLAG_NONE>; // R5
|
||||
pin-data6 = <&gpio0 21 GPIO_FLAG_NONE>; // R6
|
||||
pin-data7 = <&gpio0 14 GPIO_FLAG_NONE>; // R7
|
||||
backlight = <&display_backlight>;
|
||||
};
|
||||
|
||||
i2s0 {
|
||||
compatible = "espressif,esp32-i2s";
|
||||
port = <I2S_NUM_0>;
|
||||
pin-bclk = <&gpio0 8 GPIO_FLAG_NONE>;
|
||||
pin-ws = <&gpio0 2 GPIO_FLAG_NONE>;
|
||||
pin-data-out = <&gpio0 5 GPIO_FLAG_NONE>;
|
||||
};
|
||||
|
||||
speaker0 {
|
||||
compatible = "ti,pcm5101a";
|
||||
i2s = <&i2s0>;
|
||||
};
|
||||
|
||||
usbhost0 {
|
||||
compatible = "espressif,esp32-usbhost";
|
||||
|
||||
usbhosthid0 {
|
||||
compatible = "espressif,esp32-usbhost-hid";
|
||||
};
|
||||
|
||||
usbhostmidi0 {
|
||||
compatible = "espressif,esp32-usbhost-midi";
|
||||
};
|
||||
|
||||
usbhostmsc0 {
|
||||
compatible = "espressif,esp32-usbhost-msc";
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -12,6 +12,18 @@
|
||||
|
||||
## Higher Priority
|
||||
|
||||
- Move "# Fix error "PSRAM space not enough for the Flash instructions" on boot:" fix from T-Deck and others to device.py
|
||||
- Make it possible to override stack size for an app via config file (loaded at boot), and make it possible to set preferred memory location (e.g. internal/external)
|
||||
- Put task stacks in PSRAM when possible.
|
||||
- Wrap file operations like fopen/fclose with file_mutex
|
||||
- Add bold fonts for e-ink readability improvement
|
||||
- Split up Claude instructions: https://code.claude.com/docs/en/memory#import-additional-files
|
||||
and add https://github.com/multica-ai/andrej-karpathy-skills/blob/main/CLAUDE.md
|
||||
- Move test projects to their relevant subproject
|
||||
- tt_alertdialog start() etc is broken as it can't fetch the app instance id. Fetch automatically via thread context?
|
||||
- Migrate Tactility/Paths.cpp functions to TactilityKernel
|
||||
- app_manager_find_manifest() should make a copy, not return a pointer.
|
||||
- Httpd.cpp: warn if running on same CPU core (or task) as UI/LVGL/window manager.
|
||||
- Improve Setup: Show "Step done" screen
|
||||
- Improve Setup: Add keyboard/keypad navigation explanation
|
||||
- display.h API: get_backlight does not change ref counting, but it should
|
||||
@ -40,6 +52,8 @@
|
||||
|
||||
## Medium Priority
|
||||
|
||||
- Consider using https://github.com/Graphify-Labs/graphify
|
||||
- Consider implementing LVGL gridnav in apps https://lvgl.io/docs/open/9.3/details/auxiliary-modules/gridnav.html
|
||||
- Implement a LED kernel driver (single colour and RGB, plain GPIO and PWM)
|
||||
- Make USB host driver disabled by default, so it doesn't consume memory
|
||||
- Filtering for apps in App Hub:
|
||||
|
||||
9
Drivers/dummy-i2s-amp-module/bindings/ti,pcm5101a.yaml
Normal file
9
Drivers/dummy-i2s-amp-module/bindings/ti,pcm5101a.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
description: TI PCM5101A stereo DAC (I2S input, no register interface, no GPIO enable)
|
||||
|
||||
compatible: "ti,pcm5101a"
|
||||
|
||||
properties:
|
||||
i2s:
|
||||
type: phandle
|
||||
required: true
|
||||
description: "I2S controller device that carries audio data"
|
||||
@ -14,6 +14,7 @@ extern "C" {
|
||||
// typedef even though they all share the same underlying config layout.
|
||||
DEFINE_DEVICETREE(max98357a, struct DummyI2sAmpConfig)
|
||||
DEFINE_DEVICETREE(ns4168, struct DummyI2sAmpConfig)
|
||||
DEFINE_DEVICETREE(pcm5101a, struct DummyI2sAmpConfig)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@ -373,7 +373,7 @@ extern "C" {
|
||||
|
||||
Driver dummy_i2s_amp_driver = {
|
||||
.name = "dummy_i2s_amp",
|
||||
.compatible = (const char*[]) { "maxim,max98357a", "nsiway,ns4168", nullptr },
|
||||
.compatible = (const char*[]) { "maxim,max98357a", "nsiway,ns4168", "ti,pcm5101a", nullptr },
|
||||
.start_device = start_device,
|
||||
.stop_device = stop_device,
|
||||
.api = &API,
|
||||
|
||||
9
Drivers/esp-epaper-module/CMakeLists.txt
Normal file
9
Drivers/esp-epaper-module/CMakeLists.txt
Normal file
@ -0,0 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
tactility_add_module(esp-epaper-module
|
||||
SRCS source/esp_epaper.cpp source/module.cpp
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel platform-esp32 driver esp_epaper
|
||||
)
|
||||
195
Drivers/esp-epaper-module/LICENSE-Apache-2.0.md
Normal file
195
Drivers/esp-epaper-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### 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.
|
||||
|
||||
65
Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml
Normal file
65
Drivers/esp-epaper-module/bindings/tuanpmt,esp-epaper.yaml
Normal file
@ -0,0 +1,65 @@
|
||||
description: >
|
||||
E-paper display panels driven by the tuanpmt/esp_epaper ESP-IDF component
|
||||
(SSD1680/1681 BW, GDEY0154D67 and ACeP/BWRY color controllers). The node must
|
||||
be a child of an SPI controller: the display shares the parent's SPI host,
|
||||
clock and MOSI pins, and manages its DC/RESET/BUSY/CS pins itself via the
|
||||
component. Only 1bpp black/white panels are supported by the kernel display
|
||||
bridge (DISPLAY_COLOR_FORMAT_MONOCHROME); color panels fail to start.
|
||||
|
||||
compatible: "tuanpmt,esp-epaper"
|
||||
|
||||
bus: spi
|
||||
|
||||
properties:
|
||||
pin-dc:
|
||||
type: phandles
|
||||
required: true
|
||||
description: Data/Command GPIO pin
|
||||
pin-reset:
|
||||
type: phandles
|
||||
required: true
|
||||
description: Reset GPIO pin (the esp_epaper component drives it unconditionally)
|
||||
pin-busy:
|
||||
type: phandles
|
||||
required: true
|
||||
description: Busy GPIO pin (active high)
|
||||
pin-cs:
|
||||
type: phandles
|
||||
required: true
|
||||
description: Chip-select GPIO pin (driven manually by the component)
|
||||
clock-speed-hz:
|
||||
type: int
|
||||
default: 4000000
|
||||
description: SPI clock frequency in Hz (must be > 0)
|
||||
panel-type:
|
||||
type: text
|
||||
required: true
|
||||
description: >
|
||||
esp_epaper panel registry name. One of "gdey0154d67", "gdep073e01",
|
||||
"gdey037f51", "gdey029t71h", "ssd16xx-154", "ssd16xx-213", "ssd16xx-266",
|
||||
"ssd16xx-270", "ssd16xx-290", "ssd16xx-370", "ssd16xx-420".
|
||||
width:
|
||||
type: int
|
||||
default: 0
|
||||
description: Horizontal resolution override in pixels (0 = the panel's default, maximum 2048)
|
||||
height:
|
||||
type: int
|
||||
default: 0
|
||||
description: Vertical resolution override in pixels (0 = the panel's default, maximum 2048)
|
||||
update-mode:
|
||||
type: int
|
||||
default: 0
|
||||
description: >
|
||||
epd_update_mode_t used for frame updates: 0 = full, 1 = fast, 2 = partial.
|
||||
Full is the safest default; fast and partial refresh quickly but ghost.
|
||||
Values above 2 are rejected at driver start.
|
||||
rotation:
|
||||
type: int
|
||||
default: 0
|
||||
description: >
|
||||
Fixed display rotation applied at start. 0 = 0 degrees, 1 = 90 degrees,
|
||||
2 = 180 degrees, 3 = 270 degrees counter-clockwise (matching
|
||||
LV_DISPLAY_ROTATION_*). The kernel bridge reports the rotated resolution,
|
||||
so LVGL renders in rotated space and the driver rotates the 1bpp frame
|
||||
back to the panel's native layout before updating. Not changeable at
|
||||
runtime.
|
||||
3
Drivers/esp-epaper-module/devicetree.yaml
Normal file
3
Drivers/esp-epaper-module/devicetree.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
7
Drivers/esp-epaper-module/include/bindings/esp_epaper.h
Normal file
7
Drivers/esp-epaper-module/include/bindings/esp_epaper.h
Normal file
@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/esp_epaper.h>
|
||||
|
||||
DEFINE_DEVICETREE(esp_epaper, struct EspEpaperConfig)
|
||||
44
Drivers/esp-epaper-module/include/drivers/esp_epaper.h
Normal file
44
Drivers/esp-epaper-module/include/drivers/esp_epaper.h
Normal file
@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <epaper_config.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
/**
|
||||
* @brief Panel configuration for an esp_epaper-driven e-paper display.
|
||||
*
|
||||
* The struct field order matches the binding's property order and must stay in
|
||||
* sync with it: the devicetree compiler emits struct initializers in binding
|
||||
* order, not declaration order.
|
||||
*/
|
||||
struct EspEpaperConfig {
|
||||
struct GpioPinSpec pin_dc;
|
||||
struct GpioPinSpec pin_reset;
|
||||
struct GpioPinSpec pin_busy;
|
||||
struct GpioPinSpec pin_cs;
|
||||
/** SPI clock frequency in Hz */
|
||||
int clock_speed_hz;
|
||||
/** Panel registry name, e.g. "ssd16xx-290" (see the binding's panel-type) */
|
||||
const char* panel_type;
|
||||
/** Horizontal resolution override; 0 = the panel's default */
|
||||
uint16_t width;
|
||||
/** Vertical resolution override; 0 = the panel's default */
|
||||
uint16_t height;
|
||||
/** epd_update_mode_t used for frame updates */
|
||||
epd_update_mode_t update_mode;
|
||||
/**
|
||||
* Fixed display rotation, 0 = 0 degrees, 1 = 90, 2 = 180, 3 = 270 degrees
|
||||
* counter-clockwise (LV_DISPLAY_ROTATION_*). Not changeable at runtime.
|
||||
*/
|
||||
uint8_t rotation;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
14
Drivers/esp-epaper-module/include/esp_epaper_module.h
Normal file
14
Drivers/esp-epaper-module/include/esp_epaper_module.h
Normal file
@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module esp_epaper_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
353
Drivers/esp-epaper-module/source/esp_epaper.cpp
Normal file
353
Drivers/esp-epaper-module/source/esp_epaper.cpp
Normal file
@ -0,0 +1,353 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/esp_epaper.h>
|
||||
#include <esp_epaper_module.h>
|
||||
#include "esp_epaper_rotate.h"
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/display.h>
|
||||
#include <tactility/drivers/esp32_spi.h>
|
||||
#include <tactility/drivers/spi_controller.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <epaper.h>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
constexpr auto* TAG = "esp_epaper";
|
||||
#define GET_CONFIG(device) (static_cast<const EspEpaperConfig*>((device)->config))
|
||||
|
||||
/** Width/height overrides above this are rejected as nonsense. */
|
||||
constexpr uint16_t MAX_PANEL_DIMENSION = 2048;
|
||||
|
||||
struct EspEpaperInternal {
|
||||
/** Opaque esp_epaper device, owns the panel's pins and SPI device. */
|
||||
epd_handle_t epd;
|
||||
epd_panel_info_t info;
|
||||
/** Scratch buffer in native panel layout, for rotated frames (rotation != 0). */
|
||||
uint8_t* rotate_buffer;
|
||||
/** Serializes panel/SPI access between draw_bitmap and power state changes. */
|
||||
SemaphoreHandle_t panel_mutex;
|
||||
/** disp_on_off state; the panel is in deep sleep while false. */
|
||||
bool display_on;
|
||||
};
|
||||
|
||||
static uint16_t esp_epaper_get_display_width(const EspEpaperInternal* internal, uint8_t rotation) {
|
||||
return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.height : internal->info.width;
|
||||
}
|
||||
|
||||
static uint16_t esp_epaper_get_display_height(const EspEpaperInternal* internal, uint8_t rotation) {
|
||||
return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.width : internal->info.height;
|
||||
}
|
||||
|
||||
static bool resolve_panel_type(const char* name, epd_panel_type_t* out_type) {
|
||||
struct PanelMapping {
|
||||
const char* name;
|
||||
epd_panel_type_t type;
|
||||
};
|
||||
static constexpr PanelMapping kPanelMappings[] = {
|
||||
{ "gdey0154d67", EPD_PANEL_GDEY0154D67 },
|
||||
{ "gdep073e01", EPD_PANEL_GDEP073E01 },
|
||||
{ "gdey037f51", EPD_PANEL_GDEY037F51 },
|
||||
{ "gdey029t71h", EPD_PANEL_GDEY029T71H },
|
||||
{ "ssd16xx-154", EPD_PANEL_SSD16XX_154 },
|
||||
{ "ssd16xx-213", EPD_PANEL_SSD16XX_213 },
|
||||
{ "ssd16xx-266", EPD_PANEL_SSD16XX_266 },
|
||||
{ "ssd16xx-270", EPD_PANEL_SSD16XX_270 },
|
||||
{ "ssd16xx-290", EPD_PANEL_SSD16XX_290 },
|
||||
{ "ssd16xx-370", EPD_PANEL_SSD16XX_370 },
|
||||
{ "ssd16xx-420", EPD_PANEL_SSD16XX_420 },
|
||||
};
|
||||
for (const auto& mapping : kPanelMappings) {
|
||||
if (std::strcmp(name, mapping.name) == 0) {
|
||||
*out_type = mapping.type;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static error_t esp_epaper_reset(Device* device) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
|
||||
// epd_wake() toggles the reset pin and re-runs the full init sequence.
|
||||
const esp_err_t ret = epd_wake(internal->epd);
|
||||
// epd_wake() re-inits the panel, so it is awake (and drawable) again.
|
||||
if (ret == ESP_OK) {
|
||||
internal->display_on = true;
|
||||
}
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t esp_epaper_init(Device* device) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
|
||||
const esp_err_t ret = epd_wake(internal->epd);
|
||||
if (ret == ESP_OK) {
|
||||
internal->display_on = true;
|
||||
}
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// LVGL only ever calls this with the full frame: DISPLAY_COLOR_FORMAT_MONOCHROME forces
|
||||
// LV_DISPLAY_RENDER_MODE_FULL in the generic kernel LVGL bridge (lvgl_display.c), and FULL mode
|
||||
// only presents (calls draw_bitmap) once per render cycle, with the complete 0,0..hres,vres rect.
|
||||
// hres/vres are the rotated (display) resolution reported by get_resolution_*; color_data is
|
||||
// row-major, MSB-first 1bpp (LVGL's LV_COLOR_FORMAT_I1 with the palette header already stripped by
|
||||
// the caller); bit 1 = white, bit 0 = black. epd_update() expects exactly that layout and polarity.
|
||||
static error_t esp_epaper_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
const uint16_t display_width = esp_epaper_get_display_width(internal, config->rotation);
|
||||
const uint16_t display_height = esp_epaper_get_display_height(internal, config->rotation);
|
||||
if (x_start != 0 || y_start != 0 || x_end != display_width || y_end != display_height) {
|
||||
LOG_W(TAG, "draw_bitmap: only full-frame draws are supported (got %ld,%ld..%ld,%ld)", (long)x_start, (long)y_start, (long)x_end, (long)y_end);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
|
||||
|
||||
if (!internal->display_on) {
|
||||
// Display is off (deep sleep). Drop the frame; the mismatch is fine because the next
|
||||
// power-on triggers a full refresh of the current render anyway.
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
const uint8_t* source;
|
||||
if (config->rotation == 0) {
|
||||
source = static_cast<const uint8_t*>(color_data);
|
||||
} else {
|
||||
memset(internal->rotate_buffer, 0, internal->info.buffer_size);
|
||||
esp_epaper_rotate_frame(static_cast<const uint8_t*>(color_data), internal->rotate_buffer,
|
||||
internal->info.width, internal->info.height, config->rotation);
|
||||
source = internal->rotate_buffer;
|
||||
}
|
||||
|
||||
const esp_err_t ret = epd_update(internal->epd, source, config->update_mode);
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "epd_update failed: %s", esp_err_to_name(ret));
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t esp_epaper_disp_on_off(Device* device, bool on_off) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
|
||||
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
|
||||
if (on_off == internal->display_on) {
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
if (on_off) {
|
||||
if (epd_wake(internal->epd) != ESP_OK) {
|
||||
LOG_E(TAG, "epd_wake failed");
|
||||
ok = false;
|
||||
}
|
||||
} else {
|
||||
if (epd_sleep(internal->epd) != ESP_OK) {
|
||||
LOG_E(TAG, "epd_sleep failed");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
internal->display_on = on_off;
|
||||
}
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
return ok ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static DisplayColorFormat esp_epaper_get_color_format(Device*) {
|
||||
return DISPLAY_COLOR_FORMAT_MONOCHROME;
|
||||
}
|
||||
|
||||
static uint16_t esp_epaper_get_resolution_x(Device* device) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
return esp_epaper_get_display_width(internal, GET_CONFIG(device)->rotation);
|
||||
}
|
||||
|
||||
static uint16_t esp_epaper_get_resolution_y(Device* device) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
return esp_epaper_get_display_height(internal, GET_CONFIG(device)->rotation);
|
||||
}
|
||||
|
||||
static void esp_epaper_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
|
||||
*out_buffer = nullptr;
|
||||
}
|
||||
|
||||
static uint8_t esp_epaper_get_frame_buffer_count(Device*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const DisplayApi esp_epaper_display_api = {
|
||||
.capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH,
|
||||
.reset = esp_epaper_reset,
|
||||
.init = esp_epaper_init,
|
||||
.draw_bitmap = esp_epaper_draw_bitmap,
|
||||
.mirror = nullptr,
|
||||
.swap_xy = nullptr,
|
||||
.get_swap_xy = nullptr,
|
||||
.get_mirror_x = nullptr,
|
||||
.get_mirror_y = nullptr,
|
||||
.set_gap = nullptr,
|
||||
.get_gap_x = nullptr,
|
||||
.get_gap_y = nullptr,
|
||||
.invert_color = nullptr,
|
||||
.disp_on_off = esp_epaper_disp_on_off,
|
||||
.disp_sleep = nullptr,
|
||||
.get_color_format = esp_epaper_get_color_format,
|
||||
.get_resolution_x = esp_epaper_get_resolution_x,
|
||||
.get_resolution_y = esp_epaper_get_resolution_y,
|
||||
.get_frame_buffer = esp_epaper_get_frame_buffer,
|
||||
.get_frame_buffer_count = esp_epaper_get_frame_buffer_count,
|
||||
.get_backlight = nullptr,
|
||||
.has_capability = nullptr,
|
||||
};
|
||||
|
||||
static void free_internal(EspEpaperInternal* internal) {
|
||||
if (internal->epd != nullptr) {
|
||||
epd_deinit(internal->epd);
|
||||
}
|
||||
if (internal->rotate_buffer != nullptr) {
|
||||
free(internal->rotate_buffer);
|
||||
}
|
||||
if (internal->panel_mutex != nullptr) {
|
||||
vSemaphoreDelete(internal->panel_mutex);
|
||||
}
|
||||
free(internal);
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
|
||||
|
||||
const auto* spi_config = static_cast<const Esp32SpiConfig*>(parent->config);
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
epd_panel_type_t panel_type;
|
||||
if (!resolve_panel_type(config->panel_type, &panel_type)) {
|
||||
LOG_E(TAG, "Unknown panel type: %s", config->panel_type);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (config->clock_speed_hz == 0) {
|
||||
LOG_E(TAG, "Invalid clock_speed_hz (0)");
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (config->update_mode > EPD_UPDATE_PARTIAL) {
|
||||
LOG_E(TAG, "Invalid update_mode %d (expected %d-%d)", (int)config->update_mode, (int)EPD_UPDATE_FULL, (int)EPD_UPDATE_PARTIAL);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (config->width > MAX_PANEL_DIMENSION || config->height > MAX_PANEL_DIMENSION) {
|
||||
LOG_E(TAG, "Invalid panel size %ux%u (maximum %ux%u)", (unsigned)config->width, (unsigned)config->height, (unsigned)MAX_PANEL_DIMENSION, (unsigned)MAX_PANEL_DIMENSION);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
epd_config_t epd_config = EPD_CONFIG_DEFAULT();
|
||||
epd_config.pins.busy = config->pin_busy.pin;
|
||||
epd_config.pins.rst = config->pin_reset.pin;
|
||||
epd_config.pins.dc = config->pin_dc.pin;
|
||||
epd_config.pins.cs = config->pin_cs.pin;
|
||||
epd_config.pins.sck = spi_config->pin_sclk.pin;
|
||||
epd_config.pins.mosi = spi_config->pin_mosi.pin;
|
||||
epd_config.spi.host = spi_config->host;
|
||||
epd_config.spi.speed_hz = config->clock_speed_hz;
|
||||
epd_config.panel.type = panel_type;
|
||||
epd_config.panel.width = config->width;
|
||||
epd_config.panel.height = config->height;
|
||||
|
||||
epd_handle_t epd = nullptr;
|
||||
if (epd_init(&epd_config, &epd) != ESP_OK) {
|
||||
LOG_E(TAG, "epd_init failed");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* internal = static_cast<EspEpaperInternal*>(calloc(1, sizeof(EspEpaperInternal)));
|
||||
if (internal == nullptr) {
|
||||
epd_deinit(epd);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
internal->epd = epd;
|
||||
internal->panel_mutex = xSemaphoreCreateMutex();
|
||||
if (internal->panel_mutex == nullptr) {
|
||||
free_internal(internal);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
if (epd_get_info(epd, &internal->info) != ESP_OK) {
|
||||
LOG_E(TAG, "epd_get_info failed");
|
||||
free_internal(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (internal->info.color_mode != EPD_COLOR_BW) {
|
||||
LOG_E(TAG, "Panel color mode %d is not supported by the kernel display bridge (monochrome only)", internal->info.color_mode);
|
||||
free_internal(internal);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (config->rotation > 3) {
|
||||
LOG_E(TAG, "Invalid rotation %u (expected 0-3)", config->rotation);
|
||||
free_internal(internal);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (config->rotation != 0) {
|
||||
internal->rotate_buffer = static_cast<uint8_t*>(malloc(internal->info.buffer_size));
|
||||
if (internal->rotate_buffer == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate %lu-byte rotation buffer", internal->info.buffer_size);
|
||||
free_internal(internal);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
}
|
||||
|
||||
internal->display_on = true;
|
||||
device_set_driver_data(device, internal);
|
||||
|
||||
LOG_I(TAG, "Started %ux%u panel (buffer %lu bytes, rotation %u)", internal->info.width, internal->info.height, internal->info.buffer_size, config->rotation);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
|
||||
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
|
||||
if (internal->display_on) {
|
||||
// Leave the panel in deep sleep.
|
||||
epd_sleep(internal->epd);
|
||||
internal->display_on = false;
|
||||
}
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
|
||||
free_internal(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Driver esp_epaper_driver = {
|
||||
.name = "esp_epaper",
|
||||
.compatible = (const char*[]) { "tuanpmt,esp-epaper", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &esp_epaper_display_api,
|
||||
.device_type = &DISPLAY_TYPE,
|
||||
.owner = &esp_epaper_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
69
Drivers/esp-epaper-module/source/esp_epaper_rotate.h
Normal file
69
Drivers/esp-epaper-module/source/esp_epaper_rotate.h
Normal file
@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Rotates a full 1bpp frame (row-major, MSB-first) from the display's rotated
|
||||
* space into the panel's native space. The display is the panel's native
|
||||
* width x height turned counter-clockwise by `rotation` (0 = 0, 1 = 90, 2 = 180,
|
||||
* 3 = 270 degrees), so its own dimensions are the swapped native ones for
|
||||
* 90/270. Each mapping is the inverse of LVGL's lv_display_rotate_area()
|
||||
* (Libraries/lvgl/src/display/lv_display.c), i.e. the exact transform LVGL
|
||||
* applies when displaying on the panel.
|
||||
*
|
||||
* src points to the display frame (stride = ceil(display_width / 8) bytes,
|
||||
* MSB-first bits), dst to the native frame (stride = ceil(width / 8) bytes).
|
||||
* dst is overwritten; it may not alias src.
|
||||
*/
|
||||
static inline bool esp_epaper_rotation_swaps_axes(uint8_t rotation) {
|
||||
return rotation == 1 || rotation == 3;
|
||||
}
|
||||
|
||||
static inline void esp_epaper_rotate_frame(const uint8_t* src, uint8_t* dst, uint16_t width, uint16_t height, uint8_t rotation) {
|
||||
const uint16_t display_width = esp_epaper_rotation_swaps_axes(rotation) ? height : width;
|
||||
const uint32_t src_stride = (display_width + 7) / 8;
|
||||
const uint32_t dst_stride = (width + 7) / 8;
|
||||
|
||||
memset(dst, 0, dst_stride * height);
|
||||
|
||||
for (uint16_t y = 0; y < height; y++) {
|
||||
for (uint16_t x = 0; x < width; x++) {
|
||||
// Display pixel (u, v) that lands on native pixel (x, y).
|
||||
uint16_t u;
|
||||
uint16_t v;
|
||||
switch (rotation) {
|
||||
case 0:
|
||||
u = x;
|
||||
v = y;
|
||||
break;
|
||||
case 1:
|
||||
u = height - 1 - y;
|
||||
v = x;
|
||||
break;
|
||||
case 2:
|
||||
u = width - 1 - x;
|
||||
v = height - 1 - y;
|
||||
break;
|
||||
case 3:
|
||||
default:
|
||||
u = y;
|
||||
v = width - 1 - x;
|
||||
break;
|
||||
}
|
||||
if (src[(uint32_t)v * src_stride + u / 8] & (0x80 >> (u % 8))) {
|
||||
dst[(uint32_t)y * dst_stride + x / 8] |= (0x80 >> (x % 8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
19
Drivers/esp-epaper-module/source/module.cpp
Normal file
19
Drivers/esp-epaper-module/source/module.cpp
Normal file
@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern Driver esp_epaper_driver;
|
||||
|
||||
extern "C" {
|
||||
|
||||
static Driver* const esp_epaper_drivers[] = {
|
||||
&esp_epaper_driver,
|
||||
nullptr
|
||||
};
|
||||
|
||||
Module esp_epaper_module = {
|
||||
.name = "esp_epaper",
|
||||
.drivers = esp_epaper_drivers
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@ -30,8 +30,39 @@ properties:
|
||||
pin-reset:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Reset GPIO pin
|
||||
description: |
|
||||
Reset GPIO pin. Reset polarity is set with the GPIO descriptor's ACTIVE_HIGH or
|
||||
ACTIVE_LOW flag (active-low is the GT911's native convention; boards that invert the
|
||||
reset line, e.g. Tulip 4 R11, use ACTIVE_HIGH).
|
||||
pin-interrupt:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Interrupt GPIO pin
|
||||
reset-pulses:
|
||||
type: int
|
||||
default: 1
|
||||
description: |
|
||||
Number of reset pulses applied before the controller is initialized. Some panels
|
||||
(e.g. Tulip 4 R11) require two toggle cycles before they respond on I2C.
|
||||
x-offset:
|
||||
type: int
|
||||
default: 0
|
||||
description: |
|
||||
Signed X coordinate correction added to the raw controller output before scaling.
|
||||
Aligns the sensor with the panel (e.g. -2 on Tulip 4 R11).
|
||||
y-offset:
|
||||
type: int
|
||||
default: 0
|
||||
description: |
|
||||
Signed Y coordinate correction added to the raw controller output before scaling
|
||||
(e.g. -12 on Tulip 4 R11).
|
||||
x-scale:
|
||||
type: int
|
||||
default: 1000
|
||||
description: |
|
||||
X coordinate scale as per-mille (1000 = 1.0), applied after x-offset.
|
||||
y-scale:
|
||||
type: int
|
||||
default: 1000
|
||||
description: |
|
||||
Y coordinate scale as per-mille (1000 = 1.0), applied after y-offset.
|
||||
|
||||
@ -22,6 +22,11 @@ struct Gt911Config {
|
||||
bool mirror_y;
|
||||
struct GpioPinSpec pin_reset;
|
||||
struct GpioPinSpec pin_interrupt;
|
||||
uint8_t reset_pulses;
|
||||
int16_t x_offset;
|
||||
int16_t y_offset;
|
||||
uint16_t x_scale;
|
||||
uint16_t y_scale;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/esp32_i2c.h>
|
||||
#include <tactility/drivers/esp32_i2c_master.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/log.h>
|
||||
@ -16,6 +17,8 @@
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_touch.h>
|
||||
#include <esp_lcd_touch_gt911.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
@ -31,6 +34,42 @@ static gpio_num_t pin_or_nc(const GpioPinSpec& pin) {
|
||||
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
|
||||
}
|
||||
|
||||
// Reset polarity is carried by the pin_reset descriptor's ACTIVE_HIGH/ACTIVE_LOW flag, so the
|
||||
// pin is always pulsed with logical levels here. Some boards also require several toggle cycles
|
||||
// before the controller starts responding on I2C (e.g. Tulip 4 R11). The esp_lcd_touch_gt911
|
||||
// driver only performs a single reset, so the pin is pulsed here first
|
||||
static error_t reset_controller_pin(const GpioPinSpec& pin, uint8_t pulses) {
|
||||
if (pulses == 0 || pin.gpio_controller == nullptr) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
auto* descriptor = gpio_descriptor_acquire(pin.gpio_controller, pin.pin, pin.flags | GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO);
|
||||
if (descriptor == nullptr) {
|
||||
LOG_E(TAG, "Failed to acquire reset pin");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < pulses; ++i) {
|
||||
bool last_pulse = i == pulses - 1;
|
||||
error_t error = gpio_descriptor_set_level(descriptor, true);
|
||||
if (error == ERROR_NONE) {
|
||||
vTaskDelay(pdMS_TO_TICKS(11));
|
||||
error = gpio_descriptor_set_level(descriptor, false);
|
||||
}
|
||||
if (error == ERROR_NONE) {
|
||||
vTaskDelay(pdMS_TO_TICKS(last_pulse ? 1000 : 60));
|
||||
}
|
||||
if (error != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to pulse reset pin");
|
||||
gpio_descriptor_release(descriptor);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
gpio_descriptor_release(descriptor);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
// GT911's I2C address depends on the controller's INT pin level at power-up (board-strapped, not
|
||||
@ -62,6 +101,32 @@ static esp_err_t create_io_handle(Device* parent, esp_lcd_panel_io_handle_t* out
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// Applies the per-device affine calibration (offset + per-mille scale) in the esp_lcd_touch
|
||||
// framework's own coordinate hook, so downstream consumers always receive panel-space coords.
|
||||
// Called before mirror/swap in esp_lcd_touch_get_coordinates().
|
||||
static void gt911_process_coordinates(
|
||||
esp_lcd_touch_handle_t tp,
|
||||
uint16_t* x,
|
||||
uint16_t* y,
|
||||
uint16_t* strength,
|
||||
uint8_t* point_count,
|
||||
uint8_t max_point_count
|
||||
) {
|
||||
(void)strength;
|
||||
(void)max_point_count;
|
||||
auto* config = static_cast<const Gt911Config*>(tp->config.user_data);
|
||||
if (config == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < *point_count; i++) {
|
||||
int32_t mapped_x = (static_cast<int32_t>(x[i]) + config->x_offset) * config->x_scale / 1000;
|
||||
int32_t mapped_y = (static_cast<int32_t>(y[i]) + config->y_offset) * config->y_scale / 1000;
|
||||
x[i] = static_cast<uint16_t>(mapped_x < 0 ? 0 : (mapped_x > config->x_max ? config->x_max : mapped_x));
|
||||
y[i] = static_cast<uint16_t>(mapped_y < 0 ? 0 : (mapped_y > config->y_max ? config->y_max : mapped_y));
|
||||
}
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
||||
@ -73,6 +138,12 @@ static error_t start(Device* device) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
error_t error = reset_controller_pin(config->pin_reset, config->reset_pulses);
|
||||
if (error != ERROR_NONE) {
|
||||
free(internal);
|
||||
return error;
|
||||
}
|
||||
|
||||
esp_err_t ret = create_io_handle(parent, &internal->io_handle);
|
||||
if (ret != ESP_OK) {
|
||||
free(internal);
|
||||
@ -84,9 +155,10 @@ static error_t start(Device* device) {
|
||||
.y_max = config->y_max,
|
||||
.rst_gpio_num = pin_or_nc(config->pin_reset),
|
||||
.int_gpio_num = pin_or_nc(config->pin_interrupt),
|
||||
// GT911's reset and interrupt lines are both fixed active-low in hardware.
|
||||
// Reset polarity comes from the pin_reset descriptor's ACTIVE_HIGH/ACTIVE_LOW flag; the
|
||||
// interrupt line is fixed active-low in hardware.
|
||||
.levels = {
|
||||
.reset = 0u,
|
||||
.reset = (config->pin_reset.flags & GPIO_FLAG_ACTIVE_LOW) != 0 ? 0u : 1u,
|
||||
.interrupt = 0u,
|
||||
},
|
||||
.flags = {
|
||||
@ -94,9 +166,9 @@ static error_t start(Device* device) {
|
||||
.mirror_x = config->mirror_x ? 1u : 0u,
|
||||
.mirror_y = config->mirror_y ? 1u : 0u,
|
||||
},
|
||||
.process_coordinates = nullptr,
|
||||
.process_coordinates = gt911_process_coordinates,
|
||||
.interrupt_callback = nullptr,
|
||||
.user_data = nullptr,
|
||||
.user_data = (void*)config,
|
||||
.driver_data = nullptr,
|
||||
};
|
||||
|
||||
|
||||
@ -222,3 +222,11 @@ properties:
|
||||
type: phandle
|
||||
default: "NULL"
|
||||
description: Optional reference to this display's backlight device
|
||||
custom-pixel-format:
|
||||
type: int
|
||||
default: 0
|
||||
description: |
|
||||
Optional software pixel-format conversion applied to scan-out pixels, when the host renders
|
||||
in a different depth than the panel (LVGL always renders RGB565).
|
||||
0 = no conversion (RGB_DISPLAY_PIXEL_FORMAT_DEFAULT), 1 = RGB332
|
||||
(RGB_DISPLAY_PIXEL_FORMAT_RGB332) for 8 data-line panels.
|
||||
|
||||
@ -11,6 +11,18 @@ extern "C" {
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
/**
|
||||
* Optional software pixel-format conversion applied to scan-out pixels. The panel itself always
|
||||
* runs at bits_per_pixel; when the host renders in a different depth (LVGL always renders RGB565
|
||||
* for this driver), the selected mapper converts each tile before it reaches esp_lcd.
|
||||
*/
|
||||
enum RgbDisplayPixelFormat {
|
||||
/** No conversion: scan-out pixels must already match bits_per_pixel. */
|
||||
RGB_DISPLAY_PIXEL_FORMAT_DEFAULT = 0,
|
||||
/** Convert RGB565 to packed 8-bit RGB332 (3R, 3G, 2B), for 8 data-line panels. */
|
||||
RGB_DISPLAY_PIXEL_FORMAT_RGB332 = 1,
|
||||
};
|
||||
|
||||
struct RgbDisplayConfig {
|
||||
uint16_t horizontal_resolution;
|
||||
uint16_t vertical_resolution;
|
||||
@ -84,6 +96,9 @@ struct RgbDisplayConfig {
|
||||
|
||||
// Optional reference to this display's backlight device, NULL if none.
|
||||
struct Device* backlight;
|
||||
|
||||
// Optional software pixel-format conversion for scan-out (see enum RgbDisplayPixelFormat).
|
||||
enum RgbDisplayPixelFormat pixel_format;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* Opaque per-instance state for a software pixel mapper, allocated by SoftwarePixelMapperCreateFn
|
||||
* and released by SoftwarePixelMapperDestroyFn. The instance owns its conversion destination
|
||||
* buffer: the driver passes this handle as map()'s dst argument.
|
||||
*/
|
||||
typedef void* SoftwarePixelMapperData;
|
||||
|
||||
/**
|
||||
* Allocates a mapper instance, including its destination buffer, sized for a width x height
|
||||
* source buffer.
|
||||
* @param width source buffer width in pixels
|
||||
* @param height source buffer height in pixels
|
||||
* @return a non-null instance handle, or NULL on allocation failure
|
||||
*/
|
||||
typedef SoftwarePixelMapperData (*SoftwarePixelMapperCreateFn)(uint16_t width, uint16_t height);
|
||||
|
||||
/**
|
||||
* Converts pixel_count pixels from the mapper's source format to its destination format.
|
||||
* @param data instance handle from SoftwarePixelMapperCreateFn
|
||||
* @param src RGB565 source pixels
|
||||
* @param dst destination buffer, typically the instance's own buffer (i.e. the data handle)
|
||||
* @param pixel_count number of pixels to convert
|
||||
*/
|
||||
typedef void (*SoftwarePixelMapperMapFn)(SoftwarePixelMapperData data, const uint16_t* src, uint8_t* dst, uint32_t pixel_count);
|
||||
|
||||
/**
|
||||
* Releases a mapper instance created by SoftwarePixelMapperCreateFn, including its destination
|
||||
* buffer.
|
||||
* @param data instance handle to release
|
||||
*/
|
||||
typedef void (*SoftwarePixelMapperDestroyFn)(SoftwarePixelMapperData data);
|
||||
|
||||
/**
|
||||
* A software pixel-format conversion, used to translate a driver's native source depth into a
|
||||
* panel's scan-out depth when the two differ. Each mapper owns its own destination buffer, so it
|
||||
* can size the allocation for its output format.
|
||||
*/
|
||||
struct SoftwarePixelMapper {
|
||||
SoftwarePixelMapperCreateFn create;
|
||||
SoftwarePixelMapperMapFn map;
|
||||
SoftwarePixelMapperDestroyFn destroy;
|
||||
};
|
||||
|
||||
/**
|
||||
* RGB565 to packed 8-bit RGB332 (3 bits red, 3 bits green, 2 bits blue) mapper for panels wired
|
||||
* with only 8 data lines.
|
||||
*/
|
||||
extern const struct SoftwarePixelMapper software_pixel_mapper_rgb332;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
#include <drivers/rgb_display.h>
|
||||
#include <rgb_display_module.h>
|
||||
#include <drivers/software_pixel_mapper.h>
|
||||
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/device.h>
|
||||
@ -40,6 +41,12 @@ struct RgbDisplayInternal {
|
||||
// Signaled by on_frame_buf_complete once per real DMA scan-out of a whole frame. Only
|
||||
// waited on in draw_bitmap() when color_data is one of frame_buffers - see the comment there for why.
|
||||
SemaphoreHandle_t frame_complete_semaphore;
|
||||
// Software pixel-format conversion active when custom-pixel-format != DEFAULT (see start()).
|
||||
// LVGL always renders RGB565 for this driver, so draw_bitmap() converts each tile through the
|
||||
// mapper before handing it to esp_lcd. The mapper owns its scratch buffer, held in
|
||||
// pixel_mapper_data. Null when no conversion is active.
|
||||
const struct SoftwarePixelMapper* pixel_mapper;
|
||||
SoftwarePixelMapperData pixel_mapper_data;
|
||||
};
|
||||
|
||||
// esp_lcd_rgb_panel's draw_bitmap() has a zero-copy path when color_data is one of the panel's
|
||||
@ -128,6 +135,8 @@ static error_t start(Device* device) {
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
internal->pixel_mapper = nullptr;
|
||||
internal->pixel_mapper_data = nullptr;
|
||||
|
||||
error_t reset_error = perform_hardware_reset(config);
|
||||
if (reset_error != ERROR_NONE) {
|
||||
@ -231,6 +240,28 @@ static error_t start(Device* device) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (config->pixel_format != RGB_DISPLAY_PIXEL_FORMAT_DEFAULT) {
|
||||
switch (config->pixel_format) {
|
||||
case RGB_DISPLAY_PIXEL_FORMAT_RGB332:
|
||||
internal->pixel_mapper = &software_pixel_mapper_rgb332;
|
||||
break;
|
||||
default:
|
||||
LOG_E(TAG, "Unsupported pixel format %d", (int)config->pixel_format);
|
||||
free(internal);
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// The mapper sizes and allocates its own whole-frame destination buffer, which lands in
|
||||
// PSRAM on boards that have it (see software_pixel_mapper.cpp).
|
||||
internal->pixel_mapper_data = internal->pixel_mapper->create(config->horizontal_resolution, config->vertical_resolution);
|
||||
if (internal->pixel_mapper_data == nullptr) {
|
||||
LOG_E(TAG, "Failed to create pixel mapper");
|
||||
esp_lcd_panel_del(internal->panel_handle);
|
||||
free(internal);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
}
|
||||
|
||||
internal->frame_complete_semaphore = xSemaphoreCreateBinary();
|
||||
if (internal->frame_complete_semaphore == nullptr) {
|
||||
esp_lcd_panel_del(internal->panel_handle);
|
||||
@ -264,6 +295,9 @@ static error_t stop(Device* device) {
|
||||
}
|
||||
|
||||
vSemaphoreDelete(internal->frame_complete_semaphore);
|
||||
if (internal->pixel_mapper != nullptr) {
|
||||
internal->pixel_mapper->destroy(internal->pixel_mapper_data);
|
||||
}
|
||||
free(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
@ -299,15 +333,33 @@ static bool rgb_display_color_data_is_frame_buffer(const RgbDisplayInternal* int
|
||||
return false;
|
||||
}
|
||||
|
||||
// Converts a contiguous RGB565 region into the configured scan-out format, if any. The
|
||||
// conversion is delegated to the pixel mapper selected in start(); the result is written into the
|
||||
// mapper's scratch buffer so the zero-copy frame-buffer wait logic below can treat it like any
|
||||
// other caller-owned buffer.
|
||||
static error_t rgb_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
|
||||
auto* internal = static_cast<RgbDisplayInternal*>(device_get_driver_data(device));
|
||||
|
||||
bool wait_for_scanout = rgb_display_color_data_is_frame_buffer(internal, color_data);
|
||||
const void* source_data = color_data;
|
||||
if (internal->pixel_mapper != nullptr) {
|
||||
uint32_t pixel_count = (uint32_t)(x_end - x_start) * (uint32_t)(y_end - y_start);
|
||||
// The mapper's own destination buffer is its instance data, so it is passed as both the
|
||||
// instance handle and the output buffer.
|
||||
internal->pixel_mapper->map(
|
||||
internal->pixel_mapper_data,
|
||||
static_cast<const uint16_t*>(color_data),
|
||||
static_cast<uint8_t*>(internal->pixel_mapper_data),
|
||||
pixel_count
|
||||
);
|
||||
source_data = internal->pixel_mapper_data;
|
||||
}
|
||||
|
||||
bool wait_for_scanout = rgb_display_color_data_is_frame_buffer(internal, source_data);
|
||||
if (wait_for_scanout) {
|
||||
xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal
|
||||
}
|
||||
|
||||
if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) {
|
||||
if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, source_data) != ESP_OK) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
@ -375,7 +427,12 @@ static void rgb_display_get_frame_buffer(Device* device, uint8_t index, void** o
|
||||
|
||||
static uint8_t rgb_display_get_frame_buffer_count(Device* device) {
|
||||
auto* internal = static_cast<RgbDisplayInternal*>(device_get_driver_data(device));
|
||||
return internal->frame_buffer_count;
|
||||
// A converted-format panel's frame buffer runs at the native bits_per_pixel, which LVGL can't
|
||||
// write directly (it always renders RGB565 for this driver). Exposing it would make
|
||||
// lvgl_display.c bind LVGL straight onto it and corrupt the buffer; instead report 0 so LVGL
|
||||
// renders into its own RGB565 buffers and flushes per-tile through the conversion in
|
||||
// draw_bitmap().
|
||||
return GET_CONFIG(device)->pixel_format != RGB_DISPLAY_PIXEL_FORMAT_DEFAULT ? 0 : internal->frame_buffer_count;
|
||||
}
|
||||
|
||||
static error_t rgb_display_get_backlight(Device* device, Device** backlight) {
|
||||
|
||||
42
Drivers/rgb-display-module/source/software_pixel_mapper.cpp
Normal file
42
Drivers/rgb-display-module/source/software_pixel_mapper.cpp
Normal file
@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <soc/soc_caps.h>
|
||||
#if SOC_LCD_RGB_SUPPORTED
|
||||
|
||||
#include <drivers/software_pixel_mapper.h>
|
||||
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
// RGB332 packs each pixel into one byte: 3 bits red, 3 bits green, 2 bits blue. The byte layout
|
||||
// maps straight onto an 8-data-line panel's significant color inputs (R7..R5, G7..G5, B7..B6),
|
||||
// so each RGB565 channel's top bits land on the corresponding MSB lines
|
||||
static void rgb332_map(SoftwarePixelMapperData data, const uint16_t* src, uint8_t* dst, uint32_t pixel_count) {
|
||||
(void)data;
|
||||
for (uint32_t i = 0; i < pixel_count; i++) {
|
||||
uint16_t px = src[i];
|
||||
dst[i] = (uint8_t)(((px >> 13) & 0x07) << 5) | (((px >> 8) & 0x07) << 2) | ((px >> 3) & 0x03);
|
||||
}
|
||||
}
|
||||
|
||||
// The destination buffer must hold a whole frame (1 byte/pixel for RGB332). A 1024x600 panel
|
||||
// needs ~600KB, which only fits in PSRAM on most boards, prefer SPIRAM and fall back to whatever
|
||||
// internal RAM is available. The returned handle is this buffer, passed back as map()'s dst.
|
||||
static SoftwarePixelMapperData rgb332_create(uint16_t width, uint16_t height) {
|
||||
size_t buffer_size = (size_t)width * height;
|
||||
void* buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (buffer == nullptr) {
|
||||
buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_DEFAULT);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
static void rgb332_destroy(SoftwarePixelMapperData data) {
|
||||
heap_caps_free(data);
|
||||
}
|
||||
|
||||
const struct SoftwarePixelMapper software_pixel_mapper_rgb332 = {
|
||||
.create = rgb332_create,
|
||||
.map = rgb332_map,
|
||||
.destroy = rgb332_destroy,
|
||||
};
|
||||
|
||||
#endif // SOC_LCD_RGB_SUPPORTED
|
||||
@ -96,6 +96,8 @@ else ()
|
||||
Tactility
|
||||
TactilityFreeRtos
|
||||
lvgl-module
|
||||
lvgl-window-manager-module
|
||||
app-module
|
||||
crypt-module
|
||||
gps-module
|
||||
gps-generic-module
|
||||
|
||||
@ -25,4 +25,9 @@ else()
|
||||
target_include_directories(QRCode
|
||||
PUBLIC src
|
||||
)
|
||||
|
||||
# qrcode.h polyfills bool/true/false for pre-C23 compilers - on a host compiler that
|
||||
# defaults to C23 (where bool is a keyword), that polyfill itself fails to compile. Pin to
|
||||
# C11 for the simulator build only; ESP-IDF's own toolchain default is unaffected.
|
||||
set_target_properties(QRCode PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON)
|
||||
endif()
|
||||
|
||||
1
Libraries/esp_epaper
Submodule
1
Libraries/esp_epaper
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 0bf4f144b543048f10bdadd49f0578858bdc2a39
|
||||
195
Modules/app-esp32-module/LICENSE-Apache-2.0.md
Normal file
195
Modules/app-esp32-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### 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.
|
||||
|
||||
@ -1,24 +1,27 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "../../../TactilityKernel/include/tactility/error.h"
|
||||
#include "../../../TactilityKernel/include/tactility/filesystem/file_mutex.h"
|
||||
#include "../../app-module/include/app/loader.h"
|
||||
#include "../../app-module/include/app/location.h"
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#include <app/loader.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/location.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/filesystem/file_mutex.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <service/instance.h>
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <esp_elf.h>
|
||||
#include <esp_err.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
#include <string>
|
||||
|
||||
constexpr auto* TAG = "app_esp32_loader";
|
||||
|
||||
namespace {
|
||||
|
||||
/** load()-allocated state, passed back through run()/unload(). */
|
||||
@ -34,6 +37,7 @@ error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
|
||||
|
||||
FILE* file = fopen(path, "rb");
|
||||
if (file == nullptr) {
|
||||
LOG_E(TAG, "Failed to open %s", path);
|
||||
file_mutex_unlock(&mutex);
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
@ -68,19 +72,35 @@ error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
auto* runtime = new (std::nothrow) Esp32AppRuntime();
|
||||
if (runtime == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
// location.location can be either an app's install directory or the .elf file directly; the
|
||||
// former resolves to the per-target binary at {dir}/elf/{CONFIG_IDF_TARGET}.elf.
|
||||
std::string resolve_elf_path(const std::string& path) {
|
||||
if (path.ends_with(".elf")) {
|
||||
return path;
|
||||
}
|
||||
return path + "/elf/" + CONFIG_IDF_TARGET + ".elf";
|
||||
}
|
||||
|
||||
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
if (location.type != APP_LOCATION_PATH) {
|
||||
LOG_E(TAG, "Out of memory");
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Loading %s", static_cast<const char*>(location.location));
|
||||
|
||||
auto* runtime = new (std::nothrow) Esp32AppRuntime();
|
||||
if (runtime == nullptr) {
|
||||
LOG_E(TAG, "Out of memory");
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
auto elf_path = resolve_elf_path(static_cast<const char*>(location.location));
|
||||
|
||||
size_t size = 0;
|
||||
error_t read_result = read_file(static_cast<const char*>(location.location), &runtime->file_data, &size);
|
||||
error_t read_result = read_file(elf_path.c_str(), &runtime->file_data, &size);
|
||||
if (read_result != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to read file");
|
||||
delete runtime;
|
||||
return read_result;
|
||||
}
|
||||
@ -88,13 +108,15 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
if (esp_elf_init(&runtime->elf) != ESP_OK) {
|
||||
free(runtime->file_data);
|
||||
delete runtime;
|
||||
LOG_E(TAG, "Failed to init elf");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (esp_elf_relocate(&runtime->elf, runtime->file_data) != 0) {
|
||||
esp_elf_deinit(&runtime->elf);
|
||||
// esp_elf_relocate() already frees elf->pdata/ptext itself on a relocation failure
|
||||
free(runtime->file_data);
|
||||
delete runtime;
|
||||
LOG_E(TAG, "Failed to map elf");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
@ -104,9 +126,6 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
|
||||
int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc, char* argv[]) {
|
||||
auto* runtime = static_cast<Esp32AppRuntime*>(runtime_ptr);
|
||||
// A side-loaded ELF's own main() only ever gets a real argc/argv from esp_elf_request()'s
|
||||
// fixed signature - there's no slot for app_instance_id there, and side-loaded apps don't
|
||||
// need one yet.
|
||||
return esp_elf_request(&runtime->elf, 0, argc, argv);
|
||||
}
|
||||
|
||||
@ -132,7 +151,7 @@ void destroy_service(const ServiceManifest*, void*) {
|
||||
|
||||
} // namespace
|
||||
|
||||
extern ServiceManifest loader_service_manifest = {
|
||||
ServiceManifest loader_service_manifest = {
|
||||
.id = APP_LOADER_PATH_SERVICE_ID,
|
||||
.create_service = create_service,
|
||||
.destroy_service = destroy_service,
|
||||
|
||||
195
Modules/app-module/LICENSE-Apache-2.0.md
Normal file
195
Modules/app-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### 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.
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Identifies a running (or previously running) app instance. 0 is never a valid instance id. */
|
||||
typedef uint32_t AppInstanceId;
|
||||
|
||||
/** Lifecycle state of a running (or previously running) app instance. Every app instance owns
|
||||
* its own task for its entire lifetime - there is no "saved, task given up" state. */
|
||||
typedef enum {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@ -13,9 +13,6 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Identifies a running (or previously running) app instance. 0 is never a valid instance id. */
|
||||
typedef uint32_t AppInstanceId;
|
||||
|
||||
/**
|
||||
* Register an app manifest.
|
||||
* @retval ERROR_INVALID_ARGUMENT a manifest with the same id is already registered
|
||||
@ -34,9 +31,10 @@ error_t app_manager_remove(const char* id);
|
||||
const struct AppManifest* app_manager_find_manifest(const char* id);
|
||||
|
||||
/**
|
||||
* Calls @a visitor once for every registered manifest (e.g. for AppList/Settings to enumerate
|
||||
* apps to show). Iteration order is unspecified. Safe to call app_manager_add()/_remove() from
|
||||
* within @a visitor is NOT guaranteed - do not mutate the registry from inside the callback.
|
||||
* Calls `@a` visitor once for every registered manifest. Iteration order is unspecified.
|
||||
* `@warning` `@a` visitor runs with app-module's internal registry lock held. Do not call any
|
||||
* app_manager_*() function from inside `@a` visitor - copy out what you need and act on it after
|
||||
* this call returns.
|
||||
*/
|
||||
typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* context);
|
||||
void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context);
|
||||
@ -130,6 +128,26 @@ error_t app_manager_get_topmost_instance_id(AppInstanceId* out_app_instance_id);
|
||||
*/
|
||||
error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size);
|
||||
|
||||
/**
|
||||
* Registers @a path as a directory to scan for app manifests - each direct subdirectory of
|
||||
* @a path is expected to hold a manifest.properties (see app/metadata.h), matching the layout
|
||||
* app_install() creates ({install dir}/{app_id}/manifest.properties), though this is not
|
||||
* install/uninstall - it only ever adds/removes manifest registrations, never touches files on
|
||||
* disk or running instances. No-op if @a path is already registered. Does not scan immediately -
|
||||
* call app_manager_install_path_scan() to do that.
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_install_path_add(const char* path);
|
||||
|
||||
/**
|
||||
* Scans every path registered via app_manager_install_path_add(): registers
|
||||
* (app_manager_add()) any direct subdirectory with a valid manifest.properties that isn't
|
||||
* already registered, and unregisters (app_manager_remove() only - does not stop it if running,
|
||||
* does not delete anything) any manifest a previous scan registered whose directory has since
|
||||
* disappeared. Safe to call repeatedly (e.g. after an SD card is mounted/unmounted).
|
||||
*/
|
||||
void app_manager_install_path_scan(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/error.h>
|
||||
@ -39,8 +40,8 @@ struct AppMetadata {
|
||||
*/
|
||||
char app_version_name[APP_METADATA_APP_VERSION_NAME_LENGTH + 1];
|
||||
|
||||
/** The technical version (must be incremented with new releases of the app */
|
||||
uint64_t app_version_code = 0;
|
||||
/** The technical version (must be incremented with new releases of the app) */
|
||||
uint64_t app_version_code;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
56
Modules/app-module/include/app/paths.h
Normal file
56
Modules/app-module/include/app/paths.h
Normal file
@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Get the user data directory for an app. Survives OS upgrades. No trailing "/".
|
||||
* @param[in] app_id non-null app id
|
||||
* @param[out] out_path buffer to store the path
|
||||
* @param[in] out_path_size size of the output buffer
|
||||
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_paths_get_user_data_directory(const char* app_id, char* out_path, size_t out_path_size);
|
||||
|
||||
/**
|
||||
* @brief Get a path within the user data directory for an app.
|
||||
* @param[in] app_id non-null app id
|
||||
* @param[in] child_path path without a "/" prefix
|
||||
* @param[out] out_path buffer to store the path
|
||||
* @param[in] out_path_size size of the output buffer
|
||||
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_paths_get_user_data_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size);
|
||||
|
||||
/**
|
||||
* @brief Get the assets directory for an app. Do not store configuration data here. No trailing "/".
|
||||
* @param[in] app_id non-null app id
|
||||
* @param[out] out_path buffer to store the path
|
||||
* @param[in] out_path_size size of the output buffer
|
||||
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_t out_path_size);
|
||||
|
||||
/**
|
||||
* @brief Get a path within the assets directory for an app.
|
||||
* @param[in] app_id non-null app id
|
||||
* @param[in] child_path path without a "/" prefix
|
||||
* @param[out] out_path buffer to store the path
|
||||
* @param[in] out_path_size size of the output buffer
|
||||
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_paths_get_assets_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
21
Modules/app-module/include/app/scheduler.h
Normal file
21
Modules/app-module/include/app/scheduler.h
Normal file
@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/instance.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @return the app_instance_id of whichever app instance's task is calling this (every app
|
||||
* instance's task stashes it in its own thread-local storage when it starts), or 0 if called
|
||||
* from a task that isn't a running app instance. An app's own main() typically calls this once,
|
||||
* near the top, to learn its own instance id - see e.g. app_event_subscribe()/
|
||||
* window_manager_create(), both of which need it.
|
||||
*/
|
||||
AppInstanceId app_scheduler_current_app_id(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
72
Modules/app-module/private/app/private/app_fs.h
Normal file
72
Modules/app-module/private/app/private/app_fs.h
Normal file
@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
// Minimal filesystem helpers shared by app-module internals that need to look at on-disk app
|
||||
// directories (app_install.cpp, manager.cpp's install-path scan) - app-module may not depend
|
||||
// upward on Tactility::file, so this is a small local re-implementation (see
|
||||
// app_metadata_parsing.cpp for the same constraint applied to properties-file loading).
|
||||
|
||||
#include <tactility/filesystem/file_mutex.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <dirent.h>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <vector>
|
||||
|
||||
inline bool app_fs_is_directory(const std::string& path) {
|
||||
struct stat result {};
|
||||
FileMutex file_mutex;
|
||||
file_mutex_get(&file_mutex, path.c_str());
|
||||
file_mutex_lock(&file_mutex);
|
||||
auto is_dir = stat(path.c_str(), &result) == 0 && S_ISDIR(result.st_mode);
|
||||
file_mutex_unlock(&file_mutex);
|
||||
return is_dir;
|
||||
}
|
||||
|
||||
inline bool app_fs_is_file(const std::string& path) {
|
||||
FileMutex file_mutex;
|
||||
file_mutex_get(&file_mutex, path.c_str());
|
||||
file_mutex_lock(&file_mutex);
|
||||
struct stat result {};
|
||||
auto retval = stat(path.c_str(), &result) == 0 && S_ISREG(result.st_mode);
|
||||
file_mutex_unlock(&file_mutex);
|
||||
return retval;
|
||||
}
|
||||
|
||||
// Appends the full path of every direct subdirectory of @a path to @a out.
|
||||
// No-op (not an error) if @a path can't be opened.
|
||||
inline void app_fs_list_direct_subdirectories(const std::string& path, std::vector<std::string>& out) {
|
||||
// Collect child names while the directory lock is held, then release it before classifying
|
||||
// each one with app_fs_is_directory() - that function looks up and locks a FileMutex too,
|
||||
// and file_mutex_get() resolves a child path to the same registered mutex as its parent
|
||||
// mount. Calling it while still holding the directory's own lock would be a nested
|
||||
// acquisition of that same (possibly non-recursive) mutex, and could self-deadlock.
|
||||
std::vector<std::string> children;
|
||||
|
||||
FileMutex file_mutex;
|
||||
file_mutex_get(&file_mutex, path.c_str());
|
||||
file_mutex_lock(&file_mutex);
|
||||
DIR* dir = opendir(path.c_str());
|
||||
if (dir == nullptr) {
|
||||
file_mutex_unlock(&file_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(dir)) != nullptr) {
|
||||
if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
children.push_back(path + "/" + entry->d_name);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
file_mutex_unlock(&file_mutex);
|
||||
|
||||
for (const auto& child_path : children) {
|
||||
if (app_fs_is_directory(child_path)) {
|
||||
out.push_back(child_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,23 +5,49 @@
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
#include <tactility/freertos/semphr.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* A dedicated (not the task's shared default FreeRTOS notification, which app_event.cpp's
|
||||
* AppEventSubscription also uses - an unrelated event delivered to the same task could
|
||||
* otherwise unblock a waiter early) completion signal for one app instance's task, given as the
|
||||
* literal last action app_task_main() takes before vTaskDelete(). Heap-allocated with its own
|
||||
* refcount (protected by app_ledger().mutex, not atomic) rather than owned by the ledger
|
||||
* entry, since app_task_main() always erases that entry - and may run its exit path entirely -
|
||||
* before app_scheduler_stop() ever looks for it: whichever side (the exiting task, or a
|
||||
* concurrent app_scheduler_stop() that found the entry in time and is waiting on `semaphore`)
|
||||
* finishes with it last is the one that deletes `semaphore` and frees this struct.
|
||||
*/
|
||||
struct AppCompletionSignal {
|
||||
SemaphoreHandle_t semaphore;
|
||||
/** Starts at 1, owned by app_task_main() until its own exit. app_scheduler_stop() takes an
|
||||
* additional reference for as long as it's waiting on `semaphore`, if it finds the instance
|
||||
* still running. Reaching 0 means deletion. */
|
||||
int refcount = 1;
|
||||
};
|
||||
|
||||
/** A registered/running app instance, as tracked internally by app-module. */
|
||||
struct AppInstanceRecord {
|
||||
uint32_t id;
|
||||
const AppManifest* manifest;
|
||||
AppInstanceState state;
|
||||
/** The kernel thread currently executing AppLoaderApi::run() for this instance; NULL when not running. */
|
||||
Thread* thread;
|
||||
/** The FreeRTOS task currently executing AppLoaderApi::run() for this instance; NULL when
|
||||
* not running. */
|
||||
TaskHandle_t task;
|
||||
|
||||
/** 0 for a top-level launch (app_manager_start()). Non-zero for a modal child launched via
|
||||
* app_manager_start_for_result() - the instance that receives this child's APP_EVENT_RESULT. */
|
||||
uint32_t parent_id = 0;
|
||||
|
||||
/** This instance's completion signal - see AppCompletionSignal. Set once by
|
||||
* app_scheduler_start(), never reassigned. */
|
||||
AppCompletionSignal* completion = nullptr;
|
||||
};
|
||||
|
||||
struct AppLedger {
|
||||
@ -40,7 +66,7 @@ inline AppLedger& app_ledger() {
|
||||
}
|
||||
|
||||
/** Frees a deep-copied argv previously built by app_manager_start_with_parameters()/
|
||||
* app_manager_start_for_result() (see app_scheduler.cpp's ThreadContext::argv) - each
|
||||
* app_manager_start_for_result() (see app_scheduler.cpp's TaskContext::argv) - each
|
||||
* individually heap-allocated string, then the array itself. Safe to call with count == 0 /
|
||||
* values == nullptr (no-op). */
|
||||
inline void app_ledger_free_arguments(int count, char** values) {
|
||||
|
||||
@ -4,16 +4,12 @@
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Owns per-app task lifecycle on behalf of app_manager_*(). AppLoaderApi implementations
|
||||
* stay task-agnostic; all of thread_alloc_full()/thread_start()/thread_join() happen here.
|
||||
* Every app instance gets its own dedicated task for its entire lifetime - no task is ever
|
||||
* reused for a different instance.
|
||||
* stay task-agnostic; all of xTaskCreate()/vTaskDelete() happens here, as a plain FreeRTOS task
|
||||
* (not TactilityKernel's Thread wrapper). Every app instance gets its own dedicated task for its
|
||||
* entire lifetime - no task is ever reused for a different instance.
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
@ -30,13 +26,15 @@ extern "C" {
|
||||
* taken by the scheduler regardless of outcome (freed once the spawned task's run() returns, or
|
||||
* immediately on a failure to start it)
|
||||
*/
|
||||
error_t app_scheduler_start(uint32_t app_instance_id, struct AppLocation location, int argc, char* argv[]);
|
||||
error_t app_scheduler_start(AppInstanceId app_instance_id, struct AppLocation location, int argc, char* argv[]);
|
||||
|
||||
/**
|
||||
* Permanently stops an app instance (APP_EVENT_CLOSE if it was running), bound-waits for its
|
||||
* task to exit, and removes it from the ledger.
|
||||
*/
|
||||
error_t app_scheduler_stop(uint32_t app_instance_id, TickType_t join_timeout);
|
||||
error_t app_scheduler_stop(AppInstanceId app_instance_id, TickType_t join_timeout);
|
||||
|
||||
// app_scheduler_current_app_id() is public - see app/scheduler.h.
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include <app/manager.h>
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <app/private/app_fs.h>
|
||||
#include <app/private/app_ledger.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
@ -37,19 +38,9 @@ std::string last_path_segment(const std::string& path) {
|
||||
return index == std::string::npos ? path : path.substr(index + 1);
|
||||
}
|
||||
|
||||
bool is_directory(const std::string& path) {
|
||||
struct stat result {};
|
||||
return stat(path.c_str(), &result) == 0 && S_ISDIR(result.st_mode);
|
||||
}
|
||||
|
||||
bool is_file(const std::string& path) {
|
||||
struct stat result {};
|
||||
return stat(path.c_str(), &result) == 0 && S_ISREG(result.st_mode);
|
||||
}
|
||||
|
||||
// mkdir -p.
|
||||
bool ensure_directory(const std::string& path) {
|
||||
if (path.empty() || is_directory(path)) {
|
||||
if (path.empty() || app_fs_is_directory(path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -62,7 +53,7 @@ bool ensure_directory(const std::string& path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_directory(path);
|
||||
return app_fs_is_directory(path);
|
||||
}
|
||||
|
||||
bool ensure_directory_recursive(const std::string& path) {
|
||||
@ -75,19 +66,27 @@ bool ensure_directory_recursive(const std::string& path) {
|
||||
}
|
||||
|
||||
bool delete_recursively(const std::string& path) {
|
||||
LOG_D(TAG, "Deleting %s...", path.c_str());
|
||||
if (path.empty() || path == "/" || path == "." || path == "..") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_directory(path)) {
|
||||
if (app_fs_is_directory(path)) {
|
||||
LOG_D(TAG, "Deleting dir %s", path.c_str());
|
||||
|
||||
FileMutex file_mutex;
|
||||
file_mutex_get(&file_mutex, path.c_str());
|
||||
file_mutex_lock(&file_mutex);
|
||||
|
||||
DIR* dir = opendir(path.c_str());
|
||||
if (dir == nullptr) {
|
||||
LOG_E(TAG, "Failed to scan directory %s", path.c_str());
|
||||
file_mutex_unlock(&file_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
struct dirent* entry;
|
||||
dirent* entry;
|
||||
while (success && (entry = readdir(dir)) != nullptr) {
|
||||
if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
@ -97,18 +96,17 @@ bool delete_recursively(const std::string& path) {
|
||||
closedir(dir);
|
||||
|
||||
if (!success) {
|
||||
file_mutex_unlock(&file_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
FileMutex mutex {};
|
||||
file_mutex_get(&mutex, path.c_str());
|
||||
file_mutex_lock(&mutex);
|
||||
bool result = rmdir(path.c_str()) == 0;
|
||||
file_mutex_unlock(&mutex);
|
||||
file_mutex_unlock(&file_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (is_file(path)) {
|
||||
if (app_fs_is_file(path)) {
|
||||
LOG_D(TAG, "Deleting file %s", path.c_str());
|
||||
FileMutex mutex {};
|
||||
file_mutex_get(&mutex, path.c_str());
|
||||
file_mutex_lock(&mutex);
|
||||
@ -117,7 +115,7 @@ bool delete_recursively(const std::string& path) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Doesn't exist - nothing to do.
|
||||
LOG_D(TAG, "Deleting done");
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -213,6 +211,35 @@ InstallRegistry& install_registry() {
|
||||
return registry;
|
||||
}
|
||||
|
||||
// Registers @a app_dir_path (already confirmed to hold a valid manifest.properties, parsed into
|
||||
// @a metadata) with app_manager_add(), taking ownership of its id/name/path strings.
|
||||
// @warning Caller must hold install_registry().mutex, and must have already ensured
|
||||
// @a metadata.app_id isn't already registered (app_manager_add() rejects duplicates, but the
|
||||
// InstalledAppRecord for the earlier registration would leak since this always inserts fresh).
|
||||
error_t register_installed_app_locked(const std::string& app_dir_path, const AppMetadata& metadata) {
|
||||
auto& registry = install_registry();
|
||||
|
||||
auto record = std::make_unique<InstalledAppRecord>();
|
||||
record->id = metadata.app_id;
|
||||
record->name = metadata.app_name;
|
||||
record->path = app_dir_path;
|
||||
record->manifest = AppManifest {
|
||||
.id = record->id.c_str(),
|
||||
.name = record->name.c_str(),
|
||||
.category = APP_CATEGORY_USER,
|
||||
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
error_t add_result = app_manager_add(&record->manifest);
|
||||
if (add_result != ERROR_NONE) {
|
||||
return add_result;
|
||||
}
|
||||
|
||||
registry.apps[record->id] = std::move(record);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// Stops every currently-running instance of @a manifest. Collects matching instance ids while
|
||||
// holding the ledger lock, then calls app_manager_stop() on each after releasing it - that call
|
||||
// bound-joins the instance's thread, which must not happen while the ledger mutex (also taken by
|
||||
@ -234,7 +261,7 @@ void stop_all_instances_of(const AppManifest* manifest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Takes install_registry().mutex - caller must not already hold it.
|
||||
// Caller must already hold install_registry().mutex
|
||||
error_t uninstall_locked(const std::string& app_id) {
|
||||
auto& registry = install_registry();
|
||||
auto iterator = registry.apps.find(app_id);
|
||||
@ -312,7 +339,7 @@ error_t app_install(const char* source_path) {
|
||||
}
|
||||
|
||||
auto manifest_path = staging_path + "/manifest.properties";
|
||||
if (!is_file(manifest_path)) {
|
||||
if (!app_fs_is_file(manifest_path)) {
|
||||
LOG_E(TAG, "Manifest not found at %s", manifest_path.c_str());
|
||||
delete_recursively(staging_path);
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
@ -320,7 +347,7 @@ error_t app_install(const char* source_path) {
|
||||
|
||||
AppMetadata metadata {};
|
||||
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
|
||||
LOG_W(TAG, "Invalid manifest");
|
||||
LOG_E(TAG, "Install failed: invalid manifest");
|
||||
delete_recursively(staging_path);
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
@ -329,8 +356,18 @@ error_t app_install(const char* source_path) {
|
||||
mutex_lock(®istry.mutex);
|
||||
|
||||
// Replace any previous install of this app id (mirrors the old install()'s "already
|
||||
// running/present" handling).
|
||||
// running/present" handling). uninstall_locked() only clears app_install.cpp's own
|
||||
// registry - the same app id may instead be registered by app_manager_install_path_scan()
|
||||
// (manager.cpp's separate registry, scanning this same directory tree), which
|
||||
// uninstall_locked() doesn't know about. Clear the app-manager registration unconditionally
|
||||
// too, or app_manager_add() below rejects the re-add as a duplicate.
|
||||
uninstall_locked(metadata.app_id);
|
||||
if (app_manager_remove(metadata.app_id) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Install failed: failed to remove existing installation");
|
||||
mutex_unlock(®istry.mutex);
|
||||
delete_recursively(staging_path);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto final_path = app_parent_path + "/" + metadata.app_id;
|
||||
delete_recursively(final_path);
|
||||
@ -346,30 +383,12 @@ error_t app_install(const char* source_path) {
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
auto record = std::make_unique<InstalledAppRecord>();
|
||||
record->id = metadata.app_id;
|
||||
record->name = metadata.app_name;
|
||||
record->path = final_path;
|
||||
record->manifest = AppManifest {
|
||||
.id = record->id.c_str(),
|
||||
.name = record->name.c_str(),
|
||||
.category = APP_CATEGORY_USER,
|
||||
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
error_t add_result = app_manager_add(&record->manifest);
|
||||
if (add_result != ERROR_NONE) {
|
||||
// Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above
|
||||
// already removed any previous registration for this exact id.
|
||||
mutex_unlock(®istry.mutex);
|
||||
return add_result;
|
||||
}
|
||||
|
||||
registry.apps[record->id] = std::move(record);
|
||||
// Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above
|
||||
// already removed any previous registration for this exact id.
|
||||
error_t add_result = register_installed_app_locked(final_path, metadata);
|
||||
mutex_unlock(®istry.mutex);
|
||||
|
||||
return ERROR_NONE;
|
||||
return add_result;
|
||||
}
|
||||
|
||||
error_t app_uninstall(const char* app_id) {
|
||||
|
||||
@ -39,7 +39,7 @@ void destroy_service(const ServiceManifest*, void*) {
|
||||
|
||||
} // namespace
|
||||
|
||||
extern ServiceManifest app_internal_loader_service_manifest = {
|
||||
ServiceManifest app_internal_loader_service_manifest = {
|
||||
.id = APP_LOADER_MEMORY_SERVICE_ID,
|
||||
.create_service = create_service,
|
||||
.destroy_service = destroy_service,
|
||||
|
||||
@ -57,15 +57,16 @@ bool load_properties(const std::string& path, std::map<std::string, std::string>
|
||||
bool got_first_line = false;
|
||||
while (std::getline(file, line)) {
|
||||
auto trimmed_line = trim(line);
|
||||
if (!got_first_line) {
|
||||
out_first_line = trimmed_line;
|
||||
got_first_line = true;
|
||||
}
|
||||
|
||||
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!got_first_line) {
|
||||
out_first_line = trimmed_line;
|
||||
got_first_line = true;
|
||||
}
|
||||
|
||||
if (trimmed_line.starts_with("[")) {
|
||||
section_prefix = trimmed_line;
|
||||
continue;
|
||||
@ -123,7 +124,8 @@ bool app_metadata_is_valid_version_name(const std::string& version) {
|
||||
}
|
||||
|
||||
bool app_metadata_is_valid_version_code(const std::string& version) {
|
||||
return !version.empty() && validate_string(version, [](char c) {
|
||||
// 20 digits is the maximum decimal width of uint64_t.
|
||||
return !version.empty() && version.size() <= 20 && validate_string(version, [](char c) {
|
||||
return std::isdigit(static_cast<unsigned char>(c)) != 0;
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <app/private/app_metadata_parsing_internal.h>
|
||||
|
||||
#include <charconv>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
constexpr auto* TAG = "app_metadata_v1";
|
||||
@ -77,9 +78,14 @@ bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties,
|
||||
return false;
|
||||
}
|
||||
|
||||
out_metadata.app_version_code = std::stoull(version_code_string);
|
||||
|
||||
// [target]
|
||||
uint64_t version_code = 0;
|
||||
const auto* first = version_code_string.data();
|
||||
const auto* last = first + version_code_string.size();
|
||||
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
|
||||
LOG_E(TAG, "App version code out of range");
|
||||
return false;
|
||||
}
|
||||
out_metadata.app_version_code = version_code; // [target]
|
||||
|
||||
std::string target_sdk;
|
||||
if (!app_metadata_get_value(properties, "[target]sdk", target_sdk)) {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <app/private/app_metadata_parsing_internal.h>
|
||||
|
||||
#include <charconv>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
constexpr auto* TAG = "app_metadata_v2";
|
||||
@ -77,7 +78,14 @@ bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties,
|
||||
return false;
|
||||
}
|
||||
|
||||
out_metadata.app_version_code = std::stoull(version_code_string);
|
||||
uint64_t version_code = 0;
|
||||
const auto* first = version_code_string.data();
|
||||
const auto* last = first + version_code_string.size();
|
||||
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
|
||||
LOG_E(TAG, "App version code out of range");
|
||||
return false;
|
||||
}
|
||||
out_metadata.app_version_code = version_code; // [target]
|
||||
|
||||
// target
|
||||
|
||||
|
||||
62
Modules/app-module/source/app_paths.cpp
Normal file
62
Modules/app-module/source/app_paths.cpp
Normal file
@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include <app/paths.h>
|
||||
#include <tactility/paths.h>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t app_paths_get_user_data_directory(const char* app_id, char* out_path, size_t out_path_size) {
|
||||
char root[192];
|
||||
error_t error = paths_get_user_data_path(root, sizeof(root));
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
int written = std::snprintf(out_path, out_path_size, "%s/app/%s", root, app_id);
|
||||
if (written < 0 || (size_t)written >= out_path_size) {
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t app_paths_get_user_data_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size) {
|
||||
char directory[224];
|
||||
error_t error = app_paths_get_user_data_directory(app_id, directory, sizeof(directory));
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path);
|
||||
if (written < 0 || (size_t)written >= out_path_size) {
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_t out_path_size) {
|
||||
char directory[224];
|
||||
error_t error = app_paths_get_user_data_directory(app_id, directory, sizeof(directory));
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
int written = std::snprintf(out_path, out_path_size, "%s/assets", directory);
|
||||
if (written < 0 || (size_t)written >= out_path_size) {
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t app_paths_get_assets_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size) {
|
||||
char directory[224];
|
||||
error_t error = app_paths_get_assets_directory(app_id, directory, sizeof(directory));
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path);
|
||||
if (written < 0 || (size_t)written >= out_path_size) {
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@ -1,31 +1,44 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/private/app_ledger.h>
|
||||
#include <app/private/app_scheduler.h>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/instance.h>
|
||||
#include <app/loader.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <service/instance.h>
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <new>
|
||||
|
||||
#define TAG "app_scheduler"
|
||||
constexpr auto* TAG = "app_scheduler";
|
||||
|
||||
// Slot 0 is reserved by ESP-IDF's pthread API (see TactilityKernel's Thread wrapper for the
|
||||
// same convention/comment) - app tasks use slot 1 to stash their own app_instance_id, so any
|
||||
// code running on an app's own task can retrieve it via app_scheduler_current_app_id() without
|
||||
// needing it threaded through as a parameter.
|
||||
constexpr size_t APP_INSTANCE_ID_THREAD_SLOT_INDEX = 1;
|
||||
|
||||
// Matches TactilityKernel's Thread wrapper's THREAD_PRIORITY_NORMAL.
|
||||
constexpr UBaseType_t APP_TASK_PRIORITY = 4;
|
||||
|
||||
namespace {
|
||||
|
||||
struct ThreadContext {
|
||||
struct TaskContext {
|
||||
const AppLoaderApi* loader;
|
||||
void* runtime;
|
||||
uint32_t app_instance_id;
|
||||
AppInstanceId app_instance_id;
|
||||
int argc;
|
||||
char** argv;
|
||||
AppCompletionSignal* completion;
|
||||
};
|
||||
|
||||
void set_state(uint32_t app_instance_id, AppInstanceState state) {
|
||||
void set_state(AppInstanceId app_instance_id, AppInstanceState state) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
auto iterator = ledger.instances.find(app_instance_id);
|
||||
@ -35,25 +48,59 @@ void set_state(uint32_t app_instance_id, AppInstanceState state) {
|
||||
mutex_unlock(&ledger.mutex);
|
||||
}
|
||||
|
||||
Thread* get_thread(uint32_t app_instance_id) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
auto iterator = ledger.instances.find(app_instance_id);
|
||||
Thread* thread = (iterator != ledger.instances.end()) ? iterator->second.thread : nullptr;
|
||||
mutex_unlock(&ledger.mutex);
|
||||
return thread;
|
||||
}
|
||||
|
||||
void set_thread(uint32_t app_instance_id, Thread* thread) {
|
||||
void set_task(AppInstanceId app_instance_id, TaskHandle_t task) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
auto iterator = ledger.instances.find(app_instance_id);
|
||||
if (iterator != ledger.instances.end()) {
|
||||
iterator->second.thread = thread;
|
||||
iterator->second.task = task;
|
||||
}
|
||||
mutex_unlock(&ledger.mutex);
|
||||
}
|
||||
|
||||
void set_completion(AppInstanceId app_instance_id, AppCompletionSignal* completion) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
auto iterator = ledger.instances.find(app_instance_id);
|
||||
if (iterator != ledger.instances.end()) {
|
||||
iterator->second.completion = completion;
|
||||
}
|
||||
mutex_unlock(&ledger.mutex);
|
||||
}
|
||||
|
||||
// Takes a reference on app_instance_id's completion signal (see AppCompletionSignal), for the
|
||||
// caller to wait on. @return the signal to wait on, or NULL if the instance has already fully
|
||||
// finished (its ledger entry - and so its reference to the signal - is already gone) and so
|
||||
// there's nothing left to wait for, or if the instance is still starting up (start_internal()
|
||||
// in manager.cpp inserts the ledger entry before app_scheduler_start() has gotten as far as
|
||||
// set_completion() - `completion` is NULL for that whole window) and so there's nothing to
|
||||
// take a reference on yet.
|
||||
AppCompletionSignal* acquire_completion_signal(AppInstanceId app_instance_id) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
auto iterator = ledger.instances.find(app_instance_id);
|
||||
AppCompletionSignal* completion = nullptr;
|
||||
if (iterator != ledger.instances.end() && iterator->second.completion != nullptr) {
|
||||
completion = iterator->second.completion;
|
||||
completion->refcount++;
|
||||
}
|
||||
mutex_unlock(&ledger.mutex);
|
||||
return completion;
|
||||
}
|
||||
|
||||
// Releases a reference taken by acquire_completion_signal(), deleting the signal (and its
|
||||
// semaphore) if this was the last one.
|
||||
void release_completion_signal(AppCompletionSignal* completion) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
bool should_delete = (--completion->refcount == 0);
|
||||
mutex_unlock(&ledger.mutex);
|
||||
if (should_delete) {
|
||||
vSemaphoreDelete(completion->semaphore);
|
||||
delete completion;
|
||||
}
|
||||
}
|
||||
|
||||
const char* loader_service_id_for(AppLocationType type) {
|
||||
return (type == APP_LOCATION_MEMORY) ? APP_LOADER_MEMORY_SERVICE_ID : APP_LOADER_PATH_SERVICE_ID;
|
||||
}
|
||||
@ -69,10 +116,10 @@ const AppLoaderApi* find_loader_api(AppLocationType type) {
|
||||
// If this instance was launched via app_manager_start_for_result(), delivers @a result (its
|
||||
// own AppMainFn/AppLoaderApi::run() return value) to its parent. No-op for a top-level instance
|
||||
// (parent_id == 0).
|
||||
void deliver_result_to_parent_if_any(uint32_t app_instance_id, int32_t result) {
|
||||
void deliver_result_to_parent_if_any(AppInstanceId app_instance_id, int32_t result) {
|
||||
auto& ledger = app_ledger();
|
||||
|
||||
uint32_t parent_id;
|
||||
AppInstanceId parent_id;
|
||||
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = {} };
|
||||
|
||||
mutex_lock(&ledger.mutex);
|
||||
@ -91,13 +138,20 @@ void deliver_result_to_parent_if_any(uint32_t app_instance_id, int32_t result) {
|
||||
}
|
||||
}
|
||||
|
||||
int32_t thread_main(void* context) {
|
||||
auto* ctx = static_cast<ThreadContext*>(context);
|
||||
void app_task_main(void* context) {
|
||||
auto* ctx = static_cast<TaskContext*>(context);
|
||||
|
||||
check(pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX) == nullptr);
|
||||
vTaskSetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX, reinterpret_cast<void*>(static_cast<uintptr_t>(ctx->app_instance_id)));
|
||||
|
||||
LOG_I(TAG, "Thread for %d started", ctx->app_instance_id);
|
||||
|
||||
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_ACTIVE);
|
||||
|
||||
int32_t result = ctx->loader->run(ctx->runtime, ctx->app_instance_id, ctx->argc, ctx->argv);
|
||||
|
||||
vTaskSetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX, nullptr);
|
||||
|
||||
ctx->loader->unload(ctx->runtime);
|
||||
|
||||
deliver_result_to_parent_if_any(ctx->app_instance_id, result);
|
||||
@ -108,15 +162,39 @@ int32_t thread_main(void* context) {
|
||||
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_STOPPED);
|
||||
|
||||
app_ledger_free_arguments(ctx->argc, ctx->argv);
|
||||
|
||||
AppInstanceId app_instance_id = ctx->app_instance_id;
|
||||
AppCompletionSignal* completion = ctx->completion;
|
||||
delete ctx;
|
||||
return result;
|
||||
|
||||
LOG_I(TAG, "Thread for %d finished", app_instance_id);
|
||||
|
||||
// Erase the ledger entry before self-deleting - see "Reap self-terminated app tasks":
|
||||
// nothing else is guaranteed to ever call app_scheduler_stop() for this instance (the
|
||||
// common case is the app just closing itself), so this can't wait for that to happen.
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
ledger.instances.erase(app_instance_id);
|
||||
mutex_unlock(&ledger.mutex);
|
||||
|
||||
// Signal completion as the literal last action before this task ceases to exist, so
|
||||
// app_scheduler_stop() can't observe "stopped" one step early - unlike watching the ledger
|
||||
// entry disappear, this can only happen once the task is truly done running. A dedicated
|
||||
// semaphore rather than this task's default FreeRTOS notification, since app_event.cpp's
|
||||
// AppEventSubscription also uses that shared slot - an unrelated event (e.g. a child's
|
||||
// APP_EVENT_RESULT) delivered to this same task could otherwise unblock a concurrent
|
||||
// app_scheduler_stop() early.
|
||||
xSemaphoreGive(completion->semaphore);
|
||||
release_completion_signal(completion); // releases app_task_main()'s own reference
|
||||
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t app_scheduler_start(uint32_t app_instance_id, AppLocation location, int argc, char* argv[]) {
|
||||
error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, int argc, char* argv[]) {
|
||||
const AppLoaderApi* loader = find_loader_api(location.type);
|
||||
if (loader == nullptr) {
|
||||
LOG_E(TAG, "No app loader is registered (service '%s' not found)", loader_service_id_for(location.type));
|
||||
@ -127,54 +205,84 @@ error_t app_scheduler_start(uint32_t app_instance_id, AppLocation location, int
|
||||
void* runtime = nullptr;
|
||||
error_t load_result = loader->load(location, &runtime);
|
||||
if (load_result != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to load app: %s", error_to_string(load_result));
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return load_result;
|
||||
}
|
||||
|
||||
auto* context = new (std::nothrow) ThreadContext { loader, runtime, app_instance_id, argc, argv };
|
||||
auto* completion = new (std::nothrow) AppCompletionSignal();
|
||||
if (completion == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate app");
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
completion->semaphore = xSemaphoreCreateBinary();
|
||||
if (completion->semaphore == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate app");
|
||||
delete completion;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
auto* context = new (std::nothrow) TaskContext { loader, runtime, app_instance_id, argc, argv, completion };
|
||||
if (context == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate app");
|
||||
vSemaphoreDelete(completion->semaphore);
|
||||
delete completion;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
// -1 (no affinity) matches the FreeRTOS POSIX/simulator port; ESP-IDF's tskNO_AFFINITY is
|
||||
// a numerically equivalent SMP-only constant not available in the plain FreeRTOS-Kernel port.
|
||||
Thread* thread = thread_alloc_full("app", 8192, thread_main, context, -1);
|
||||
if (thread == nullptr) {
|
||||
char task_name[16];
|
||||
snprintf(task_name, sizeof(task_name), "app_%lu", static_cast<unsigned long>(app_instance_id));
|
||||
|
||||
TaskHandle_t task_handle = nullptr;
|
||||
// 8192 bytes -> stack depth in words, matching what TactilityKernel's Thread wrapper does with the stack size it's given.
|
||||
// Created at idle priority so it can't preempt us before vTaskSuspend() below runs, then suspended immediately -
|
||||
// the ledger must record the handle (set_task()) before the task can possibly observe or erase its own entry.
|
||||
// (see app_scheduler_stop()'s liveness check and app_task_main()'s exit path)
|
||||
BaseType_t create_result = xTaskCreate(app_task_main, task_name, 8192 / sizeof(StackType_t), context, tskIDLE_PRIORITY, &task_handle);
|
||||
if (create_result != pdPASS) {
|
||||
delete context;
|
||||
vSemaphoreDelete(completion->semaphore);
|
||||
delete completion;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
vTaskSuspend(task_handle);
|
||||
|
||||
set_thread(app_instance_id, thread);
|
||||
|
||||
error_t start_result = thread_start(thread);
|
||||
if (start_result != ERROR_NONE) {
|
||||
set_thread(app_instance_id, nullptr);
|
||||
thread_free(thread);
|
||||
delete context;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return start_result;
|
||||
}
|
||||
set_task(app_instance_id, task_handle);
|
||||
set_completion(app_instance_id, completion);
|
||||
vTaskPrioritySet(task_handle, APP_TASK_PRIORITY);
|
||||
vTaskResume(task_handle);
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t app_scheduler_stop(uint32_t app_instance_id, TickType_t join_timeout) {
|
||||
Thread* thread = get_thread(app_instance_id);
|
||||
if (thread != nullptr) {
|
||||
error_t app_scheduler_stop(AppInstanceId app_instance_id, TickType_t join_timeout) {
|
||||
AppCompletionSignal* completion = acquire_completion_signal(app_instance_id);
|
||||
if (completion != nullptr) {
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(app_instance_id, &event);
|
||||
|
||||
if (thread_join(thread, join_timeout, pdMS_TO_TICKS(10)) != ERROR_NONE) {
|
||||
// Blocks until app_task_main() gives this dedicated semaphore as the literal last
|
||||
// thing it does before vTaskDelete() - unlike polling the ledger for the task handle to
|
||||
// clear, this can't observe "stopped" while the task is still mid-exit (still running
|
||||
// its own cleanup/vTaskDelete()). A dedicated semaphore rather than this task's default
|
||||
// FreeRTOS notification, since app_event.cpp's AppEventSubscription also uses that
|
||||
// shared slot - an unrelated event (e.g. a different child's APP_EVENT_RESULT)
|
||||
// delivered to this same task could otherwise unblock this early.
|
||||
BaseType_t taken = xSemaphoreTake(completion->semaphore, join_timeout);
|
||||
release_completion_signal(completion);
|
||||
|
||||
if (taken == pdFALSE) {
|
||||
LOG_W(TAG, "App instance %u did not stop in time", app_instance_id);
|
||||
return ERROR_TIMEOUT;
|
||||
}
|
||||
thread_free(thread);
|
||||
set_thread(app_instance_id, nullptr);
|
||||
}
|
||||
|
||||
set_state(app_instance_id, APP_INSTANCE_STATE_STOPPED);
|
||||
@ -187,4 +295,9 @@ error_t app_scheduler_stop(uint32_t app_instance_id, TickType_t join_timeout) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
AppInstanceId app_scheduler_current_app_id(void) {
|
||||
void* value = pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX);
|
||||
return reinterpret_cast<uintptr_t>(value);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/manager.h>
|
||||
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <app/private/app_fs.h>
|
||||
#include <app/private/app_ledger.h>
|
||||
#include <app/private/app_scheduler.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#define TAG "app_manager"
|
||||
|
||||
@ -197,3 +205,133 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) {
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
namespace {
|
||||
|
||||
// Owns the AppManifest (and its id/name/path strings) that app_manager_add() only keeps a
|
||||
// non-owning pointer to (see app_manager_add()'s contract), for manifests registered by
|
||||
// app_manager_install_path_scan() specifically - separate from app_install.cpp's own registry,
|
||||
// since scanning only ever adds/removes manifest registrations and never touches files on disk
|
||||
// or running instances (unlike app_install()/app_uninstall()).
|
||||
struct ScannedAppManifest {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string path;
|
||||
AppManifest manifest {};
|
||||
};
|
||||
|
||||
struct InstallPathRegistry {
|
||||
std::vector<std::string> paths;
|
||||
std::unordered_map<std::string, std::unique_ptr<ScannedAppManifest>> scanned;
|
||||
Mutex mutex {};
|
||||
|
||||
InstallPathRegistry() { mutex_construct(&mutex); }
|
||||
};
|
||||
|
||||
InstallPathRegistry& install_path_registry() {
|
||||
static InstallPathRegistry registry;
|
||||
return registry;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t app_manager_install_path_add(const char* path) {
|
||||
auto& registry = install_path_registry();
|
||||
mutex_lock(®istry.mutex);
|
||||
if (std::ranges::find(registry.paths, path) == registry.paths.end()) {
|
||||
registry.paths.emplace_back(path);
|
||||
}
|
||||
mutex_unlock(®istry.mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
void app_manager_install_path_scan(void) {
|
||||
auto& registry = install_path_registry();
|
||||
|
||||
mutex_lock(®istry.mutex);
|
||||
auto paths_copy = registry.paths;
|
||||
mutex_unlock(®istry.mutex);
|
||||
|
||||
std::vector<std::string> found_app_dirs;
|
||||
for (const auto& root : paths_copy) {
|
||||
app_fs_list_direct_subdirectories(root, found_app_dirs);
|
||||
}
|
||||
|
||||
// Snapshot of what's already registered, taken once so the rest of this scan can run without holding registry.mutex
|
||||
mutex_lock(®istry.mutex);
|
||||
std::unordered_map<std::string, std::string> known_paths; // id -> path
|
||||
for (const auto& [id, record] : registry.scanned) {
|
||||
known_paths.emplace(id, record->path);
|
||||
}
|
||||
mutex_unlock(®istry.mutex);
|
||||
|
||||
// Stat each manifest and parse it entirely without registry.mutex held (due to filesystem IO being slow)
|
||||
std::vector<std::unique_ptr<ScannedAppManifest>> new_records;
|
||||
for (const auto& app_dir : found_app_dirs) {
|
||||
auto manifest_path = app_dir + "/manifest.properties";
|
||||
if (!app_fs_is_file(manifest_path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AppMetadata metadata {};
|
||||
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
|
||||
LOG_W(TAG, "Invalid manifest at %s", manifest_path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (known_paths.contains(metadata.app_id)) {
|
||||
continue; // already registered by an earlier scan
|
||||
}
|
||||
|
||||
auto record = std::make_unique<ScannedAppManifest>();
|
||||
record->id = metadata.app_id;
|
||||
record->name = metadata.app_name;
|
||||
record->path = app_dir;
|
||||
record->manifest = AppManifest {
|
||||
.id = record->id.c_str(),
|
||||
.name = record->name.c_str(),
|
||||
.category = APP_CATEGORY_USER,
|
||||
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
|
||||
.flags = 0,
|
||||
};
|
||||
new_records.push_back(std::move(record));
|
||||
}
|
||||
|
||||
// Anything a previous scan registered whose directory has since disappeared gets unregistered below.
|
||||
std::vector<std::string> missing_ids;
|
||||
for (const auto& [id, path] : known_paths) {
|
||||
if (!app_fs_is_directory(path)) {
|
||||
missing_ids.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
// app_manager_add()/app_manager_remove() take app-module's own ledger mutex internally -
|
||||
// calling them while holding registry.mutex would establish a registry.mutex -> ledger-
|
||||
// mutex lock order that any future opposite-order path would deadlock against, so these
|
||||
// also run with registry.mutex released. registry.mutex is taken only afterward, briefly,
|
||||
// to publish the results (plain in-memory map updates, no I/O or other locks involved).
|
||||
for (const auto& id : missing_ids) {
|
||||
app_manager_remove(id.c_str());
|
||||
}
|
||||
std::vector<std::unique_ptr<ScannedAppManifest>> added_records;
|
||||
for (auto& record : new_records) {
|
||||
if (app_manager_add(&record->manifest) == ERROR_NONE) {
|
||||
added_records.push_back(std::move(record));
|
||||
} else {
|
||||
LOG_E(TAG, "Failed to register app %s (duplicate id?)", record->id.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
mutex_lock(®istry.mutex);
|
||||
for (const auto& id : missing_ids) {
|
||||
registry.scanned.erase(id);
|
||||
}
|
||||
for (auto& record : added_records) {
|
||||
registry.scanned[record->id] = std::move(record);
|
||||
}
|
||||
mutex_unlock(®istry.mutex);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/module.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern ServiceManifest app_internal_loader_service_manifest;
|
||||
|
||||
static error_t start() {
|
||||
return service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
return service_manager_remove(app_internal_loader_service_manifest.id);
|
||||
}
|
||||
|
||||
Module app_module = {
|
||||
.name = "app",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
65
Modules/app-module/source/symbols.cpp
Normal file
65
Modules/app-module/source/symbols.cpp
Normal file
@ -0,0 +1,65 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/event.h>
|
||||
#include <app/install.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/module.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern ServiceManifest app_internal_loader_service_manifest;
|
||||
|
||||
const ModuleSymbol app_module_symbols[] = {
|
||||
// app/scheduler
|
||||
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id),
|
||||
// app/event
|
||||
DEFINE_MODULE_SYMBOL(app_event_subscribe),
|
||||
DEFINE_MODULE_SYMBOL(app_event_unsubscribe),
|
||||
DEFINE_MODULE_SYMBOL(app_event_emit),
|
||||
DEFINE_MODULE_SYMBOL(app_event_await),
|
||||
// app/manager
|
||||
DEFINE_MODULE_SYMBOL(app_manager_start),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_start_with_parameters),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_start_for_result),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_stop),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_finish),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_get_state),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_find_manifest),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_for_each_manifest),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_add),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_remove),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_instance_id),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_app_id),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_install_path_add),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_install_path_scan),
|
||||
// app/install
|
||||
DEFINE_MODULE_SYMBOL(app_get_install_path),
|
||||
DEFINE_MODULE_SYMBOL(app_install),
|
||||
DEFINE_MODULE_SYMBOL(app_uninstall),
|
||||
// terminator
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
|
||||
static error_t start() {
|
||||
return service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
return service_manager_remove(app_internal_loader_service_manifest.id);
|
||||
}
|
||||
|
||||
Module app_module = {
|
||||
.name = "app",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = app_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@ -53,6 +53,13 @@ struct LvglDisplayConfig {
|
||||
* the LV_COLOR_FORMAT_I1 path (already always-full-frame).
|
||||
*/
|
||||
bool force_full_frame;
|
||||
|
||||
/**
|
||||
* Opts owned draw buffer(s) OUT of DMA-capable memory, falling back to PSRAM instead of
|
||||
* scarce internal RAM. Default false keeps existing behavior. Only set true if the driver
|
||||
* never DMAs directly from the buffer pointer LVGL hands it in the flush callback.
|
||||
*/
|
||||
bool prefer_external_ram;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -67,11 +67,13 @@ void lvgl_devices_attach() {
|
||||
// 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);
|
||||
bool prefer_external_ram_buffer = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM);
|
||||
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
|
||||
.force_full_frame = display_requires_full_frame,
|
||||
.prefer_external_ram = prefer_external_ram_buffer
|
||||
};
|
||||
lv_disp_t* added_display = NULL;
|
||||
if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &added_display) == ERROR_NONE) {
|
||||
|
||||
@ -59,13 +59,18 @@ struct LvglDisplayCtx {
|
||||
bool byte_swap;
|
||||
};
|
||||
|
||||
static void* lvgl_display_alloc_buffer(size_t size_bytes) {
|
||||
static void* lvgl_display_alloc_buffer(size_t size_bytes, bool prefer_external_ram) {
|
||||
#ifdef ESP_PLATFORM
|
||||
// Must match LV_DRAW_BUF_ALIGN (can be > 4 - e.g. 64, tied to the cache line size for
|
||||
// DMA2D/PPA coherency on some targets - see sdkconfig's CONFIG_LV_DRAW_BUF_ALIGN). A buffer
|
||||
// allocated less strictly than that fails lv_display_set_buffers()'s alignment assert, which
|
||||
// is configured to LV_ASSERT_HANDLER (while(1);) rather than a clean abort - i.e. a silent hang.
|
||||
void* buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
|
||||
// MALLOC_CAP_DMA is scarce internal RAM - skip it for displays that don't DMA directly from
|
||||
// this buffer (see prefer_external_ram_buffer). Dropping MALLOC_CAP_DMA alone isn't enough to
|
||||
// land in PSRAM though: MALLOC_CAP_8BIT alone is still satisfied by internal RAM, so
|
||||
// MALLOC_CAP_SPIRAM must be requested explicitly (confirmed on real hardware).
|
||||
uint32_t caps = prefer_external_ram ? (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) : (MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
|
||||
void* buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, caps);
|
||||
if (buf == NULL) {
|
||||
buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DEFAULT);
|
||||
}
|
||||
@ -104,6 +109,13 @@ static bool lvgl_display_map_color_format(enum DisplayColorFormat in, lv_color_f
|
||||
// (e.g. ssd1306_draw_bitmap()'s row-to-page transpose).
|
||||
*out = LV_COLOR_FORMAT_I1;
|
||||
return true;
|
||||
case DISPLAY_COLOR_FORMAT_GRAYSCALE8:
|
||||
// Row-major, 1 byte/pixel luminance (0x00=black, 0xFF=white) - matches LV_COLOR_FORMAT_L8
|
||||
// directly, no repacking needed. Deliberately NOT routed through the I1 branch below in
|
||||
// lvgl_display_add(): I1 is hardcoded to LV_DISPLAY_RENDER_MODE_FULL there, which is what
|
||||
// this format exists to avoid for panels that want real partial/tile updates.
|
||||
*out = LV_COLOR_FORMAT_L8;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@ -396,7 +408,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
|
||||
// buffer's start (see lvgl_display_flush_cb()). Always redraw the whole frame in one
|
||||
// owned buffer instead of computing partial-region byte offsets against that packing.
|
||||
buf_size_bytes = (size_t)((hres + 7) / 8) * vres + 8;
|
||||
ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes);
|
||||
ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram);
|
||||
if (ctx->buf1 == NULL) {
|
||||
delete wrapper;
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
@ -416,13 +428,13 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
|
||||
? vres : config->buffer_height;
|
||||
buf_size_bytes = (size_t)hres * buf_height * bpp;
|
||||
|
||||
ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes);
|
||||
ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram);
|
||||
if (ctx->buf1 == NULL) {
|
||||
delete wrapper;
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (config->double_buffer) {
|
||||
ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes);
|
||||
ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram);
|
||||
if (ctx->buf2 == NULL) {
|
||||
lvgl_display_free_buffer(ctx->buf1);
|
||||
delete wrapper;
|
||||
@ -436,7 +448,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
|
||||
ctx->buf_size_bytes = buf_size_bytes;
|
||||
|
||||
if (ctx->sw_rotate) {
|
||||
ctx->rotate_buf = lvgl_display_alloc_buffer(buf_size_bytes);
|
||||
ctx->rotate_buf = lvgl_display_alloc_buffer(buf_size_bytes, config->prefer_external_ram);
|
||||
if (ctx->rotate_buf == NULL) {
|
||||
if (ctx->owns_buffers) {
|
||||
lvgl_display_free_buffer(ctx->buf1);
|
||||
|
||||
@ -4,8 +4,8 @@ include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(lvgl-window-manager
|
||||
tactility_add_module(lvgl-window-manager-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel lvgl-module
|
||||
REQUIRES TactilityKernel lvgl-module app-module
|
||||
)
|
||||
195
Modules/lvgl-window-manager-module/LICENSE-Apache-2.0.md
Normal file
195
Modules/lvgl-window-manager-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### 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.
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include "../../../app-module/include/app/instance.h"
|
||||
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
@ -33,6 +36,10 @@ enum WindowState {
|
||||
* @return the widget windows should actually be placed into - @a root_widget itself, or a
|
||||
* child of it. Returning NULL falls back to @a root_widget.
|
||||
* @warning Called on the LVGL task with the LVGL lock already held.
|
||||
* @warning Also called with window-manager's internal lifecycle_mutex held (non-recursive) -
|
||||
* do NOT call window_manager_start()/window_manager_stop()/window_manager_create()/
|
||||
* window_manager_remove() or any other window-manager API from this callback, that would
|
||||
* deadlock.
|
||||
*/
|
||||
typedef lv_obj_t* (*WindowManagerScreenInitFn)(lv_obj_t* root_widget);
|
||||
|
||||
@ -73,6 +80,10 @@ error_t window_manager_stop(void);
|
||||
* window_manager_remove() for the window that used to be on top (e.g. a dialog's own thread as
|
||||
* it closes). Do NOT rely on thread_local state set by this window's own app thread; use
|
||||
* @a user_data instead.
|
||||
* @warning Also called with window-manager's internal lifecycle_mutex held (non-recursive) -
|
||||
* do NOT call window_manager_start()/window_manager_stop()/window_manager_create()/
|
||||
* window_manager_remove() or any other window-manager API from this callback, that would
|
||||
* deadlock.
|
||||
*/
|
||||
typedef void (*WindowCreateWidgetsFn)(lv_obj_t* root, void* user_data);
|
||||
|
||||
@ -80,12 +91,13 @@ typedef void (*WindowCreateWidgetsFn)(lv_obj_t* root, void* user_data);
|
||||
* Creates a new window on top of the stack (last created = topmost). Deletes the previously
|
||||
* topmost window's widgets (if any) and builds this window's widgets immediately via
|
||||
* @a create_widgets - only the topmost window ever has live widgets.
|
||||
* @param[in] app_instance_id the application instance this window belongs to, should not be 0
|
||||
* @param[in] user_data opaque; passed back to @a create_widgets on every call, including a
|
||||
* later rebuild triggered by window_manager_remove() - see its @warning about which thread that
|
||||
* can run on. Typically the calling app's own Context*.
|
||||
* @return the new window's id, or 0 if window_manager_start() hasn't been called
|
||||
*/
|
||||
WindowId window_manager_create(uint32_t app_instance_id, WindowCreateWidgetsFn create_widgets, void* user_data);
|
||||
WindowId window_manager_create(AppInstanceId app_instance_id, WindowCreateWidgetsFn create_widgets, void* user_data);
|
||||
|
||||
/**
|
||||
* Removes a window, wherever it is in the stack - not necessarily the topmost one. If it was
|
||||
@ -101,6 +113,10 @@ enum WindowState window_manager_get_state(WindowId id);
|
||||
* Blocks the calling task until @a id's state changes away from WINDOW_STATE_GRANTED, or
|
||||
* @a timeout elapses. Returns immediately with WINDOW_STATE_REVOKED if @a id isn't currently
|
||||
* topmost (nothing to wait for).
|
||||
* @warning At most one task may have an outstanding await() call per window at a time (each
|
||||
* window tracks a single waiter). A second concurrent call for the same @a id asserts. Calls
|
||||
* for different windows (e.g. from different app tasks in a stacked window manager) don't
|
||||
* conflict with each other.
|
||||
* @return the state after waking (or immediately, if there was nothing to wait for)
|
||||
*/
|
||||
enum WindowState window_manager_await_state_change(WindowId id, TickType_t timeout);
|
||||
@ -7,12 +7,21 @@
|
||||
|
||||
extern "C" {
|
||||
|
||||
const ModuleSymbol lvgl_window_manager_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(window_manager_create),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_remove),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_get_state),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_await_state_change),
|
||||
// terminator
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
|
||||
Module lvgl_window_manager_module = {
|
||||
.name = "lvgl-window-manager",
|
||||
.start = window_manager_start,
|
||||
.stop = window_manager_stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = nullptr,
|
||||
.symbols = lvgl_window_manager_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
439
Modules/lvgl-window-manager-module/source/window_manager.cpp
Normal file
439
Modules/lvgl-window-manager-module/source/window_manager.cpp
Normal file
@ -0,0 +1,439 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <app/instance.h>
|
||||
|
||||
#include <lvgl/lvgl.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/freertos/semphr.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <new>
|
||||
#include <vector>
|
||||
|
||||
constexpr auto* TAG = "window_manager";
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* A dedicated (not the waiting task's shared default FreeRTOS notification, which other
|
||||
* subsystems - e.g. app_event.cpp's AppEventSubscription - also use; an unrelated notification
|
||||
* delivered to the same task could otherwise unblock a wait early) completion signal for one
|
||||
* window_manager_await_state_change() call. Heap-allocated with its own refcount (protected by
|
||||
* WindowManagerState::mutex, not atomic) rather than owned solely by the WindowRecord, since
|
||||
* window_manager_create()/remove() claim (read + clear) a window's signal under the lock but
|
||||
* give it after releasing that lock - refcounting lets whichever side (the waiting task waking
|
||||
* up, or the claimer after it gives) finishes last safely delete it.
|
||||
*/
|
||||
struct WindowWaitSignal {
|
||||
SemaphoreHandle_t semaphore;
|
||||
/** Starts at 1, owned by window_manager_await_state_change() until it's done waiting.
|
||||
* Whoever claims this signal from a WindowRecord (see claim_waiter_locked()) takes an
|
||||
* additional reference for as long as it takes to give the semaphore. Reaching 0 means
|
||||
* deletion. */
|
||||
int refcount = 1;
|
||||
};
|
||||
|
||||
struct WindowRecord {
|
||||
WindowId id;
|
||||
uint32_t app_instance_id;
|
||||
WindowCreateWidgetsFn create_widgets;
|
||||
void* user_data;
|
||||
|
||||
/** Set by window_manager_await_state_change() for this specific window, if a task is
|
||||
* currently blocked there - see that function's @warning on at most one concurrent awaiter
|
||||
* per window. Per-window rather than a single manager-wide slot, since a stacked window
|
||||
* manager serving several app tasks can have more than one window (though only ever one of
|
||||
* them topmost/GRANTED at a time) with a live await() call outstanding. */
|
||||
WindowWaitSignal* waiting_signal = nullptr;
|
||||
};
|
||||
|
||||
struct WindowManagerState {
|
||||
/** Mutex for read/write operations. Shortly held. */
|
||||
Mutex mutex {};
|
||||
|
||||
/** Serializes the full start()/stop()/create()/remove() transitions against each other,
|
||||
* including the LVGL work done with `mutex` released (and any create_widgets()/
|
||||
* screen_init() callback invoked as part of that work). Without this, e.g.
|
||||
* window_manager_stop() could delete real_root_widget/content_root_widget/top_widget
|
||||
* between a concurrent create()/remove() capturing one of those pointers under `mutex` and
|
||||
* actually using it via build_window_widget()/delete_widget() after releasing `mutex` -
|
||||
* touching an LVGL object it no longer holds a valid reference to. */
|
||||
Mutex lifecycle_mutex {};
|
||||
|
||||
bool started = false;
|
||||
WindowManagerScreenInitFn screen_init = nullptr;
|
||||
|
||||
/** The raw, full-size container window_manager_start() creates; owns (and deletion
|
||||
* cascades to) whatever the screen-init callback added under it. */
|
||||
lv_obj_t* real_root_widget = nullptr;
|
||||
/** The stable parent each window's own widget is created under - real_root_widget itself,
|
||||
* unless the screen-init callback returned a nested content widget instead. */
|
||||
lv_obj_t* content_root_widget = nullptr;
|
||||
|
||||
WindowId next_id = 1;
|
||||
/** windows.back() is topmost; only it ever has a live widget (top_widget). */
|
||||
std::vector<WindowRecord> windows;
|
||||
lv_obj_t* top_widget = nullptr;
|
||||
|
||||
WindowManagerState() {
|
||||
mutex_construct(&mutex);
|
||||
mutex_construct(&lifecycle_mutex);
|
||||
}
|
||||
};
|
||||
|
||||
WindowManagerState& state() {
|
||||
static WindowManagerState instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
lv_obj_t* build_window_widget(lv_obj_t* content, WindowCreateWidgetsFn create_widgets, void* user_data) {
|
||||
if (content == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
lvgl_lock();
|
||||
lv_obj_t* widget = lv_obj_create(content);
|
||||
lv_obj_set_size(widget, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_pad_all(widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_radius(widget, 0, LV_STATE_DEFAULT);
|
||||
if (create_widgets != nullptr) {
|
||||
create_widgets(widget, user_data);
|
||||
}
|
||||
lvgl_unlock();
|
||||
return widget;
|
||||
}
|
||||
|
||||
void delete_widget(lv_obj_t* widget) {
|
||||
if (widget == nullptr) {
|
||||
return;
|
||||
}
|
||||
lvgl_lock();
|
||||
lv_obj_delete(widget);
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
// Call while holding WindowManagerState::mutex. Transfers ownership of `window`'s waiting
|
||||
// signal (if any) to the caller, taking an additional reference on the caller's behalf - the
|
||||
// caller must eventually pass the result to give_and_release() exactly once, outside the lock.
|
||||
WindowWaitSignal* claim_waiter_locked(WindowRecord& window) {
|
||||
WindowWaitSignal* signal = window.waiting_signal;
|
||||
window.waiting_signal = nullptr;
|
||||
if (signal != nullptr) {
|
||||
signal->refcount++;
|
||||
}
|
||||
return signal;
|
||||
}
|
||||
|
||||
// Gives `signal`'s semaphore (waking window_manager_await_state_change() if it's still
|
||||
// waiting) and releases the caller's reference (see claim_waiter_locked()), deleting the
|
||||
// signal if that was the last one. No-op if `signal` is NULL.
|
||||
void give_and_release(WindowWaitSignal* signal) {
|
||||
if (signal == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
xSemaphoreGive(signal->semaphore);
|
||||
|
||||
auto& s = state();
|
||||
mutex_lock(&s.mutex);
|
||||
bool should_delete = (--signal->refcount == 0);
|
||||
mutex_unlock(&s.mutex);
|
||||
if (should_delete) {
|
||||
vSemaphoreDelete(signal->semaphore);
|
||||
delete signal;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
void window_manager_configure(WindowManagerScreenInitFn screen_init) {
|
||||
auto& s = state();
|
||||
|
||||
// Serializes against window_manager_start()/stop()
|
||||
mutex_lock(&s.lifecycle_mutex);
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (!s.started) {
|
||||
s.screen_init = screen_init;
|
||||
} else {
|
||||
LOG_W(TAG, "Ignoring window_manager_configure: module is already started");
|
||||
}
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
}
|
||||
|
||||
error_t window_manager_start(void) {
|
||||
auto& s = state();
|
||||
|
||||
// Held for the whole transition (including the LVGL work below, done with `mutex`
|
||||
// released) - blocks a concurrent start() from also passing the `started` check and
|
||||
// building its own root widget, and blocks a concurrent stop() from running while this
|
||||
// start is still mid-flight.
|
||||
mutex_lock(&s.lifecycle_mutex);
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (s.started) {
|
||||
mutex_unlock(&s.mutex);
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
WindowManagerScreenInitFn screen_init = s.screen_init;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
lv_obj_t* real_widget = nullptr;
|
||||
lv_obj_t* content_widget = nullptr;
|
||||
|
||||
lvgl_lock();
|
||||
lv_obj_t* screen = lv_screen_active();
|
||||
if (screen != nullptr) {
|
||||
real_widget = lv_obj_create(screen);
|
||||
lv_obj_set_size(real_widget, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_pad_all(real_widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(real_widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_radius(real_widget, 0, LV_STATE_DEFAULT);
|
||||
|
||||
content_widget = (screen_init != nullptr) ? screen_init(real_widget) : nullptr;
|
||||
if (content_widget == nullptr) {
|
||||
content_widget = real_widget;
|
||||
}
|
||||
}
|
||||
lvgl_unlock();
|
||||
|
||||
if (real_widget == nullptr) {
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
s.real_root_widget = real_widget;
|
||||
s.content_root_widget = content_widget;
|
||||
s.started = true;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t window_manager_stop(void) {
|
||||
auto& s = state();
|
||||
|
||||
// See window_manager_start() - blocks until any in-flight start() has fully completed (or
|
||||
// failed) before this stop can observe/tear down state.
|
||||
mutex_lock(&s.lifecycle_mutex);
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (!s.started) {
|
||||
mutex_unlock(&s.mutex);
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
lv_obj_t* widget = s.real_root_widget;
|
||||
// Claim every window's waiter before clearing - normally at most the topmost window's is
|
||||
// ever set, but every window is being torn down here, so every one is checked.
|
||||
std::vector<WindowWaitSignal*> waiters;
|
||||
for (auto& window : s.windows) {
|
||||
if (auto* signal = claim_waiter_locked(window); signal != nullptr) {
|
||||
waiters.push_back(signal);
|
||||
}
|
||||
}
|
||||
s.real_root_widget = nullptr;
|
||||
s.content_root_widget = nullptr;
|
||||
s.top_widget = nullptr;
|
||||
s.windows.clear();
|
||||
s.started = false;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
for (WindowWaitSignal* waiter : waiters) {
|
||||
give_and_release(waiter);
|
||||
}
|
||||
|
||||
// Deleting the real widget cascades to everything under it - chrome and top_widget alike.
|
||||
delete_widget(widget);
|
||||
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
WindowId window_manager_create(AppInstanceId app_instance_id, WindowCreateWidgetsFn create_widgets, void* user_data) {
|
||||
if (app_instance_id == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto& s = state();
|
||||
|
||||
// See lifecycle_mutex's comment - blocks a concurrent window_manager_stop() (or another
|
||||
// create()/remove()) from touching real_root_widget/content_root_widget/top_widget while
|
||||
// this call still holds pointers to them.
|
||||
mutex_lock(&s.lifecycle_mutex);
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (!s.started) {
|
||||
mutex_unlock(&s.mutex);
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return 0;
|
||||
}
|
||||
lv_obj_t* content = s.content_root_widget;
|
||||
lv_obj_t* old_top_widget = s.top_widget;
|
||||
// The current topmost window (if any) is about to be superseded - claim its waiter (if
|
||||
// any) here so it gets notified below, since it's no longer topmost after this.
|
||||
WindowWaitSignal* waiter = !s.windows.empty() ? claim_waiter_locked(s.windows.back()) : nullptr;
|
||||
s.top_widget = nullptr;
|
||||
WindowId new_id = s.next_id++;
|
||||
s.windows.push_back(WindowRecord { new_id, app_instance_id, create_widgets, user_data });
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
give_and_release(waiter);
|
||||
|
||||
delete_widget(old_top_widget);
|
||||
lv_obj_t* new_widget = build_window_widget(content, create_widgets, user_data);
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
bool still_topmost = !s.windows.empty() && s.windows.back().id == new_id;
|
||||
if (still_topmost) {
|
||||
s.top_widget = new_widget;
|
||||
new_widget = nullptr; // consumed
|
||||
}
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
// Something else became topmost while we were building (e.g. a concurrent create() from
|
||||
// another app thread) - discard what we just made.
|
||||
delete_widget(new_widget);
|
||||
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return new_id;
|
||||
}
|
||||
|
||||
void window_manager_remove(WindowId id) {
|
||||
auto& s = state();
|
||||
|
||||
// See lifecycle_mutex's comment - blocks a concurrent window_manager_stop() (or another
|
||||
// create()/remove()) from touching real_root_widget/content_root_widget/top_widget while
|
||||
// this call still holds pointers to them.
|
||||
mutex_lock(&s.lifecycle_mutex);
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
auto iterator = std::find_if(s.windows.begin(), s.windows.end(),
|
||||
[id](const WindowRecord& window) { return window.id == id; });
|
||||
if (iterator == s.windows.end()) {
|
||||
mutex_unlock(&s.mutex);
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return;
|
||||
}
|
||||
bool was_topmost = (iterator + 1 == s.windows.end());
|
||||
// The window being removed owns its own waiter (if any) - a waiter is only ever registered
|
||||
// while its window is topmost (see window_manager_await_state_change()), and if this window
|
||||
// later stopped being topmost without being removed, window_manager_create() would already
|
||||
// have claimed/cleared it - so a buried window's waiting_signal is always already null.
|
||||
WindowWaitSignal* waiter = claim_waiter_locked(*iterator);
|
||||
s.windows.erase(iterator);
|
||||
|
||||
lv_obj_t* content = s.content_root_widget;
|
||||
lv_obj_t* old_widget = nullptr;
|
||||
WindowCreateWidgetsFn next_create_widgets = nullptr;
|
||||
void* next_user_data = nullptr;
|
||||
WindowId next_id = 0;
|
||||
bool has_next = false;
|
||||
|
||||
if (was_topmost) {
|
||||
old_widget = s.top_widget;
|
||||
s.top_widget = nullptr;
|
||||
if (!s.windows.empty()) {
|
||||
next_create_widgets = s.windows.back().create_widgets;
|
||||
next_user_data = s.windows.back().user_data;
|
||||
next_id = s.windows.back().id;
|
||||
has_next = true;
|
||||
}
|
||||
}
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
give_and_release(waiter);
|
||||
|
||||
if (!was_topmost) {
|
||||
// A buried window was removed - the topmost window's widgets are unaffected.
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
delete_widget(old_widget);
|
||||
lv_obj_t* new_widget = has_next ? build_window_widget(content, next_create_widgets, next_user_data) : nullptr;
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
bool still_topmost = has_next && !s.windows.empty() && s.windows.back().id == next_id;
|
||||
if (still_topmost) {
|
||||
s.top_widget = new_widget;
|
||||
new_widget = nullptr; // consumed
|
||||
}
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
delete_widget(new_widget);
|
||||
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
}
|
||||
|
||||
WindowState window_manager_get_state(WindowId id) {
|
||||
auto& s = state();
|
||||
mutex_lock(&s.mutex);
|
||||
bool is_top = !s.windows.empty() && s.windows.back().id == id;
|
||||
mutex_unlock(&s.mutex);
|
||||
return is_top ? WINDOW_STATE_GRANTED : WINDOW_STATE_REVOKED;
|
||||
}
|
||||
|
||||
WindowState window_manager_await_state_change(WindowId id, TickType_t timeout) {
|
||||
auto& s = state();
|
||||
|
||||
// Dedicated semaphore rather than this task's default FreeRTOS notification - other
|
||||
// subsystems (e.g. app_event.cpp's AppEventSubscription) use that same shared slot, so an
|
||||
// unrelated notification delivered to this task could otherwise wake this wait early.
|
||||
auto* signal = new (std::nothrow) WindowWaitSignal();
|
||||
if (signal == nullptr) {
|
||||
return window_manager_get_state(id);
|
||||
}
|
||||
signal->semaphore = xSemaphoreCreateBinary();
|
||||
if (signal->semaphore == nullptr) {
|
||||
delete signal;
|
||||
return window_manager_get_state(id);
|
||||
}
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
bool is_top = !s.windows.empty() && s.windows.back().id == id;
|
||||
if (!is_top) {
|
||||
mutex_unlock(&s.mutex);
|
||||
vSemaphoreDelete(signal->semaphore);
|
||||
delete signal;
|
||||
return WINDOW_STATE_REVOKED;
|
||||
}
|
||||
// At most one concurrent awaiter per window - see the @warning on this function.
|
||||
check(s.windows.back().waiting_signal == nullptr);
|
||||
s.windows.back().waiting_signal = signal;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
xSemaphoreTake(signal->semaphore, timeout);
|
||||
|
||||
// Deregister ourselves if a create()/remove() hasn't already claimed us (the ordinary,
|
||||
// intended wakeup) - otherwise a later create()/remove() could read a signal that's already
|
||||
// been given away here. Re-locate the record by id - it may have been erased
|
||||
// (window_manager_remove()) while we waited. Either way, release our own reference:
|
||||
// whichever side (us or a claimer) does this last is the one that actually deletes it.
|
||||
mutex_lock(&s.mutex);
|
||||
auto iterator = std::find_if(s.windows.begin(), s.windows.end(),
|
||||
[id](const WindowRecord& window) { return window.id == id; });
|
||||
if (iterator != s.windows.end() && iterator->waiting_signal == signal) {
|
||||
iterator->waiting_signal = nullptr;
|
||||
}
|
||||
bool should_delete = (--signal->refcount == 0);
|
||||
mutex_unlock(&s.mutex);
|
||||
if (should_delete) {
|
||||
vSemaphoreDelete(signal->semaphore);
|
||||
delete signal;
|
||||
}
|
||||
|
||||
return window_manager_get_state(id);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@ -1,279 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <lvgl/lvgl.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
struct WindowRecord {
|
||||
WindowId id;
|
||||
uint32_t app_instance_id;
|
||||
WindowCreateWidgetsFn create_widgets;
|
||||
void* user_data;
|
||||
};
|
||||
|
||||
struct WindowManagerState {
|
||||
Mutex mutex {};
|
||||
|
||||
bool started = false;
|
||||
WindowManagerScreenInitFn screen_init = nullptr;
|
||||
|
||||
/** The raw, full-size container window_manager_start() creates; owns (and deletion
|
||||
* cascades to) whatever the screen-init callback added under it. */
|
||||
lv_obj_t* real_root_widget = nullptr;
|
||||
/** The stable parent each window's own widget is created under - real_root_widget itself,
|
||||
* unless the screen-init callback returned a nested content widget instead. */
|
||||
lv_obj_t* content_root_widget = nullptr;
|
||||
|
||||
WindowId next_id = 1;
|
||||
/** windows.back() is topmost; only it ever has a live widget (top_widget). */
|
||||
std::vector<WindowRecord> windows;
|
||||
lv_obj_t* top_widget = nullptr;
|
||||
|
||||
/** Task blocked in window_manager_await_state_change(), if any. */
|
||||
TaskHandle_t waiting_task = nullptr;
|
||||
|
||||
WindowManagerState() { mutex_construct(&mutex); }
|
||||
};
|
||||
|
||||
WindowManagerState& state() {
|
||||
static WindowManagerState instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
lv_obj_t* build_window_widget(lv_obj_t* content, WindowCreateWidgetsFn create_widgets, void* user_data) {
|
||||
if (content == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
lvgl_lock();
|
||||
lv_obj_t* widget = lv_obj_create(content);
|
||||
lv_obj_set_size(widget, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_pad_all(widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_radius(widget, 0, LV_STATE_DEFAULT);
|
||||
if (create_widgets != nullptr) {
|
||||
create_widgets(widget, user_data);
|
||||
}
|
||||
lvgl_unlock();
|
||||
return widget;
|
||||
}
|
||||
|
||||
void delete_widget(lv_obj_t* widget) {
|
||||
if (widget == nullptr) {
|
||||
return;
|
||||
}
|
||||
lvgl_lock();
|
||||
lv_obj_delete(widget);
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
void window_manager_configure(WindowManagerScreenInitFn screen_init) {
|
||||
auto& s = state();
|
||||
mutex_lock(&s.mutex);
|
||||
s.screen_init = screen_init;
|
||||
mutex_unlock(&s.mutex);
|
||||
}
|
||||
|
||||
error_t window_manager_start(void) {
|
||||
auto& s = state();
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (s.started) {
|
||||
mutex_unlock(&s.mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
WindowManagerScreenInitFn screen_init = s.screen_init;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
lv_obj_t* real_widget = nullptr;
|
||||
lv_obj_t* content_widget = nullptr;
|
||||
|
||||
lvgl_lock();
|
||||
lv_obj_t* screen = lv_screen_active();
|
||||
if (screen != nullptr) {
|
||||
real_widget = lv_obj_create(screen);
|
||||
lv_obj_set_size(real_widget, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_pad_all(real_widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(real_widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_radius(real_widget, 0, LV_STATE_DEFAULT);
|
||||
|
||||
content_widget = (screen_init != nullptr) ? screen_init(real_widget) : nullptr;
|
||||
if (content_widget == nullptr) {
|
||||
content_widget = real_widget;
|
||||
}
|
||||
}
|
||||
lvgl_unlock();
|
||||
|
||||
if (real_widget == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
s.real_root_widget = real_widget;
|
||||
s.content_root_widget = content_widget;
|
||||
s.started = true;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t window_manager_stop(void) {
|
||||
auto& s = state();
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (!s.started) {
|
||||
mutex_unlock(&s.mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
lv_obj_t* widget = s.real_root_widget;
|
||||
TaskHandle_t waiter = s.waiting_task;
|
||||
s.real_root_widget = nullptr;
|
||||
s.content_root_widget = nullptr;
|
||||
s.top_widget = nullptr;
|
||||
s.windows.clear();
|
||||
s.started = false;
|
||||
s.waiting_task = nullptr;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
if (waiter != nullptr) {
|
||||
xTaskNotifyGive(waiter);
|
||||
}
|
||||
|
||||
// Deleting the real widget cascades to everything under it - chrome and top_widget alike.
|
||||
delete_widget(widget);
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
WindowId window_manager_create(uint32_t app_instance_id, WindowCreateWidgetsFn create_widgets, void* user_data) {
|
||||
auto& s = state();
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (!s.started) {
|
||||
mutex_unlock(&s.mutex);
|
||||
return 0;
|
||||
}
|
||||
lv_obj_t* content = s.content_root_widget;
|
||||
lv_obj_t* old_top_widget = s.top_widget;
|
||||
TaskHandle_t waiter = s.waiting_task;
|
||||
s.waiting_task = nullptr;
|
||||
s.top_widget = nullptr;
|
||||
WindowId new_id = s.next_id++;
|
||||
s.windows.push_back(WindowRecord { new_id, app_instance_id, create_widgets, user_data });
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
if (waiter != nullptr) {
|
||||
xTaskNotifyGive(waiter);
|
||||
}
|
||||
|
||||
delete_widget(old_top_widget);
|
||||
lv_obj_t* new_widget = build_window_widget(content, create_widgets, user_data);
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
bool still_topmost = !s.windows.empty() && s.windows.back().id == new_id;
|
||||
if (still_topmost) {
|
||||
s.top_widget = new_widget;
|
||||
new_widget = nullptr; // consumed
|
||||
}
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
// Something else became topmost while we were building (e.g. a concurrent create() from
|
||||
// another app thread) - discard what we just made.
|
||||
delete_widget(new_widget);
|
||||
|
||||
return new_id;
|
||||
}
|
||||
|
||||
void window_manager_remove(WindowId id) {
|
||||
auto& s = state();
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
auto iterator = std::find_if(s.windows.begin(), s.windows.end(),
|
||||
[id](const WindowRecord& window) { return window.id == id; });
|
||||
if (iterator == s.windows.end()) {
|
||||
mutex_unlock(&s.mutex);
|
||||
return;
|
||||
}
|
||||
bool was_topmost = (iterator + 1 == s.windows.end());
|
||||
s.windows.erase(iterator);
|
||||
|
||||
lv_obj_t* content = s.content_root_widget;
|
||||
lv_obj_t* old_widget = nullptr;
|
||||
WindowCreateWidgetsFn next_create_widgets = nullptr;
|
||||
void* next_user_data = nullptr;
|
||||
WindowId next_id = 0;
|
||||
bool has_next = false;
|
||||
|
||||
if (was_topmost) {
|
||||
old_widget = s.top_widget;
|
||||
s.top_widget = nullptr;
|
||||
if (!s.windows.empty()) {
|
||||
next_create_widgets = s.windows.back().create_widgets;
|
||||
next_user_data = s.windows.back().user_data;
|
||||
next_id = s.windows.back().id;
|
||||
has_next = true;
|
||||
}
|
||||
}
|
||||
|
||||
TaskHandle_t waiter = s.waiting_task;
|
||||
s.waiting_task = nullptr;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
if (waiter != nullptr) {
|
||||
xTaskNotifyGive(waiter);
|
||||
}
|
||||
|
||||
if (!was_topmost) {
|
||||
// A buried window was removed - the topmost window's widgets are unaffected.
|
||||
return;
|
||||
}
|
||||
|
||||
delete_widget(old_widget);
|
||||
lv_obj_t* new_widget = has_next ? build_window_widget(content, next_create_widgets, next_user_data) : nullptr;
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
bool still_topmost = has_next && !s.windows.empty() && s.windows.back().id == next_id;
|
||||
if (still_topmost) {
|
||||
s.top_widget = new_widget;
|
||||
new_widget = nullptr; // consumed
|
||||
}
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
delete_widget(new_widget);
|
||||
}
|
||||
|
||||
WindowState window_manager_get_state(WindowId id) {
|
||||
auto& s = state();
|
||||
mutex_lock(&s.mutex);
|
||||
bool is_top = !s.windows.empty() && s.windows.back().id == id;
|
||||
mutex_unlock(&s.mutex);
|
||||
return is_top ? WINDOW_STATE_GRANTED : WINDOW_STATE_REVOKED;
|
||||
}
|
||||
|
||||
WindowState window_manager_await_state_change(WindowId id, TickType_t timeout) {
|
||||
auto& s = state();
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
bool is_top = !s.windows.empty() && s.windows.back().id == id;
|
||||
if (!is_top) {
|
||||
mutex_unlock(&s.mutex);
|
||||
return WINDOW_STATE_REVOKED;
|
||||
}
|
||||
s.waiting_task = xTaskGetCurrentTaskHandle();
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
ulTaskNotifyTake(pdTRUE, timeout);
|
||||
|
||||
return window_manager_get_state(id);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@ -108,7 +108,16 @@ static uint32_t hid_keycode_to_key(uint8_t modifier, uint8_t key_code,
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (ctrl || alt) return 0;
|
||||
/*
|
||||
* Ctrl and Alt no longer suppress the key.
|
||||
*
|
||||
* They used to return 0 here, which meant a chord like Ctrl+C produced nothing at all and a
|
||||
* terminal application could never see it. The modifiers are now reported alongside the key in
|
||||
* UsbHidEvent instead, so the plain character still comes through and a consumer that wants a
|
||||
* control code derives it. Alt is passed through on the same basis.
|
||||
*/
|
||||
(void)ctrl;
|
||||
(void)alt;
|
||||
|
||||
if (key_code < (sizeof(keycode2ascii) / sizeof(keycode2ascii[0]))) {
|
||||
bool is_letter = (key_code >= 0x04 && key_code <= 0x1D);
|
||||
@ -157,6 +166,10 @@ static void hid_interface_callback(hid_host_device_handle_t handle,
|
||||
if (params.proto == HID_PROTOCOL_KEYBOARD) {
|
||||
if (data_len < sizeof(hid_keyboard_input_report_boot_t)) break;
|
||||
auto* kb = reinterpret_cast<const hid_keyboard_input_report_boot_t*>(data);
|
||||
const bool with_ctrl = (kb->modifier.val & HID_LEFT_CONTROL) ||
|
||||
(kb->modifier.val & HID_RIGHT_CONTROL);
|
||||
const bool with_alt = (kb->modifier.val & HID_LEFT_ALT) ||
|
||||
(kb->modifier.val & HID_RIGHT_ALT);
|
||||
|
||||
for (int i = 0; i < HID_KEYBOARD_KEY_MAX; i++) {
|
||||
uint8_t prev_hid = ctx->prev_keys[i];
|
||||
@ -169,7 +182,7 @@ static void hid_interface_callback(hid_host_device_handle_t handle,
|
||||
uint32_t lv_key = ctx->pressed_lv_keys[prev_hid];
|
||||
ctx->pressed_lv_keys[prev_hid] = 0;
|
||||
if (lv_key) {
|
||||
UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, false } };
|
||||
UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, false, with_ctrl, with_alt } };
|
||||
publish_event(ctx, &evt);
|
||||
}
|
||||
}
|
||||
@ -208,7 +221,10 @@ static void hid_interface_callback(hid_host_device_handle_t handle,
|
||||
uint32_t lv_key = hid_keycode_to_key(kb->modifier.val, hid_code,
|
||||
ctx->caps_lock_active, ctx->num_lock_active);
|
||||
if (lv_key) {
|
||||
UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, true } };
|
||||
UsbHidEvent evt = {
|
||||
.type = USB_HID_EVENT_KEY,
|
||||
.key = { lv_key, true, with_ctrl, with_alt }
|
||||
};
|
||||
publish_event(ctx, &evt);
|
||||
ctx->pressed_lv_keys[hid_code] = lv_key;
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ list(APPEND REQUIRES_LIST
|
||||
TactilityKernel
|
||||
TactilityFreeRtos
|
||||
lvgl-module
|
||||
lvgl-window-manager-module
|
||||
app-module
|
||||
crypt-module
|
||||
gps-module
|
||||
gps-generic-module
|
||||
@ -20,6 +22,7 @@ list(APPEND REQUIRES_LIST
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
|
||||
list(APPEND REQUIRES_LIST
|
||||
app-esp32-module
|
||||
platform-esp32
|
||||
driver
|
||||
elf_loader
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
/**
|
||||
* @brief key-value storage for general purpose.
|
||||
* Maps strings on a fixed set of data types.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace tt {
|
||||
|
||||
/**
|
||||
* A dictionary that maps keys (strings) onto several atomary types.
|
||||
*/
|
||||
class Bundle final {
|
||||
|
||||
typedef uint32_t Hash;
|
||||
|
||||
enum class Type {
|
||||
Bool,
|
||||
Int32,
|
||||
Int64,
|
||||
String,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
Type type;
|
||||
union {
|
||||
bool value_bool;
|
||||
int32_t value_int32;
|
||||
int64_t value_int64;
|
||||
};
|
||||
std::string value_string;
|
||||
} Value;
|
||||
|
||||
std::unordered_map<std::string, Value> entries;
|
||||
|
||||
public:
|
||||
|
||||
Bundle() = default;
|
||||
|
||||
Bundle(const Bundle& bundle) {
|
||||
this->entries = bundle.entries;
|
||||
}
|
||||
|
||||
bool getBool(const std::string& key) const;
|
||||
int32_t getInt32(const std::string& key) const;
|
||||
int64_t getInt64(const std::string& key) const;
|
||||
std::string getString(const std::string& key) const;
|
||||
|
||||
bool hasBool(const std::string& key) const;
|
||||
bool hasInt32(const std::string& key) const;
|
||||
bool hasInt64(const std::string& key) const;
|
||||
bool hasString(const std::string& key) const;
|
||||
|
||||
bool optBool(const std::string& key, bool& out) const;
|
||||
bool optInt32(const std::string& key, int32_t& out) const;
|
||||
bool optInt64(const std::string& key, int64_t& out) const;
|
||||
bool optString(const std::string& key, std::string& out) const;
|
||||
|
||||
void putBool(const std::string& key, bool value);
|
||||
void putInt32(const std::string& key, int32_t value);
|
||||
void putInt64(const std::string& key, int64_t value);
|
||||
void putString(const std::string& key, const std::string& value);
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@ -1,41 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace tt {
|
||||
|
||||
/**
|
||||
* Settings that persist on NVS flash for ESP32.
|
||||
* On simulator, the settings are only in-memory.
|
||||
*
|
||||
* Note that on ESP32, there are limitations:
|
||||
* - namespace name is limited by NVS_NS_NAME_MAX_SIZE (generally 16 characters)
|
||||
* - key is limited by NVS_KEY_NAME_MAX_SIZE (generally 16 characters)
|
||||
*/
|
||||
class Preferences {
|
||||
|
||||
const char* namespace_;
|
||||
|
||||
public:
|
||||
explicit Preferences(const char* namespace_) {
|
||||
this->namespace_ = namespace_;
|
||||
}
|
||||
|
||||
bool hasBool(const std::string& key) const;
|
||||
bool hasInt32(const std::string& key) const;
|
||||
bool hasInt64(const std::string& key) const;
|
||||
bool hasString(const std::string& key) const;
|
||||
|
||||
bool optBool(const std::string& key, bool& out) const;
|
||||
bool optInt32(const std::string& key, int32_t& out) const;
|
||||
bool optInt64(const std::string& key, int64_t& out) const;
|
||||
bool optString(const std::string& key, std::string& out) const;
|
||||
|
||||
void putBool(const std::string& key, bool value);
|
||||
void putInt32(const std::string& key, int32_t value);
|
||||
void putInt64(const std::string& key, int64_t value);
|
||||
void putString(const std::string& key, const std::string& value);
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@ -3,7 +3,6 @@
|
||||
#include <tactility/concurrent/dispatcher.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/module.h>
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Tactility/app/AppContext.h"
|
||||
|
||||
#include <Tactility/Bundle.h>
|
||||
#include <Tactility/Mutex.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
// Forward declarations
|
||||
typedef struct _lv_obj_t lv_obj_t;
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
// Forward declarations
|
||||
class AppContext;
|
||||
enum class Result;
|
||||
|
||||
typedef unsigned int LaunchId;
|
||||
|
||||
class App {
|
||||
|
||||
Mutex mutex;
|
||||
|
||||
struct ResultHolder {
|
||||
Result result;
|
||||
std::unique_ptr<Bundle> resultData;
|
||||
|
||||
explicit ResultHolder(Result result) : result(result), resultData(nullptr) {}
|
||||
|
||||
ResultHolder(Result result, std::unique_ptr<Bundle> resultData) :
|
||||
result(result),
|
||||
resultData(std::move(resultData)) {}
|
||||
};
|
||||
|
||||
std::unique_ptr<ResultHolder> resultHolder;
|
||||
|
||||
public:
|
||||
|
||||
App() = default;
|
||||
virtual ~App() = default;
|
||||
|
||||
virtual void onCreate(AppContext& appContext) {}
|
||||
virtual void onDestroy(AppContext& appContext) {}
|
||||
virtual void onShow(AppContext& appContext, lv_obj_t* parent) {}
|
||||
virtual void onHide(AppContext& appContext) {}
|
||||
/** resultData could be null */
|
||||
virtual void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> resultData) {}
|
||||
|
||||
Mutex& getMutex() { return mutex; }
|
||||
|
||||
bool hasResult() const { return resultHolder != nullptr; }
|
||||
|
||||
void setResult(Result result, std::unique_ptr<Bundle> resultData = nullptr) {
|
||||
auto lock = getMutex().asScopedLock();
|
||||
lock.lock();
|
||||
resultHolder = std::make_unique<ResultHolder>(result, std::move(resultData));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by system to extract the result data when this application is finished.
|
||||
* Note that this removes the data from the class!
|
||||
*/
|
||||
bool moveResult(Result& outResult, std::unique_ptr<Bundle>& outBundle) {
|
||||
auto lock = getMutex().asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
if (resultHolder == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outResult = resultHolder->result;
|
||||
outBundle = std::move(resultHolder->resultData);
|
||||
resultHolder = nullptr;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
std::shared_ptr<App> create() { return std::shared_ptr<T>(new T); }
|
||||
|
||||
/**
|
||||
* @brief Start an app
|
||||
* @param[in] id application name or id
|
||||
* @param[in] parameters optional parameters to pass onto the application. can be nullptr.
|
||||
*/
|
||||
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> parameters = nullptr);
|
||||
|
||||
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
|
||||
void stop();
|
||||
|
||||
/** @brief Stop a specific app and any apps it might have launched on the stack.
|
||||
* @param[in] id the app id
|
||||
*/
|
||||
void stop(const std::string& id);
|
||||
|
||||
/** @brief Stop all app instances that match with this identifier and also stop the apps they started.
|
||||
* @warning onResult() will only be called for the resulting app that gets shown (if any)
|
||||
* @param[in] id the id of the app to stop
|
||||
*/
|
||||
void stopAll(const std::string& id);
|
||||
|
||||
/** @return true if the app is running somewhere in the app stack (doesn't have to be the top-most app) */
|
||||
bool isRunning(const std::string& id);
|
||||
|
||||
/** @return the currently running app context (it is only ever null before the splash screen is shown) */
|
||||
std::shared_ptr<AppContext> getCurrentAppContext();
|
||||
|
||||
/** @return the currently running app (it is only ever null before the splash screen is shown) */
|
||||
std::shared_ptr<App> getCurrentApp();
|
||||
|
||||
bool install(const std::string& path);
|
||||
|
||||
bool uninstall(const std::string& appId);
|
||||
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/Bundle.h>
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
// Forward declarations
|
||||
class App;
|
||||
class AppPaths;
|
||||
struct AppManifest;
|
||||
enum class Result;
|
||||
|
||||
typedef union {
|
||||
struct {
|
||||
bool hideStatusbar : 1;
|
||||
};
|
||||
unsigned char flags;
|
||||
} Flags;
|
||||
|
||||
/**
|
||||
* The public representation of an application instance.
|
||||
* @warning Do not store references or pointers to these! You can retrieve them via the service registry.
|
||||
*/
|
||||
class AppContext {
|
||||
|
||||
protected:
|
||||
|
||||
virtual ~AppContext() = default;
|
||||
|
||||
public:
|
||||
|
||||
virtual const AppManifest& getManifest() const = 0;
|
||||
virtual std::shared_ptr<const Bundle> getParameters() const = 0;
|
||||
virtual std::unique_ptr<AppPaths> getPaths() const = 0;
|
||||
|
||||
virtual std::shared_ptr<App> getApp() const = 0;
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/app/AppRegistration.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
class App;
|
||||
class AppContext;
|
||||
|
||||
/** Application types */
|
||||
enum class Category {
|
||||
/** Standard apps, provided by the system. */
|
||||
System,
|
||||
/** The apps that are launched/shown by the Settings app. The Settings app itself is of type AppTypeSystem. */
|
||||
Settings,
|
||||
/** User-provided apps. */
|
||||
User
|
||||
};
|
||||
|
||||
/** Result status code for application result callback. */
|
||||
enum class Result {
|
||||
Ok = 0U,
|
||||
Cancelled = 1U,
|
||||
Error = 2U
|
||||
};
|
||||
|
||||
class Location {
|
||||
|
||||
std::string path;
|
||||
Location() = default;
|
||||
explicit Location(const std::string& path) : path(path) {}
|
||||
|
||||
public:
|
||||
|
||||
static Location internal() { return {}; }
|
||||
|
||||
static Location external(const std::string& path) {
|
||||
return Location(path);
|
||||
}
|
||||
|
||||
/** Internal apps are all apps that are part of the firmware release. */
|
||||
bool isInternal() const { return path.empty(); }
|
||||
|
||||
/**
|
||||
* External apps are all apps that are not part of the firmware release.
|
||||
* e.g. an application on the sd card or one that is installed in /data
|
||||
*/
|
||||
bool isExternal() const { return !path.empty(); }
|
||||
const std::string& getPath() const { return path; }
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<App>(*CreateApp)();
|
||||
|
||||
struct AppManifest {
|
||||
|
||||
struct Flags {
|
||||
constexpr static uint32_t None = 0;
|
||||
/** Don't show the statusbar */
|
||||
constexpr static uint32_t HideStatusBar = 1 << 0;
|
||||
/** Hint to other systems to not show this app (e.g. in launcher or settings) */
|
||||
constexpr static uint32_t Hidden = 1 << 1;
|
||||
};
|
||||
|
||||
/** The SDK version that was used to compile this app. (e.g. "0.6.0") */
|
||||
std::string targetSdk = {};
|
||||
|
||||
/** Comma-separated list of platforms, e.g. "esp32,esp32s3" */
|
||||
std::string targetPlatforms = {};
|
||||
|
||||
/** The identifier by which the app is launched by the system and other apps. */
|
||||
std::string appId = {};
|
||||
|
||||
/** The user-readable name of the app. Used in UI. */
|
||||
std::string appName = {};
|
||||
|
||||
/** Optional icon. */
|
||||
std::string appIcon = {};
|
||||
|
||||
/** The version as it is displayed to the user (e.g. "1.2.0") */
|
||||
std::string appVersionName = {};
|
||||
|
||||
/** The technical version (must be incremented with new releases of the app */
|
||||
uint64_t appVersionCode = 0;
|
||||
|
||||
/** App category helps with listing apps in Launcher, app list or settings apps. */
|
||||
Category appCategory = Category::User;
|
||||
|
||||
/** Where the app is located */
|
||||
Location appLocation = Location::internal();
|
||||
|
||||
/** Controls various settings */
|
||||
uint16_t appFlags = Flags::None;
|
||||
|
||||
/** Create the instance of the app */
|
||||
CreateApp createApp = nullptr;
|
||||
};
|
||||
|
||||
struct {
|
||||
bool operator()(const std::shared_ptr<AppManifest>& left, const std::shared_ptr<AppManifest>& right) const { return left->appName < right->appName; }
|
||||
} SortAppManifestByName;
|
||||
|
||||
} // namespace
|
||||
@ -1,47 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
// Forward declarations
|
||||
class AppManifest;
|
||||
|
||||
class AppPaths {
|
||||
|
||||
const AppManifest& manifest;
|
||||
|
||||
public:
|
||||
|
||||
explicit AppPaths(const AppManifest& manifest) : manifest(manifest) {}
|
||||
|
||||
/**
|
||||
* The user data directory is intended to survive OS upgrades.
|
||||
* The path will not end with a "/".
|
||||
*/
|
||||
std::string getUserDataPath() const;
|
||||
|
||||
/**
|
||||
* The user data directory is intended to survive OS upgrades.
|
||||
* Configuration data should be stored here.
|
||||
* @param[in] childPath the path without a "/" prefix
|
||||
*/
|
||||
std::string getUserDataPath(const std::string& childPath) const;
|
||||
|
||||
/**
|
||||
* You should not store configuration data here.
|
||||
* The path will not end with a "/".
|
||||
* This is mainly used for core apps (system/boot/settings type).
|
||||
*/
|
||||
std::string getAssetsPath() const;
|
||||
|
||||
/**
|
||||
* You should not store configuration data here.
|
||||
* This is mainly used for core apps (system/boot/settings type).
|
||||
* @param[in] childPath the path without a "/" prefix
|
||||
*/
|
||||
std::string getAssetsPath(const std::string& childPath) const;
|
||||
};
|
||||
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "App.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
struct AppManifest;
|
||||
|
||||
/** Register an application with its manifest */
|
||||
void addAppManifest(const AppManifest& manifest);
|
||||
|
||||
/** Remove an app from the registry */
|
||||
bool removeAppManifest(const std::string& id);
|
||||
|
||||
/** Find an application manifest by its id
|
||||
* @param[in] id the manifest id
|
||||
* @return the application manifest if it was found
|
||||
*/
|
||||
std::shared_ptr<AppManifest> findAppManifestById(const std::string& id);
|
||||
|
||||
/** @return a list of all registered apps. This includes user and system apps. */
|
||||
std::vector<std::shared_ptr<AppManifest>> getAppManifests();
|
||||
|
||||
} // namespace
|
||||
@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppManifest.h"
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
typedef void* (*CreateData)();
|
||||
typedef void (*DestroyData)(void* data);
|
||||
/** data is nullable */
|
||||
typedef void (*OnCreate)(void* appContext, void* data);
|
||||
/** data is nullable */
|
||||
typedef void (*OnDestroy)(void* appContext, void* data);
|
||||
/** data is nullable */
|
||||
typedef void (*OnShow)(void* appContext, void* data, lv_obj_t* parent);
|
||||
/** data is nullable */
|
||||
typedef void (*OnHide)(void* appContext, void* data);
|
||||
/** data is nullable, resultData is nullable. */
|
||||
typedef void (*OnResult)(void* appContext, void* data, LaunchId launchId, Result result, Bundle* resultData);
|
||||
|
||||
/** All fields are nullable */
|
||||
void setElfAppParameters(
|
||||
CreateData createData,
|
||||
DestroyData destroyData,
|
||||
OnCreate onCreate,
|
||||
OnDestroy onDestroy,
|
||||
OnShow onShow,
|
||||
OnHide onHide,
|
||||
OnResult onResult
|
||||
);
|
||||
|
||||
std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest);
|
||||
|
||||
}
|
||||
#endif // ESP_PLATFORM
|
||||
@ -1,49 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/Bundle.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <Tactility/app/App.h>
|
||||
|
||||
/**
|
||||
* Start the app by its ID and provide:
|
||||
* - a title
|
||||
* - a text
|
||||
* - 0, 1 or more buttons
|
||||
* Show a dialog with a title, a message and 0, 1 or more buttons.
|
||||
*/
|
||||
namespace tt::app::alertdialog {
|
||||
|
||||
/**
|
||||
* Show a dialog with the provided title, message and 0, 1 or more buttons.
|
||||
* @param[in] title the title to show in the toolbar
|
||||
* @param[in] message the message to display
|
||||
* @param[in] buttonLabels the buttons to show
|
||||
* @return the launch id
|
||||
* Show a dialog with the provided title, message and buttons, as a modal child of
|
||||
* @a callerAppInstanceId (a new-model app - see app/manager.h). The caller receives the
|
||||
* result as an APP_EVENT_RESULT in its own event loop: result is the pressed button's index
|
||||
* (>= 0), or a value not matching any button (currently always 1) if the dialog was dismissed
|
||||
* without a button press. No result_bundle. The caller is responsible for calling
|
||||
* app_manager_stop() on the returned instance id once it has handled the result.
|
||||
* @return the new dialog's app instance id
|
||||
*/
|
||||
LaunchId start(const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels);
|
||||
/**
|
||||
* Show a dialog with the provided title, message and 0, 1 or more buttons.
|
||||
* @param[in] title the title to show in the toolbar
|
||||
* @param[in] message the message to display
|
||||
* @param[in] buttonLabels the buttons to show
|
||||
* @return the launch id
|
||||
*/
|
||||
LaunchId start(const std::string& title, const std::string& message, const std::vector<const char*>& buttonLabels);
|
||||
uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels);
|
||||
|
||||
/**
|
||||
* Show a dialog with the provided title, message and an OK button
|
||||
* @param[in] title the title to show in the toolbar
|
||||
* @param[in] message the message to display
|
||||
* @return the launch id
|
||||
*/
|
||||
LaunchId start(const std::string& title, const std::string& message);
|
||||
|
||||
/**
|
||||
* Get the index of the button that the user selected.
|
||||
*
|
||||
* @return a value greater than 0 when a selection was done, or -1 when the app was closed clicking one of the selection buttons.
|
||||
*/
|
||||
int32_t getResultIndex(const Bundle& bundle);
|
||||
/** @copydoc start(uint32_t, const std::string&, const std::string&, const std::vector<std::string>&)
|
||||
* Shows a single "OK" button. */
|
||||
uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message);
|
||||
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/app/App.h>
|
||||
#include <cstdint>
|
||||
|
||||
namespace tt::app::btmanage {
|
||||
|
||||
LaunchId start();
|
||||
uint32_t start();
|
||||
|
||||
} // namespace tt::app::btmanage
|
||||
|
||||
@ -1,28 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/app/App.h>
|
||||
#include <Tactility/Bundle.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace tt::app::fileselection {
|
||||
|
||||
/**
|
||||
* Show a file selection dialog that allows the user to select an existing file.
|
||||
* This app returns the absolute file path as a result.
|
||||
* Show a file selection dialog that allows the user to select an existing file, as a modal
|
||||
* child of @a callerAppInstanceId (see app_manager_start_for_result()). Result (0 = Ok,
|
||||
* 1 = Cancelled) is delivered back via APP_EVENT_RESULT once this app's thread exits - call
|
||||
* getLastPath() right after receiving it, on result == 0. The caller must call
|
||||
* app_manager_stop() on the returned instance id once that event arrives, to fully reap this
|
||||
* instance.
|
||||
* @return the new app instance id
|
||||
*/
|
||||
LaunchId startForExistingFile();
|
||||
uint32_t startForExistingFile(uint32_t callerAppInstanceId);
|
||||
|
||||
/**
|
||||
* Show a file selection dialog that allows the user to select a new or existing file.
|
||||
* This app returns the absolute file path as a result.
|
||||
* Same as startForExistingFile(), but also allows picking a path that doesn't exist yet (for
|
||||
* "save as"-style flows).
|
||||
*/
|
||||
LaunchId startForExistingOrNewFile();
|
||||
uint32_t startForExistingOrNewFile(uint32_t callerAppInstanceId);
|
||||
|
||||
/**
|
||||
* @param bundle the result bundle of an app
|
||||
* @return the path from the bundle, or empty string if none is present
|
||||
*/
|
||||
std::string getResultPath(const Bundle& bundle);
|
||||
/** @return the path picked by the last FileSelection dialog that closed with result == Ok. Only
|
||||
* one dialog is expected to be open at a time. */
|
||||
std::string getLastPath();
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/app/App.h>
|
||||
#include <string>
|
||||
|
||||
namespace tt::app::imageviewer {
|
||||
|
||||
LaunchId start(const std::string& file);
|
||||
/**
|
||||
* Show a full-screen viewer for a single image file. Fire-and-forget: doesn't report any result
|
||||
* back to the caller.
|
||||
* @param file the path to the image file to display
|
||||
*/
|
||||
void start(const std::string& file);
|
||||
|
||||
}
|
||||
@ -1,21 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/app/App.h>
|
||||
#include <Tactility/Bundle.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Start the app by its ID and provide:
|
||||
* - a title
|
||||
* - a text
|
||||
* Show a dialog with a title, a message and a text field.
|
||||
*/
|
||||
namespace tt::app::inputdialog {
|
||||
|
||||
LaunchId start(const std::string& title, const std::string& message, const std::string& prefilled = "");
|
||||
/**
|
||||
* Show a dialog with the provided title, message and prefilled text, as a modal child of
|
||||
* @a callerAppInstanceId (a new-model app - see app/manager.h). The caller receives the result
|
||||
* as an APP_EVENT_RESULT in its own event loop: 0 = OK (call getLastText() for the entered
|
||||
* text), 1 = Cancelled or dismissed without a press. The caller is responsible for calling
|
||||
* app_manager_stop() on the returned instance id once it has handled the result.
|
||||
* @return the new dialog's app instance id
|
||||
*/
|
||||
uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::string& prefilled = "");
|
||||
|
||||
/**
|
||||
* @return the text that was in the field when OK was pressed, or otherwise empty string
|
||||
* @return the text entered the last time any InputDialog instance was closed with OK. Only one
|
||||
* dialog is expected to be open at a time - call this right after receiving its
|
||||
* APP_EVENT_RESULT with result == 0.
|
||||
*/
|
||||
std::string getResult(const Bundle& bundle);
|
||||
std::string getLastText();
|
||||
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/app/App.h>
|
||||
#include <string>
|
||||
|
||||
namespace tt::app::notes {
|
||||
|
||||
/**
|
||||
* Start the notes app with the specified text file.
|
||||
* @param[in] filePath the path to the text file to open
|
||||
* @return the launch id
|
||||
*/
|
||||
LaunchId start(const std::string& filePath);
|
||||
void start(const std::string& filePath);
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user