Compare commits

...

10 Commits

Author SHA1 Message Date
Ken Van Hoeylandt
3dfc27e93e
Fixes for colours and margins in GPIO app and more (#284)
Some checks failed
Build Firmware / cyd-2432s024c (push) Has been cancelled
Build Firmware / cyd-2432s032c (push) Has been cancelled
Build Firmware / cyd-jc2432w328c (push) Has been cancelled
Build Firmware / cyd-8048s043c (push) Has been cancelled
Build Firmware / cyd-jc8048w550c (push) Has been cancelled
Build Firmware / cyd-4848s040c (push) Has been cancelled
Build Firmware / elecrow-crowpanel-advance-28 (push) Has been cancelled
Build Firmware / elecrow-crowpanel-advance-35 (push) Has been cancelled
Build Firmware / elecrow-crowpanel-advance-50 (push) Has been cancelled
Build Firmware / elecrow-crowpanel-basic-28 (push) Has been cancelled
Build Firmware / elecrow-crowpanel-basic-35 (push) Has been cancelled
Build Firmware / elecrow-crowpanel-basic-50 (push) Has been cancelled
Build Firmware / lilygo-tdeck (push) Has been cancelled
Build Firmware / m5stack-core2 (push) Has been cancelled
Build Firmware / m5stack-cores3 (push) Has been cancelled
Build Firmware / unphone (push) Has been cancelled
Build Firmware / waveshare-s3-touch-43 (push) Has been cancelled
Build SDK / esp32 (push) Has been cancelled
Build SDK / esp32s3 (push) Has been cancelled
Build Simulator / Build-Simulator-Linux (push) Has been cancelled
Build Simulator / Build-Simulator-macOS (push) Has been cancelled
Tests / Run (push) Has been cancelled
- Fixes for colours and margins in GPIO app
- Removed unused imports
2025-06-01 17:52:09 +02:00
Ken Van Hoeylandt
e4ecec64c9
Reduced desktop icon sizes (#283)
This improves loading performance (and uses less memory on ESP32 devices
without PSRAM)
2025-06-01 17:11:29 +02:00
Shadowtrance
ce96474d84
Cleanup of Notes.cpp (#282) 2025-05-27 08:08:04 +02:00
Ken Van Hoeylandt
2691dbb014
Implemented LaunchId and FileSelection, updated Notes (#281)
- Implemented `LaunchId` to keep track of the apps that are started
- Implemented `FileSelection` app to select existing and/or new files.
- Moved some re-usable file functionality to `tt::file::`
- Renamed `Files` app to `FileBrowser`
- Updated `Notes` app to use new `FileSelection` functionality, and cleaned it up a bit.
- General code cleanliness improvements
2025-05-25 22:11:50 +02:00
Ken Van Hoeylandt
74eb830870
Buildscript and kernel improvements (#280)
- Implemented `constexpr long int getMicros()` in `Kernel.h`
- Changed `secondsToTicks()`, `minutesToTicks()` and `bool isIsr()` to `constexpr`
- Added more relevant build info
2025-05-24 17:27:37 +02:00
Shadowtrance
870924229a
Switch to dark theme (#278) 2025-05-06 19:52:55 +02:00
Ken Van Hoeylandt
b2647f46bb
Cleanup (#277) 2025-04-11 21:56:16 +02:00
Shadowtrance
d1c1a3a369
Add Notes app (#276) 2025-04-11 21:35:29 +02:00
Ken Van Hoeylandt
a5090ec194
Refactor display app (#275)
- Moved more code into class
- Refactored some logic
- UI is more dynamic: hide elements that are not supported by the hardware (gamma and/or brightness slider)
2025-04-08 22:38:03 +02:00
Ken Van Hoeylandt
f67cb241b7
SDK fixes (#272)
- Fix for `ExternalApps/HelloWorld/build.sh`
- Fix portability issue with regards to the SDK release script
2025-04-03 23:38:47 +02:00
122 changed files with 1511 additions and 470 deletions

View File

@ -1,4 +1,5 @@
#pragma once
#include <Tactility/hal/Configuration.h>
#ifdef ESP_PLATFORM
#include <sdkconfig.h>

View File

@ -1,7 +1,6 @@
#pragma once
#include "Main.h"
#include <Tactility/hal/Configuration.h>
namespace simulator {
/** Set the function pointer of the real app_main() */

View File

@ -1,3 +1,12 @@
if (NOT WIN32)
string(ASCII 27 Esc)
set(ColorReset "${Esc}[m")
set(Cyan "${Esc}[36m")
else ()
set(ColorReset "")
set(Cyan "")
endif ()
function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE)
get_filename_component(SDKCONFIG_FILE_ABS ${SDKCONFIG_FILE} ABSOLUTE)
# Find the board identifier in the sdkconfig file
@ -10,7 +19,7 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE)
set(id_length 0)
math(EXPR id_length "${sdkconfig_board_id_length} - 21")
string(SUBSTRING ${sdkconfig_board_id} 20 ${id_length} board_id)
message("Building board ${board_id}")
message("Building board: ${Cyan}${board_id}${ColorReset}")
if (board_id STREQUAL "cyd-2432s024c")
set(TACTILITY_BOARD_PROJECT CYD-2432S024C)
@ -53,7 +62,7 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE)
if (TACTILITY_BOARD_PROJECT STREQUAL "")
message(FATAL_ERROR "No subproject mapped to \"${TACTILITY_BOARD_ID}\" in root Buildscripts/board.cmake")
else ()
message("Board project: Boards/${TACTILITY_BOARD_PROJECT}")
message("Board project: ${Cyan}Boards/${TACTILITY_BOARD_PROJECT}${ColorReset}\n")
set_property(GLOBAL PROPERTY TACTILITY_BOARD_PROJECT ${TACTILITY_BOARD_PROJECT})
set_property(GLOBAL PROPERTY TACTILITY_BOARD_ID ${board_id})
endif ()

14
Buildscripts/colors.cmake Normal file
View File

@ -0,0 +1,14 @@
if (NOT WIN32)
string(ASCII 27 Esc)
set(ColorReset "${Esc}[m")
set(Cyan "${Esc}[36m")
set(Grey "${Esc}[37m")
set(LightPurple "${Esc}[1;35m")
set(White "${Esc}[1;37m")
else ()
set(ColorReset "")
set(Cyan "")
set(Grey "")
set(LightPurple "")
set(White "")
endif ()

View File

@ -4,24 +4,24 @@ file(READ ${VERSION_TEXT_FILE} TACTILITY_VERSION)
if (DEFINED ENV{ESP_IDF_VERSION})
set(TACTILITY_TARGET " @ ESP-IDF")
else()
else ()
set(TACTILITY_TARGET " @ Simulator")
endif()
endif ()
if(NOT WIN32)
if (NOT WIN32)
string(ASCII 27 Esc)
set(ColourReset "${Esc}[m")
set(Cyan "${Esc}[36m")
set(Grey "${Esc}[37m")
set(ColorReset "${Esc}[m")
set(Cyan "${Esc}[36m")
set(Grey "${Esc}[37m")
set(LightPurple "${Esc}[1;35m")
set(White "${Esc}[1;37m")
else()
set(ColourReset "")
set(Cyan "")
set(Grey "")
set(White "${Esc}[1;37m")
else ()
set(ColorReset "")
set(Cyan "")
set(Grey "")
set(LightPurple "")
set(White "")
endif()
set(White "")
endif ()
# Some terminals (e.g. GitHub Actions) reset colour for every in a multiline message(),
# so we add the colour to each line instead of assuming it would automatically be re-used.
@ -41,4 +41,4 @@ message("\n\n\
${Cyan}@@@\n\
${Cyan}@@@\n\
${Cyan}@@@\n\
${Cyan}@@\n\n${ColourReset}")
${Cyan}@@\n\n${ColorReset}")

View File

@ -46,4 +46,4 @@ cp Libraries/elf_loader/license.txt $elf_loader_library_path/
cp Buildscripts/CMake/TactilitySDK.cmake $target_path/
cp Buildscripts/CMake/CMakeLists.txt $target_path/
echo -n $ESP_IDF_VERSION >> $target_path/idf-version.txt
printf '%s' "$ESP_IDF_VERSION" >> $target_path/idf-version.txt

View File

@ -8,14 +8,24 @@ set(CMAKE_ASM_COMPILE_OBJECT "${CMAKE_CXX_COMPILER_TARGET}")
include("Buildscripts/logo.cmake")
if (NOT WIN32)
string(ASCII 27 Esc)
set(ColorReset "${Esc}[m")
set(Cyan "${Esc}[36m")
else ()
set(ColorReset "")
set(Cyan "")
endif ()
file(READ version.txt TACTILITY_VERSION)
add_compile_definitions(TT_VERSION="${TACTILITY_VERSION}")
if (DEFINED ENV{ESP_IDF_VERSION})
message("Building with ESP-IDF v$ENV{ESP_IDF_VERSION}")
message("Building with ESP-IDF ${Cyan}v$ENV{ESP_IDF_VERSION}${ColorReset}")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
include("Buildscripts/board.cmake")
init_tactility_globals("sdkconfig")
get_property(TACTILITY_BOARD_PROJECT GLOBAL PROPERTY TACTILITY_BOARD_PROJECT)
@ -45,11 +55,11 @@ if (DEFINED ENV{ESP_IDF_VERSION})
add_compile_definitions(LV_CONF_PATH="${LVGL_CONFIG_FULL_PATH}/lv_conf_kconfig.h")
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_panic_handler" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_log_write" APPEND)
else()
else ()
message("Building for sim target")
add_compile_definitions(CONFIG_TT_BOARD_ID="simulator")
add_compile_definitions(CONFIG_TT_BOARD_NAME="Simulator")
endif()
endif ()
project(Tactility)
@ -58,7 +68,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Tactility)
add_subdirectory(TactilityCore)
add_subdirectory(Boards/Simulator)
endif()
endif ()
if (NOT DEFINED ENV{ESP_IDF_VERSION})
# FreeRTOS
@ -94,4 +104,4 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
target_compile_definitions(lvgl PUBLIC "-DLV_CONF_PATH=\"${PROJECT_SOURCE_DIR}/Libraries/lvgl_conf/lv_conf_simulator.h\"")
target_link_libraries(lvgl PRIVATE SDL2-static)
endif()
endif ()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 724 B

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

View File

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 333 B

View File

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 240 B

View File

Before

Width:  |  Height:  |  Size: 286 B

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

View File

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

View File

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 299 B

View File

Before

Width:  |  Height:  |  Size: 535 B

After

Width:  |  Height:  |  Size: 535 B

View File

Before

Width:  |  Height:  |  Size: 436 B

After

Width:  |  Height:  |  Size: 436 B

View File

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 392 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M240-160q-33 0-56.5-23.5T160-240q0-33 23.5-56.5T240-320q33 0 56.5 23.5T320-240q0 33-23.5 56.5T240-160Zm240 0q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm240 0q-33 0-56.5-23.5T640-240q0-33 23.5-56.5T720-320q33 0 56.5 23.5T800-240q0 33-23.5 56.5T720-160ZM240-400q-33 0-56.5-23.5T160-480q0-33 23.5-56.5T240-560q33 0 56.5 23.5T320-480q0 33-23.5 56.5T240-400Zm240 0q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm240 0q-33 0-56.5-23.5T640-480q0-33 23.5-56.5T720-560q33 0 56.5 23.5T800-480q0 33-23.5 56.5T720-400ZM240-640q-33 0-56.5-23.5T160-720q0-33 23.5-56.5T240-800q33 0 56.5 23.5T320-720q0 33-23.5 56.5T240-640Zm240 0q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Zm240 0q-33 0-56.5-23.5T640-720q0-33 23.5-56.5T720-800q33 0 56.5 23.5T800-720q0 33-23.5 56.5T720-640Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h240l80 80h320q33 0 56.5 23.5T880-640v400q0 33-23.5 56.5T800-160H160Zm0-80h640v-400H447l-80-80H160v480Zm0 0v-480 480Z"/></svg>

Before

Width:  |  Height:  |  Size: 301 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>

Before

Width:  |  Height:  |  Size: 771 B

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="apps.svg"
inkscape:export-filename="apps.png"
inkscape:export-xdpi="192"
inkscape:export-ydpi="192"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="17.088414"
inkscape:cx="14.629795"
inkscape:cy="18.901696"
inkscape:window-width="2560"
inkscape:window-height="1371"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 122.37644,-3.1685927 q -49.173234,0 -84.190541,-35.0173063 -35.0173063,-35.017307 -35.0173063,-84.190541 0,-49.17324 35.0173063,-84.19055 35.017307,-35.01731 84.190541,-35.01731 49.17324,0 84.19055,35.01731 35.01731,35.01731 35.01731,84.19055 0,49.173234 -35.01731,84.190541 -35.01731,35.0173063 -84.19055,35.0173063 z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173063 -35.0173,-35.017307 -35.0173,-84.190541 0,-49.17324 35.0173,-84.19055 35.01731,-35.01731 84.19055,-35.01731 49.17324,0 84.19055,35.01731 35.0173,35.01731 35.0173,84.19055 0,49.173234 -35.0173,84.190541 Q 529.17324,-3.1685927 480,-3.1685927 Z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173063 -35.01731,-35.017307 -35.01731,-84.190541 0,-49.17324 35.01731,-84.19055 35.01731,-35.01731 84.19055,-35.01731 49.17323,0 84.19054,35.01731 35.01731,35.01731 35.01731,84.19055 0,49.173234 -35.01731,84.190541 -35.01731,35.0173063 -84.19054,35.0173063 z M 122.37644,-360.79215 q -49.173234,0 -84.190541,-35.0173 Q 3.1685927,-430.82676 3.1685927,-480 q 0,-49.17324 35.0173063,-84.19055 35.017307,-35.0173 84.190541,-35.0173 49.17324,0 84.19055,35.0173 35.01731,35.01731 35.01731,84.19055 0,49.17324 -35.01731,84.19055 -35.01731,35.0173 -84.19055,35.0173 z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173 -35.0173,-35.01731 -35.0173,-84.19055 0,-49.17324 35.0173,-84.19055 35.01731,-35.0173 84.19055,-35.0173 49.17324,0 84.19055,35.0173 35.0173,35.01731 35.0173,84.19055 0,49.17324 -35.0173,84.19055 -35.01731,35.0173 -84.19055,35.0173 z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173 Q 718.4157,-430.82676 718.4157,-480 q 0,-49.17324 35.01731,-84.19055 35.01731,-35.0173 84.19055,-35.0173 49.17323,0 84.19054,35.0173 35.01731,35.01731 35.01731,84.19055 0,49.17324 -35.01731,84.19055 -35.01731,35.0173 -84.19054,35.0173 z M 122.37644,-718.4157 q -49.173234,0 -84.190541,-35.01731 -35.0173063,-35.01731 -35.0173063,-84.19055 0,-49.17323 35.0173063,-84.19054 35.017307,-35.01731 84.190541,-35.01731 49.17324,0 84.19055,35.01731 35.01731,35.01731 35.01731,84.19054 0,49.17324 -35.01731,84.19055 -35.01731,35.01731 -84.19055,35.01731 z m 357.62356,0 q -49.17324,0 -84.19055,-35.01731 -35.0173,-35.01731 -35.0173,-84.19055 0,-49.17323 35.0173,-84.19054 35.01731,-35.01731 84.19055,-35.01731 49.17324,0 84.19055,35.01731 35.0173,35.01731 35.0173,84.19054 0,49.17324 -35.0173,84.19055 Q 529.17324,-718.4157 480,-718.4157 Z m 357.62356,0 q -49.17324,0 -84.19055,-35.01731 -35.01731,-35.01731 -35.01731,-84.19055 0,-49.17323 35.01731,-84.19054 35.01731,-35.01731 84.19055,-35.01731 49.17323,0 84.19054,35.01731 35.01731,35.01731 35.01731,84.19054 0,49.17324 -35.01731,84.19055 -35.01731,35.01731 -84.19054,35.01731 z"
id="path1"
style="stroke-width:1.4901" />
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="folder.svg"
inkscape:export-filename="folder.png"
inkscape:export-xdpi="192"
inkscape:export-ydpi="192"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="48.333333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="2560"
inkscape:window-height="1371"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 96.441379,-96.441379 q -39.554482,0 -67.722069,-28.167591 -28.16758586,-28.16758 -28.16758586,-67.72206 v -575.33794 q 0,-39.55448 28.16758586,-67.72206 28.167587,-28.16759 67.722069,-28.16759 H 384.11034 L 480,-767.66897 h 383.55862 q 39.55448,0 67.72207,28.16759 28.16759,28.16759 28.16759,67.72207 v 479.44828 q 0,39.55448 -28.16759,67.72206 -28.16759,28.167591 -67.72207,28.167591 z m 0,-95.889651 H 863.55862 v -479.44828 h -423.1131 l -95.88966,-95.88966 H 96.441379 Z m 0,0 v -575.33794 z"
id="path1"
style="stroke-width:1.19862" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="settings.svg"
inkscape:export-filename="settings.png"
inkscape:export-xdpi="192"
inkscape:export-ydpi="192"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="48.333333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="1898"
inkscape:window-height="1269"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<path
d="M 347.80751,0.69994853 328.57952,-153.12403 q -15.62275,-6.00875 -29.44288,-14.421 -13.82012,-8.41225 -27.03937,-18.02625 L 129.08904,-125.48379 -3.1034483,-353.81626 120.67679,-447.55275 q -1.20175,-8.41225 -1.20175,-16.22363 v -32.44724 q 0,-7.81138 1.20175,-16.22363 L -3.1034483,-606.18374 129.08904,-834.51621 l 143.00823,60.08749 q 13.21925,-9.614 27.64025,-18.02625 14.421,-8.41225 28.842,-14.421 l 19.22799,-153.82398 h 264.38498 l 19.22799,153.82398 q 15.62275,6.00875 29.44288,14.421 13.82012,8.41225 27.03937,18.02625 l 143.00823,-60.08749 132.19249,228.33247 -123.78024,93.73649 q 1.20175,8.41225 1.20175,16.22363 v 32.44724 q 0,7.81138 -2.4035,16.22363 l 123.78024,93.73649 -132.19249,228.33247 -141.80648,-60.08749 q -13.21925,9.614 -27.64025,18.02625 -14.421,8.41225 -28.842,14.421 L 612.19249,0.69994853 Z m 84.1225,-96.13998953 h 94.93823 l 16.8245,-127.385489 q 37.25425,-9.614 69.10062,-28.24112 31.84637,-18.62712 58.28487,-45.06562 l 118.97324,49.27175 46.86824,-81.719 -103.35049,-78.11374 q 6.00875,-16.8245 8.41225,-35.45162 2.4035,-18.62712 2.4035,-37.85512 0,-19.228 -2.4035,-37.85512 -2.4035,-18.62712 -8.41225,-35.45162 l 103.35049,-78.11374 -46.86824,-81.719 -118.97324,50.4735 q -26.4385,-27.64025 -58.28487,-46.26737 -31.84637,-18.62712 -69.10062,-28.24112 l -15.62275,-127.38549 h -94.93823 l -16.8245,127.38549 q -37.25425,9.614 -69.10062,28.24112 -31.84637,18.62712 -58.28487,45.06562 l -118.97324,-49.27175 -46.86824,81.719 103.35049,76.91199 q -6.00875,18.02625 -8.41225,36.05249 -2.4035,18.02625 -2.4035,38.456 0,19.228 2.4035,37.25425 2.4035,18.02624 8.41225,36.05249 l -103.35049,78.11374 46.86824,81.719 118.97324,-50.4735 q 26.4385,27.64025 58.28487,46.26737 31.84637,18.62712 69.10062,28.24112 z M 482.4035,-311.75502 q 69.70149,0 118.97324,-49.27174 49.27174,-49.27175 49.27174,-118.97324 0,-69.70149 -49.27174,-118.97324 -49.27175,-49.27174 -118.97324,-49.27174 -70.90324,0 -119.57411,49.27174 -48.67087,49.27175 -48.67087,118.97324 0,69.70149 48.67087,118.97324 48.67087,49.27174 119.57411,49.27174 z M 480,-480 Z"
id="path1"
style="stroke-width:1.20175" />
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License: Apache. Made by Iconscout: https://github.com/Iconscout/unicons -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="M12.71,17.29a1,1,0,0,0-.16-.12.56.56,0,0,0-.17-.09.6.6,0,0,0-.19-.06.93.93,0,0,0-.57.06.9.9,0,0,0-.54.54A.84.84,0,0,0,11,18a1,1,0,0,0,.07.38,1.46,1.46,0,0,0,.22.33A1,1,0,0,0,12,19a.84.84,0,0,0,.38-.08,1.15,1.15,0,0,0,.33-.21A1,1,0,0,0,13,18a1,1,0,0,0-.08-.38A1,1,0,0,0,12.71,17.29ZM8.55,13.17a.56.56,0,0,0-.17-.09A.6.6,0,0,0,8.19,13a.86.86,0,0,0-.39,0l-.18.06-.18.09-.15.12A1.05,1.05,0,0,0,7,14a1,1,0,0,0,.29.71,1.15,1.15,0,0,0,.33.21A1,1,0,0,0,9,14a1.05,1.05,0,0,0-.29-.71Zm.16,4.12a1,1,0,0,0-.33-.21A1,1,0,0,0,7.8,17l-.18.06a.76.76,0,0,0-.18.09,1.58,1.58,0,0,0-.15.12,1,1,0,0,0-.21.33.94.94,0,0,0,0,.76,1.15,1.15,0,0,0,.21.33A1,1,0,0,0,8,19a.84.84,0,0,0,.38-.08,1.15,1.15,0,0,0,.33-.21,1.15,1.15,0,0,0,.21-.33.94.94,0,0,0,0-.76A1,1,0,0,0,8.71,17.29Zm2.91-4.21a1,1,0,0,0-.33.21A1.05,1.05,0,0,0,11,14a1,1,0,0,0,1.38.92,1.15,1.15,0,0,0,.33-.21A1,1,0,0,0,13,14a1.05,1.05,0,0,0-.29-.71A1,1,0,0,0,11.62,13.08Zm5.09,4.21a1.15,1.15,0,0,0-.33-.21,1,1,0,0,0-1.09.21,1,1,0,0,0-.21.33.94.94,0,0,0,0,.76,1.15,1.15,0,0,0,.21.33A1,1,0,0,0,16,19a.84.84,0,0,0,.38-.08,1.15,1.15,0,0,0,.33-.21,1,1,0,0,0,.21-1.09A1,1,0,0,0,16.71,17.29ZM16,5H8A1,1,0,0,0,7,6v4a1,1,0,0,0,1,1h8a1,1,0,0,0,1-1V6A1,1,0,0,0,16,5ZM15,9H9V7h6Zm3-8H6A3,3,0,0,0,3,4V20a3,3,0,0,0,3,3H18a3,3,0,0,0,3-3V4A3,3,0,0,0,18,1Zm1,19a1,1,0,0,1-1,1H6a1,1,0,0,1-1-1V4A1,1,0,0,1,6,3H18a1,1,0,0,1,1,1Zm-2.45-6.83a.56.56,0,0,0-.17-.09.6.6,0,0,0-.19-.06.86.86,0,0,0-.39,0l-.18.06-.18.09-.15.12A1.05,1.05,0,0,0,15,14a1,1,0,0,0,1.38.92,1.15,1.15,0,0,0,.33-.21A1,1,0,0,0,17,14a1.05,1.05,0,0,0-.29-.71Z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License: PD. Made by Mary Akveo: https://maryakveo.com/ -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" id="chat" data-name="Line Color" xmlns="http://www.w3.org/2000/svg" class="icon line-color"><path id="primary" d="M18.81,16.23,20,21l-4.95-2.48A9.84,9.84,0,0,1,12,19c-5,0-9-3.58-9-8s4-8,9-8,9,3.58,9,8A7.49,7.49,0,0,1,18.81,16.23Z" style="fill: none; stroke: #000000; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"></path></svg>

After

Width:  |  Height:  |  Size: 518 B

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License: PD. Made by Mary Akveo: https://maryakveo.com/ -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" id="chip" data-name="Flat Line" xmlns="http://www.w3.org/2000/svg" class="icon flat-line"><rect id="secondary" x="6" y="6" width="12" height="12" rx="1" style="fill: none; stroke-width: 2;"></rect><path id="primary" d="M12,6V3m4,3V4M8,6V4m10,8h3m-3,4h2M18,8h2M12,18v3M8,18v2m8-2v2M6,12H3M6,8H4m2,8H4m14,1V7a1,1,0,0,0-1-1H7A1,1,0,0,0,6,7V17a1,1,0,0,0,1,1H17A1,1,0,0,0,18,17Z" style="fill: none; stroke: #333333; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"></path></svg>

After

Width:  |  Height:  |  Size: 662 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License: MIT. Made by vmware: https://github.com/vmware/clarity-assets -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 36 36" version="1.1" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>note-line</title>
<path d="M28,30H6V8H19.22l2-2H6A2,2,0,0,0,4,8V30a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V15l-2,2Z" class="clr-i-outline clr-i-outline-path-1"></path><path d="M33.53,5.84,30.16,2.47a1.61,1.61,0,0,0-2.28,0L14.17,16.26l-1.11,4.81A1.61,1.61,0,0,0,14.63,23,1.69,1.69,0,0,0,15,23l4.85-1.07L33.53,8.12A1.61,1.61,0,0,0,33.53,5.84ZM18.81,20.08l-3.66.81L16,17.26,26.32,6.87l2.82,2.82ZM30.27,8.56,27.45,5.74,29,4.16,31.84,7Z" class="clr-i-outline clr-i-outline-path-2"></path>
<rect x="0" y="0" width="36" height="36" fill-opacity="0"/>
</svg>

After

Width:  |  Height:  |  Size: 878 B

View File

@ -2,24 +2,16 @@
- Add a Keyboard setting app to override the behaviour of soft keyboard hiding (e.g. keyboard hardware is present, but user wants soft keyboard)
- HAL for display touch calibration
- Display app: hide controls for unsupported features, instead of disabling them
- Split up boot stages, so the last stage can be done from the splash screen
- Start using non_null (either via MS GSL, or custom)
- `hal/Configuration.h` defines C function types: Use C++ std::function instead
- Fix system time to not be 1980 (use build year as minimum)
- Use std::span or string_view in StringUtils https://youtu.be/FRkJCvHWdwQ?t=2754
- Fix bug in T-Deck/etc: esp_lvgl_port settings has a large stack size (~9kB) to fix an issue where the T-Deck would get a stackoverflow. This sometimes happens when WiFi is auto-enabled and you open the app while it is still connecting.
- Clean up static_cast when casting to base class.
- Mutex: Implement give/take from ISR support (works only for non-recursive ones)
- Extend unPhone power driver: add charging status, usb connection status, etc.
- Expose app::Paths to TactilityC
- Experiment with what happens when using C++ code in an external app (without using standard library!)
- Boards' CMakeLists.txt manually declare each source folder. Update them all to do a recursive search of all folders.
- We currently build all boards for a given platform (e.g. ESP32S3), but it's better to filter all irrelevant ones based on the Kconfig board settings:
Projects will load and compile faster as it won't compile all the dependencies of all these other boards
- Make a ledger for setting CPU affinity of various services and tasks
- Boot hooks instead of a single boot method in config. Define different boot phases/levels in enum.
- Add toggle to Display app for sysmon overlay: https://docs.lvgl.io/master/API/others/sysmon/index.html
- CrashHandler: use "corrupted" flag
- CrashHandler: process other types of crashes (WDT?)
- Call tt::lvgl::isSyncSet after HAL init and show error (and crash?) when it is not set.
@ -53,7 +45,6 @@
- Capacity based on voltage: estimation for various devices uses linear voltage curve, but it should use some sort of battery discharge curve.
- Statusbar widget to show how much memory is in use?
- Wrapper for Slider that shows "+" and "-" buttons, and also the value in a label.
- Display app: Add toggle to display performance measurement overlay (consider showing FPS in statusbar!)
- Files app: copy/paste actions
- On crash, try to save current log to flash or SD card? (this is risky, though, so ask in Discord first)
- Support more than 1 hardware keyboard (see lvgl::hardware_keyboard_set_indev()). LVGL init currently calls keyboard init, but that part should probably be done from the KeyboardDevice base class.
@ -66,8 +57,6 @@
https://github.com/portapack-mayhem/mayhem-firmware/releases
- Weather app: https://lab.flipper.net/apps/flip_weather
- wget app: https://lab.flipper.net/apps/web_crawler (add profiles for known public APIs?)
- USB implementation to make device act as mass storage device.
- System logger
- BlueTooth keyboard app
- Chip 8 emulator
- BadUSB (in December 2024, TinyUSB has a bug where uninstalling and re-installing the driver fails)
@ -76,7 +65,5 @@
- GPS app
- Investigate CSI https://stevenmhernandez.github.io/ESP32-CSI-Tool/
- Compile unix tools to ELF apps?
- Calculator
- Text editor
- Todo list
- Calendar

View File

@ -1,4 +1,6 @@
rm sdkconfig
cp ../../sdkconfig sdkconfig
cat sdkconfig.override >> sdkconfig
idf.py elf_app
# First we must run "build" because otherwise "idf.py elf" is not a valid command
idf.py build
idf.py elf

View File

@ -558,7 +558,7 @@
#if LV_USE_THEME_DEFAULT
/*0: Light mode; 1: Dark mode*/
#define LV_THEME_DEFAULT_DARK 0
#define LV_THEME_DEFAULT_DARK 1
/*1: Enable grow on press*/
#define LV_THEME_DEFAULT_GROW 1

View File

@ -7,11 +7,15 @@
#define TT_ASSETS_UI_SPINNER TT_ASSET("spinner.png")
// App icons
#define TT_ASSETS_APP_ICON_FALLBACK TT_ASSET("app_icon_fallback.png")
#define TT_ASSETS_APP_ICON_FILES TT_ASSET("app_icon_files.png")
#define TT_ASSETS_APP_ICON_DISPLAY_SETTINGS TT_ASSET("app_icon_display_settings.png")
#define TT_ASSETS_APP_ICON_POWER_SETTINGS TT_ASSET("app_icon_power_settings.png")
#define TT_ASSETS_APP_ICON_I2C_SETTINGS TT_ASSET("app_icon_i2c.png")
#define TT_ASSETS_APP_ICON_SETTINGS TT_ASSET("app_icon_settings.png")
#define TT_ASSETS_APP_ICON_SYSTEM_INFO TT_ASSET("app_icon_system_info.png")
#define TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS TT_ASSET("app_icon_time_date_settings.png")
#define TT_ASSETS_APP_ICON_FALLBACK TT_ASSET("app_icon_fallback_dark_mode.png")
#define TT_ASSETS_APP_ICON_FILES TT_ASSET("app_icon_files_dark_mode.png")
#define TT_ASSETS_APP_ICON_DISPLAY_SETTINGS TT_ASSET("app_icon_display_settings_dark_mode.png")
#define TT_ASSETS_APP_ICON_POWER_SETTINGS TT_ASSET("app_icon_power_settings_dark_mode.png")
#define TT_ASSETS_APP_ICON_I2C_SETTINGS TT_ASSET("app_icon_i2c_dark_mode.png")
#define TT_ASSETS_APP_ICON_SETTINGS TT_ASSET("app_icon_settings_dark_mode.png")
#define TT_ASSETS_APP_ICON_SYSTEM_INFO TT_ASSET("app_icon_system_info_dark_mode.png")
#define TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS TT_ASSET("app_icon_time_date_settings_dark_mode.png")
#define TT_ASSETS_APP_ICON_CALCULATOR TT_ASSET("app_icon_calculator_dark_mode.png")
#define TT_ASSETS_APP_ICON_NOTES TT_ASSET("app_icon_notes_dark_mode.png")
#define TT_ASSETS_APP_ICON_CHAT TT_ASSET("app_icon_chat_dark_mode.png")
#define TT_ASSETS_APP_ICON_GPIO TT_ASSET("app_icon_gpio_dark_mode.png")

View File

@ -16,9 +16,9 @@ namespace tt::app {
class AppContext;
enum class Result;
class App {
typedef unsigned int LaunchId;
private:
class App {
Mutex mutex;
@ -44,7 +44,7 @@ public:
virtual void onDestroy(AppContext& appContext) {}
virtual void onShow(AppContext& appContext, lv_obj_t* parent) {}
virtual void onHide(AppContext& appContext) {}
virtual void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> _Nullable resultData) {}
virtual void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> _Nullable resultData) {}
Mutex& getMutex() { return mutex; }
@ -83,15 +83,15 @@ std::shared_ptr<App> create() { return std::shared_ptr<T>(new T); }
* @param[in] id application name or id
* @param[in] parameters optional parameters to pass onto the application
*/
void start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
void stop();
/** @return the currently running app context (it is only ever null before the splash screen is shown) */
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
std::shared_ptr<AppContext> _Nullable getCurrentAppContext();
/** @return the currently running app (it is only ever null before the splash screen is shown) */
std::shared_ptr<app::App> _Nullable getCurrentApp();
std::shared_ptr<App> _Nullable getCurrentApp();
}

View File

@ -12,7 +12,7 @@ typedef void (*OnCreate)(void* appContext, void* _Nullable data);
typedef void (*OnDestroy)(void* appContext, void* _Nullable data);
typedef void (*OnShow)(void* appContext, void* _Nullable data, lv_obj_t* parent);
typedef void (*OnHide)(void* appContext, void* _Nullable data);
typedef void (*OnResult)(void* appContext, void* _Nullable data, Result result, Bundle* resultData);
typedef void (*OnResult)(void* appContext, void* _Nullable data, LaunchId launchId, Result result, Bundle* resultData);
void setElfAppManifest(
const char* name,

View File

@ -0,0 +1,7 @@
#pragma once
namespace tt::app::filebrowser {
void start();
} // namespace

View File

@ -1,15 +0,0 @@
#pragma once
#include "app/files/View.h"
#include "app/files/State.h"
#include "app/AppManifest.h"
#include <lvgl.h>
#include <dirent.h>
#include <memory>
namespace tt::app::files {
void start();
} // namespace

View File

@ -0,0 +1,23 @@
#pragma once
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.
*/
LaunchId startForExistingFile();
/**
* 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.
*/
LaunchId startForExistingOrNewFile();
/**
* @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);
} // namespace

View File

@ -98,6 +98,8 @@ public:
lv_dropdown_set_options(busDropdown, bus_options.c_str());
lv_obj_align(busDropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_set_width(busDropdown, LV_PCT(50));
lv_obj_set_style_border_color(busDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
lv_obj_set_style_border_width(busDropdown, 1, LV_PART_MAIN);
int32_t bus_index = 0;
preferences.optInt32("bus", bus_index);
if (bus_index < uartNames.size()) {

View File

@ -1,6 +1,5 @@
#pragma once
#include "Tactility/hal/gps/GpsDevice.h"
#include "Tactility/hal/sdcard/SdCardDevice.h"
#include "Tactility/hal/spi/Spi.h"
#include "Tactility/hal/uart/Uart.h"

View File

@ -53,7 +53,7 @@ std::shared_ptr<SdCardDevice> _Nullable find(const std::string& path);
* Always calls the function, but doesn't lock if the path is not an SD card path.
*/
template<typename ReturnType>
inline ReturnType withSdCardLock(const std::string& path, std::function<ReturnType()> fn) {
ReturnType withSdCardLock(const std::string& path, std::function<ReturnType()> fn) {
auto sdcard = find(path);
if (sdcard != nullptr) {
auto scoped_lockable = sdcard->getLock().asScopedLock();

View File

@ -0,0 +1,9 @@
#pragma once
#include <lvgl.h>
lv_color_t lv_color_foreground();
lv_color_t lv_color_background();
lv_color_t lv_color_background_darkest();

View File

@ -1,6 +1,6 @@
#pragma once
#include "lvgl.h"
#include <lvgl.h>
namespace tt::lvgl {

View File

@ -0,0 +1,5 @@
#pragma once
#include <lvgl.h>
#include "./Colors.h"

View File

@ -1,4 +1,4 @@
#include "lvgl.h"
#include <lvgl.h>
namespace tt::lvgl {

View File

@ -1,6 +1,6 @@
#pragma once
#include "lvgl.h"
#include <lvgl.h>
namespace tt::lvgl {

View File

@ -1,7 +1,7 @@
#pragma once
#include "lvgl.h"
#include "../app/AppContext.h"
#include <lvgl.h>
namespace tt::lvgl {

View File

@ -4,7 +4,6 @@
#include <Tactility/Bundle.h>
#include <Tactility/PubSub.h>
#include <Tactility/service/ServiceManifest.h>
#include <memory>
@ -30,7 +29,7 @@ struct LoaderEvent {
* @param[in] id application name or id
* @param[in] parameters optional parameters to pass onto the application
*/
void startApp(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
void stopApp();

View File

@ -25,22 +25,21 @@ enum class State {
*/
class AppInstance : public AppContext {
private:
Mutex mutex = Mutex(Mutex::Type::Normal);
const std::shared_ptr<AppManifest> manifest;
State state = State::Initial;
LaunchId launchId;
Flags flags = { .showStatusbar = true };
/** @brief Optional parameters to start the app with
* When these are stored in the app struct, the struct takes ownership.
* Do not mutate after app creation.
*/
std::shared_ptr<const tt::Bundle> _Nullable parameters;
std::shared_ptr<const Bundle> _Nullable parameters;
std::shared_ptr<App> app;
static std::shared_ptr<app::App> createApp(
const std::shared_ptr<app::AppManifest>& manifest
static std::shared_ptr<App> createApp(
const std::shared_ptr<AppManifest>& manifest
) {
if (manifest->location.isInternal()) {
assert(manifest->createApp != nullptr);
@ -50,7 +49,7 @@ private:
TT_LOG_W("", "Manifest specifies createApp, but this is not used with external apps");
}
#ifdef ESP_PLATFORM
return app::createElfApp(manifest);
return createElfApp(manifest);
#else
tt_crash("not supported");
#endif
@ -61,18 +60,23 @@ private:
public:
explicit AppInstance(const std::shared_ptr<AppManifest>& manifest) :
explicit AppInstance(const std::shared_ptr<AppManifest>& manifest, LaunchId launchId) :
manifest(manifest),
launchId(launchId),
app(createApp(manifest))
{}
AppInstance(const std::shared_ptr<AppManifest>& manifest, std::shared_ptr<const Bundle> parameters) :
AppInstance(const std::shared_ptr<AppManifest>& manifest, LaunchId launchId, std::shared_ptr<const Bundle> parameters) :
manifest(manifest),
launchId(launchId),
parameters(std::move(parameters)),
app(createApp(manifest)) {}
app(createApp(manifest))
{}
~AppInstance() override = default;
LaunchId getLaunchId() const { return launchId; }
void setState(State state);
State getState() const;

View File

@ -6,7 +6,7 @@
#include <vector>
#include <dirent.h>
namespace tt::app::files {
namespace tt::app::filebrowser {
class State {

View File

@ -0,0 +1,11 @@
#pragma once
#include <string>
namespace tt::app::filebrowser {
bool isSupportedExecutableFile(const std::string& filename);
bool isSupportedImageFile(const std::string& filename);
bool isSupportedTextFile(const std::string& filename);
} // namespace

View File

@ -7,7 +7,7 @@
#include <lvgl.h>
#include <memory>
namespace tt::app::files {
namespace tt::app::filebrowser {
class View {
std::shared_ptr<State> state;

View File

@ -1,66 +0,0 @@
#pragma once
#include <dirent.h>
#include <string>
#include <sys/stat.h>
#include <vector>
namespace tt::app::files {
/** File types for `dirent`'s `d_type`. */
enum {
TT_DT_UNKNOWN = 0,
#define TT_DT_UNKNOWN TT_DT_UNKNOWN // Unknown type
TT_DT_FIFO = 1,
#define TT_DT_FIFO TT_DT_FIFO // Named pipe or FIFO
TT_DT_CHR = 2,
#define TT_DT_CHR TT_DT_CHR // Character device
TT_DT_DIR = 4,
#define TT_DT_DIR TT_DT_DIR // Directory
TT_DT_BLK = 6,
#define TT_DT_BLK TT_DT_BLK // Block device
TT_DT_REG = 8,
#define TT_DT_REG TT_DT_REG // Regular file
TT_DT_LNK = 10,
#define TT_DT_LNK TT_DT_LNK // Symbolic link
TT_DT_SOCK = 12,
#define TT_DT_SOCK TT_DT_SOCK // Local-domain socket
TT_DT_WHT = 14
#define TT_DT_WHT TT_DT_WHT // Whiteout inodes
};
std::string getChildPath(const std::string& basePath, const std::string& childPath);
typedef int (*ScandirFilter)(const struct dirent*);
typedef bool (*ScandirSort)(const struct dirent&, const struct dirent&);
bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right);
int dirent_filter_dot_entries(const struct dirent* entry);
/**
* A scandir()-like implementation that works on ESP32.
* It does not return "." and ".." items but otherwise functions the same.
* It returns an allocated output array with allocated dirent instances.
* The caller is responsible for free-ing the memory of these.
*
* @param[in] path path the scan for files and directories
* @param[out] outList a pointer to vector of dirent
* @param[in] filter an optional filter to filter out specific items
* @param[in] sort an optional sorting function
* @return the amount of items that were stored in "output" or -1 when an error occurred
*/
int scandir(
const std::string& path,
std::vector<dirent>& outList,
ScandirFilter _Nullable filter,
ScandirSort _Nullable sort
);
bool isSupportedExecutableFile(const std::string& filename);
bool isSupportedImageFile(const std::string& filename);
bool isSupportedTextFile(const std::string& filename);
} // namespace

View File

@ -0,0 +1,14 @@
#pragma once
#include <Tactility/Bundle.h>
namespace tt::app::fileselection {
enum class Mode {
Existing = 0,
ExistingOrNew = 1
};
Mode getMode(const Bundle& bundle);
}

View File

@ -0,0 +1,59 @@
#pragma once
#include <Tactility/Mutex.h>
#include <string>
#include <vector>
#include <dirent.h>
namespace tt::app::fileselection {
class State {
Mutex mutex = Mutex(Mutex::Type::Recursive);
std::vector<dirent> dir_entries;
std::string current_path;
std::string selected_child_entry;
public:
State();
void freeEntries() {
dir_entries.clear();
}
~State() {
freeEntries();
}
bool setEntriesForChildPath(const std::string& child_path);
bool setEntriesForPath(const std::string& path);
template <std::invocable<const std::vector<dirent> &> Func>
void withEntries(Func&& onEntries) const {
mutex.withLock([&]() {
std::invoke(std::forward<Func>(onEntries), dir_entries);
});
}
bool getDirent(uint32_t index, dirent& dirent);
void setSelectedChildEntry(const std::string& newFile) {
selected_child_entry = newFile;
}
std::string getSelectedChildEntry() const { return selected_child_entry; }
std::string getCurrentPath() const { return current_path; }
std::string getCurrentPathWithTrailingSlash() const {
if (current_path.length() > 1) {
return current_path + "/";
} else {
return current_path;
}
}
std::string getSelectedChildPath() const;
};
}

View File

@ -0,0 +1,44 @@
#pragma once
#include "./State.h"
#include "./FileSelectionPrivate.h"
#include "Tactility/app/AppManifest.h"
#include <lvgl.h>
#include <memory>
namespace tt::app::fileselection {
class View {
std::shared_ptr<State> state;
lv_obj_t* dir_entry_list = nullptr;
lv_obj_t* navigate_up_button = nullptr;
lv_obj_t* path_textarea = nullptr;
lv_obj_t* select_button = nullptr;
std::function<void(std::string path)> on_file_selected;
void onTapFile(const std::string&path, const std::string&filename);
static void onSelectButtonPressed(lv_event_t* event);
static void onPathTextChanged(lv_event_t* event);
void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry);
public:
explicit View(const std::shared_ptr<State>& state, std::function<void(const std::string& path)> onFileSelected) :
state(state),
on_file_selected(std::move(onFileSelected))
{}
void init(lv_obj_t* parent, Mode mode);
void update();
void onNavigateUpPressed();
void onDirEntryPressed(uint32_t index);
void onFileSelected(const std::string& path) const {
on_file_selected(path);
}
};
}

View File

@ -1,13 +1,5 @@
#pragma once
#include <TactilityCore.h>
#include <Mutex.h>
#include <Thread.h>
#include "lvgl.h"
#include <Tactility/hal/i2c/I2c.h>
#include "Timer.h"
#include <memory>
namespace tt::app::i2cscanner {
void start();

View File

@ -37,7 +37,8 @@ namespace app {
namespace chat { extern const AppManifest manifest; }
namespace boot { extern const AppManifest manifest; }
namespace display { extern const AppManifest manifest; }
namespace files { extern const AppManifest manifest; }
namespace filebrowser { extern const AppManifest manifest; }
namespace fileselection { extern const AppManifest manifest; }
namespace gpio { extern const AppManifest manifest; }
namespace gpssettings { extern const AppManifest manifest; }
namespace i2cscanner { extern const AppManifest manifest; }
@ -46,6 +47,7 @@ namespace app {
namespace inputdialog { extern const AppManifest manifest; }
namespace launcher { extern const AppManifest manifest; }
namespace log { extern const AppManifest manifest; }
namespace notes { extern const AppManifest manifest; }
namespace power { extern const AppManifest manifest; }
namespace selectiondialog { extern const AppManifest manifest; }
namespace serialconsole { extern const AppManifest manifest; }
@ -77,7 +79,8 @@ static void registerSystemApps() {
addApp(app::applist::manifest);
addApp(app::calculator::manifest);
addApp(app::display::manifest);
addApp(app::files::manifest);
addApp(app::filebrowser::manifest);
addApp(app::fileselection::manifest);
addApp(app::gpio::manifest);
addApp(app::gpssettings::manifest);
addApp(app::i2cscanner::manifest);
@ -86,6 +89,7 @@ static void registerSystemApps() {
addApp(app::inputdialog::manifest);
addApp(app::launcher::manifest);
addApp(app::log::manifest);
addApp(app::notes::manifest);
addApp(app::serialconsole::manifest);
addApp(app::settings::manifest);
addApp(app::selectiondialog::manifest);

View File

@ -4,19 +4,19 @@
namespace tt::app {
void start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters) {
service::loader::startApp(id, std::move(parameters));
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters) {
return service::loader::startApp(id, std::move(parameters));
}
void stop() {
service::loader::stopApp();
}
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext() {
std::shared_ptr<AppContext> _Nullable getCurrentAppContext() {
return service::loader::getCurrentAppContext();
}
std::shared_ptr<app::App> _Nullable getCurrentApp() {
std::shared_ptr<App> _Nullable getCurrentApp() {
return service::loader::getCurrentApp();
}

View File

@ -36,8 +36,6 @@ static ElfManifest elfManifest;
class ElfApp : public App {
private:
const std::string filePath;
std::unique_ptr<uint8_t[]> elfFileData;
esp_elf_t elf;
@ -143,9 +141,9 @@ public:
}
}
void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> resultBundle) override {
void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> resultBundle) override {
if (manifest != nullptr && manifest->onResult != nullptr) {
manifest->onResult(&appContext, data, result, resultBundle.get());
manifest->onResult(&appContext, data, launchId, result, resultBundle.get());
}
}
};

View File

@ -101,6 +101,8 @@ public:
lv_dropdown_set_options(uartDropdown, uart_options.c_str());
lv_obj_align(uartDropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_set_width(uartDropdown, LV_PCT(50));
lv_obj_set_style_border_color(uartDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
lv_obj_set_style_border_width(uartDropdown, 1, LV_PART_MAIN);
auto* uart_label = lv_label_create(uart_wrapper);
lv_obj_align(uart_label, LV_ALIGN_TOP_LEFT, 0, 10);
@ -121,6 +123,8 @@ public:
lv_dropdown_set_options(modelDropdown, model_options.c_str());
lv_obj_align(modelDropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_set_width(modelDropdown, LV_PCT(50));
lv_obj_set_style_border_color(modelDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
lv_obj_set_style_border_width(modelDropdown, 1, LV_PART_MAIN);
auto* model_label = lv_label_create(model_wrapper);
lv_obj_align(model_label, LV_ALIGN_TOP_LEFT, 0, 10);
@ -139,6 +143,8 @@ public:
lv_dropdown_set_options(baudDropdown, baudRatesDropdownValues);
lv_obj_align(baudDropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_set_width(baudDropdown, LV_PCT(50));
lv_obj_set_style_border_color(baudDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
lv_obj_set_style_border_width(baudDropdown, 1, LV_PART_MAIN);
auto* baud_rate_label = lv_label_create(baud_wrapper);
lv_obj_align(baud_rate_label, LV_ALIGN_TOP_LEFT, 0, 10);

View File

@ -1,5 +1,6 @@
#include <Tactility/app/AppManifest.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/Assets.h>
#include <cstdlib>
#include <cstring>
#include <lvgl.h>
@ -208,6 +209,7 @@ class CalculatorApp : public App {
lv_obj_set_style_pad_row(btnm, 10, LV_PART_MAIN);
lv_obj_set_style_pad_column(btnm, 5, LV_PART_MAIN);
lv_obj_set_style_border_width(btnm, 0, LV_PART_MAIN);
lv_obj_set_style_bg_color(btnm, lv_palette_main(LV_PALETTE_BLUE), LV_PART_ITEMS);
lv_obj_set_style_border_width(btnm, 0, LV_PART_MAIN);
if (lv_display_get_horizontal_resolution(nullptr) <= 240 || lv_display_get_vertical_resolution(nullptr) <= 240) { //small screens
@ -224,6 +226,7 @@ class CalculatorApp : public App {
extern const AppManifest manifest = {
.id = "Calculator",
.name = "Calculator",
.icon = TT_ASSETS_APP_ICON_CALCULATOR,
.createApp = create<CalculatorApp>
};

View File

@ -2,6 +2,7 @@
#include <Tactility/app/AppManifest.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/Assets.h>
#include <Tactility/service/espnow/EspNow.h>
#include "Tactility/service/gui/Gui.h"
@ -115,7 +116,7 @@ public:
msg_list = lv_list_create(parent);
lv_obj_set_size(msg_list, lv_pct(75), lv_pct(43));
lv_obj_align(msg_list, LV_ALIGN_TOP_LEFT, 5, toolbar_height + 45);
lv_obj_set_style_bg_color(msg_list, lv_color_hex(0xEEEEEE), 0);
lv_obj_set_style_bg_color(msg_list, lv_color_hex(0x262626), 0);
lv_obj_set_style_border_width(msg_list, 1, 0);
lv_obj_set_style_pad_all(msg_list, 5, 0);
@ -171,6 +172,7 @@ public:
extern const AppManifest manifest = {
.id = "Chat",
.name = "Chat",
.icon = TT_ASSETS_APP_ICON_CHAT,
.createApp = create<ChatApp>
};

View File

@ -22,80 +22,85 @@ static uint8_t gamma = 255;
#define ROTATION_270 2
#define ROTATION_90 3
static std::shared_ptr<tt::hal::display::DisplayDevice> getHalDisplay() {
static std::shared_ptr<hal::display::DisplayDevice> getHalDisplay() {
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
}
static void onBacklightSliderEvent(lv_event_t* event) {
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
auto hal_display = getHalDisplay();
assert(hal_display != nullptr);
if (hal_display->supportsBacklightDuty()) {
int32_t slider_value = lv_slider_get_value(slider);
backlight_duty = (uint8_t)slider_value;
backlight_duty_set = true;
hal_display->setBacklightDuty(backlight_duty);
}
}
static void onGammaSliderEvent(lv_event_t* event) {
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
auto* lvgl_display = lv_display_get_default();
assert(lvgl_display != nullptr);
auto* hal_display = (tt::hal::display::DisplayDevice*)lv_display_get_user_data(lvgl_display);
assert(hal_display != nullptr);
if (hal_display->getGammaCurveCount() > 0) {
int32_t slider_value = lv_slider_get_value(slider);
gamma = (uint8_t)slider_value;
hal_display->setGammaCurve(gamma);
tt::app::display::setGammaCurve(gamma);
}
}
static lv_display_rotation_t orientationSettingToDisplayRotation(uint32_t setting) {
if (setting == ROTATION_180) {
return LV_DISPLAY_ROTATION_180;
} else if (setting == ROTATION_270) {
return LV_DISPLAY_ROTATION_270;
} else if (setting == ROTATION_90) {
return LV_DISPLAY_ROTATION_90;
} else {
return LV_DISPLAY_ROTATION_0;
switch (setting) {
case ROTATION_180:
return LV_DISPLAY_ROTATION_180;
case ROTATION_270:
return LV_DISPLAY_ROTATION_270;
case ROTATION_90:
return LV_DISPLAY_ROTATION_90;
default:
return LV_DISPLAY_ROTATION_0;
}
}
static uint32_t dipslayOrientationToOrientationSetting(lv_display_rotation_t orientation) {
if (orientation == LV_DISPLAY_ROTATION_90) {
return ROTATION_90;
} else if (orientation == LV_DISPLAY_ROTATION_180) {
return ROTATION_180;
} else if (orientation == LV_DISPLAY_ROTATION_270) {
return ROTATION_270;
} else {
return ROTATION_DEFAULT;
}
}
static void onOrientationSet(lv_event_t* event) {
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
uint32_t selected = lv_dropdown_get_selected(dropdown);
TT_LOG_I(TAG, "Selected %ld", selected);
lv_display_rotation_t rotation = orientationSettingToDisplayRotation(selected);
if (lv_display_get_rotation(lv_display_get_default()) != rotation) {
lv_display_set_rotation(lv_display_get_default(), rotation);
setRotation(rotation);
static uint32_t displayOrientationToOrientationSetting(lv_display_rotation_t orientation) {
switch (orientation) {
case LV_DISPLAY_ROTATION_90:
return ROTATION_90;
case LV_DISPLAY_ROTATION_180:
return ROTATION_180;
case LV_DISPLAY_ROTATION_270:
return ROTATION_270;
default:
return ROTATION_DEFAULT;
}
}
class DisplayApp : public App {
static void onBacklightSliderEvent(lv_event_t* event) {
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
auto hal_display = getHalDisplay();
assert(hal_display != nullptr);
if (hal_display->supportsBacklightDuty()) {
int32_t slider_value = lv_slider_get_value(slider);
backlight_duty = static_cast<uint8_t>(slider_value);
backlight_duty_set = true;
hal_display->setBacklightDuty(backlight_duty);
}
}
static void onGammaSliderEvent(lv_event_t* event) {
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
auto* lvgl_display = lv_display_get_default();
assert(lvgl_display != nullptr);
auto* hal_display = static_cast<hal::display::DisplayDevice*>(lv_display_get_user_data(lvgl_display));
assert(hal_display != nullptr);
if (hal_display->getGammaCurveCount() > 0) {
int32_t slider_value = lv_slider_get_value(slider);
gamma = static_cast<uint8_t>(slider_value);
hal_display->setGammaCurve(gamma);
setGammaCurve(gamma);
}
}
static void onOrientationSet(lv_event_t* event) {
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
uint32_t selected = lv_dropdown_get_selected(dropdown);
TT_LOG_I(TAG, "Selected %ld", selected);
lv_display_rotation_t rotation = orientationSettingToDisplayRotation(selected);
if (lv_display_get_rotation(lv_display_get_default()) != rotation) {
lv_display_set_rotation(lv_display_get_default(), rotation);
setRotation(rotation);
}
}
public:
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
@ -109,34 +114,22 @@ class DisplayApp : public App {
lv_obj_set_width(main_wrapper, LV_PCT(100));
lv_obj_set_flex_grow(main_wrapper, 1);
auto* wrapper = lv_obj_create(main_wrapper);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_style_pad_all(wrapper, 8, 0);
lv_obj_set_style_border_width(wrapper, 0, 0);
if (hal_display->supportsBacklightDuty()) {
auto* brightness_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(brightness_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_hor(brightness_wrapper, 0, 0);
lv_obj_set_style_pad_ver(brightness_wrapper, 6, 0);
lv_obj_set_style_border_width(brightness_wrapper, 0, 0);
auto* brightness_label = lv_label_create(wrapper);
lv_label_set_text(brightness_label, "Brightness");
auto* brightness_label = lv_label_create(brightness_wrapper);
lv_label_set_text(brightness_label, "Brightness");
auto* brightness_slider = lv_slider_create(wrapper);
lv_obj_set_width(brightness_slider, LV_PCT(50));
lv_obj_align(brightness_slider, LV_ALIGN_TOP_RIGHT, -8, 0);
lv_slider_set_range(brightness_slider, 0, 255);
lv_obj_add_event_cb(brightness_slider, onBacklightSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr);
auto* brightness_slider = lv_slider_create(brightness_wrapper);
lv_obj_set_width(brightness_slider, LV_PCT(50));
lv_obj_align(brightness_slider, LV_ALIGN_TOP_RIGHT, -8, 0);
lv_slider_set_range(brightness_slider, 0, 255);
lv_obj_add_event_cb(brightness_slider, onBacklightSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr);
auto* gamma_label = lv_label_create(wrapper);
lv_label_set_text(gamma_label, "Gamma");
lv_obj_set_y(gamma_label, 40);
auto* gamma_slider = lv_slider_create(wrapper);
lv_obj_set_width(gamma_slider, LV_PCT(50));
lv_obj_align(gamma_slider, LV_ALIGN_TOP_RIGHT, -8, 40);
lv_slider_set_range(gamma_slider, 0, hal_display->getGammaCurveCount());
lv_obj_add_event_cb(gamma_slider, onGammaSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr);
if (!hal_display->supportsBacklightDuty()) {
lv_slider_set_value(brightness_slider, 255, LV_ANIM_OFF);
lv_obj_add_state(brightness_slider, LV_STATE_DISABLED);
} else {
uint8_t value;
if (getBacklightDuty(value)) {
lv_slider_set_value(brightness_slider, value, LV_ANIM_OFF);
@ -145,10 +138,23 @@ class DisplayApp : public App {
}
}
if (hal_display->getGammaCurveCount() == 0) {
lv_slider_set_value(gamma_slider, 0, LV_ANIM_OFF);
lv_obj_add_state(gamma_slider, LV_STATE_DISABLED);
} else {
if (hal_display->getGammaCurveCount() > 0) {
auto* gamma_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(gamma_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_hor(gamma_wrapper, 0, 0);
lv_obj_set_style_pad_ver(gamma_wrapper, 6, 0);
lv_obj_set_style_border_width(gamma_wrapper, 0, 0);
auto* gamma_label = lv_label_create(gamma_wrapper);
lv_label_set_text(gamma_label, "Gamma");
lv_obj_set_y(gamma_label, 0);
auto* gamma_slider = lv_slider_create(gamma_wrapper);
lv_obj_set_width(gamma_slider, LV_PCT(50));
lv_obj_align(gamma_slider, LV_ALIGN_TOP_RIGHT, -8, 0);
lv_slider_set_range(gamma_slider, 0, hal_display->getGammaCurveCount());
lv_obj_add_event_cb(gamma_slider, onGammaSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr);
uint8_t curve_index;
if (getGammaCurve(curve_index)) {
lv_slider_set_value(gamma_slider, curve_index, LV_ANIM_OFF);
@ -157,25 +163,32 @@ class DisplayApp : public App {
}
}
auto* orientation_label = lv_label_create(wrapper);
auto* orientation_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(orientation_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(orientation_wrapper, 0, 0);
lv_obj_set_style_border_width(orientation_wrapper, 0, 0);
auto* orientation_label = lv_label_create(orientation_wrapper);
lv_label_set_text(orientation_label, "Orientation");
lv_obj_align(orientation_label, LV_ALIGN_TOP_LEFT, 0, 80);
lv_obj_align(orientation_label, LV_ALIGN_TOP_LEFT, 0, 8);
auto lvgl_display = lv_obj_get_display(parent);
auto horizontal_px = lv_display_get_horizontal_resolution(lvgl_display);
auto vertical_px = lv_display_get_vertical_resolution(lvgl_display);
bool is_landscape_display = horizontal_px > vertical_px;
auto* orientation_dropdown = lv_dropdown_create(wrapper);
auto* orientation_dropdown = lv_dropdown_create(orientation_wrapper);
if (is_landscape_display) {
lv_dropdown_set_options(orientation_dropdown, "Landscape\nLandscape (flipped)\nPortrait Left\nPortrait Right");
} else {
lv_dropdown_set_options(orientation_dropdown, "Portrait\nPortrait (flipped)\nLandscape Left\nLandscape Right");
}
lv_obj_align(orientation_dropdown, LV_ALIGN_TOP_RIGHT, 0, 72);
lv_obj_align(orientation_dropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_set_style_border_color(orientation_dropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
lv_obj_set_style_border_width(orientation_dropdown, 1, LV_PART_MAIN);
lv_obj_add_event_cb(orientation_dropdown, onOrientationSet, LV_EVENT_VALUE_CHANGED, nullptr);
uint32_t orientation_selected = dipslayOrientationToOrientationSetting(
uint32_t orientation_selected = displayOrientationToOrientationSetting(
lv_display_get_rotation(lv_display_get_default())
);
lv_dropdown_set_selected(orientation_dropdown, orientation_selected);

View File

@ -1,5 +1,5 @@
#include "Tactility/app/files/View.h"
#include "Tactility/app/files/State.h"
#include "Tactility/app/filebrowser/View.h"
#include "Tactility/app/filebrowser/State.h"
#include "Tactility/app/AppContext.h"
#include <Tactility/Assets.h>
@ -7,18 +7,18 @@
#include <memory>
namespace tt::app::files {
namespace tt::app::filebrowser {
#define TAG "files_app"
#define TAG "filebrowser_app"
extern const AppManifest manifest;
class FilesApp : public App {
class FileBrowser : public App {
std::unique_ptr<View> view;
std::shared_ptr<State> state;
public:
FilesApp() {
FileBrowser() {
state = std::make_shared<State>();
view = std::make_unique<View>(state);
}
@ -27,7 +27,7 @@ public:
view->init(parent);
}
void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> bundle) override {
void onResult(AppContext& appContext, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
view->onResult(result, std::move(bundle));
}
};
@ -37,7 +37,7 @@ extern const AppManifest manifest = {
.name = "Files",
.icon = TT_ASSETS_APP_ICON_FILES,
.type = Type::Hidden,
.createApp = create<FilesApp>
.createApp = create<FileBrowser>
};
void start() {

View File

@ -1,6 +1,6 @@
#include "Tactility/app/files/State.h"
#include "Tactility/app/files/FileUtils.h"
#include "Tactility/app/filebrowser/State.h"
#include <Tactility/file/File.h>
#include "Tactility/hal/sdcard/SdCardDevice.h"
#include <Tactility/Log.h>
#include <Tactility/Partitions.h>
@ -11,9 +11,9 @@
#include <vector>
#include <dirent.h>
#define TAG "files_app"
#define TAG "filebrowser_app"
namespace tt::app::files {
namespace tt::app::filebrowser {
State::State() {
if (kernel::getPlatform() == kernel::PlatformSimulator) {
@ -30,7 +30,7 @@ State::State() {
}
std::string State::getSelectedChildPath() const {
return getChildPath(current_path, selected_child_entry);
return file::getChildPath(current_path, selected_child_entry);
}
bool State::setEntriesForPath(const std::string& path) {
@ -52,12 +52,12 @@ bool State::setEntriesForPath(const std::string& path) {
dir_entries.clear();
dir_entries.push_back(dirent{
.d_ino = 0,
.d_type = TT_DT_DIR,
.d_type = file::TT_DT_DIR,
.d_name = SYSTEM_PARTITION_NAME
});
dir_entries.push_back(dirent{
.d_ino = 1,
.d_type = TT_DT_DIR,
.d_type = file::TT_DT_DIR,
.d_name = DATA_PARTITION_NAME
});
@ -68,7 +68,7 @@ bool State::setEntriesForPath(const std::string& path) {
auto mount_name = sdcard->getMountPath().substr(1);
auto dir_entry = dirent {
.d_ino = 2,
.d_type = TT_DT_DIR,
.d_type = file::TT_DT_DIR,
.d_name = { 0 }
};
assert(mount_name.length() < sizeof(dirent::d_name));
@ -83,7 +83,7 @@ bool State::setEntriesForPath(const std::string& path) {
return true;
} else {
dir_entries.clear();
int count = tt::app::files::scandir(path, dir_entries, &dirent_filter_dot_entries, dirent_sort_alpha_and_type);
int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType);
if (count >= 0) {
TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count);
current_path = path;
@ -97,8 +97,8 @@ bool State::setEntriesForPath(const std::string& path) {
}
}
bool State::setEntriesForChildPath(const std::string& child_path) {
auto path = getChildPath(current_path, child_path);
bool State::setEntriesForChildPath(const std::string& childPath) {
auto path = file::getChildPath(current_path, childPath);
TT_LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str());
return setEntriesForPath(path);
}

View File

@ -0,0 +1,34 @@
#include <Tactility/StringUtils.h>
#include <Tactility/TactilityCore.h>
namespace tt::app::filebrowser {
#define TAG "filebrowser_app"
bool isSupportedExecutableFile(const std::string& filename) {
#ifdef ESP_PLATFORM
// Currently only the PNG library is built into Tactility
return filename.ends_with(".elf");
#else
return false;
#endif
}
bool isSupportedImageFile(const std::string& filename) {
// Currently only the PNG library is built into Tactility
return string::lowercase(filename).ends_with(".png");
}
bool isSupportedTextFile(const std::string& filename) {
std::string filename_lower = string::lowercase(filename);
return filename_lower.ends_with(".txt") ||
filename_lower.ends_with(".ini") ||
filename_lower.ends_with(".json") ||
filename_lower.ends_with(".yaml") ||
filename_lower.ends_with(".yml") ||
filename_lower.ends_with(".lua") ||
filename_lower.ends_with(".js") ||
filename_lower.ends_with(".properties");
}
} // namespace tt::app::filebrowser

View File

@ -1,5 +1,5 @@
#include "Tactility/app/files/FileUtils.h"
#include "Tactility/app/files/View.h"
#include "Tactility/app/filebrowser/View.h"
#include "Tactility/app/filebrowser/SupportedFiles.h"
#include "Tactility/app/alertdialog/AlertDialog.h"
#include "Tactility/app/imageviewer/ImageViewer.h"
@ -10,6 +10,7 @@
#include "Tactility/lvgl/LvglSync.h"
#include <Tactility/Tactility.h>
#include "Tactility/file/File.h"
#include <Tactility/StringUtils.h>
#include <cstring>
@ -19,43 +20,43 @@
#include "Tactility/service/loader/Loader.h"
#endif
#define TAG "files_app"
#define TAG "filebrowser_app"
namespace tt::app::files {
namespace tt::app::filebrowser {
// region Callbacks
static void dirEntryListScrollBeginCallback(lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
auto* view = static_cast<View*>(lv_event_get_user_data(event));
view->onDirEntryListScrollBegin();
}
static void onDirEntryPressedCallback(lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
auto* view = static_cast<View*>(lv_event_get_user_data(event));
auto* button = lv_event_get_target_obj(event);
auto index = lv_obj_get_index(button);
view->onDirEntryPressed(index);
}
static void onDirEntryLongPressedCallback(lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
auto* view = static_cast<View*>(lv_event_get_user_data(event));
auto* button = lv_event_get_target_obj(event);
auto index = lv_obj_get_index(button);
view->onDirEntryLongPressed(index);
}
static void onRenamePressedCallback(lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
auto* view = static_cast<View*>(lv_event_get_user_data(event));
view->onRenamePressed();
}
static void onDeletePressedCallback(lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
auto* view = static_cast<View*>(lv_event_get_user_data(event));
view->onDeletePressed();
}
static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
auto* view = static_cast<View*>(lv_event_get_user_data(event));
view->onNavigateUpPressed();
}
@ -86,18 +87,18 @@ void View::viewFile(const std::string& path, const std::string& filename) {
if (isSupportedExecutableFile(filename)) {
#ifdef ESP_PLATFORM
app::registerElfApp(processed_filepath);
auto app_id = app::getElfAppId(processed_filepath);
registerElfApp(processed_filepath);
auto app_id = getElfAppId(processed_filepath);
service::loader::startApp(app_id);
#endif
} else if (isSupportedImageFile(filename)) {
app::imageviewer::start(processed_filepath);
imageviewer::start(processed_filepath);
} else if (isSupportedTextFile(filename)) {
if (kernel::getPlatform() == kernel::PlatformEsp) {
app::textviewer::start(processed_filepath);
textviewer::start(processed_filepath);
} else {
// Remove forward slash, because we need a relative path
app::textviewer::start(processed_filepath.substr(1));
textviewer::start(processed_filepath.substr(1));
}
} else {
TT_LOG_W(TAG, "opening files of this type is not supported");
@ -111,6 +112,7 @@ void View::onDirEntryPressed(uint32_t index) {
if (state->getDirent(index, dir_entry)) {
TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type);
state->setSelectedChildEntry(dir_entry.d_name);
using namespace tt::file;
switch (dir_entry.d_type) {
case TT_DT_DIR:
case TT_DT_CHR:
@ -140,6 +142,7 @@ void View::onDirEntryLongPressed(int32_t index) {
if (state->getDirent(index, dir_entry)) {
TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type);
state->setSelectedChildEntry(dir_entry.d_name);
using namespace file;
switch (dir_entry.d_type) {
case TT_DT_DIR:
case TT_DT_CHR:
@ -161,15 +164,14 @@ void View::onDirEntryLongPressed(int32_t index) {
}
void View::createDirEntryWidget(lv_obj_t* parent, struct dirent& dir_entry) {
tt_check(parent);
auto* list = (lv_obj_t*)parent;
void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
tt_check(list);
const char* symbol;
if (dir_entry.d_type == TT_DT_DIR || dir_entry.d_type == TT_DT_CHR) {
if (dir_entry.d_type == file::TT_DT_DIR || dir_entry.d_type == file::TT_DT_CHR) {
symbol = LV_SYMBOL_DIRECTORY;
} else if (isSupportedImageFile(dir_entry.d_name)) {
symbol = LV_SYMBOL_IMAGE;
} else if (dir_entry.d_type == TT_DT_LNK) {
} else if (dir_entry.d_type == file::TT_DT_LNK) {
symbol = LV_SYMBOL_LOOP;
} else {
symbol = LV_SYMBOL_FILE;
@ -195,7 +197,7 @@ void View::onRenamePressed() {
std::string entry_name = state->getSelectedChildEntry();
TT_LOG_I(TAG, "Pending rename %s", entry_name.c_str());
state->setPendingAction(State::ActionRename);
app::inputdialog::start("Rename", "", entry_name);
inputdialog::start("Rename", "", entry_name);
}
void View::onDeletePressed() {
@ -204,7 +206,7 @@ void View::onDeletePressed() {
state->setPendingAction(State::ActionDelete);
std::string message = "Do you want to delete this?\n" + file_path;
const std::vector<std::string> choices = { "Yes", "No" };
app::alertdialog::start("Are you sure?", message, choices);
alertdialog::start("Are you sure?", message, choices);
}
void View::showActionsForDirectory() {
@ -301,7 +303,7 @@ void View::onResult(Result result, std::unique_ptr<Bundle> bundle) {
switch (state->getPendingAction()) {
case State::ActionDelete: {
if (alertdialog::getResultIndex(*bundle) == 0) {
int delete_count = (int)remove(filepath.c_str());
int delete_count = remove(filepath.c_str());
if (delete_count > 0) {
TT_LOG_I(TAG, "Deleted %d items", delete_count);
} else {
@ -313,9 +315,9 @@ void View::onResult(Result result, std::unique_ptr<Bundle> bundle) {
break;
}
case State::ActionRename: {
auto new_name = app::inputdialog::getResult(*bundle);
auto new_name = inputdialog::getResult(*bundle);
if (!new_name.empty() && new_name != state->getSelectedChildEntry()) {
std::string rename_to = getChildPath(state->getCurrentPath(), new_name);
std::string rename_to = file::getChildPath(state->getCurrentPath(), new_name);
if (rename(filepath.c_str(), rename_to.c_str())) {
TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str());
} else {

View File

@ -1,91 +0,0 @@
#include "Tactility/app/files/FileUtils.h"
#include <Tactility/StringUtils.h>
#include <Tactility/TactilityCore.h>
#include <cstring>
namespace tt::app::files {
#define TAG "file_utils"
std::string getChildPath(const std::string& basePath, const std::string& childPath) {
// Postfix with "/" when the current path isn't "/"
if (basePath.length() != 1) {
return basePath + "/" + childPath;
} else {
return "/" + childPath;
}
}
int dirent_filter_dot_entries(const struct dirent* entry) {
return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0;
}
bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right) {
bool left_is_dir = left.d_type == TT_DT_DIR || left.d_type == TT_DT_CHR;
bool right_is_dir = right.d_type == TT_DT_DIR || right.d_type == TT_DT_CHR;
if (left_is_dir == right_is_dir) {
return strcmp(left.d_name, right.d_name) < 0;
} else {
return left_is_dir > right_is_dir;
}
}
int scandir(
const std::string& path,
std::vector<dirent>& outList,
ScandirFilter _Nullable filterMethod,
ScandirSort _Nullable sortMethod
) {
TT_LOG_I(TAG, "scandir start");
DIR* dir = opendir(path.c_str());
if (dir == nullptr) {
TT_LOG_E(TAG, "Failed to open dir %s", path.c_str());
return -1;
}
struct dirent* current_entry;
while ((current_entry = readdir(dir)) != nullptr) {
if (filterMethod(current_entry) == 0) {
outList.push_back(*current_entry);
}
}
closedir(dir);
if (sortMethod != nullptr) {
sort(outList.begin(), outList.end(), sortMethod);
}
TT_LOG_I(TAG, "scandir finish");
return (int)outList.size();
};
bool isSupportedExecutableFile(const std::string& filename) {
#ifdef ESP_PLATFORM
// Currently only the PNG library is built into Tactility
return filename.ends_with(".elf");
#else
return false;
#endif
}
bool isSupportedImageFile(const std::string& filename) {
// Currently only the PNG library is built into Tactility
return string::lowercase(filename).ends_with(".png");
}
bool isSupportedTextFile(const std::string& filename) {
std::string filename_lower = string::lowercase(filename);
return filename_lower.ends_with(".txt") ||
filename_lower.ends_with(".ini") ||
filename_lower.ends_with(".json") ||
filename_lower.ends_with(".yaml") ||
filename_lower.ends_with(".yml") ||
filename_lower.ends_with(".lua") ||
filename_lower.ends_with(".js") ||
filename_lower.ends_with(".properties");
}
} // namespace tt::app::files

View File

@ -0,0 +1,78 @@
#include "Tactility/app/fileselection/FileSelectionPrivate.h"
#include "Tactility/app/fileselection/View.h"
#include "Tactility/app/fileselection/State.h"
#include "Tactility/app/AppContext.h"
#include <Tactility/Assets.h>
#include <Tactility/service/loader/Loader.h>
#include <memory>
namespace tt::app::fileselection {
#define TAG "fileselection_app"
extern const AppManifest manifest;
std::string getResultPath(const Bundle& bundle) {
std::string result;
if (bundle.optString("path", result)) {
return result;
} else {
return "";
}
}
Mode getMode(const Bundle& bundle) {
int32_t mode = static_cast<int32_t>(Mode::ExistingOrNew);
bundle.optInt32("mode", mode);
return static_cast<Mode>(mode);
}
void setMode(Bundle& bundle, Mode mode) {
auto mode_int = static_cast<int32_t>(mode);
bundle.putInt32("mode", mode_int);
}
class FileSelection : public App {
std::unique_ptr<View> view;
std::shared_ptr<State> state;
public:
FileSelection() {
state = std::make_shared<State>();
view = std::make_unique<View>(state, [this](const std::string& path) {
auto bundle = std::make_unique<Bundle>();
bundle->putString("path", path);
setResult(Result::Ok, std::move(bundle));
service::loader::stopApp();
});
}
void onShow(AppContext& appContext, lv_obj_t* parent) override {
auto mode = getMode(*appContext.getParameters());
view->init(parent, mode);
}
};
extern const AppManifest manifest = {
.id = "FileSelection",
.name = "File Selection",
.icon = TT_ASSETS_APP_ICON_FILES,
.type = Type::Hidden,
.createApp = create<FileSelection>
};
LaunchId startForExistingFile() {
auto bundle = std::make_shared<Bundle>();
setMode(*bundle, Mode::Existing);
return service::loader::startApp(manifest.id, bundle);
}
LaunchId startForExistingOrNewFile() {
auto bundle = std::make_shared<Bundle>();
setMode(*bundle, Mode::ExistingOrNew);
return service::loader::startApp(manifest.id, bundle);
}
} // namespace

View File

@ -0,0 +1,118 @@
#include "Tactility/app/fileselection/State.h"
#include <Tactility/file/File.h>
#include "Tactility/hal/sdcard/SdCardDevice.h"
#include <Tactility/Log.h>
#include <Tactility/Partitions.h>
#include <Tactility/kernel/Kernel.h>
#include <cstring>
#include <unistd.h>
#include <vector>
#include <dirent.h>
#define TAG "fileselection_app"
namespace tt::app::fileselection {
State::State() {
if (kernel::getPlatform() == kernel::PlatformSimulator) {
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) != nullptr) {
setEntriesForPath(cwd);
} else {
TT_LOG_E(TAG, "Failed to get current work directory files");
setEntriesForPath("/");
}
} else {
setEntriesForPath("/");
}
}
std::string State::getSelectedChildPath() const {
return file::getChildPath(current_path, selected_child_entry);
}
bool State::setEntriesForPath(const std::string& path) {
auto lock = mutex.asScopedLock();
if (!lock.lock(100)) {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "setEntriesForPath");
return false;
}
TT_LOG_I(TAG, "Changing path: %s -> %s", current_path.c_str(), path.c_str());
/**
* ESP32 does not have a root directory, so we have to create it manually.
* We'll add the NVS Flash partitions and the binding for the sdcard.
*/
bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/");
if (show_custom_root) {
TT_LOG_I(TAG, "Setting custom root");
dir_entries.clear();
dir_entries.push_back(dirent{
.d_ino = 0,
.d_type = file::TT_DT_DIR,
.d_name = SYSTEM_PARTITION_NAME
});
dir_entries.push_back(dirent{
.d_ino = 1,
.d_type = file::TT_DT_DIR,
.d_name = DATA_PARTITION_NAME
});
auto sdcards = tt::hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
for (auto& sdcard : sdcards) {
auto state = sdcard->getState();
if (state == hal::sdcard::SdCardDevice::State::Mounted) {
auto mount_name = sdcard->getMountPath().substr(1);
auto dir_entry = dirent {
.d_ino = 2,
.d_type = file::TT_DT_DIR,
.d_name = { 0 }
};
assert(mount_name.length() < sizeof(dirent::d_name));
strcpy(dir_entry.d_name, mount_name.c_str());
dir_entries.push_back(dir_entry);
}
}
current_path = path;
selected_child_entry = "";
return true;
} else {
dir_entries.clear();
int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType);
if (count >= 0) {
TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count);
current_path = path;
selected_child_entry = "";
return true;
} else {
TT_LOG_E(TAG, "Failed to fetch entries for %s", path.c_str());
return false;
}
}
}
bool State::setEntriesForChildPath(const std::string& childPath) {
auto path = file::getChildPath(current_path, childPath);
TT_LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str());
return setEntriesForPath(path);
}
bool State::getDirent(uint32_t index, dirent& dirent) {
auto lock = mutex.asScopedLock();
if (!lock.lock(50 / portTICK_PERIOD_MS)) {
return false;
}
if (index < dir_entries.size()) {
dirent = dir_entries[index];
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,215 @@
#include "Tactility/app/fileselection/View.h"
#include "Tactility/app/alertdialog/AlertDialog.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/lvgl/LvglSync.h"
#include <Tactility/Tactility.h>
#include "Tactility/file/File.h"
#include <Tactility/StringUtils.h>
#include <cstring>
#include <unistd.h>
#include <Tactility/service/gui/Gui.h>
#ifdef ESP_PLATFORM
#include "Tactility/service/loader/Loader.h"
#endif
#define TAG "fileselection_app"
namespace tt::app::fileselection {
// region Callbacks
static void onDirEntryPressedCallback(lv_event_t* event) {
auto* view = static_cast<View*>(lv_event_get_user_data(event));
auto* button = lv_event_get_target_obj(event);
auto index = lv_obj_get_index(button);
view->onDirEntryPressed(index);
}
static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) {
auto* view = static_cast<View*>(lv_event_get_user_data(event));
view->onNavigateUpPressed();
}
// endregion
void View::onTapFile(const std::string& path, const std::string& filename) {
std::string file_path = path + "/" + filename;
// For PC we need to make the path relative to the current work directory,
// because that's how LVGL maps its 'drive letter' to the file system.
std::string processed_filepath;
if (kernel::getPlatform() == kernel::PlatformSimulator) {
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) == nullptr) {
TT_LOG_E(TAG, "Failed to get current working directory");
return;
}
if (!file_path.starts_with(cwd)) {
TT_LOG_E(TAG, "Can only work with files in working directory %s", cwd);
return;
}
processed_filepath = file_path.substr(strlen(cwd));
} else {
processed_filepath = file_path;
}
TT_LOG_I(TAG, "Clicked %s", processed_filepath.c_str());
lv_textarea_set_text(path_textarea, processed_filepath.c_str());
}
void View::onDirEntryPressed(uint32_t index) {
dirent dir_entry;
if (state->getDirent(index, dir_entry)) {
TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type);
state->setSelectedChildEntry(dir_entry.d_name);
using namespace tt::file;
switch (dir_entry.d_type) {
case TT_DT_DIR:
case TT_DT_CHR:
state->setEntriesForChildPath(dir_entry.d_name);
lv_textarea_set_text(path_textarea, state->getCurrentPathWithTrailingSlash().c_str());
update();
break;
case TT_DT_LNK:
TT_LOG_W(TAG, "opening links is not supported");
break;
case TT_DT_REG:
onTapFile(state->getCurrentPath(), dir_entry.d_name);
break;
default:
// Assume it's a file
// TODO: Find a better way to identify a file
onTapFile(state->getCurrentPath(), dir_entry.d_name);
break;
}
}
}
void View::onSelectButtonPressed(lv_event_t* event) {
auto* view = static_cast<View*>(lv_event_get_user_data(event));
const char* path = lv_textarea_get_text(view->path_textarea);
if (path == nullptr || strlen(path) == 0) {
TT_LOG_W(TAG, "Select pressed, but not path found in textarea");
return;
}
view->onFileSelected(std::string(path));
}
static bool isSelectableFilePath(const char* path) {
if (path == nullptr) {
return false;
}
auto len = strlen(path);
if (len == 0) {
return false;
}
return path[len - 1] != '/';
}
void View::onPathTextChanged(lv_event_t* event) {
auto* view = static_cast<View*>(lv_event_get_user_data(event));
const char* path = lv_textarea_get_text(view->path_textarea);
if (isSelectableFilePath(path)) {
lv_obj_remove_flag(view->select_button, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(view->select_button, LV_OBJ_FLAG_HIDDEN);
}
}
void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
tt_check(list);
const char* symbol;
if (dir_entry.d_type == file::TT_DT_DIR || dir_entry.d_type == file::TT_DT_CHR) {
symbol = LV_SYMBOL_DIRECTORY;
} else {
symbol = LV_SYMBOL_FILE;
}
lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry.d_name);
lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this);
}
void View::onNavigateUpPressed() {
if (state->getCurrentPath() != "/") {
TT_LOG_I(TAG, "Navigating upwards");
std::string new_absolute_path;
if (string::getPathParent(state->getCurrentPath(), new_absolute_path)) {
state->setEntriesForPath(new_absolute_path);
}
if (new_absolute_path.length() > 1) {
lv_textarea_set_text(path_textarea, (new_absolute_path + "/").c_str());
} else {
lv_textarea_set_text(path_textarea, new_absolute_path.c_str());
}
update();
}
}
void View::update() {
auto scoped_lockable = lvgl::getSyncLock()->asScopedLock();
if (scoped_lockable.lock(lvgl::defaultLockTime)) {
lv_obj_clean(dir_entry_list);
state->withEntries([this](const std::vector<dirent>& entries) {
for (auto entry : entries) {
TT_LOG_D(TAG, "Entry: %s %d", entry.d_name, entry.d_type);
createDirEntryWidget(dir_entry_list, entry);
}
});
if (state->getCurrentPath() == "/") {
lv_obj_add_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_remove_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
}
} else {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "lvgl");
}
}
void View::init(lv_obj_t* parent, Mode mode) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
auto* toolbar = lvgl::toolbar_create(parent, "Select File");
navigate_up_button = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this);
auto* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_style_pad_all(wrapper, 0, 0);
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
dir_entry_list = lv_list_create(wrapper);
lv_obj_set_height(dir_entry_list, LV_PCT(100));
lv_obj_set_flex_grow(dir_entry_list, 1);
auto* bottom_wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(bottom_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_size(bottom_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_border_width(bottom_wrapper, 0, 0);
lv_obj_set_style_pad_all(bottom_wrapper, 0, 0);
path_textarea = lv_textarea_create(bottom_wrapper);
lv_textarea_set_one_line(path_textarea, true);
lv_obj_set_flex_grow(path_textarea, 1);
service::gui::keyboardAddTextArea(path_textarea);
lv_obj_add_event_cb(path_textarea, onPathTextChanged, LV_EVENT_VALUE_CHANGED, this);
select_button = lv_button_create(bottom_wrapper);
auto* select_button_label = lv_label_create(select_button);
lv_label_set_text(select_button_label, "Select");
lv_obj_add_event_cb(select_button, onSelectButtonPressed, LV_EVENT_SHORT_CLICKED, this);
lv_obj_add_flag(select_button, LV_OBJ_FLAG_HIDDEN);
update();
}
}

View File

@ -1,10 +1,10 @@
#include "Tactility/service/loader/Loader.h"
#include "Tactility/lvgl/Toolbar.h"
#include <Tactility/Assets.h>
#include <Tactility/app/gpio/GpioHal.h>
#include "Tactility/lvgl/Toolbar.h"
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Color.h>
#include <Tactility/Mutex.h>
#include <Tactility/Thread.h>
#include <Tactility/Timer.h>
namespace tt::app::gpio {
@ -13,10 +13,8 @@ extern const AppManifest manifest;
class GpioApp : public App {
private:
lv_obj_t* lvPins[GPIO_NUM_MAX] = {0 };
uint8_t pinStates[GPIO_NUM_MAX] = {0 };
lv_obj_t* lvPins[GPIO_NUM_MAX] = { nullptr };
uint8_t pinStates[GPIO_NUM_MAX] = { 0 };
std::unique_ptr<Timer> timer;
Mutex mutex;
@ -40,7 +38,7 @@ void GpioApp::updatePinStates() {
// Update pin states
for (int i = 0; i < GPIO_NUM_MAX; ++i) {
#ifdef ESP_PLATFORM
pinStates[i] = gpio_get_level((gpio_num_t)i);
pinStates[i] = gpio_get_level(static_cast<gpio_num_t>(i));
#else
pinStates[i] = gpio_get_level(i);
#endif
@ -60,9 +58,9 @@ void GpioApp::updatePinWidgets() {
if (reinterpret_cast<void*>(level) != label_user_data) {
lv_obj_set_user_data(label, reinterpret_cast<void*>(level));
if (level == 0) {
lv_obj_set_style_text_color(label, lv_color_black(), 0);
lv_obj_set_style_text_color(label, lv_color_background_darkest(), LV_STATE_DEFAULT);
} else {
lv_obj_set_style_text_color(label, lv_color_make(0, 200, 0), 0);
lv_obj_set_style_text_color(label, lv_color_make(0, 200, 0), LV_STATE_DEFAULT);
}
}
}
@ -71,8 +69,8 @@ void GpioApp::updatePinWidgets() {
lv_obj_t* GpioApp::createGpioRowWrapper(lv_obj_t* parent) {
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_style_pad_all(wrapper, 0, 0);
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_style_pad_all(wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_size(wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
return wrapper;
}
@ -87,7 +85,7 @@ void GpioApp::onTimer() {
void GpioApp::startTask() {
mutex.lock();
assert(timer == nullptr);
timer = std::make_unique<Timer>(Timer::Type::Periodic, [this]() {
timer = std::make_unique<Timer>(Timer::Type::Periodic, [this] {
onTimer();
});
timer->start(100 / portTICK_PERIOD_MS);
@ -113,7 +111,7 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) {
auto* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
auto* display = lv_obj_get_display(parent);
auto horizontal_px = lv_display_get_horizontal_resolution(display);
@ -122,7 +120,8 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) {
int32_t x_spacing = 20;
uint8_t column = 0;
uint8_t column_limit = is_landscape_display ? 10 : 5;
const uint8_t offset_from_left_label = 4;
const uint8_t column_limit = is_landscape_display ? 10 : 5;
auto* row_wrapper = createGpioRowWrapper(wrapper);
lv_obj_align(row_wrapper, LV_ALIGN_TOP_MID, 0, 0);
@ -138,8 +137,9 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) {
// Add a new GPIO status indicator
auto* status_label = lv_label_create(row_wrapper);
lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing), 0);
lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing + offset_from_left_label), 0);
lv_label_set_text_fmt(status_label, "%s", LV_SYMBOL_STOP);
lv_obj_set_style_text_color(status_label, lv_color_background_darkest(), LV_STATE_DEFAULT);
lvPins[i] = status_label;
column++;
@ -148,7 +148,7 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) {
// Add the GPIO number after the last item on a row
auto* postfix = lv_label_create(row_wrapper);
lv_label_set_text_fmt(postfix, "%02d", i);
lv_obj_set_pos(postfix, (int32_t)((column+1) * x_spacing), 0);
lv_obj_set_pos(postfix, (int32_t)((column+1) * x_spacing + offset_from_left_label), 0);
// Add a new row wrapper underneath the last one
auto* new_row_wrapper = createGpioRowWrapper(wrapper);
@ -170,6 +170,7 @@ void GpioApp::onHide(AppContext& app) {
extern const AppManifest manifest = {
.id = "Gpio",
.name = "GPIO",
.icon = TT_ASSETS_APP_ICON_GPIO,
.type = Type::System,
.createApp = create<GpioApp>
};

View File

@ -125,6 +125,8 @@ void I2cScannerApp::onShow(AppContext& app, lv_obj_t* parent) {
lv_dropdown_set_options(port_dropdown, dropdown_items.c_str());
lv_obj_set_width(port_dropdown, LV_PCT(48));
lv_obj_align(port_dropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_set_style_border_color(port_dropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
lv_obj_set_style_border_width(port_dropdown, 1, LV_PART_MAIN);
lv_obj_add_event_cb(port_dropdown, onSelectBusCallback, LV_EVENT_VALUE_CHANGED, this);
auto selected_bus = getLastBusIndex();
lv_dropdown_set_selected(port_dropdown, selected_bus);

View File

@ -27,33 +27,37 @@ static lv_obj_t* createAppButton(lv_obj_t* parent, const char* title, const char
auto* apps_button = lv_button_create(wrapper);
lv_obj_set_style_pad_hor(apps_button, 0, 0);
lv_obj_set_style_pad_top(apps_button, 0, 0);
lv_obj_set_style_pad_bottom(apps_button, 16, 0);
lv_obj_set_style_pad_bottom(apps_button, 8, 0);
lv_obj_set_style_shadow_width(apps_button, 0, 0);
lv_obj_set_style_border_width(apps_button, 0, 0);
lv_obj_set_style_bg_color(apps_button, lv_color_white(), 0);
lv_obj_set_style_bg_opa(apps_button, 0, LV_PART_MAIN);
auto* button_image = lv_image_create(apps_button);
lv_image_set_src(button_image, imageFile);
lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId);
lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), 0);
lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, 0);
lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT);
lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, LV_STATE_DEFAULT);
// Ensure buttons are still tappable when asset fails to load
// Icon images are 40x40, so we get some extra padding too
lv_obj_set_size(button_image, 64, 64);
auto* label = lv_label_create(wrapper);
lv_label_set_text(label, title);
lv_obj_align(label, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_add_event_cb(wrapper, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId);
lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId);
lv_obj_add_event_cb(label, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId);
return wrapper;
}
class LauncherApp : public App {
void onCreate(TT_UNUSED AppContext& app) override {
auto* config = tt::getConfiguration();
auto* config = getConfiguration();
if (!config->autoStartAppId.empty()) {
TT_LOG_I(TAG, "auto-starting %s", config->autoStartAppId.c_str());
tt::service::loader::startApp(config->autoStartAppId);
service::loader::startApp(config->autoStartAppId);
}
}

View File

@ -91,10 +91,9 @@ public:
updateLogEntries();
}
void onResult(AppContext& app, Result result, std::unique_ptr<Bundle> bundle) override {
void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
if (result == Result::Ok && bundle != nullptr) {
auto resultIndex = selectiondialog::getResultIndex(*bundle);
switch (resultIndex) {
switch (selectiondialog::getResultIndex(*bundle)) {
case 0:
filterLevel = LogLevel::Verbose;
break;

View File

@ -0,0 +1,207 @@
#include <Tactility/app/AppManifest.h>
#include <Tactility/file/File.h>
#include <Tactility/lvgl/Keyboard.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/Assets.h>
#include <lvgl.h>
#include <Tactility/app/fileselection/FileSelection.h>
#include <Tactility/hal/sdcard/SdCardDevice.h>
#include <Tactility/lvgl/LvglSync.h>
namespace tt::app::notes {
constexpr const char* TAG = "Notes";
class NotesApp : public App {
lv_obj_t* uiCurrentFileName;
lv_obj_t* uiDropDownMenu;
lv_obj_t* uiNoteText;
std::string filePath;
std::string saveBuffer;
LaunchId loadFileLaunchId = 0;
LaunchId saveFileLaunchId = 0;
#pragma region Main_Events_Functions
void appNotesEventCb(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t* obj = lv_event_get_target_obj(e);
if (code == LV_EVENT_VALUE_CHANGED) {
if (obj == uiDropDownMenu) {
switch (lv_dropdown_get_selected(obj)) {
case 0: // New
resetFileContent();
break;
case 1: // Save
if (!filePath.empty()) {
lvgl::getSyncLock()->lock();
saveBuffer = lv_textarea_get_text(uiNoteText);
lvgl::getSyncLock()->unlock();
saveFile(filePath);
}
break;
case 2: // Save as...
lvgl::getSyncLock()->lock();
saveBuffer = lv_textarea_get_text(uiNoteText);
lvgl::getSyncLock()->unlock();
saveFileLaunchId = fileselection::startForExistingOrNewFile();
TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId);
break;
case 3: // Load
loadFileLaunchId = fileselection::startForExistingFile();
TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId);
break;
}
} else {
auto* cont = lv_event_get_current_target_obj(e);
if (obj == cont) return;
if (lv_obj_get_child(cont, 1)) {
saveFileLaunchId = fileselection::startForExistingOrNewFile();
TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId);
} else { //Reset
resetFileContent();
}
}
}
}
void resetFileContent() {
lv_textarea_set_text(uiNoteText, "");
filePath = "";
saveBuffer = "";
lv_label_set_text(uiCurrentFileName, "Untitled");
}
#pragma region Open_Events_Functions
void openFile(const std::string& path) {
// We might be reading from the SD card, which could share a SPI bus with other devices (display)
hal::sdcard::withSdCardLock<void>(path, [this, path]() {
auto data = file::readString(path);
if (data != nullptr) {
auto lock = lvgl::getSyncLock()->asScopedLock();
lock.lock();
lv_textarea_set_text(uiNoteText, reinterpret_cast<const char*>(data.get()));
lv_label_set_text(uiCurrentFileName, path.c_str());
filePath = path;
TT_LOG_I(TAG, "Loaded from %s", path.c_str());
}
});
}
bool saveFile(const std::string& path) {
// We might be writing to SD card, which could share a SPI bus with other devices (display)
return hal::sdcard::withSdCardLock<bool>(path, [this, path]() {
if (file::writeString(path, saveBuffer.c_str())) {
TT_LOG_I(TAG, "Saved to %s", path.c_str());
filePath = path;
return true;
} else {
return false;
}
});
}
#pragma endregion Open_Events_Functions
void onShow(AppContext& context, lv_obj_t* parent) override {
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = tt::lvgl::toolbar_create(parent, context);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
uiDropDownMenu = lv_dropdown_create(toolbar);
lv_dropdown_set_options(uiDropDownMenu, LV_SYMBOL_FILE " New File\n" LV_SYMBOL_SAVE " Save\n" LV_SYMBOL_SAVE " Save As...\n" LV_SYMBOL_DIRECTORY " Open File");
lv_dropdown_set_text(uiDropDownMenu, "Menu");
lv_dropdown_set_symbol(uiDropDownMenu, LV_SYMBOL_DOWN);
lv_dropdown_set_selected_highlight(uiDropDownMenu, false);
lv_obj_set_style_border_color(uiDropDownMenu, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
lv_obj_set_style_border_width(uiDropDownMenu, 1, LV_PART_MAIN);
lv_obj_align(uiDropDownMenu, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(uiDropDownMenu,
[](lv_event_t* e) {
auto *self = static_cast<NotesApp *>(lv_event_get_user_data(e));
self->appNotesEventCb(e);
},
LV_EVENT_VALUE_CHANGED,
this
);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_height(wrapper, LV_PCT(100));
lv_obj_set_style_pad_all(wrapper, 0, LV_PART_MAIN);
lv_obj_set_style_pad_row(wrapper, 0, LV_PART_MAIN);
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_remove_flag(wrapper, LV_OBJ_FLAG_SCROLLABLE);
uiNoteText = lv_textarea_create(wrapper);
lv_obj_set_width(uiNoteText, LV_PCT(100));
lv_obj_set_height(uiNoteText, LV_PCT(86));
lv_textarea_set_password_mode(uiNoteText, false);
lv_obj_set_style_bg_color(uiNoteText, lv_color_hex(0x262626), LV_PART_MAIN);
lv_textarea_set_placeholder_text(uiNoteText, "Notes...");
lv_obj_t* footer = lv_obj_create(wrapper);
lv_obj_set_flex_flow(footer, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(footer, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_bg_color(footer, lv_color_hex(0x262626), LV_PART_MAIN);
lv_obj_set_width(footer, LV_PCT(100));
lv_obj_set_height(footer, LV_PCT(14));
lv_obj_set_style_pad_all(footer, 0, LV_PART_MAIN);
lv_obj_set_style_border_width(footer, 0, 0);
lv_obj_remove_flag(footer, LV_OBJ_FLAG_SCROLLABLE);
uiCurrentFileName = lv_label_create(footer);
lv_label_set_long_mode(uiCurrentFileName, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
lv_obj_set_width(uiCurrentFileName, LV_SIZE_CONTENT);
lv_obj_set_height(uiCurrentFileName, LV_SIZE_CONTENT);
lv_label_set_text(uiCurrentFileName, "Untitled");
lv_obj_align(uiCurrentFileName, LV_ALIGN_CENTER, 0, 0);
//TODO: Move this to SD Card?
if (!file::findOrCreateDirectory(context.getPaths()->getDataDirectory(), 0777)) {
TT_LOG_E(TAG, "Failed to find or create path %s", context.getPaths()->getDataDirectory().c_str());
}
lvgl::keyboard_add_textarea(uiNoteText);
}
void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> resultData) override {
TT_LOG_I(TAG, "Result for launch id %d", launchId);
if (launchId == loadFileLaunchId) {
loadFileLaunchId = 0;
if (result == Result::Ok && resultData != nullptr) {
auto path = fileselection::getResultPath(*resultData);
openFile(path);
}
} else if (launchId == saveFileLaunchId) {
saveFileLaunchId = 0;
if (result == Result::Ok && resultData != nullptr) {
auto path = fileselection::getResultPath(*resultData);
// Must re-open file, because UI was cleared after opening other app
if (saveFile(path)) {
openFile(path);
}
}
}
}
};
extern const AppManifest manifest = {
.id = "Notes",
.name = "Notes",
.icon = TT_ASSETS_APP_ICON_NOTES,
.createApp = create<NotesApp>
};
} // namespace tt::app::notes

View File

@ -168,6 +168,8 @@ void ScreenshotApp::createModeSettingWidgets(lv_obj_t* parent) {
modeDropdown = lv_dropdown_create(mode_wrapper);
lv_dropdown_set_options(modeDropdown, "Timer\nApp start");
lv_obj_align_to(modeDropdown, mode_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
lv_obj_set_style_border_color(modeDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
lv_obj_set_style_border_width(modeDropdown, 1, LV_PART_MAIN);
lv_obj_add_event_cb(modeDropdown, onModeSetCallback, LV_EVENT_VALUE_CHANGED, nullptr);
service::screenshot::Mode mode = service->getMode();
if (mode == service::screenshot::Mode::Apps) {

View File

@ -88,7 +88,7 @@ public:
}
}
void onResult(AppContext& app, Result result, std::unique_ptr<Bundle> bundle) override {
void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
if (result == Result::Ok && bundle != nullptr) {
auto name = timezone::getResultName(*bundle);
auto code = timezone::getResultCode(*bundle);

View File

@ -112,7 +112,7 @@ class WifiApSettings : public App {
}
}
void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED Result result, std::unique_ptr<Bundle> bundle) override {
void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED LaunchId launchId, TT_UNUSED Result result, std::unique_ptr<Bundle> bundle) override {
if (result == Result::Ok && bundle != nullptr) {
auto index = alertdialog::getResultIndex(*bundle);
if (index == 0) { // Yes

View File

@ -0,0 +1,13 @@
#include "Tactility/lvgl/Color.h"
lv_color_t lv_color_foreground() {
return lv_color_make(0xFF, 0xFF, 0xFF);
}
lv_color_t lv_color_background() {
return lv_color_make(0x28, 0x2B, 0x30);
}
lv_color_t lv_color_background_darkest() {
return lv_color_make(0x00, 0x00, 0x00);
}

View File

@ -1,14 +1,18 @@
#include <Tactility/lvgl/LabelUtils.h>
#include <Tactility/file/File.h>
#include <Tactility/hal/sdcard/SdCardDevice.h>
namespace tt::lvgl {
#define TAG "tt_lv_label"
bool label_set_text_file(lv_obj_t* label, const char* filepath) {
auto text = file::readString(filepath);
auto text = hal::sdcard::withSdCardLock<std::unique_ptr<uint8_t[]>>(std::string(filepath), [filepath]() {
return file::readString(filepath);
});
if (text != nullptr) {
lv_label_set_text(label, (const char*)text.get());
lv_label_set_text(label, reinterpret_cast<const char*>(text.get()));
return true;
} else {
return false;

View File

@ -47,17 +47,17 @@ static const char* appStateToString(app::State state) {
class LoaderService final : public Service {
private:
std::shared_ptr<PubSub> pubsubExternal = std::make_shared<PubSub>();
Mutex mutex = Mutex(Mutex::Type::Recursive);
std::stack<std::shared_ptr<app::AppInstance>> appStack;
app::LaunchId nextLaunchId = 0;
/** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods.
* This includes full LVGL redraw via Gui::redraw()
*/
std::unique_ptr<DispatcherThread> dispatcherThread = std::make_unique<DispatcherThread>("loader_dispatcher", 6144); // Files app requires ~5k
void onStartAppMessage(const std::string& id, std::shared_ptr<const Bundle> parameters);
void onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters);
void onStopAppMessage(const std::string& id);
void transitionAppToState(const std::shared_ptr<app::AppInstance>& app, app::State state);
@ -75,7 +75,7 @@ public:
});
}
void startApp(const std::string& id, std::shared_ptr<const Bundle> parameters);
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> parameters);
void stopApp();
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
@ -86,8 +86,7 @@ std::shared_ptr<LoaderService> _Nullable optScreenshotService() {
return service::findServiceById<LoaderService>(manifest.id);
}
void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr<const Bundle> parameters) {
void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters) {
TT_LOG_I(TAG, "Start by id %s", id.c_str());
auto app_manifest = app::findAppById(id);
@ -103,7 +102,7 @@ void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr<con
}
auto previous_app = !appStack.empty() ? appStack.top() : nullptr;
auto new_app = std::make_shared<app::AppInstance>(app_manifest, parameters);
auto new_app = std::make_shared<app::AppInstance>(app_manifest, launchId, parameters);
new_app->mutableFlags().showStatusbar = (app_manifest->type != app::Type::Boot);
@ -123,7 +122,6 @@ void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr<con
}
void LoaderService::onStopAppMessage(const std::string& id) {
auto lock = mutex.asScopedLock();
if (!lock.lock(LOADER_TIMEOUT)) {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
@ -157,6 +155,8 @@ void LoaderService::onStopAppMessage(const std::string& id) {
result_set = true;
}
auto app_to_stop_launch_id = app_to_stop->getLaunchId();
transitionAppToState(app_to_stop, app::State::Hiding);
transitionAppToState(app_to_stop, app::State::Stopped);
@ -196,12 +196,14 @@ void LoaderService::onStopAppMessage(const std::string& id) {
if (result_bundle != nullptr) {
instance_to_resume->getApp()->onResult(
*instance_to_resume,
app_to_stop_launch_id,
result,
std::move(result_bundle)
);
} else {
instance_to_resume->getApp()->onResult(
*instance_to_resume,
app_to_stop_launch_id,
result,
nullptr
);
@ -210,6 +212,7 @@ void LoaderService::onStopAppMessage(const std::string& id) {
const Bundle empty_bundle;
instance_to_resume->getApp()->onResult(
*instance_to_resume,
app_to_stop_launch_id,
app::Result::Cancelled,
nullptr
);
@ -255,10 +258,12 @@ void LoaderService::transitionAppToState(const std::shared_ptr<app::AppInstance>
app->setState(state);
}
void LoaderService::startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
dispatcherThread->dispatch([this, id, parameters]() {
onStartAppMessage(id, parameters);
app::LaunchId LoaderService::startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
auto launch_id = nextLaunchId++;
dispatcherThread->dispatch([this, id, launch_id, parameters]() {
onStartAppMessage(id, launch_id, parameters);
});
return launch_id;
}
void LoaderService::stopApp() {
@ -278,11 +283,11 @@ std::shared_ptr<app::AppContext> _Nullable LoaderService::getCurrentAppContext()
// region Public API
void startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
TT_LOG_I(TAG, "Start app %s", id.c_str());
auto service = optScreenshotService();
assert(service);
service->startApp(id, std::move(parameters));
return service->startApp(id, std::move(parameters));
}
void stopApp() {

View File

@ -16,6 +16,8 @@ typedef enum {
typedef void* AppHandle;
typedef unsigned int LaunchId;
/** Important: These function types must map to t::app types exactly */
typedef void* (*AppCreateData)();
typedef void (*AppDestroyData)(void* data);
@ -23,7 +25,7 @@ typedef void (*AppOnCreate)(AppHandle app, void* _Nullable data);
typedef void (*AppOnDestroy)(AppHandle app, void* _Nullable data);
typedef void (*AppOnShow)(AppHandle app, void* _Nullable data, lv_obj_t* parent);
typedef void (*AppOnHide)(AppHandle app, void* _Nullable data);
typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, Result result, BundleHandle resultData);
typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, LaunchId launchId, Result result, BundleHandle resultData);
typedef struct {
/** The application's human-readable name */

View File

@ -12,16 +12,16 @@ void tt_app_register(
) {
#ifdef ESP_PLATFORM
assert((manifest->createData == nullptr) == (manifest->destroyData == nullptr));
tt::app::setElfAppManifest(
setElfAppManifest(
manifest->name,
manifest->icon,
(tt::app::CreateData)manifest->createData,
(tt::app::DestroyData)manifest->destroyData,
(tt::app::OnCreate)manifest->onCreate,
(tt::app::OnDestroy)manifest->onDestroy,
(tt::app::OnShow)manifest->onShow,
(tt::app::OnHide)manifest->onHide,
(tt::app::OnResult)manifest->onResult
manifest->createData,
manifest->destroyData,
manifest->onCreate,
manifest->onDestroy,
manifest->onShow,
manifest->onHide,
reinterpret_cast<tt::app::OnResult>(manifest->onResult)
);
#else
tt_crash("TactilityC is not intended for PC/Simulator");

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