From 84049658dbda32e0b293b96d1b689a6dddb15fc2 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Fri, 12 Sep 2025 16:24:22 +0200 Subject: [PATCH] Merge develop into main (#327) ## New features - Implemented support for app packaging in firmware and `tactility.py`: load `.app` files instead of `.elf` files. Install apps remotely or via `FileBrowser`. - Ensure headless mode works: all services that require LVGL can deal with the absence of a display - Service `onStart()` is now allowed to fail (return `bool` result) - Added and improved various file-related helper functions ## Improvements - Completely revamped the SystemInfo app UI - Improved Calculator UI of internal and external variant - Fix Chat UI and removed the emoji buttons for now - Fix for toolbar bottom padding issue in all apps ## Fixes - Fix for allowing recursive locking for certain SPI SD cards & more --- .gitmodules | 3 + .../CYD-2432S024C/Source/devices/SdCard.cpp | 2 +- .../CYD-2432S028R/Source/devices/SdCard.cpp | 2 +- .../CYD-2432S032C/Source/devices/SdCard.cpp | 2 +- Boards/CYD-E32R28T/Source/devices/SdCard.cpp | 2 +- .../CYD-JC2432W328C/Source/devices/SdCard.cpp | 2 +- .../CYD-JC8048W550C/Source/devices/SdCard.cpp | 2 +- Boards/Simulator/Source/hal/SimulatorSdCard.h | 2 +- CMakeLists.txt | 2 + Documentation/ideas.md | 24 +- Documentation/releasing.md | 2 +- .../Calculator/main/Source/Calculator.cpp | 4 +- ExternalApps/Calculator/manifest.properties | 13 + ExternalApps/Calculator/tactility.properties | 2 - ExternalApps/Calculator/tactility.py | 312 +++++++++++++----- ExternalApps/GraphicsDemo/manifest.properties | 13 + .../GraphicsDemo/tactility.properties | 2 - ExternalApps/GraphicsDemo/tactility.py | 312 +++++++++++++----- ExternalApps/HelloWorld/.gitignore | 2 +- ExternalApps/HelloWorld/assets/message.txt | 1 + ExternalApps/HelloWorld/manifest.properties | 13 + ExternalApps/HelloWorld/tactility.properties | 2 - ExternalApps/HelloWorld/tactility.py | 312 +++++++++++++----- Libraries/minitar/CMakeLists.txt | 22 ++ Libraries/minitar/minitar | 1 + Tactility/CMakeLists.txt | 3 +- Tactility/Include/Tactility/app/App.h | 6 + Tactility/Include/Tactility/app/ElfApp.h | 7 - .../Tactility/app/alertdialog/AlertDialog.h | 7 +- Tactility/Include/Tactility/service/Service.h | 2 +- .../Include/Tactility/service/espnow/EspNow.h | 2 +- .../Tactility/service/gps/GpsService.h | 2 +- .../Include/Tactility/service/loader/Loader.h | 1 + .../app/filebrowser/SupportedFiles.h | 2 +- .../Private/Tactility/app/filebrowser/View.h | 5 +- .../service/development/DevelopmentService.h | 2 +- .../Tactility/service/espnow/EspNowService.h | 2 +- .../Tactility/service/gui/GuiService.h | 15 +- .../Tactility/service/screenshot/Screenshot.h | 4 +- Tactility/Source/Tactility.cpp | 76 ++++- Tactility/Source/app/App.cpp | 3 +- Tactility/Source/app/AppInstall.cpp | 226 +++++++++++++ Tactility/Source/app/AppRegistration.cpp | 8 +- Tactility/Source/app/ElfApp.cpp | 27 +- Tactility/Source/app/addgps/AddGps.cpp | 1 + .../Source/app/alertdialog/AlertDialog.cpp | 8 +- Tactility/Source/app/boot/Boot.cpp | 40 ++- .../Source/app/calculator/Calculator.cpp | 6 +- Tactility/Source/app/chat/ChatApp.cpp | 82 ++--- .../Source/app/development/Development.cpp | 1 + Tactility/Source/app/display/Display.cpp | 1 + .../Source/app/filebrowser/FileBrowser.cpp | 2 +- .../Source/app/filebrowser/SupportedFiles.cpp | 9 +- Tactility/Source/app/filebrowser/View.cpp | 21 +- Tactility/Source/app/fileselection/View.cpp | 1 + Tactility/Source/app/gpio/Gpio.cpp | 1 + .../Source/app/gpssettings/GpsSettings.cpp | 1 + .../Source/app/i2cscanner/I2cScanner.cpp | 1 + .../Source/app/i2csettings/I2cSettings.cpp | 1 + .../app/localesettings/LocaleSettings.cpp | 1 + Tactility/Source/app/log/Log.cpp | 2 + Tactility/Source/app/notes/Notes.cpp | 1 + Tactility/Source/app/power/Power.cpp | 1 + .../Source/app/screenshot/Screenshot.cpp | 2 + .../app/selectiondialog/SelectionDialog.cpp | 2 + .../app/serialconsole/SerialConsole.cpp | 2 + Tactility/Source/app/settings/Settings.cpp | 1 + .../Source/app/systeminfo/SystemInfo.cpp | 103 +++--- .../app/timedatesettings/TimeDateSettings.cpp | 1 + Tactility/Source/app/timezone/TimeZone.cpp | 2 + .../app/wifiapsettings/WifiApSettings.cpp | 2 + Tactility/Source/app/wificonnect/View.cpp | 3 +- Tactility/Source/app/wifimanage/View.cpp | 5 +- Tactility/Source/file/PropertiesFile.cpp | 17 +- .../Source/service/ServiceRegistration.cpp | 11 +- .../development/DevelopmentService.cpp | 59 ++-- .../Source/service/espnow/EspNowService.cpp | 4 +- Tactility/Source/service/gps/GpsService.cpp | 10 +- Tactility/Source/service/gui/GuiService.cpp | 15 +- Tactility/Source/service/loader/Loader.cpp | 3 +- .../Source/service/screenshot/Screenshot.cpp | 16 +- Tactility/Source/service/sdcard/Sdcard.cpp | 11 +- .../Source/service/statusbar/Statusbar.cpp | 9 +- Tactility/Source/service/wifi/WifiEsp.cpp | 7 +- Tactility/Source/service/wifi/WifiMock.cpp | 3 +- TactilityCore/Include/Tactility/file/File.h | 27 +- TactilityCore/Source/file/File.cpp | 75 ++++- 87 files changed, 1490 insertions(+), 537 deletions(-) create mode 100644 ExternalApps/Calculator/manifest.properties delete mode 100644 ExternalApps/Calculator/tactility.properties create mode 100644 ExternalApps/GraphicsDemo/manifest.properties delete mode 100644 ExternalApps/GraphicsDemo/tactility.properties create mode 100644 ExternalApps/HelloWorld/assets/message.txt create mode 100644 ExternalApps/HelloWorld/manifest.properties delete mode 100644 ExternalApps/HelloWorld/tactility.properties create mode 100644 Libraries/minitar/CMakeLists.txt create mode 160000 Libraries/minitar/minitar create mode 100644 Tactility/Source/app/AppInstall.cpp diff --git a/.gitmodules b/.gitmodules index 748090d7..8ec5ac04 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "Libraries/SDL"] path = Libraries/SDL url = https://github.com/libsdl-org/SDL.git +[submodule "Libraries/minitar/minitar"] + path = Libraries/minitar/minitar + url = git@github.com:gabscoarnec/minitar.git diff --git a/Boards/CYD-2432S024C/Source/devices/SdCard.cpp b/Boards/CYD-2432S024C/Source/devices/SdCard.cpp index da3090cc..158d5eab 100644 --- a/Boards/CYD-2432S024C/Source/devices/SdCard.cpp +++ b/Boards/CYD-2432S024C/Source/devices/SdCard.cpp @@ -16,7 +16,7 @@ std::shared_ptr createSdCard() { GPIO_NUM_NC, GPIO_NUM_NC, SdCardDevice::MountBehaviour::AtBoot, - std::make_shared(), + std::make_shared(tt::Mutex::Type::Recursive), std::vector(), SDCARD_SPI_HOST ); diff --git a/Boards/CYD-2432S028R/Source/devices/SdCard.cpp b/Boards/CYD-2432S028R/Source/devices/SdCard.cpp index 0a4cdba0..03a26341 100644 --- a/Boards/CYD-2432S028R/Source/devices/SdCard.cpp +++ b/Boards/CYD-2432S028R/Source/devices/SdCard.cpp @@ -11,7 +11,7 @@ std::shared_ptr createSdCard() { GPIO_NUM_NC, GPIO_NUM_NC, SdCardDevice::MountBehaviour::AtBoot, - std::make_shared(), + std::make_shared(tt::Mutex::Type::Recursive), std::vector(), SPI3_HOST ); diff --git a/Boards/CYD-2432S032C/Source/devices/SdCard.cpp b/Boards/CYD-2432S032C/Source/devices/SdCard.cpp index 0caa946a..01bdd7bb 100644 --- a/Boards/CYD-2432S032C/Source/devices/SdCard.cpp +++ b/Boards/CYD-2432S032C/Source/devices/SdCard.cpp @@ -15,7 +15,7 @@ std::shared_ptr createSdCard() { GPIO_NUM_NC, GPIO_NUM_NC, SdCardDevice::MountBehaviour::AtBoot, - std::make_shared(), + std::make_shared(tt::Mutex::Type::Recursive), std::vector(), SDCARD_SPI_HOST ); diff --git a/Boards/CYD-E32R28T/Source/devices/SdCard.cpp b/Boards/CYD-E32R28T/Source/devices/SdCard.cpp index f9636e39..8b2a210d 100644 --- a/Boards/CYD-E32R28T/Source/devices/SdCard.cpp +++ b/Boards/CYD-E32R28T/Source/devices/SdCard.cpp @@ -10,7 +10,7 @@ std::shared_ptr createSdCard() { GPIO_NUM_NC, GPIO_NUM_NC, SdCardDevice::MountBehaviour::AtBoot, - std::make_shared(), + std::make_shared(tt::Mutex::Type::Recursive), std::vector(), SPI3_HOST ); diff --git a/Boards/CYD-JC2432W328C/Source/devices/SdCard.cpp b/Boards/CYD-JC2432W328C/Source/devices/SdCard.cpp index 0caa946a..01bdd7bb 100644 --- a/Boards/CYD-JC2432W328C/Source/devices/SdCard.cpp +++ b/Boards/CYD-JC2432W328C/Source/devices/SdCard.cpp @@ -15,7 +15,7 @@ std::shared_ptr createSdCard() { GPIO_NUM_NC, GPIO_NUM_NC, SdCardDevice::MountBehaviour::AtBoot, - std::make_shared(), + std::make_shared(tt::Mutex::Type::Recursive), std::vector(), SDCARD_SPI_HOST ); diff --git a/Boards/CYD-JC8048W550C/Source/devices/SdCard.cpp b/Boards/CYD-JC8048W550C/Source/devices/SdCard.cpp index 97d064eb..8a64a5ad 100644 --- a/Boards/CYD-JC8048W550C/Source/devices/SdCard.cpp +++ b/Boards/CYD-JC8048W550C/Source/devices/SdCard.cpp @@ -12,7 +12,7 @@ std::shared_ptr createSdCard() { GPIO_NUM_NC, GPIO_NUM_NC, SdCardDevice::MountBehaviour::AtBoot, - std::make_shared(), + std::make_shared(tt::Mutex::Type::Recursive), std::vector(), SPI2_HOST ); diff --git a/Boards/Simulator/Source/hal/SimulatorSdCard.h b/Boards/Simulator/Source/hal/SimulatorSdCard.h index 6c8fc5a0..3d3bbbaf 100644 --- a/Boards/Simulator/Source/hal/SimulatorSdCard.h +++ b/Boards/Simulator/Source/hal/SimulatorSdCard.h @@ -16,7 +16,7 @@ public: SimulatorSdCard() : SdCardDevice(MountBehaviour::AtBoot), state(State::Unmounted), - lock(std::make_shared()) + lock(std::make_shared(tt::Mutex::Type::Recursive)) {} std::string getName() const override { return "Mock SD Card"; } diff --git a/CMakeLists.txt b/CMakeLists.txt index 50938ae7..c22d6d45 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ if (DEFINED ENV{ESP_IDF_VERSION}) "Libraries/elf_loader" "Libraries/lvgl" "Libraries/lv_screenshot" + "Libraries/minitar" "Libraries/minmea" "Libraries/QRCode" ) @@ -78,6 +79,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION}) add_subdirectory(Libraries/FreeRTOS-Kernel) add_subdirectory(Libraries/lv_screenshot) add_subdirectory(Libraries/QRCode) + add_subdirectory(Libraries/minitar) add_subdirectory(Libraries/minmea) target_compile_definitions(freertos_kernel PUBLIC "projCOVERAGE_TEST=0") target_include_directories(freertos_kernel diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 0656d0c7..69cff13e 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -2,7 +2,6 @@ ## Higher Priority -- System Info should show the version of Tactility - Fix Development service: when no SD card is present, the app fails to install. Consider installing to `/data` Note: Change app install to "transfer file" functionality. We can have a proper install when we have app packaging. Note: Consider installation path option in interface @@ -11,34 +10,30 @@ Create some kind of "intent" handler like on Android. The intent can have an action (e.g. view), a URL and an optional bundle. The manifest can provide the intent handler -- Bug: GraphicsDemo should check if display supports the DisplayDriver interface (and same for touch) and show an AlertDialog error if there's a problem - Update ILI934x to v2.0.1 -- App packaging -- Create an "app install paths" settings app to add/remove paths. - Scan these paths on startup. - Make the AppList use the scan results. -- Apps with update timer should check `lvgl::isStarted()` +- Apps with update timer in onCreate() should check `lvgl::isStarted()` - CrowPanel Basic 3.5": check why GraphicsDemo fails - CrowPanel Basic 3.5": check why System Info doesn't show storage info +- Update to LVGL v9.3 stable +- Files app: delete folder recursively ## Medium Priority +- Statusbar icon that shows low/critical memory warnings +- Make WiFi setup app that starts an access point and hosts a webpage to set up the device. + This will be useful for devices without a screen, a small screen or a non-touch screen. - Unify the way displays are dimmed. Some implementations turn off the display when it's fully dimmed. Make this a separate functionality. - Try out ILI9342 https://github.com/jbrilha/esp_lcd_ili9342 - All drivers (e.g. display, touch, etc.) should call stop() in their destructor, or at least assert that they should not be running. -- Create different partition files for different ESP flash size targets (N4, N8, N16, N32) - Consider a dev variant for quick flashing. - Bug: Turn on WiFi (when testing it wasn't connected/connecting - just active). Open chat. Observe crash. ## Lower Priority - Localize all apps - Support hot-plugging SD card (note: this is not possible if they require the CS pin hack) -- Create more unit tests for `tactility` - Explore LVGL9's FreeRTOS functionality - CrashHandler: use "corrupted" flag - CrashHandler: process other types of crashes (WDT?) -- Add a Keyboard setting in `keyboard.properties` to override the behaviour of soft keyboard hiding (e.g. keyboard hardware is present, but the user wants to use a soft keyboard) - Use GPS time to set/update the current time - 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. - Consider using non_null (either via MS GSL, or custom) @@ -50,21 +45,20 @@ - T-Deck: Use trackball as input device (with optional mouse functionality for LVGL) - Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials. - Remove flex_flow from app_container in Gui.cpp +- Files app: copy/cut/paste actions # Nice-to-haves - Considering the lack of callstack debugging for external apps: allow for some debugging to be exposed during a device crash. Apps could report their state (e.g. an integer value) which can be stored during app operation and retrieve after crash. The same can be done for various OS apps and states. We can keep an array of these numbers to keep track of the last X states, to get an idea of what's going on. -- Give external app a different icon. Allow an external app to update their id, icon, type and name once they are running (and persist that info?). Loader will need to be able to find app by (external) location. - Audio player app - Audio recording app - OTA updates - T-Deck Plus: Create separate board config? - Support for displays with different DPI. Consider the layer-based system like on Android. - If present, use LED to show boot/wifi status -- Capacity based on voltage: estimation for various devices uses a linear voltage curve, but it should use some sort of battery discharge curve. -- Statusbar widget to show how much memory is in use? +- Capacity based on voltage: estimation for various devices uses a linear voltage curve, but it should use a battery discharge curve. - Wrapper for lvgl slider widget that shows "+" and "-" buttons, and also the value in a label. -- Files app: copy/paste actions + Note: consider Spinbox - On crash, try to save the 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. diff --git a/Documentation/releasing.md b/Documentation/releasing.md index 1fffdf06..66a337a0 100644 --- a/Documentation/releasing.md +++ b/Documentation/releasing.md @@ -25,4 +25,4 @@ 1. Mention on Discord 2. Consider notifying vendors/stakeholders -3. Remove dev versions in `sdk.json`from [TactilityTool](https://github.com/ByteWelder/TactilityTool) and upload it to [CDN](https://dash.cloudflare.com) \ No newline at end of file +3. Update SDK updates to CDN at [TactilityTool](https://github.com/ByteWelder/TactilityTool) and upload it to [CDN](https://dash.cloudflare.com) \ No newline at end of file diff --git a/ExternalApps/Calculator/main/Source/Calculator.cpp b/ExternalApps/Calculator/main/Source/Calculator.cpp index b6d0ad67..9a0c161c 100644 --- a/ExternalApps/Calculator/main/Source/Calculator.cpp +++ b/ExternalApps/Calculator/main/Source/Calculator.cpp @@ -145,6 +145,7 @@ void Calculator::resetCalculator() { void Calculator::onShow(AppHandle appHandle, lv_obj_t* parent) { lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); @@ -187,10 +188,9 @@ void Calculator::onShow(AppHandle appHandle, lv_obj_t* parent) { lv_obj_set_style_pad_all(buttonmatrix, 5, LV_PART_MAIN); lv_obj_set_style_pad_row(buttonmatrix, 10, LV_PART_MAIN); lv_obj_set_style_pad_column(buttonmatrix, 5, LV_PART_MAIN); - lv_obj_set_style_border_width(buttonmatrix, 0, LV_PART_MAIN); + lv_obj_set_style_border_width(buttonmatrix, 2, LV_PART_MAIN); lv_obj_set_style_bg_color(buttonmatrix, lv_palette_main(LV_PALETTE_BLUE), LV_PART_ITEMS); - lv_obj_set_style_border_width(buttonmatrix, 0, LV_PART_MAIN); if (lv_display_get_horizontal_resolution(nullptr) <= 240 || lv_display_get_vertical_resolution(nullptr) <= 240) { //small screens lv_obj_set_size(buttonmatrix, lv_pct(100), lv_pct(60)); diff --git a/ExternalApps/Calculator/manifest.properties b/ExternalApps/Calculator/manifest.properties new file mode 100644 index 00000000..c193497f --- /dev/null +++ b/ExternalApps/Calculator/manifest.properties @@ -0,0 +1,13 @@ +[manifest] +version=0.1 +[target] +sdk=0.5.0 +platforms=esp32,esp32s3 +[app] +id=com.bytewelder.calculator +version=0.1.0 +name=Calculator +description=Math is cool +[author] +name=ByteWelder +website=https://bytewelder.com diff --git a/ExternalApps/Calculator/tactility.properties b/ExternalApps/Calculator/tactility.properties deleted file mode 100644 index 8543281b..00000000 --- a/ExternalApps/Calculator/tactility.properties +++ /dev/null @@ -1,2 +0,0 @@ -[sdk] -version = 0.5.0 diff --git a/ExternalApps/Calculator/tactility.py b/ExternalApps/Calculator/tactility.py index a1ede4ea..267b459d 100644 --- a/ExternalApps/Calculator/tactility.py +++ b/ExternalApps/Calculator/tactility.py @@ -8,22 +8,19 @@ import subprocess import time import urllib.request import zipfile - import requests +import tarfile +import shutil +import configparser -# Targetable platforms that represent a specific hardware target -platform_targets = ["esp32", "esp32s3"] -# All valid platform commandline arguments -platform_arguments = platform_targets.copy() -platform_arguments.append("all") ttbuild_path = ".tactility" -ttbuild_version = "1.2.1" -ttbuild_properties_file = "tactility.properties" +ttbuild_version = "2.0.0" ttbuild_cdn = "https://cdn.tactility.one" ttbuild_sdk_json_validity = 3600 # seconds ttport = 6666 verbose = False use_local_sdk = False +valid_platforms = ["esp32", "esp32s3"] spinner_pattern = [ "⠋", @@ -57,21 +54,24 @@ def print_help(): print("Usage: python tactility.py [action] [options]") print("") print("Actions:") - print(" build [esp32,esp32s3,all,local] Build the app for the specified platform") + print(" build [esp32,esp32s3] Build the app. Optionally specify a platform.") print(" esp32: ESP32") print(" esp32s3: ESP32 S3") - print(" all: all supported ESP platforms") - print(" clean Clean the build folders") - print(" clearcache Clear the SDK cache") - print(" updateself Update this tool") - print(" run [ip] [app id] Run an application") - print(" install [ip] [esp32,esp32s3] Install an application") + print(" clean Clean the build folders") + print(" clearcache Clear the SDK cache") + print(" updateself Update this tool") + print(" run [ip] Run an application") + print(" install [ip] Install an application") + print(" bir [ip] [esp32,esp32s3] Build, install then run. Optionally specify a platform.") + print(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.") print("") print("Options:") - print(" --help Show this commandline info") - print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH") - print(" --skip-build Run everything except the idf.py/CMake commands") - print(" --verbose Show extra console output") + print(" --help Show this commandline info") + print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH") + print(" --skip-build Run everything except the idf.py/CMake commands") + print(" --verbose Show extra console output") + +# region Core def download_file(url, filepath): global verbose @@ -108,25 +108,19 @@ def exit_with_error(message): def get_url(ip, path): return f"http://{ip}:{ttport}{path}" -def is_valid_platform_name(name): - global platform_arguments - return name in platform_arguments +def read_properties_file(path): + config = configparser.RawConfigParser() + config.read(path) + return config -def validate_environment(): - global ttbuild_properties_file, use_local_sdk - if os.environ.get("IDF_PATH") is None: - exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") - if not os.path.exists(ttbuild_properties_file): - exit_with_error(f"{ttbuild_properties_file} file not found") - if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: - print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.") - print_warning("If you want to use it, use the 'build local' parameters.") - elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None: - exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.") +#endregion Core -def setup_environment(): - global ttbuild_path - os.makedirs(ttbuild_path, exist_ok=True) +#region SDK helpers + +def read_sdk_json(): + json_file_path = os.path.join(ttbuild_path, "sdk.json") + json_file = open(json_file_path) + return json.load(json_file) def get_sdk_dir(version, platform): global use_local_sdk @@ -136,15 +130,6 @@ def get_sdk_dir(version, platform): global ttbuild_cdn return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK") -def get_sdk_version(): - global ttbuild_properties_file - parser = configparser.RawConfigParser() - parser.read(ttbuild_properties_file) - sdk_dict = dict(parser.items("sdk")) - if not "version" in sdk_dict: - exit_with_error(f"Could not find 'version' in [sdk] section in {ttbuild_properties_file}") - return sdk_dict["version"] - def get_sdk_root_dir(version, platform): global ttbuild_cdn return os.path.join(ttbuild_path, f"{version}-{platform}") @@ -175,20 +160,34 @@ def update_sdk_json(): json_filepath = os.path.join(ttbuild_path, "sdk.json") return download_file(json_url, json_filepath) -def should_fetch_sdkconfig_files(): +def should_fetch_sdkconfig_files(platform_targets): for platform in platform_targets: sdkconfig_filename = f"sdkconfig.app.{platform}" if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)): return True return False -def fetch_sdkconfig_files(): +def fetch_sdkconfig_files(platform_targets): for platform in platform_targets: sdkconfig_filename = f"sdkconfig.app.{platform}" target_path = os.path.join(ttbuild_path, sdkconfig_filename) if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path): exit_with_error(f"Failed to download sdkconfig file for {platform}") +#endregion SDK helpers + +#region Validation + +def validate_environment(): + if os.environ.get("IDF_PATH") is None: + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") + if not os.path.exists("manifest.properties"): + exit_with_error("manifest.properties not found") + if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: + print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.") + print_warning("If you want to use it, use the 'build local' parameters.") + elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None: + exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.") def validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build): version_map = sdk_json["versions"] @@ -217,6 +216,64 @@ def validate_self(sdk_json): print_error("Run 'tactility.py updateself' to update.") sys.exit(1) +#endregion Validation + +#region Manifest + +def read_manifest(): + return read_properties_file("manifest.properties") + +def validate_manifest(manifest): + # [manifest] + if not "manifest" in manifest: + exit_with_error("Invalid manifest format: [manifest] not found") + if not "version" in manifest["manifest"]: + exit_with_error("Invalid manifest format: [manifest] version not found") + # [target] + if not "target" in manifest: + exit_with_error("Invalid manifest format: [target] not found") + if not "sdk" in manifest["target"]: + exit_with_error("Invalid manifest format: [target] sdk not found") + if not "platforms" in manifest["target"]: + exit_with_error("Invalid manifest format: [target] platforms not found") + # [app] + if not "app" in manifest: + exit_with_error("Invalid manifest format: [app] not found") + if not "id" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] id not found") + if not "version" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] version not found") + if not "name" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] name not found") + if not "description" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] description not found") + # [author] + if not "author" in manifest: + exit_with_error("Invalid manifest format: [author] not found") + if not "name" in manifest["author"]: + exit_with_error("Invalid manifest format: [author] name not found") + if not "website" in manifest["author"]: + exit_with_error("Invalid manifest format: [author] website not found") + +def is_valid_manifest_platform(manifest, platform): + manifest_platforms = manifest["target"]["platforms"].split(",") + return platform in manifest_platforms + +def validate_manifest_platform(manifest, platform): + if not is_valid_manifest_platform(manifest, platform): + exit_with_error(f"Platform {platform} is not available in the manifest.") + +def get_manifest_target_platforms(manifest, requested_platform): + if requested_platform == "" or requested_platform is None: + return manifest["target"]["platforms"].split(",") + else: + validate_manifest_platform(manifest, requested_platform) + return [requested_platform] + +#endregion Manifest + +#region SDK download + def sdk_download(version, platform): sdk_root_dir = get_sdk_root_dir(version, platform) os.makedirs(sdk_root_dir, exist_ok=True) @@ -240,12 +297,19 @@ def sdk_download_all(version, platforms): print(f"Using cached download for SDK version {version} and platform {platform}") return True +#endregion SDK download + +#region Building + +def get_cmake_path(platform): + return os.path.join("build", f"cmake-build-{platform}") + def find_elf_file(platform): - build_dir = f"build-{platform}" - if os.path.exists(build_dir): - for file in os.listdir(build_dir): + cmake_dir = get_cmake_path(platform) + if os.path.exists(cmake_dir): + for file in os.listdir(cmake_dir): if file.endswith(".app.elf"): - return os.path.join(build_dir, file) + return os.path.join(cmake_dir, file) return None def build_all(version, platforms, skip_build): @@ -295,7 +359,8 @@ def build_first(version, platform, skip_build): if skip_build: return True print("Building first build") - with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + cmake_path = get_cmake_path(platform) + with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: build_output = wait_for_build(process, platform) # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case if process.returncode == 0: @@ -320,7 +385,8 @@ def build_consecutively(version, platform, skip_build): os.system(f"cp {sdkconfig_path} sdkconfig") if skip_build: return True - with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + cmake_path = get_cmake_path(platform) + with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: build_output = wait_for_build(process, platform) if process.returncode == 0: print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}") @@ -331,41 +397,85 @@ def build_consecutively(version, platform, skip_build): print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}") return False -def read_sdk_json(): - json_file_path = os.path.join(ttbuild_path, "sdk.json") - json_file = open(json_file_path) - return json.load(json_file) +#endregion Building -def build_action(platform_arg): +#region Packaging + +def package_intermediate_manifest(target_path): + if not os.path.isfile("manifest.properties"): + print_error("manifest.properties not found") + return + shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties")) + +def package_intermediate_binaries(target_path, platforms): + elf_dir = os.path.join(target_path, "elf") + os.makedirs(elf_dir, exist_ok=True) + for platform in platforms: + elf_path = find_elf_file(platform) + if elf_path is None: + print_error(f"ELF file not found at {elf_path}") + return + shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}.elf")) + +def package_intermediate_assets(target_path): + if os.path.isdir("assets"): + shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True) + +def package_intermediate(platforms): + target_path = os.path.join("build", "package-intermediate") + if os.path.isdir(target_path): + shutil.rmtree(target_path) + os.makedirs(target_path, exist_ok=True) + package_intermediate_manifest(target_path) + package_intermediate_binaries(target_path, platforms) + package_intermediate_assets(target_path) + +def package_name(platforms): + elf_path = find_elf_file(platforms[0]) + elf_base_name = os.path.basename(elf_path).removesuffix(".app.elf") + return os.path.join("build", f"{elf_base_name}.app") + +def package_all(platforms): + print("Packaging app") + package_intermediate(platforms) + # Create build/something.app + tar_path = package_name(platforms) + tar = tarfile.open(tar_path, mode="w", format=tarfile.USTAR_FORMAT) + tar.add(os.path.join("build", "package-intermediate"), arcname="") + tar.close() + +#endregion Packaging + +def setup_environment(): + global ttbuild_path + os.makedirs(ttbuild_path, exist_ok=True) + +def build_action(manifest, platform_arg): # Environment validation validate_environment() - platforms_to_build = platform_targets if platform_arg == "all" else [platform_arg] - if not is_valid_platform_name(platform_arg): - print_help() - exit_with_error("Invalid platform name") + platforms_to_build = get_manifest_target_platforms(manifest, platform_arg) if not use_local_sdk: - if should_fetch_sdkconfig_files(): - fetch_sdkconfig_files() + if should_fetch_sdkconfig_files(platforms_to_build): + fetch_sdkconfig_files(platforms_to_build) sdk_json = read_sdk_json() validate_self(sdk_json) if not "versions" in sdk_json: exit_with_error("Version data not found in sdk.json") # Build - sdk_version = get_sdk_version() + sdk_version = manifest["target"]["sdk"] if not use_local_sdk: validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build) if not sdk_download_all(sdk_version, platforms_to_build): exit_with_error("Failed to download one or more SDKs") build_all(sdk_version, platforms_to_build, skip_build) # Environment validation + if not skip_build: + package_all(platforms_to_build) def clean_action(): - count = 0 - for path in os.listdir("."): - if path.startswith("build-"): - print(f"Removing {path}/") - shutil.rmtree(path) - count = count + 1 - if count == 0: + if os.path.exists("build"): + print(f"Removing build/") + shutil.rmtree("build") + else: print("Nothing to clean") def clear_cache_action(): @@ -396,7 +506,8 @@ def get_device_info(ip): except requests.RequestException as e: print(f"Request failed: {e}") -def run_action(ip, app_id): +def run_action(manifest, ip): + app_id = manifest["app"]["id"] print(f"Running {app_id} on {ip}") url = get_url(ip, "/app/run") params = {'id': app_id} @@ -409,16 +520,17 @@ def run_action(ip, app_id): except requests.RequestException as e: print(f"Request failed: {e}") -def install_action(ip, platform): - file_path = find_elf_file(platform) - if file_path is None: - print_error(f"File not found: {file_path}") - return - print(f"Installing {file_path} to {ip}") +def install_action(ip, platforms): + for platform in platforms: + elf_path = find_elf_file(platform) + if elf_path is None: + exit_with_error(f"ELF file not built for {platform}") + package_path = package_name(platforms) + print(f"Installing {package_path} to {ip}") url = get_url(ip, "/app/install") try: # Prepare multipart form data - with open(file_path, 'rb') as file: + with open(package_path, 'rb') as file: files = { 'elf': file } @@ -432,6 +544,7 @@ def install_action(ip, platform): except IOError as e: print_error(f"File error: {e}") +#region Main if __name__ == "__main__": print(f"Tactility Build System v{ttbuild_version}") @@ -448,15 +561,23 @@ if __name__ == "__main__": use_local_sdk = "--local-sdk" in sys.argv # Environment setup setup_environment() + if not os.path.isfile("manifest.properties"): + exit_with_error("manifest.properties not found") + manifest = read_manifest() + validate_manifest(manifest) + all_platform_targets = manifest["target"]["platforms"].split(",") # Update SDK cache (sdk.json) if should_update_sdk_json() and not update_sdk_json(): exit_with_error("Failed to retrieve SDK info") # Actions if action_arg == "build": - if len(sys.argv) < 3: + if len(sys.argv) < 2: print_help() exit_with_error("Commandline parameter missing") - build_action(sys.argv[2]) + platform = None + if len(sys.argv) > 2: + platform = sys.argv[2] + build_action(manifest, platform) elif action_arg == "clean": clean_action() elif action_arg == "clearcache": @@ -464,15 +585,34 @@ if __name__ == "__main__": elif action_arg == "updateself": update_self_action() elif action_arg == "run": - if len(sys.argv) < 4: + if len(sys.argv) < 3: print_help() exit_with_error("Commandline parameter missing") - run_action(sys.argv[2], sys.argv[3]) + run_action(manifest, sys.argv[2]) elif action_arg == "install": - if len(sys.argv) < 4: + if len(sys.argv) < 3: print_help() exit_with_error("Commandline parameter missing") - install_action(sys.argv[2], sys.argv[3]) + platform = None + platforms_to_install = all_platform_targets + if len(sys.argv) >= 4: + platform = sys.argv[3] + platforms_to_install = [platform] + install_action(sys.argv[2], platforms_to_install) + elif action_arg == "bir": + if len(sys.argv) < 3: + print_help() + exit_with_error("Commandline parameter missing") + platform = None + platforms_to_install = all_platform_targets + if len(sys.argv) >= 4: + platform = sys.argv[3] + platforms_to_install = [platform] + build_action(manifest, platform) + install_action(sys.argv[2], platforms_to_install) + run_action(manifest, sys.argv[2]) else: print_help() exit_with_error("Unknown commandline parameter") + +#endregion Main diff --git a/ExternalApps/GraphicsDemo/manifest.properties b/ExternalApps/GraphicsDemo/manifest.properties new file mode 100644 index 00000000..aa1ffef7 --- /dev/null +++ b/ExternalApps/GraphicsDemo/manifest.properties @@ -0,0 +1,13 @@ +[manifest] +version=0.1 +[target] +sdk=0.5.0 +platforms=esp32,esp32s3 +[app] +id=com.bytewelder.graphicsdemo +version=0.1.0 +name=Graphics Demo +description=A graphics and touch driver demonstration +[author] +name=ByteWelder +website=https://bytewelder.com diff --git a/ExternalApps/GraphicsDemo/tactility.properties b/ExternalApps/GraphicsDemo/tactility.properties deleted file mode 100644 index 8543281b..00000000 --- a/ExternalApps/GraphicsDemo/tactility.properties +++ /dev/null @@ -1,2 +0,0 @@ -[sdk] -version = 0.5.0 diff --git a/ExternalApps/GraphicsDemo/tactility.py b/ExternalApps/GraphicsDemo/tactility.py index a1ede4ea..267b459d 100644 --- a/ExternalApps/GraphicsDemo/tactility.py +++ b/ExternalApps/GraphicsDemo/tactility.py @@ -8,22 +8,19 @@ import subprocess import time import urllib.request import zipfile - import requests +import tarfile +import shutil +import configparser -# Targetable platforms that represent a specific hardware target -platform_targets = ["esp32", "esp32s3"] -# All valid platform commandline arguments -platform_arguments = platform_targets.copy() -platform_arguments.append("all") ttbuild_path = ".tactility" -ttbuild_version = "1.2.1" -ttbuild_properties_file = "tactility.properties" +ttbuild_version = "2.0.0" ttbuild_cdn = "https://cdn.tactility.one" ttbuild_sdk_json_validity = 3600 # seconds ttport = 6666 verbose = False use_local_sdk = False +valid_platforms = ["esp32", "esp32s3"] spinner_pattern = [ "⠋", @@ -57,21 +54,24 @@ def print_help(): print("Usage: python tactility.py [action] [options]") print("") print("Actions:") - print(" build [esp32,esp32s3,all,local] Build the app for the specified platform") + print(" build [esp32,esp32s3] Build the app. Optionally specify a platform.") print(" esp32: ESP32") print(" esp32s3: ESP32 S3") - print(" all: all supported ESP platforms") - print(" clean Clean the build folders") - print(" clearcache Clear the SDK cache") - print(" updateself Update this tool") - print(" run [ip] [app id] Run an application") - print(" install [ip] [esp32,esp32s3] Install an application") + print(" clean Clean the build folders") + print(" clearcache Clear the SDK cache") + print(" updateself Update this tool") + print(" run [ip] Run an application") + print(" install [ip] Install an application") + print(" bir [ip] [esp32,esp32s3] Build, install then run. Optionally specify a platform.") + print(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.") print("") print("Options:") - print(" --help Show this commandline info") - print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH") - print(" --skip-build Run everything except the idf.py/CMake commands") - print(" --verbose Show extra console output") + print(" --help Show this commandline info") + print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH") + print(" --skip-build Run everything except the idf.py/CMake commands") + print(" --verbose Show extra console output") + +# region Core def download_file(url, filepath): global verbose @@ -108,25 +108,19 @@ def exit_with_error(message): def get_url(ip, path): return f"http://{ip}:{ttport}{path}" -def is_valid_platform_name(name): - global platform_arguments - return name in platform_arguments +def read_properties_file(path): + config = configparser.RawConfigParser() + config.read(path) + return config -def validate_environment(): - global ttbuild_properties_file, use_local_sdk - if os.environ.get("IDF_PATH") is None: - exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") - if not os.path.exists(ttbuild_properties_file): - exit_with_error(f"{ttbuild_properties_file} file not found") - if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: - print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.") - print_warning("If you want to use it, use the 'build local' parameters.") - elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None: - exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.") +#endregion Core -def setup_environment(): - global ttbuild_path - os.makedirs(ttbuild_path, exist_ok=True) +#region SDK helpers + +def read_sdk_json(): + json_file_path = os.path.join(ttbuild_path, "sdk.json") + json_file = open(json_file_path) + return json.load(json_file) def get_sdk_dir(version, platform): global use_local_sdk @@ -136,15 +130,6 @@ def get_sdk_dir(version, platform): global ttbuild_cdn return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK") -def get_sdk_version(): - global ttbuild_properties_file - parser = configparser.RawConfigParser() - parser.read(ttbuild_properties_file) - sdk_dict = dict(parser.items("sdk")) - if not "version" in sdk_dict: - exit_with_error(f"Could not find 'version' in [sdk] section in {ttbuild_properties_file}") - return sdk_dict["version"] - def get_sdk_root_dir(version, platform): global ttbuild_cdn return os.path.join(ttbuild_path, f"{version}-{platform}") @@ -175,20 +160,34 @@ def update_sdk_json(): json_filepath = os.path.join(ttbuild_path, "sdk.json") return download_file(json_url, json_filepath) -def should_fetch_sdkconfig_files(): +def should_fetch_sdkconfig_files(platform_targets): for platform in platform_targets: sdkconfig_filename = f"sdkconfig.app.{platform}" if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)): return True return False -def fetch_sdkconfig_files(): +def fetch_sdkconfig_files(platform_targets): for platform in platform_targets: sdkconfig_filename = f"sdkconfig.app.{platform}" target_path = os.path.join(ttbuild_path, sdkconfig_filename) if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path): exit_with_error(f"Failed to download sdkconfig file for {platform}") +#endregion SDK helpers + +#region Validation + +def validate_environment(): + if os.environ.get("IDF_PATH") is None: + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") + if not os.path.exists("manifest.properties"): + exit_with_error("manifest.properties not found") + if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: + print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.") + print_warning("If you want to use it, use the 'build local' parameters.") + elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None: + exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.") def validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build): version_map = sdk_json["versions"] @@ -217,6 +216,64 @@ def validate_self(sdk_json): print_error("Run 'tactility.py updateself' to update.") sys.exit(1) +#endregion Validation + +#region Manifest + +def read_manifest(): + return read_properties_file("manifest.properties") + +def validate_manifest(manifest): + # [manifest] + if not "manifest" in manifest: + exit_with_error("Invalid manifest format: [manifest] not found") + if not "version" in manifest["manifest"]: + exit_with_error("Invalid manifest format: [manifest] version not found") + # [target] + if not "target" in manifest: + exit_with_error("Invalid manifest format: [target] not found") + if not "sdk" in manifest["target"]: + exit_with_error("Invalid manifest format: [target] sdk not found") + if not "platforms" in manifest["target"]: + exit_with_error("Invalid manifest format: [target] platforms not found") + # [app] + if not "app" in manifest: + exit_with_error("Invalid manifest format: [app] not found") + if not "id" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] id not found") + if not "version" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] version not found") + if not "name" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] name not found") + if not "description" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] description not found") + # [author] + if not "author" in manifest: + exit_with_error("Invalid manifest format: [author] not found") + if not "name" in manifest["author"]: + exit_with_error("Invalid manifest format: [author] name not found") + if not "website" in manifest["author"]: + exit_with_error("Invalid manifest format: [author] website not found") + +def is_valid_manifest_platform(manifest, platform): + manifest_platforms = manifest["target"]["platforms"].split(",") + return platform in manifest_platforms + +def validate_manifest_platform(manifest, platform): + if not is_valid_manifest_platform(manifest, platform): + exit_with_error(f"Platform {platform} is not available in the manifest.") + +def get_manifest_target_platforms(manifest, requested_platform): + if requested_platform == "" or requested_platform is None: + return manifest["target"]["platforms"].split(",") + else: + validate_manifest_platform(manifest, requested_platform) + return [requested_platform] + +#endregion Manifest + +#region SDK download + def sdk_download(version, platform): sdk_root_dir = get_sdk_root_dir(version, platform) os.makedirs(sdk_root_dir, exist_ok=True) @@ -240,12 +297,19 @@ def sdk_download_all(version, platforms): print(f"Using cached download for SDK version {version} and platform {platform}") return True +#endregion SDK download + +#region Building + +def get_cmake_path(platform): + return os.path.join("build", f"cmake-build-{platform}") + def find_elf_file(platform): - build_dir = f"build-{platform}" - if os.path.exists(build_dir): - for file in os.listdir(build_dir): + cmake_dir = get_cmake_path(platform) + if os.path.exists(cmake_dir): + for file in os.listdir(cmake_dir): if file.endswith(".app.elf"): - return os.path.join(build_dir, file) + return os.path.join(cmake_dir, file) return None def build_all(version, platforms, skip_build): @@ -295,7 +359,8 @@ def build_first(version, platform, skip_build): if skip_build: return True print("Building first build") - with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + cmake_path = get_cmake_path(platform) + with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: build_output = wait_for_build(process, platform) # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case if process.returncode == 0: @@ -320,7 +385,8 @@ def build_consecutively(version, platform, skip_build): os.system(f"cp {sdkconfig_path} sdkconfig") if skip_build: return True - with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + cmake_path = get_cmake_path(platform) + with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: build_output = wait_for_build(process, platform) if process.returncode == 0: print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}") @@ -331,41 +397,85 @@ def build_consecutively(version, platform, skip_build): print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}") return False -def read_sdk_json(): - json_file_path = os.path.join(ttbuild_path, "sdk.json") - json_file = open(json_file_path) - return json.load(json_file) +#endregion Building -def build_action(platform_arg): +#region Packaging + +def package_intermediate_manifest(target_path): + if not os.path.isfile("manifest.properties"): + print_error("manifest.properties not found") + return + shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties")) + +def package_intermediate_binaries(target_path, platforms): + elf_dir = os.path.join(target_path, "elf") + os.makedirs(elf_dir, exist_ok=True) + for platform in platforms: + elf_path = find_elf_file(platform) + if elf_path is None: + print_error(f"ELF file not found at {elf_path}") + return + shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}.elf")) + +def package_intermediate_assets(target_path): + if os.path.isdir("assets"): + shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True) + +def package_intermediate(platforms): + target_path = os.path.join("build", "package-intermediate") + if os.path.isdir(target_path): + shutil.rmtree(target_path) + os.makedirs(target_path, exist_ok=True) + package_intermediate_manifest(target_path) + package_intermediate_binaries(target_path, platforms) + package_intermediate_assets(target_path) + +def package_name(platforms): + elf_path = find_elf_file(platforms[0]) + elf_base_name = os.path.basename(elf_path).removesuffix(".app.elf") + return os.path.join("build", f"{elf_base_name}.app") + +def package_all(platforms): + print("Packaging app") + package_intermediate(platforms) + # Create build/something.app + tar_path = package_name(platforms) + tar = tarfile.open(tar_path, mode="w", format=tarfile.USTAR_FORMAT) + tar.add(os.path.join("build", "package-intermediate"), arcname="") + tar.close() + +#endregion Packaging + +def setup_environment(): + global ttbuild_path + os.makedirs(ttbuild_path, exist_ok=True) + +def build_action(manifest, platform_arg): # Environment validation validate_environment() - platforms_to_build = platform_targets if platform_arg == "all" else [platform_arg] - if not is_valid_platform_name(platform_arg): - print_help() - exit_with_error("Invalid platform name") + platforms_to_build = get_manifest_target_platforms(manifest, platform_arg) if not use_local_sdk: - if should_fetch_sdkconfig_files(): - fetch_sdkconfig_files() + if should_fetch_sdkconfig_files(platforms_to_build): + fetch_sdkconfig_files(platforms_to_build) sdk_json = read_sdk_json() validate_self(sdk_json) if not "versions" in sdk_json: exit_with_error("Version data not found in sdk.json") # Build - sdk_version = get_sdk_version() + sdk_version = manifest["target"]["sdk"] if not use_local_sdk: validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build) if not sdk_download_all(sdk_version, platforms_to_build): exit_with_error("Failed to download one or more SDKs") build_all(sdk_version, platforms_to_build, skip_build) # Environment validation + if not skip_build: + package_all(platforms_to_build) def clean_action(): - count = 0 - for path in os.listdir("."): - if path.startswith("build-"): - print(f"Removing {path}/") - shutil.rmtree(path) - count = count + 1 - if count == 0: + if os.path.exists("build"): + print(f"Removing build/") + shutil.rmtree("build") + else: print("Nothing to clean") def clear_cache_action(): @@ -396,7 +506,8 @@ def get_device_info(ip): except requests.RequestException as e: print(f"Request failed: {e}") -def run_action(ip, app_id): +def run_action(manifest, ip): + app_id = manifest["app"]["id"] print(f"Running {app_id} on {ip}") url = get_url(ip, "/app/run") params = {'id': app_id} @@ -409,16 +520,17 @@ def run_action(ip, app_id): except requests.RequestException as e: print(f"Request failed: {e}") -def install_action(ip, platform): - file_path = find_elf_file(platform) - if file_path is None: - print_error(f"File not found: {file_path}") - return - print(f"Installing {file_path} to {ip}") +def install_action(ip, platforms): + for platform in platforms: + elf_path = find_elf_file(platform) + if elf_path is None: + exit_with_error(f"ELF file not built for {platform}") + package_path = package_name(platforms) + print(f"Installing {package_path} to {ip}") url = get_url(ip, "/app/install") try: # Prepare multipart form data - with open(file_path, 'rb') as file: + with open(package_path, 'rb') as file: files = { 'elf': file } @@ -432,6 +544,7 @@ def install_action(ip, platform): except IOError as e: print_error(f"File error: {e}") +#region Main if __name__ == "__main__": print(f"Tactility Build System v{ttbuild_version}") @@ -448,15 +561,23 @@ if __name__ == "__main__": use_local_sdk = "--local-sdk" in sys.argv # Environment setup setup_environment() + if not os.path.isfile("manifest.properties"): + exit_with_error("manifest.properties not found") + manifest = read_manifest() + validate_manifest(manifest) + all_platform_targets = manifest["target"]["platforms"].split(",") # Update SDK cache (sdk.json) if should_update_sdk_json() and not update_sdk_json(): exit_with_error("Failed to retrieve SDK info") # Actions if action_arg == "build": - if len(sys.argv) < 3: + if len(sys.argv) < 2: print_help() exit_with_error("Commandline parameter missing") - build_action(sys.argv[2]) + platform = None + if len(sys.argv) > 2: + platform = sys.argv[2] + build_action(manifest, platform) elif action_arg == "clean": clean_action() elif action_arg == "clearcache": @@ -464,15 +585,34 @@ if __name__ == "__main__": elif action_arg == "updateself": update_self_action() elif action_arg == "run": - if len(sys.argv) < 4: + if len(sys.argv) < 3: print_help() exit_with_error("Commandline parameter missing") - run_action(sys.argv[2], sys.argv[3]) + run_action(manifest, sys.argv[2]) elif action_arg == "install": - if len(sys.argv) < 4: + if len(sys.argv) < 3: print_help() exit_with_error("Commandline parameter missing") - install_action(sys.argv[2], sys.argv[3]) + platform = None + platforms_to_install = all_platform_targets + if len(sys.argv) >= 4: + platform = sys.argv[3] + platforms_to_install = [platform] + install_action(sys.argv[2], platforms_to_install) + elif action_arg == "bir": + if len(sys.argv) < 3: + print_help() + exit_with_error("Commandline parameter missing") + platform = None + platforms_to_install = all_platform_targets + if len(sys.argv) >= 4: + platform = sys.argv[3] + platforms_to_install = [platform] + build_action(manifest, platform) + install_action(sys.argv[2], platforms_to_install) + run_action(manifest, sys.argv[2]) else: print_help() exit_with_error("Unknown commandline parameter") + +#endregion Main diff --git a/ExternalApps/HelloWorld/.gitignore b/ExternalApps/HelloWorld/.gitignore index 89baa26e..eac918a0 100644 --- a/ExternalApps/HelloWorld/.gitignore +++ b/ExternalApps/HelloWorld/.gitignore @@ -1,2 +1,2 @@ -build*/ +build/ .tactility/ diff --git a/ExternalApps/HelloWorld/assets/message.txt b/ExternalApps/HelloWorld/assets/message.txt new file mode 100644 index 00000000..af5626b4 --- /dev/null +++ b/ExternalApps/HelloWorld/assets/message.txt @@ -0,0 +1 @@ +Hello, world! diff --git a/ExternalApps/HelloWorld/manifest.properties b/ExternalApps/HelloWorld/manifest.properties new file mode 100644 index 00000000..beff38c7 --- /dev/null +++ b/ExternalApps/HelloWorld/manifest.properties @@ -0,0 +1,13 @@ +[manifest] +version=0.1 +[target] +sdk=0.5.0 +platforms=esp32,esp32s3 +[app] +id=com.bytewelder.helloworld +version=0.1.0 +name=Hello World +description=A demonstration app that says hi +[author] +name=ByteWelder +website=https://bytewelder.com diff --git a/ExternalApps/HelloWorld/tactility.properties b/ExternalApps/HelloWorld/tactility.properties deleted file mode 100644 index 8543281b..00000000 --- a/ExternalApps/HelloWorld/tactility.properties +++ /dev/null @@ -1,2 +0,0 @@ -[sdk] -version = 0.5.0 diff --git a/ExternalApps/HelloWorld/tactility.py b/ExternalApps/HelloWorld/tactility.py index a1ede4ea..267b459d 100644 --- a/ExternalApps/HelloWorld/tactility.py +++ b/ExternalApps/HelloWorld/tactility.py @@ -8,22 +8,19 @@ import subprocess import time import urllib.request import zipfile - import requests +import tarfile +import shutil +import configparser -# Targetable platforms that represent a specific hardware target -platform_targets = ["esp32", "esp32s3"] -# All valid platform commandline arguments -platform_arguments = platform_targets.copy() -platform_arguments.append("all") ttbuild_path = ".tactility" -ttbuild_version = "1.2.1" -ttbuild_properties_file = "tactility.properties" +ttbuild_version = "2.0.0" ttbuild_cdn = "https://cdn.tactility.one" ttbuild_sdk_json_validity = 3600 # seconds ttport = 6666 verbose = False use_local_sdk = False +valid_platforms = ["esp32", "esp32s3"] spinner_pattern = [ "⠋", @@ -57,21 +54,24 @@ def print_help(): print("Usage: python tactility.py [action] [options]") print("") print("Actions:") - print(" build [esp32,esp32s3,all,local] Build the app for the specified platform") + print(" build [esp32,esp32s3] Build the app. Optionally specify a platform.") print(" esp32: ESP32") print(" esp32s3: ESP32 S3") - print(" all: all supported ESP platforms") - print(" clean Clean the build folders") - print(" clearcache Clear the SDK cache") - print(" updateself Update this tool") - print(" run [ip] [app id] Run an application") - print(" install [ip] [esp32,esp32s3] Install an application") + print(" clean Clean the build folders") + print(" clearcache Clear the SDK cache") + print(" updateself Update this tool") + print(" run [ip] Run an application") + print(" install [ip] Install an application") + print(" bir [ip] [esp32,esp32s3] Build, install then run. Optionally specify a platform.") + print(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.") print("") print("Options:") - print(" --help Show this commandline info") - print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH") - print(" --skip-build Run everything except the idf.py/CMake commands") - print(" --verbose Show extra console output") + print(" --help Show this commandline info") + print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH") + print(" --skip-build Run everything except the idf.py/CMake commands") + print(" --verbose Show extra console output") + +# region Core def download_file(url, filepath): global verbose @@ -108,25 +108,19 @@ def exit_with_error(message): def get_url(ip, path): return f"http://{ip}:{ttport}{path}" -def is_valid_platform_name(name): - global platform_arguments - return name in platform_arguments +def read_properties_file(path): + config = configparser.RawConfigParser() + config.read(path) + return config -def validate_environment(): - global ttbuild_properties_file, use_local_sdk - if os.environ.get("IDF_PATH") is None: - exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") - if not os.path.exists(ttbuild_properties_file): - exit_with_error(f"{ttbuild_properties_file} file not found") - if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: - print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.") - print_warning("If you want to use it, use the 'build local' parameters.") - elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None: - exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.") +#endregion Core -def setup_environment(): - global ttbuild_path - os.makedirs(ttbuild_path, exist_ok=True) +#region SDK helpers + +def read_sdk_json(): + json_file_path = os.path.join(ttbuild_path, "sdk.json") + json_file = open(json_file_path) + return json.load(json_file) def get_sdk_dir(version, platform): global use_local_sdk @@ -136,15 +130,6 @@ def get_sdk_dir(version, platform): global ttbuild_cdn return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK") -def get_sdk_version(): - global ttbuild_properties_file - parser = configparser.RawConfigParser() - parser.read(ttbuild_properties_file) - sdk_dict = dict(parser.items("sdk")) - if not "version" in sdk_dict: - exit_with_error(f"Could not find 'version' in [sdk] section in {ttbuild_properties_file}") - return sdk_dict["version"] - def get_sdk_root_dir(version, platform): global ttbuild_cdn return os.path.join(ttbuild_path, f"{version}-{platform}") @@ -175,20 +160,34 @@ def update_sdk_json(): json_filepath = os.path.join(ttbuild_path, "sdk.json") return download_file(json_url, json_filepath) -def should_fetch_sdkconfig_files(): +def should_fetch_sdkconfig_files(platform_targets): for platform in platform_targets: sdkconfig_filename = f"sdkconfig.app.{platform}" if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)): return True return False -def fetch_sdkconfig_files(): +def fetch_sdkconfig_files(platform_targets): for platform in platform_targets: sdkconfig_filename = f"sdkconfig.app.{platform}" target_path = os.path.join(ttbuild_path, sdkconfig_filename) if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path): exit_with_error(f"Failed to download sdkconfig file for {platform}") +#endregion SDK helpers + +#region Validation + +def validate_environment(): + if os.environ.get("IDF_PATH") is None: + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") + if not os.path.exists("manifest.properties"): + exit_with_error("manifest.properties not found") + if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: + print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.") + print_warning("If you want to use it, use the 'build local' parameters.") + elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None: + exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.") def validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build): version_map = sdk_json["versions"] @@ -217,6 +216,64 @@ def validate_self(sdk_json): print_error("Run 'tactility.py updateself' to update.") sys.exit(1) +#endregion Validation + +#region Manifest + +def read_manifest(): + return read_properties_file("manifest.properties") + +def validate_manifest(manifest): + # [manifest] + if not "manifest" in manifest: + exit_with_error("Invalid manifest format: [manifest] not found") + if not "version" in manifest["manifest"]: + exit_with_error("Invalid manifest format: [manifest] version not found") + # [target] + if not "target" in manifest: + exit_with_error("Invalid manifest format: [target] not found") + if not "sdk" in manifest["target"]: + exit_with_error("Invalid manifest format: [target] sdk not found") + if not "platforms" in manifest["target"]: + exit_with_error("Invalid manifest format: [target] platforms not found") + # [app] + if not "app" in manifest: + exit_with_error("Invalid manifest format: [app] not found") + if not "id" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] id not found") + if not "version" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] version not found") + if not "name" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] name not found") + if not "description" in manifest["app"]: + exit_with_error("Invalid manifest format: [app] description not found") + # [author] + if not "author" in manifest: + exit_with_error("Invalid manifest format: [author] not found") + if not "name" in manifest["author"]: + exit_with_error("Invalid manifest format: [author] name not found") + if not "website" in manifest["author"]: + exit_with_error("Invalid manifest format: [author] website not found") + +def is_valid_manifest_platform(manifest, platform): + manifest_platforms = manifest["target"]["platforms"].split(",") + return platform in manifest_platforms + +def validate_manifest_platform(manifest, platform): + if not is_valid_manifest_platform(manifest, platform): + exit_with_error(f"Platform {platform} is not available in the manifest.") + +def get_manifest_target_platforms(manifest, requested_platform): + if requested_platform == "" or requested_platform is None: + return manifest["target"]["platforms"].split(",") + else: + validate_manifest_platform(manifest, requested_platform) + return [requested_platform] + +#endregion Manifest + +#region SDK download + def sdk_download(version, platform): sdk_root_dir = get_sdk_root_dir(version, platform) os.makedirs(sdk_root_dir, exist_ok=True) @@ -240,12 +297,19 @@ def sdk_download_all(version, platforms): print(f"Using cached download for SDK version {version} and platform {platform}") return True +#endregion SDK download + +#region Building + +def get_cmake_path(platform): + return os.path.join("build", f"cmake-build-{platform}") + def find_elf_file(platform): - build_dir = f"build-{platform}" - if os.path.exists(build_dir): - for file in os.listdir(build_dir): + cmake_dir = get_cmake_path(platform) + if os.path.exists(cmake_dir): + for file in os.listdir(cmake_dir): if file.endswith(".app.elf"): - return os.path.join(build_dir, file) + return os.path.join(cmake_dir, file) return None def build_all(version, platforms, skip_build): @@ -295,7 +359,8 @@ def build_first(version, platform, skip_build): if skip_build: return True print("Building first build") - with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + cmake_path = get_cmake_path(platform) + with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: build_output = wait_for_build(process, platform) # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case if process.returncode == 0: @@ -320,7 +385,8 @@ def build_consecutively(version, platform, skip_build): os.system(f"cp {sdkconfig_path} sdkconfig") if skip_build: return True - with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + cmake_path = get_cmake_path(platform) + with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: build_output = wait_for_build(process, platform) if process.returncode == 0: print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}") @@ -331,41 +397,85 @@ def build_consecutively(version, platform, skip_build): print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}") return False -def read_sdk_json(): - json_file_path = os.path.join(ttbuild_path, "sdk.json") - json_file = open(json_file_path) - return json.load(json_file) +#endregion Building -def build_action(platform_arg): +#region Packaging + +def package_intermediate_manifest(target_path): + if not os.path.isfile("manifest.properties"): + print_error("manifest.properties not found") + return + shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties")) + +def package_intermediate_binaries(target_path, platforms): + elf_dir = os.path.join(target_path, "elf") + os.makedirs(elf_dir, exist_ok=True) + for platform in platforms: + elf_path = find_elf_file(platform) + if elf_path is None: + print_error(f"ELF file not found at {elf_path}") + return + shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}.elf")) + +def package_intermediate_assets(target_path): + if os.path.isdir("assets"): + shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True) + +def package_intermediate(platforms): + target_path = os.path.join("build", "package-intermediate") + if os.path.isdir(target_path): + shutil.rmtree(target_path) + os.makedirs(target_path, exist_ok=True) + package_intermediate_manifest(target_path) + package_intermediate_binaries(target_path, platforms) + package_intermediate_assets(target_path) + +def package_name(platforms): + elf_path = find_elf_file(platforms[0]) + elf_base_name = os.path.basename(elf_path).removesuffix(".app.elf") + return os.path.join("build", f"{elf_base_name}.app") + +def package_all(platforms): + print("Packaging app") + package_intermediate(platforms) + # Create build/something.app + tar_path = package_name(platforms) + tar = tarfile.open(tar_path, mode="w", format=tarfile.USTAR_FORMAT) + tar.add(os.path.join("build", "package-intermediate"), arcname="") + tar.close() + +#endregion Packaging + +def setup_environment(): + global ttbuild_path + os.makedirs(ttbuild_path, exist_ok=True) + +def build_action(manifest, platform_arg): # Environment validation validate_environment() - platforms_to_build = platform_targets if platform_arg == "all" else [platform_arg] - if not is_valid_platform_name(platform_arg): - print_help() - exit_with_error("Invalid platform name") + platforms_to_build = get_manifest_target_platforms(manifest, platform_arg) if not use_local_sdk: - if should_fetch_sdkconfig_files(): - fetch_sdkconfig_files() + if should_fetch_sdkconfig_files(platforms_to_build): + fetch_sdkconfig_files(platforms_to_build) sdk_json = read_sdk_json() validate_self(sdk_json) if not "versions" in sdk_json: exit_with_error("Version data not found in sdk.json") # Build - sdk_version = get_sdk_version() + sdk_version = manifest["target"]["sdk"] if not use_local_sdk: validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build) if not sdk_download_all(sdk_version, platforms_to_build): exit_with_error("Failed to download one or more SDKs") build_all(sdk_version, platforms_to_build, skip_build) # Environment validation + if not skip_build: + package_all(platforms_to_build) def clean_action(): - count = 0 - for path in os.listdir("."): - if path.startswith("build-"): - print(f"Removing {path}/") - shutil.rmtree(path) - count = count + 1 - if count == 0: + if os.path.exists("build"): + print(f"Removing build/") + shutil.rmtree("build") + else: print("Nothing to clean") def clear_cache_action(): @@ -396,7 +506,8 @@ def get_device_info(ip): except requests.RequestException as e: print(f"Request failed: {e}") -def run_action(ip, app_id): +def run_action(manifest, ip): + app_id = manifest["app"]["id"] print(f"Running {app_id} on {ip}") url = get_url(ip, "/app/run") params = {'id': app_id} @@ -409,16 +520,17 @@ def run_action(ip, app_id): except requests.RequestException as e: print(f"Request failed: {e}") -def install_action(ip, platform): - file_path = find_elf_file(platform) - if file_path is None: - print_error(f"File not found: {file_path}") - return - print(f"Installing {file_path} to {ip}") +def install_action(ip, platforms): + for platform in platforms: + elf_path = find_elf_file(platform) + if elf_path is None: + exit_with_error(f"ELF file not built for {platform}") + package_path = package_name(platforms) + print(f"Installing {package_path} to {ip}") url = get_url(ip, "/app/install") try: # Prepare multipart form data - with open(file_path, 'rb') as file: + with open(package_path, 'rb') as file: files = { 'elf': file } @@ -432,6 +544,7 @@ def install_action(ip, platform): except IOError as e: print_error(f"File error: {e}") +#region Main if __name__ == "__main__": print(f"Tactility Build System v{ttbuild_version}") @@ -448,15 +561,23 @@ if __name__ == "__main__": use_local_sdk = "--local-sdk" in sys.argv # Environment setup setup_environment() + if not os.path.isfile("manifest.properties"): + exit_with_error("manifest.properties not found") + manifest = read_manifest() + validate_manifest(manifest) + all_platform_targets = manifest["target"]["platforms"].split(",") # Update SDK cache (sdk.json) if should_update_sdk_json() and not update_sdk_json(): exit_with_error("Failed to retrieve SDK info") # Actions if action_arg == "build": - if len(sys.argv) < 3: + if len(sys.argv) < 2: print_help() exit_with_error("Commandline parameter missing") - build_action(sys.argv[2]) + platform = None + if len(sys.argv) > 2: + platform = sys.argv[2] + build_action(manifest, platform) elif action_arg == "clean": clean_action() elif action_arg == "clearcache": @@ -464,15 +585,34 @@ if __name__ == "__main__": elif action_arg == "updateself": update_self_action() elif action_arg == "run": - if len(sys.argv) < 4: + if len(sys.argv) < 3: print_help() exit_with_error("Commandline parameter missing") - run_action(sys.argv[2], sys.argv[3]) + run_action(manifest, sys.argv[2]) elif action_arg == "install": - if len(sys.argv) < 4: + if len(sys.argv) < 3: print_help() exit_with_error("Commandline parameter missing") - install_action(sys.argv[2], sys.argv[3]) + platform = None + platforms_to_install = all_platform_targets + if len(sys.argv) >= 4: + platform = sys.argv[3] + platforms_to_install = [platform] + install_action(sys.argv[2], platforms_to_install) + elif action_arg == "bir": + if len(sys.argv) < 3: + print_help() + exit_with_error("Commandline parameter missing") + platform = None + platforms_to_install = all_platform_targets + if len(sys.argv) >= 4: + platform = sys.argv[3] + platforms_to_install = [platform] + build_action(manifest, platform) + install_action(sys.argv[2], platforms_to_install) + run_action(manifest, sys.argv[2]) else: print_help() exit_with_error("Unknown commandline parameter") + +#endregion Main diff --git a/Libraries/minitar/CMakeLists.txt b/Libraries/minitar/CMakeLists.txt new file mode 100644 index 00000000..f438afd2 --- /dev/null +++ b/Libraries/minitar/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.20) + +set(C_STANDARD 11) +set(CC_STANDARD_REQUIRED ON) + +if (DEFINED ENV{ESP_IDF_VERSION}) + idf_component_register( + SRC_DIRS "minitar/src/" + INCLUDE_DIRS "minitar/" + ) + +else() + file(GLOB SOURCES "minitar/src/*.*") + file(GLOB HEADERS "minitar/*.h") + + add_library(minitar STATIC) + + target_sources(minitar PRIVATE ${SOURCES}) + include_directories(minitar "minitar/src/") + target_include_directories(minitar PUBLIC "minitar/") + +endif() diff --git a/Libraries/minitar/minitar b/Libraries/minitar/minitar new file mode 160000 index 00000000..78c254ba --- /dev/null +++ b/Libraries/minitar/minitar @@ -0,0 +1 @@ +Subproject commit 78c254ba114f6b66d888149d4ad0eff178dceb88 diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index db26fe3e..75e8b9c3 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) if (DEFINED ENV{ESP_IDF_VERSION}) file(GLOB_RECURSE SOURCE_FILES Source/*.c*) - list(APPEND REQUIRES_LIST TactilityCore lvgl driver elf_loader lv_screenshot QRCode esp_lvgl_port minmea esp_wifi nvs_flash spiffs vfs fatfs lwip esp_http_server) + list(APPEND REQUIRES_LIST TactilityCore lvgl driver elf_loader lv_screenshot QRCode esp_lvgl_port minitar minmea esp_wifi nvs_flash spiffs vfs fatfs lwip esp_http_server) if ("${IDF_TARGET}" STREQUAL "esp32s3") list(APPEND REQUIRES_LIST esp_tinyusb) endif () @@ -55,6 +55,7 @@ else() PUBLIC lvgl PUBLIC lv_screenshot PUBLIC minmea + PUBLIC minitar ) endif() diff --git a/Tactility/Include/Tactility/app/App.h b/Tactility/Include/Tactility/app/App.h index e9808323..bcb11e7b 100644 --- a/Tactility/Include/Tactility/app/App.h +++ b/Tactility/Include/Tactility/app/App.h @@ -94,4 +94,10 @@ std::shared_ptr _Nullable getCurrentAppContext(); /** @return the currently running app (it is only ever null before the splash screen is shown) */ std::shared_ptr _Nullable getCurrentApp(); +std::string getTempPath(); + +std::string getInstallPath(); + +bool install(const std::string& path); + } diff --git a/Tactility/Include/Tactility/app/ElfApp.h b/Tactility/Include/Tactility/app/ElfApp.h index d9a592d8..d68e1b52 100644 --- a/Tactility/Include/Tactility/app/ElfApp.h +++ b/Tactility/Include/Tactility/app/ElfApp.h @@ -26,13 +26,6 @@ void setElfAppManifest( OnResult _Nullable onResult ); -/** - * @return the app ID based on the executable's file path. - */ -std::string getElfAppId(const std::string& filePath); - -void registerElfApp(const std::string& filePath); - std::shared_ptr createElfApp(const std::shared_ptr& manifest); } diff --git a/Tactility/Include/Tactility/app/alertdialog/AlertDialog.h b/Tactility/Include/Tactility/app/alertdialog/AlertDialog.h index 32d439e1..ff04a74e 100644 --- a/Tactility/Include/Tactility/app/alertdialog/AlertDialog.h +++ b/Tactility/Include/Tactility/app/alertdialog/AlertDialog.h @@ -4,6 +4,7 @@ #include #include +#include /** * Start the app by its ID and provide: @@ -18,15 +19,17 @@ namespace tt::app::alertdialog { * @param[in] title the title to show in the toolbar * @param[in] message the message to display * @param[in] buttonLabels the buttons to show + * @return the launch id */ - void start(const std::string& title, const std::string& message, const std::vector& buttonLabels); + LaunchId start(const std::string& title, const std::string& message, const std::vector& buttonLabels); /** * Show a dialog with the provided title, message and an OK button * @param[in] title the title to show in the toolbar * @param[in] message the message to display + * @return the launch id */ - void start(const std::string& title, const std::string& message); + LaunchId start(const std::string& title, const std::string& message); /** * Get the index of the button that the user selected. diff --git a/Tactility/Include/Tactility/service/Service.h b/Tactility/Include/Tactility/service/Service.h index 3c772d55..dfb81b87 100644 --- a/Tactility/Include/Tactility/service/Service.h +++ b/Tactility/Include/Tactility/service/Service.h @@ -21,7 +21,7 @@ public: Service() = default; virtual ~Service() = default; - virtual void onStart(ServiceContext& serviceContext) {} + virtual bool onStart(ServiceContext& serviceContext) { return true; } virtual void onStop(ServiceContext& serviceContext) {} }; diff --git a/Tactility/Include/Tactility/service/espnow/EspNow.h b/Tactility/Include/Tactility/service/espnow/EspNow.h index 0b9dcc5b..044ba9da 100644 --- a/Tactility/Include/Tactility/service/espnow/EspNow.h +++ b/Tactility/Include/Tactility/service/espnow/EspNow.h @@ -31,7 +31,7 @@ struct EspNowConfig { bool longRange, bool encrypt ) : mode(mode), channel(channel), longRange(longRange), encrypt(encrypt) { - memcpy((void*)this->masterKey, (void*)masterKey, 16); + memcpy(this->masterKey, masterKey, 16); } }; diff --git a/Tactility/Include/Tactility/service/gps/GpsService.h b/Tactility/Include/Tactility/service/gps/GpsService.h index 64e37f53..f0314429 100644 --- a/Tactility/Include/Tactility/service/gps/GpsService.h +++ b/Tactility/Include/Tactility/service/gps/GpsService.h @@ -44,7 +44,7 @@ class GpsService final : public Service { public: - void onStart(ServiceContext &serviceContext) override; + bool onStart(ServiceContext &serviceContext) override; void onStop(ServiceContext &serviceContext) override; bool addGpsConfiguration(hal::gps::GpsConfiguration configuration); diff --git a/Tactility/Include/Tactility/service/loader/Loader.h b/Tactility/Include/Tactility/service/loader/Loader.h index 6db1bdf3..f43974b6 100644 --- a/Tactility/Include/Tactility/service/loader/Loader.h +++ b/Tactility/Include/Tactility/service/loader/Loader.h @@ -24,6 +24,7 @@ enum class LoaderEvent{ * @brief Start an app * @param[in] id application name or id * @param[in] parameters optional parameters to pass onto the application + * @return the launch id */ app::LaunchId startApp(const std::string& id, std::shared_ptr _Nullable parameters = nullptr); diff --git a/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h b/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h index 0ae68fbb..563d8bdd 100644 --- a/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h +++ b/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h @@ -4,7 +4,7 @@ namespace tt::app::filebrowser { -bool isSupportedExecutableFile(const std::string& filename); +bool isSupportedAppFile(const std::string& filename); bool isSupportedImageFile(const std::string& filename); bool isSupportedTextFile(const std::string& filename); diff --git a/Tactility/Private/Tactility/app/filebrowser/View.h b/Tactility/Private/Tactility/app/filebrowser/View.h index 2d7f3729..bd1320b2 100644 --- a/Tactility/Private/Tactility/app/filebrowser/View.h +++ b/Tactility/Private/Tactility/app/filebrowser/View.h @@ -16,6 +16,9 @@ class View { lv_obj_t* action_list = nullptr; lv_obj_t* navigate_up_button = nullptr; + std::string installAppPath = { 0 }; + LaunchId installAppLaunchId = 0; + void showActionsForDirectory(); void showActionsForFile(); @@ -36,7 +39,7 @@ public: void onRenamePressed(); void onDeletePressed(); void onDirEntryListScrollBegin(); - void onResult(Result result, std::unique_ptr bundle); + void onResult(LaunchId launchId, Result result, std::unique_ptr bundle); }; } diff --git a/Tactility/Private/Tactility/service/development/DevelopmentService.h b/Tactility/Private/Tactility/service/development/DevelopmentService.h index 4feebf59..3e45ca91 100644 --- a/Tactility/Private/Tactility/service/development/DevelopmentService.h +++ b/Tactility/Private/Tactility/service/development/DevelopmentService.h @@ -55,7 +55,7 @@ public: // region Overrides - void onStart(ServiceContext& service) override; + bool onStart(ServiceContext& service) override; void onStop(ServiceContext& service) override; // endregion Overrides diff --git a/Tactility/Private/Tactility/service/espnow/EspNowService.h b/Tactility/Private/Tactility/service/espnow/EspNowService.h index a93d2b0c..277a1580 100644 --- a/Tactility/Private/Tactility/service/espnow/EspNowService.h +++ b/Tactility/Private/Tactility/service/espnow/EspNowService.h @@ -42,7 +42,7 @@ public: // region Overrides - void onStart(ServiceContext& service) override; + bool onStart(ServiceContext& service) override; void onStop(ServiceContext& service) override; // endregion Overrides diff --git a/Tactility/Private/Tactility/service/gui/GuiService.h b/Tactility/Private/Tactility/service/gui/GuiService.h index 89729f29..47177f0a 100644 --- a/Tactility/Private/Tactility/service/gui/GuiService.h +++ b/Tactility/Private/Tactility/service/gui/GuiService.h @@ -52,18 +52,17 @@ class GuiService : public Service { tt_check(mutex.unlock()); } -public: - - void onStart(TT_UNUSED ServiceContext& service) override; - - void onStop(TT_UNUSED ServiceContext& service) override; - - void requestDraw(); - void showApp(std::shared_ptr app); void hideApp(); +public: + + bool onStart(TT_UNUSED ServiceContext& service) override; + + void onStop(TT_UNUSED ServiceContext& service) override; + + void requestDraw(); /** * Show the on-screen keyboard. diff --git a/Tactility/Private/Tactility/service/screenshot/Screenshot.h b/Tactility/Private/Tactility/service/screenshot/Screenshot.h index d726f024..0e4dd641 100644 --- a/Tactility/Private/Tactility/service/screenshot/Screenshot.h +++ b/Tactility/Private/Tactility/service/screenshot/Screenshot.h @@ -19,14 +19,14 @@ enum class Mode { class ScreenshotService final : public Service { -private: - Mutex mutex; std::unique_ptr task; Mode mode = Mode::None; public: + bool onStart(ServiceContext& serviceContext) override; + bool isTaskStarted(); /** The state of the service. */ diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 87f93a58..50fd3b70 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -1,10 +1,11 @@ #include #include - #include #include +#include +#include +#include #include -#include #include #include #include @@ -12,6 +13,9 @@ #include #include +#include +#include + #ifdef ESP_PLATFORM #include #endif @@ -137,6 +141,67 @@ static void registerSystemApps() { } } +static void registerInstalledApp(std::string path) { + TT_LOG_I(TAG, "Registering app at %s", path.c_str()); + std::string manifest_path = path + "/manifest.properties"; + if (!file::isFile(manifest_path)) { + TT_LOG_E(TAG, "Manifest not found at %s", manifest_path.c_str()); + return; + } + + std::map manifest; + if (!file::loadPropertiesFile(manifest_path, manifest)) { + TT_LOG_E(TAG, "Failed to load manifest at %s", manifest_path.c_str()); + } + + auto app_id_entry = manifest.find("[app]id"); + if (app_id_entry == manifest.end()) { + TT_LOG_E(TAG, "Failed to find app id in manifest"); + return; + } + + auto app_name_entry = manifest.find("[app]name"); + if (app_name_entry == manifest.end()) { + TT_LOG_E(TAG, "Failed to find app name in manifest"); + return; + } + + app::addApp({ + .id = app_id_entry->second, + .name = app_name_entry->second, + .type = app::Type::User, + .location = app::Location::external(path) + }); +} + +static void registerInstalledApps(const std::string& path) { + file::listDirectory(path, [&path](const auto& entry) { + auto absolute_path = std::format("{}/{}", path, entry.d_name); + if (file::isDirectory(absolute_path)) { + registerInstalledApp(absolute_path); + } + }); +} + +static void registerInstalledAppsFromSdCard(const std::shared_ptr& sdcard) { + auto sdcard_root_path = sdcard->getMountPath(); + auto app_path = std::format("{}/apps", sdcard_root_path); + sdcard->getLock()->lock(); + if (file::isDirectory(app_path)) { + registerInstalledApps(app_path); + } + sdcard->getLock()->unlock(); +} + +static void registerInstalledAppsFromSdCards() { + auto sdcard_devices = hal::findDevices(hal::Device::Type::SdCard); + for (const auto& sdcard : sdcard_devices) { + if (sdcard->isMounted()) { + registerInstalledAppsFromSdCard(sdcard); + } + } +} + static void registerUserApps(const std::vector& apps) { TT_LOG_I(TAG, "Registering user apps"); for (auto* manifest : apps) { @@ -176,8 +241,13 @@ static void registerAndStartUserServices(const std::vectorservices); diff --git a/Tactility/Source/app/App.cpp b/Tactility/Source/app/App.cpp index 91ea12e3..806b2be5 100644 --- a/Tactility/Source/app/App.cpp +++ b/Tactility/Source/app/App.cpp @@ -1,5 +1,4 @@ -#include "Tactility/app/App.h" - +#include #include namespace tt::app { diff --git a/Tactility/Source/app/AppInstall.cpp b/Tactility/Source/app/AppInstall.cpp new file mode 100644 index 00000000..20c9db9e --- /dev/null +++ b/Tactility/Source/app/AppInstall.cpp @@ -0,0 +1,226 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +constexpr auto* TAG = "App"; + +namespace tt::app { + +static int untarFile(const minitar_entry* entry, const void* buf, const std::string& destinationPath) { + auto absolute_path = destinationPath + "/" + entry->metadata.path; + if (!file::findOrCreateDirectory(destinationPath, 0777)) return 1; + + int fd = open(absolute_path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644); + if (fd < 0) return 1; + + if (write(fd, buf, entry->metadata.size) < 0) return 1; + + // Note: fchmod() doesn't exist on ESP-IDF and chmod() does nothing on that platform + if (chmod(absolute_path.c_str(), entry->metadata.mode) < 0) return 1; + + return close(fd); +} + +static bool untar_directory(const minitar_entry* entry, const std::string& destinationPath) { + auto absolute_path = destinationPath + "/" + entry->metadata.path; + if (!file::findOrCreateDirectory(absolute_path, 0777)) return false; + return true; +} + +static bool untar(const std::string& tarPath, const std::string& destinationPath) { + minitar mp; + if (minitar_open(tarPath.c_str(), &mp) != 0) { + perror(tarPath.c_str()); + return 1; + } + bool success = true; + minitar_entry entry; + + do { + if (minitar_read_entry(&mp, &entry) == 0) { + TT_LOG_I(TAG, "Extracting %s", entry.metadata.path); + if (entry.metadata.type == MTAR_DIRECTORY) { + if (!strcmp(entry.metadata.name, ".") || !strcmp(entry.metadata.name, "..") || !strcmp(entry.metadata.name, "/")) continue; + if (!untar_directory(&entry, destinationPath)) { + TT_LOG_E(TAG, "Failed to create directory %s/%s: %s", destinationPath.c_str(), entry.metadata.name, strerror(errno)); + success = false; + break; + } + } else if (entry.metadata.type == MTAR_REGULAR) { + auto file_buffer = static_cast(malloc(entry.metadata.size)); + if (!file_buffer) { + TT_LOG_E(TAG, "Failed to allocate %d bytes for file %s", entry.metadata.size, entry.metadata.path);; + success = false; + break; + } + + minitar_read_contents(&mp, &entry, file_buffer, entry.metadata.size); + int status = untarFile(&entry, file_buffer, destinationPath); + free(file_buffer); + if (status != 0) { + TT_LOG_E(TAG, "Failed to extract file %s: %s", entry.metadata.path, strerror(errno)); + success = false; + break; + } + } else if (entry.metadata.type == MTAR_SYMLINK) { + TT_LOG_E(TAG, "SYMLINK not supported"); + } else if (entry.metadata.type == MTAR_HARDLINK) { + TT_LOG_E(TAG, "HARDLINK not supported"); + } else if (entry.metadata.type == MTAR_FIFO) { + TT_LOG_E(TAG, "FIFO not supported"); + } else if (entry.metadata.type == MTAR_BLKDEV) { + TT_LOG_E(TAG, "BLKDEV not supported"); + } else if (entry.metadata.type == MTAR_CHRDEV) { + TT_LOG_E(TAG, "CHRDEV not supported"); + } else { + TT_LOG_E(TAG, "Unknown entry type: %d", entry.metadata.type); + success = false; + break; + } + } else break; + } while (true); + minitar_close(&mp); + return success; +} + +bool findFirstMountedSdCardPath(std::string& path) { + // const auto sdcards = hal::findDevices(hal::Device::Type::SdCard); + bool is_set = false; + hal::findDevices(hal::Device::Type::SdCard, [&is_set, &path](const auto& device) { + if (device->isMounted()) { + path = device->getMountPath(); + is_set = true; + return false; // stop iterating + } else { + return true; + } + }); + return is_set; +} + +std::string getTempPath() { + std::string root_path; + if (!findFirstMountedSdCardPath(root_path)) { + root_path = file::MOUNT_POINT_DATA; + } + return root_path + "/tmp"; +} + +std::string getInstallPath() { + std::string root_path; + if (!findFirstMountedSdCardPath(root_path)) { + root_path = file::MOUNT_POINT_DATA; + } + return root_path + "/apps"; +} + +bool install(const std::string& path) { + // TODO: Make better: lock for each path type properly (source vs target) + + // We lock and unlock frequently because SPI SD card devices share + // the lock with the display. We don't want to lock the display for very long. + + auto app_parent_path = getInstallPath(); + TT_LOG_I(TAG, "Installing app %s to %s", path.c_str(), app_parent_path.c_str()); + + auto lock = file::getLock(app_parent_path)->asScopedLock(); + + lock.lock(); + auto filename = file::getLastPathSegment(path); + const std::string app_target_path = std::format("{}/{}", app_parent_path, filename); + if (file::isDirectory(app_target_path) && !file::deleteRecursively(app_target_path)) { + TT_LOG_W(TAG, "Failed to delete %s", app_target_path.c_str()); + } + lock.unlock(); + + lock.lock(); + if (!file::findOrCreateDirectory(app_target_path, 0777)) { + TT_LOG_I(TAG, "Failed to create directory %s", app_target_path.c_str()); + return false; + } + lock.unlock(); + + lock.lock(); + TT_LOG_I(TAG, "Extracting app from %s to %s", path.c_str(), app_target_path.c_str()); + if (!untar(path, app_target_path)) { + TT_LOG_E(TAG, "Failed to extract"); + return false; + } + lock.unlock(); + + lock.lock(); + auto manifest_path = app_target_path + "/manifest.properties"; + if (!file::isFile(manifest_path)) { + TT_LOG_E(TAG, "Manifest not found at %s", manifest_path.c_str()); + return false; + } + lock.unlock(); + + lock.lock(); + std::map properties; + if (!file::loadPropertiesFile(manifest_path, properties)) { + TT_LOG_E(TAG, "Failed to load manifest at %s", manifest_path.c_str()); + return false; + } + lock.unlock(); + + auto app_id_iterator = properties.find("[app]id"); + if (app_id_iterator == properties.end()) { + TT_LOG_E(TAG, "Failed to find app id in manifest"); + return false; + } + + auto app_name_entry = properties.find("[app]name"); + if (app_name_entry == properties.end()) { + TT_LOG_E(TAG, "Failed to find app name in manifest"); + return false; + } + + lock.lock(); + const std::string renamed_target_path = std::format("{}/{}", app_parent_path, app_id_iterator->second); + if (file::isDirectory(renamed_target_path)) { + if (!file::deleteRecursively(renamed_target_path)) { + TT_LOG_W(TAG, "Failed to delete existing installation at %s", renamed_target_path.c_str()); + return false; + } + } + lock.unlock(); + + lock.lock(); + if (rename(app_target_path.c_str(), renamed_target_path.c_str()) != 0) { + TT_LOG_E(TAG, "Failed to rename %s to %s", app_target_path.c_str(), app_id_iterator->second.c_str()); + return false; + } + lock.unlock(); + + addApp({ + .id = app_id_iterator->second, + .name = app_name_entry->second, + .type = Type::User, + .location = Location::external(renamed_target_path) + }); + + return true; +} + +} // namespace \ No newline at end of file diff --git a/Tactility/Source/app/AppRegistration.cpp b/Tactility/Source/app/AppRegistration.cpp index 514dc04f..5f2a4383 100644 --- a/Tactility/Source/app/AppRegistration.cpp +++ b/Tactility/Source/app/AppRegistration.cpp @@ -20,12 +20,12 @@ void addApp(const AppManifest& manifest) { hash_mutex.lock(); - if (!app_manifest_map.contains(manifest.id)) { - app_manifest_map[manifest.id] = std::make_shared(manifest); - } else { - TT_LOG_E(TAG, "App id in use: %s", manifest.id.c_str()); + if (app_manifest_map.contains(manifest.id)) { + TT_LOG_W(TAG, "Overwriting existing manifest for %s", manifest.id.c_str()); } + app_manifest_map[manifest.id] = std::make_shared(manifest); + hash_mutex.unlock(); } diff --git a/Tactility/Source/app/ElfApp.cpp b/Tactility/Source/app/ElfApp.cpp index 298ad7a4..dfb2bee1 100644 --- a/Tactility/Source/app/ElfApp.cpp +++ b/Tactility/Source/app/ElfApp.cpp @@ -38,7 +38,7 @@ static std::shared_ptr elfManifestLock = std::make_shared(); class ElfApp : public App { - const std::string filePath; + const std::string appPath; std::unique_ptr elfFileData; esp_elf_t elf { .psegment = nullptr, @@ -54,12 +54,13 @@ class ElfApp : public App { std::string lastError = ""; bool startElf() { - TT_LOG_I(TAG, "Starting ELF %s", filePath.c_str()); + const std::string elf_path = std::format("{}/elf/{}.elf", appPath, CONFIG_IDF_TARGET); + TT_LOG_I(TAG, "Starting ELF %s", elf_path.c_str()); assert(elfFileData == nullptr); size_t size = 0; - file::withLock(filePath, [this, &size]{ - elfFileData = file::readBinary(filePath, size); + file::withLock(elf_path, [this, &elf_path, &size]{ + elfFileData = file::readBinary(elf_path, size); }); if (elfFileData == nullptr) { @@ -111,7 +112,7 @@ class ElfApp : public App { public: - explicit ElfApp(std::string filePath) : filePath(std::move(filePath)) {} + explicit ElfApp(std::string appPath) : appPath(std::move(appPath)) {} void onCreate(AppContext& appContext) override { // Because we use global variables, we have to ensure that we are not starting 2 apps in parallel @@ -205,22 +206,6 @@ void setElfAppManifest( elfManifestSetCount++; } -std::string getElfAppId(const std::string& filePath) { - return filePath; -} - -void registerElfApp(const std::string& filePath) { - if (findAppById(filePath) == nullptr) { - auto manifest = AppManifest { - .id = getElfAppId(filePath), - .name = string::removeFileExtension(string::getLastPathSegment(filePath)), - .type = Type::User, - .location = Location::external(filePath) - }; - addApp(manifest); - } -} - std::shared_ptr createElfApp(const std::shared_ptr& manifest) { TT_LOG_I(TAG, "createElfApp"); assert(manifest != nullptr); diff --git a/Tactility/Source/app/addgps/AddGps.cpp b/Tactility/Source/app/addgps/AddGps.cpp index 66eb7d38..1c693958 100644 --- a/Tactility/Source/app/addgps/AddGps.cpp +++ b/Tactility/Source/app/addgps/AddGps.cpp @@ -74,6 +74,7 @@ public: void onShow(AppContext& app, lv_obj_t* parent) final { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lvgl::toolbar_create(parent, app); diff --git a/Tactility/Source/app/alertdialog/AlertDialog.cpp b/Tactility/Source/app/alertdialog/AlertDialog.cpp index 3e2539d2..6b5a2387 100644 --- a/Tactility/Source/app/alertdialog/AlertDialog.cpp +++ b/Tactility/Source/app/alertdialog/AlertDialog.cpp @@ -22,21 +22,21 @@ namespace tt::app::alertdialog { extern const AppManifest manifest; -void start(const std::string& title, const std::string& message, const std::vector& buttonLabels) { +LaunchId start(const std::string& title, const std::string& message, const std::vector& buttonLabels) { std::string items_joined = string::join(buttonLabels, PARAMETER_ITEM_CONCATENATION_TOKEN); auto bundle = std::make_shared(); bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title); bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message); bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_joined); - service::loader::startApp(manifest.id, bundle); + return service::loader::startApp(manifest.id, bundle); } -void start(const std::string& title, const std::string& message) { +LaunchId start(const std::string& title, const std::string& message) { auto bundle = std::make_shared(); bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title); bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message); bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, "OK"); - service::loader::startApp(manifest.id, bundle); + return service::loader::startApp(manifest.id, bundle); } int32_t getResultIndex(const Bundle& bundle) { diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index 65f0d331..6a62e121 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -39,7 +39,9 @@ class BootApp : public App { static void setupDisplay() { const auto hal_display = getHalDisplay(); - assert(hal_display != nullptr); + if (hal_display == nullptr) { + return; + } settings::display::DisplaySettings settings; if (settings::display::load(settings)) { @@ -81,13 +83,27 @@ class BootApp : public App { } static int32_t bootThreadCallback() { + TT_LOG_I(TAG, "Starting boot thread"); const auto start_time = kernel::getTicks(); - kernel::publishSystemEvent(kernel::SystemEvent::BootSplash); + // Give the UI some time to redraw + // If we don't do this, various init calls will read files and block SPI IO for the display + // This would result in a blank/black screen being shown during this phase of the boot process + // This works with 5 ms on a T-Lora Pager, so we give it 10 ms to be safe + TT_LOG_I(TAG, "Delay"); + kernel::delayMillis(10); + // TODO: Support for multiple displays + TT_LOG_I(TAG, "Setup display"); setupDisplay(); // Set backlight + // This event will likely block as other systems are initialized + // e.g. Wi-Fi reads AP configs from SD card + TT_LOG_I(TAG, "Publish event"); + kernel::publishSystemEvent(kernel::SystemEvent::BootSplash); + if (!setupUsbBootMode()) { + TT_LOG_I(TAG, "initFromBootApp"); initFromBootApp(); waitForMinimalSplashDuration(start_time); service::loader::stopApp(); @@ -117,6 +133,17 @@ class BootApp : public App { public: + void onCreate(AppContext& app) override { + // Just in case this app is somehow resumed + if (thread.getState() == Thread::State::Stopped) { + thread.start(); + } + } + + void onDestroy(AppContext& app) override { + thread.join(); + } + void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override { auto* image = lv_image_create(parent); lv_obj_set_size(image, LV_PCT(100), LV_PCT(100)); @@ -128,15 +155,6 @@ public: lv_image_set_src(image, logo_path.c_str()); lvgl::obj_set_style_bg_blacken(parent); - - // Just in case this app is somehow resumed - if (thread.getState() == Thread::State::Stopped) { - thread.start(); - } - } - - void onDestroy(AppContext& app) override { - thread.join(); } }; diff --git a/Tactility/Source/app/calculator/Calculator.cpp b/Tactility/Source/app/calculator/Calculator.cpp index 6294be53..431332d8 100644 --- a/Tactility/Source/app/calculator/Calculator.cpp +++ b/Tactility/Source/app/calculator/Calculator.cpp @@ -166,8 +166,9 @@ class CalculatorApp : public App { 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_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); - lv_obj_t* toolbar = tt::lvgl::toolbar_create(parent, context); + lv_obj_t* toolbar = lvgl::toolbar_create(parent, context); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_t* wrapper = lv_obj_create(parent); @@ -208,10 +209,9 @@ class CalculatorApp : public App { lv_obj_set_style_pad_all(btnm, 5, LV_PART_MAIN); 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_border_width(btnm, 2, 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 lv_obj_set_size(btnm, lv_pct(100), lv_pct(60)); } else { //large screens diff --git a/Tactility/Source/app/chat/ChatApp.cpp b/Tactility/Source/app/chat/ChatApp.cpp index 726e83bb..5bd2d1ed 100644 --- a/Tactility/Source/app/chat/ChatApp.cpp +++ b/Tactility/Source/app/chat/ChatApp.cpp @@ -33,28 +33,15 @@ class ChatApp : public App { lv_obj_scroll_to_y(msg_list, lv_obj_get_scroll_y(msg_list) + 20, LV_ANIM_ON); } - static void onQuickSendClicked(lv_event_t* e) { - auto* self = static_cast(lv_event_get_user_data(e)); - auto* btn = static_cast(lv_event_get_target(e)); - const char* message = lv_label_get_text(lv_obj_get_child(btn, 0)); - - if (message) { - self->addMessage(message); - if (!service::espnow::send(BROADCAST_ADDRESS, (const uint8_t*)message, strlen(message))) { - TT_LOG_E(TAG, "Failed to send message"); - } - } - } - static void onSendClicked(lv_event_t* e) { auto* self = static_cast(lv_event_get_user_data(e)); auto* msg = lv_textarea_get_text(self->input_field); - auto msg_len = strlen(msg); + const auto msg_len = strlen(msg); if (self->msg_list && msg && msg_len) { self->addMessage(msg); - if (!service::espnow::send(BROADCAST_ADDRESS, (const uint8_t*)msg, msg_len)) { + if (!service::espnow::send(BROADCAST_ADDRESS, reinterpret_cast(msg), msg_len)) { TT_LOG_E(TAG, "Failed to send message"); } @@ -64,14 +51,14 @@ class ChatApp : public App { void onReceive(const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length) { // Append \0 to make it a string - char* buffer = (char*)malloc(length + 1); + auto buffer = static_cast(malloc(length + 1)); memcpy(buffer, data, length); buffer[length] = 0x00; - std::string message_prefixed = std::string("Received: ") + buffer; + const std::string message_prefixed = std::string("Received: ") + buffer; - tt::lvgl::getSyncLock()->lock(); + lvgl::getSyncLock()->lock(); addMessage(message_prefixed.c_str()); - tt::lvgl::getSyncLock()->unlock(); + lvgl::getSyncLock()->unlock(); free(buffer); } @@ -82,7 +69,7 @@ public: // TODO: Move this to a configuration screen/app static const uint8_t key[ESP_NOW_KEY_LEN] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; auto config = service::espnow::EspNowConfig( - (uint8_t*)key, + const_cast(key), service::espnow::Mode::Station, 1, false, @@ -105,58 +92,39 @@ public: } void onShow(AppContext& context, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); - // Create toolbar - auto* toolbar = tt::lvgl::toolbar_create(parent, context); - lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); - const int toolbar_height = lv_obj_get_height(toolbar); + lvgl::toolbar_create(parent, context); // Message list 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_flex_grow(msg_list, 1); + lv_obj_set_width(msg_list, LV_PCT(100)); + lv_obj_set_flex_grow(msg_list, 1); 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); - - // Quick message panel - auto* quick_panel = lv_obj_create(parent); - lv_obj_set_size(quick_panel, lv_pct(20), lv_pct(58)); - lv_obj_align(quick_panel, LV_ALIGN_TOP_RIGHT, -5, toolbar_height + 15); // Adjusted to match - lv_obj_set_flex_flow(quick_panel, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(quick_panel, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_all(quick_panel, 5, 0); - - // Quick message buttons - const char* quick_msgs[] = {":-)", "+1", ":-("}; - for (size_t i = 0; i < sizeof(quick_msgs)/sizeof(quick_msgs[0]); i++) { - lv_obj_t* btn = lv_btn_create(quick_panel); - lv_obj_set_size(btn, lv_pct(75), 25); - lv_obj_add_event_cb(btn, onQuickSendClicked, LV_EVENT_CLICKED, this); - - lv_obj_t* label = lv_label_create(btn); - lv_label_set_text(label, quick_msgs[i]); - lv_obj_center(label); - } + lv_obj_set_style_pad_ver(msg_list, 0, 0); + lv_obj_set_style_pad_hor(msg_list, 4, 0); // Input panel - auto* input_panel = lv_obj_create(parent); - lv_obj_set_size(input_panel, lv_pct(95), 60); - lv_obj_align(input_panel, LV_ALIGN_BOTTOM_MID, 0, -5); - lv_obj_set_flex_flow(input_panel, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(input_panel, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_all(input_panel, 5, 0); + 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_pad_all(bottom_wrapper, 0, 0); + lv_obj_set_style_pad_column(bottom_wrapper, 4, 0); + lv_obj_set_style_border_opa(bottom_wrapper, 0, LV_STATE_DEFAULT); // Input field - input_field = lv_textarea_create(input_panel); + input_field = lv_textarea_create(bottom_wrapper); lv_obj_set_flex_grow(input_field, 1); - lv_obj_set_height(input_field, LV_PCT(100)); lv_textarea_set_placeholder_text(input_field, "Type a message..."); lv_textarea_set_one_line(input_field, true); // Send button - auto* send_btn = lv_btn_create(input_panel); - lv_obj_set_size(send_btn, 80, LV_PCT(100)); + auto* send_btn = lv_button_create(bottom_wrapper); + lv_obj_set_style_margin_all(send_btn, 0, LV_STATE_DEFAULT); + lv_obj_set_style_margin_top(send_btn, 2, LV_STATE_DEFAULT); // Hack to fix alignment lv_obj_add_event_cb(send_btn, onSendClicked, LV_EVENT_CLICKED, this); auto* btn_label = lv_label_create(send_btn); diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp index 3089e73a..afe78a53 100644 --- a/Tactility/Source/app/development/Development.cpp +++ b/Tactility/Source/app/development/Development.cpp @@ -91,6 +91,7 @@ public: void onShow(AppContext& app, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); // Toolbar lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); diff --git a/Tactility/Source/app/display/Display.cpp b/Tactility/Source/app/display/Display.cpp index b853f80c..7280eb83 100644 --- a/Tactility/Source/app/display/Display.cpp +++ b/Tactility/Source/app/display/Display.cpp @@ -67,6 +67,7 @@ public: displaySettings = settings::display::loadOrGetDefault(); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); auto hal_display = getHalDisplay(); assert(hal_display != nullptr); diff --git a/Tactility/Source/app/filebrowser/FileBrowser.cpp b/Tactility/Source/app/filebrowser/FileBrowser.cpp index e8ed7ebd..e2e4c6f8 100644 --- a/Tactility/Source/app/filebrowser/FileBrowser.cpp +++ b/Tactility/Source/app/filebrowser/FileBrowser.cpp @@ -28,7 +28,7 @@ public: } void onResult(AppContext& appContext, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr bundle) override { - view->onResult(result, std::move(bundle)); + view->onResult(launchId, result, std::move(bundle)); } }; diff --git a/Tactility/Source/app/filebrowser/SupportedFiles.cpp b/Tactility/Source/app/filebrowser/SupportedFiles.cpp index 953c9a86..09ad198a 100644 --- a/Tactility/Source/app/filebrowser/SupportedFiles.cpp +++ b/Tactility/Source/app/filebrowser/SupportedFiles.cpp @@ -5,13 +5,8 @@ namespace tt::app::filebrowser { constexpr auto* TAG = "FileBrowser"; -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 isSupportedAppFile(const std::string& filename) { + return filename.ends_with(".app"); } bool isSupportedImageFile(const std::string& filename) { diff --git a/Tactility/Source/app/filebrowser/View.cpp b/Tactility/Source/app/filebrowser/View.cpp index 1dc4f35e..2406c237 100644 --- a/Tactility/Source/app/filebrowser/View.cpp +++ b/Tactility/Source/app/filebrowser/View.cpp @@ -86,11 +86,12 @@ void View::viewFile(const std::string& path, const std::string& filename) { TT_LOG_I(TAG, "Clicked %s", file_path.c_str()); - if (isSupportedExecutableFile(filename)) { + if (isSupportedAppFile(filename)) { #ifdef ESP_PLATFORM - registerElfApp(processed_filepath); - auto app_id = getElfAppId(processed_filepath); - service::loader::startApp(app_id); + // install(filename); + auto message = std::format("Do you want to install {}?", filename); + installAppPath = processed_filepath; + installAppLaunchId = alertdialog::start("Install?", message, { "Yes", "No" }); #endif } else if (isSupportedImageFile(filename)) { imageviewer::start(processed_filepath); @@ -253,6 +254,7 @@ void View::update() { void View::init(lv_obj_t* parent) { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); auto* toolbar = lvgl::toolbar_create(parent, "Files"); navigate_up_button = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this); @@ -292,11 +294,20 @@ void View::onNavigate() { } } -void View::onResult(Result result, std::unique_ptr bundle) { +void View::onResult(LaunchId launchId, Result result, std::unique_ptr bundle) { if (result != Result::Ok || bundle == nullptr) { return; } + if ( + launchId == installAppLaunchId && + result == Result::Ok && + alertdialog::getResultIndex(*bundle) == 0 + ) { + install(installAppPath); + return; + } + std::string filepath = state->getSelectedChildPath(); TT_LOG_I(TAG, "Result for %s", filepath.c_str()); diff --git a/Tactility/Source/app/fileselection/View.cpp b/Tactility/Source/app/fileselection/View.cpp index dafa487a..ece9d28c 100644 --- a/Tactility/Source/app/fileselection/View.cpp +++ b/Tactility/Source/app/fileselection/View.cpp @@ -175,6 +175,7 @@ void View::update() { void View::init(lv_obj_t* parent, Mode mode) { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); auto* toolbar = lvgl::toolbar_create(parent, "Select File"); navigate_up_button = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this); diff --git a/Tactility/Source/app/gpio/Gpio.cpp b/Tactility/Source/app/gpio/Gpio.cpp index 88abddd4..96df2861 100644 --- a/Tactility/Source/app/gpio/Gpio.cpp +++ b/Tactility/Source/app/gpio/Gpio.cpp @@ -104,6 +104,7 @@ void GpioApp::stopTask() { void GpioApp::onShow(AppContext& app, lv_obj_t* parent) { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); auto* toolbar = lvgl::toolbar_create(parent, app); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); diff --git a/Tactility/Source/app/gpssettings/GpsSettings.cpp b/Tactility/Source/app/gpssettings/GpsSettings.cpp index 250c87be..62a1d341 100644 --- a/Tactility/Source/app/gpssettings/GpsSettings.cpp +++ b/Tactility/Source/app/gpssettings/GpsSettings.cpp @@ -276,6 +276,7 @@ public: void onShow(AppContext& app, lv_obj_t* parent) final { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); auto* toolbar = lvgl::toolbar_create(parent, app); diff --git a/Tactility/Source/app/i2cscanner/I2cScanner.cpp b/Tactility/Source/app/i2cscanner/I2cScanner.cpp index 8c4c977e..aba780a3 100644 --- a/Tactility/Source/app/i2cscanner/I2cScanner.cpp +++ b/Tactility/Source/app/i2cscanner/I2cScanner.cpp @@ -95,6 +95,7 @@ int32_t I2cScannerApp::getLastBusIndex() { void I2cScannerApp::onShow(AppContext& app, lv_obj_t* parent) { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lvgl::toolbar_create(parent, app); diff --git a/Tactility/Source/app/i2csettings/I2cSettings.cpp b/Tactility/Source/app/i2csettings/I2cSettings.cpp index 08c10bcb..4de39ce5 100644 --- a/Tactility/Source/app/i2csettings/I2cSettings.cpp +++ b/Tactility/Source/app/i2csettings/I2cSettings.cpp @@ -75,6 +75,7 @@ class I2cSettingsApp : public App { void onShow(AppContext& app, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lvgl::toolbar_create(parent, app); auto* wrapper = lv_obj_create(parent); diff --git a/Tactility/Source/app/localesettings/LocaleSettings.cpp b/Tactility/Source/app/localesettings/LocaleSettings.cpp index cb4bb560..cd50b16c 100644 --- a/Tactility/Source/app/localesettings/LocaleSettings.cpp +++ b/Tactility/Source/app/localesettings/LocaleSettings.cpp @@ -85,6 +85,7 @@ public: textResources.load(); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lvgl::toolbar_create(parent, app); diff --git a/Tactility/Source/app/log/Log.cpp b/Tactility/Source/app/log/Log.cpp index f2877e05..6a957a3e 100644 --- a/Tactility/Source/app/log/Log.cpp +++ b/Tactility/Source/app/log/Log.cpp @@ -74,6 +74,8 @@ public: void onShow(AppContext& app, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + auto* toolbar = lvgl::toolbar_create(parent, app); lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_EDIT, onLevelFilterPressedCallback, this); diff --git a/Tactility/Source/app/notes/Notes.cpp b/Tactility/Source/app/notes/Notes.cpp index e26e6bf6..949e7cd8 100644 --- a/Tactility/Source/app/notes/Notes.cpp +++ b/Tactility/Source/app/notes/Notes.cpp @@ -123,6 +123,7 @@ class NotesApp : public App { 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_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lv_obj_t* toolbar = lvgl::toolbar_create(parent, context); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); diff --git a/Tactility/Source/app/power/Power.cpp b/Tactility/Source/app/power/Power.cpp index 594fab5a..af541599 100644 --- a/Tactility/Source/app/power/Power.cpp +++ b/Tactility/Source/app/power/Power.cpp @@ -144,6 +144,7 @@ public: void onShow(AppContext& app, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lvgl::toolbar_create(parent, app); diff --git a/Tactility/Source/app/screenshot/Screenshot.cpp b/Tactility/Source/app/screenshot/Screenshot.cpp index f19c8b30..c7334c87 100644 --- a/Tactility/Source/app/screenshot/Screenshot.cpp +++ b/Tactility/Source/app/screenshot/Screenshot.cpp @@ -255,6 +255,8 @@ void ScreenshotApp::onShow(AppContext& appContext, lv_obj_t* parent) { } lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + auto* toolbar = lvgl::toolbar_create(parent, appContext); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); diff --git a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp index 16c8bec4..cc24a0de 100644 --- a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp +++ b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp @@ -73,6 +73,8 @@ public: void onShow(AppContext& app, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + std::string title = getTitleParameter(app.getParameters()); lvgl::toolbar_create(parent, title); diff --git a/Tactility/Source/app/serialconsole/SerialConsole.cpp b/Tactility/Source/app/serialconsole/SerialConsole.cpp index 782dde31..667df8d1 100644 --- a/Tactility/Source/app/serialconsole/SerialConsole.cpp +++ b/Tactility/Source/app/serialconsole/SerialConsole.cpp @@ -61,6 +61,8 @@ public: void onShow(AppContext& app, lv_obj_t* parent) final { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + auto* toolbar = lvgl::toolbar_create(parent, app); disconnectButton = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_POWER, onDisconnectPressed, this); diff --git a/Tactility/Source/app/settings/Settings.cpp b/Tactility/Source/app/settings/Settings.cpp index b276b4c8..8dea762c 100644 --- a/Tactility/Source/app/settings/Settings.cpp +++ b/Tactility/Source/app/settings/Settings.cpp @@ -27,6 +27,7 @@ class SettingsApp : public App { void onShow(AppContext& app, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lvgl::toolbar_create(parent, app); diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index 640396b7..a4d8f4dd 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -84,17 +85,17 @@ static std::string getStorageUnitString(StorageUnit unit) { } } -static uint64_t getStorageValue(StorageUnit unit, uint64_t bytes) { +static std::string getStorageValue(StorageUnit unit, uint64_t bytes) { using enum StorageUnit; switch (unit) { case Bytes: - return bytes; + return std::to_string(bytes); case Kilobytes: - return bytes / 1024; + return std::to_string(bytes / 1024); case Megabytes: - return bytes / 1024 / 1024; + return std::format("{:.1f}", static_cast(bytes) / 1024.f / 1024.f); case Gigabytes: - return bytes / 1024 / 1024 / 1024; + return std::format("{:.1f}", static_cast(bytes) / 1024.f / 1024.f / 1024.f); default: std::unreachable(); } @@ -107,6 +108,7 @@ static void addMemoryBar(lv_obj_t* parent, const char* label, uint64_t free, uin lv_obj_set_style_pad_all(container, 0, 0); lv_obj_set_style_border_width(container, 0, 0); lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW); + lv_obj_set_style_bg_opa(container, 0, LV_STATE_DEFAULT); auto* left_label = lv_label_create(container); lv_label_set_text(left_label, label); @@ -115,22 +117,31 @@ static void addMemoryBar(lv_obj_t* parent, const char* label, uint64_t free, uin auto* bar = lv_bar_create(container); lv_obj_set_flex_grow(bar, 1); + // Scale down the uint64_t until it fits int32_t for the lv_bar + uint64_t free_scaled = free; + uint64_t total_scaled = total; + while (total_scaled > static_cast(INT32_MAX)) { + free_scaled /= 1024; + total_scaled /= 1024; + } + if (total > 0) { - lv_bar_set_range(bar, 0, (int32_t)total); + lv_bar_set_range(bar, 0, total_scaled); } else { lv_bar_set_range(bar, 0, 1); } - lv_bar_set_value(bar, (int32_t)used, LV_ANIM_OFF); + lv_bar_set_value(bar, (total_scaled - free_scaled), LV_ANIM_OFF); auto* bottom_label = lv_label_create(parent); const auto unit = getStorageUnit(total); const auto unit_label = getStorageUnitString(unit); const auto used_converted = getStorageValue(unit, used); const auto total_converted = getStorageValue(unit, total); - lv_label_set_text_fmt(bottom_label, "%llu / %llu %s", used_converted, total_converted, unit_label.c_str()); + lv_label_set_text_fmt(bottom_label, "%s / %s %s used", used_converted.c_str(), total_converted.c_str(), unit_label.c_str()); lv_obj_set_width(bottom_label, LV_PCT(100)); lv_obj_set_style_text_align(bottom_label, LV_TEXT_ALIGN_RIGHT, 0); + lv_obj_set_style_pad_bottom(bottom_label, 12, LV_STATE_DEFAULT); } #if configUSE_TRACE_FACILITY @@ -164,6 +175,7 @@ static void addRtosTasks(lv_obj_t* parent) { auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); uint32_t totalRuntime = 0; UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime); + for (int i = 0; i < actual; ++i) { const TaskStatus_t& task = tasks[i]; addRtosTask(parent, task); @@ -189,6 +201,7 @@ class SystemInfoApp : public App { void onShow(AppContext& app, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lvgl::toolbar_create(parent, app); // This wrapper automatically has its children added vertically underneath eachother @@ -197,43 +210,54 @@ class SystemInfoApp : public App { lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_style_pad_all(wrapper, 0, LV_STATE_DEFAULT); - // Wrapper for the memory usage bars - auto* memory_label = lv_label_create(wrapper); - lv_label_set_text(memory_label, "Memory usage"); - auto* memory_wrapper = lv_obj_create(wrapper); - lv_obj_set_flex_flow(memory_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_size(memory_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + auto* tabview = lv_tabview_create(wrapper); + lv_tabview_set_tab_bar_position(tabview, LV_DIR_LEFT); + lv_tabview_set_tab_bar_size(tabview, 80); - addMemoryBar(memory_wrapper, "Internal", getHeapFree(), getHeapTotal()); + // Tabs + + auto* memory_tab = lv_tabview_add_tab(tabview, "Memory"); + lv_obj_set_flex_flow(memory_tab, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(memory_tab, 0, LV_STATE_DEFAULT); + auto* storage_tab = lv_tabview_add_tab(tabview, "Storage"); + lv_obj_set_flex_flow(storage_tab, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(storage_tab, 0, LV_STATE_DEFAULT); + auto* tasks_tab = lv_tabview_add_tab(tabview, "Tasks"); + lv_obj_set_flex_flow(tasks_tab, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(tasks_tab, 4, LV_STATE_DEFAULT); + auto* devices_tab = lv_tabview_add_tab(tabview, "Devices"); + lv_obj_set_flex_flow(devices_tab, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(devices_tab, 4, LV_STATE_DEFAULT); + auto* about_tab = lv_tabview_add_tab(tabview, "About"); + lv_obj_set_flex_flow(about_tab, LV_FLEX_FLOW_COLUMN); + + // Memory tab content + + addMemoryBar(memory_tab, "Internal", getHeapFree(), getHeapTotal()); if (getSpiTotal() > 0) { - addMemoryBar(memory_wrapper, "External", getSpiFree(), getSpiTotal()); + addMemoryBar(memory_tab, "External", getSpiFree(), getSpiTotal()); } #ifdef ESP_PLATFORM // Wrapper for the memory usage bars - auto* storage_label = lv_label_create(wrapper); - lv_label_set_text(storage_label, "Storage usage"); - auto* storage_wrapper = lv_obj_create(wrapper); - lv_obj_set_flex_flow(storage_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_size(storage_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - uint64_t storage_total = 0; uint64_t storage_free = 0; if (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK) { - addMemoryBar(storage_wrapper, file::MOUNT_POINT_SYSTEM, storage_free, storage_total); + addMemoryBar(storage_tab, file::MOUNT_POINT_SYSTEM, storage_free, storage_total); } if (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK) { - addMemoryBar(storage_wrapper, file::MOUNT_POINT_DATA, storage_free, storage_total); + addMemoryBar(storage_tab, file::MOUNT_POINT_DATA, storage_free, storage_total); } const auto sdcard_devices = hal::findDevices(hal::Device::Type::SdCard); for (const auto& sdcard : sdcard_devices) { if (sdcard->isMounted() && esp_vfs_fat_info(sdcard->getMountPath().c_str(), &storage_total, &storage_free) == ESP_OK) { addMemoryBar( - storage_wrapper, + storage_tab, sdcard->getMountPath().c_str(), storage_free, storage_total @@ -243,30 +267,17 @@ class SystemInfoApp : public App { #endif #if configUSE_TRACE_FACILITY - auto* tasks_label = lv_label_create(wrapper); - lv_label_set_text(tasks_label, "Tasks"); - auto* tasks_wrapper = lv_obj_create(wrapper); - lv_obj_set_flex_flow(tasks_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_size(tasks_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - addRtosTasks(tasks_wrapper); + addRtosTasks(tasks_tab); #endif - auto* devices_label = lv_label_create(wrapper); - lv_label_set_text(devices_label, "Devices"); - auto* devices_wrapper = lv_obj_create(wrapper); - lv_obj_set_flex_flow(devices_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_size(devices_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - addDevices(devices_wrapper); -#ifdef ESP_PLATFORM + addDevices(devices_tab); + // Build info - auto* build_info_label = lv_label_create(wrapper); - lv_label_set_text(build_info_label, "Build info"); - auto* build_info_wrapper = lv_obj_create(wrapper); - lv_obj_set_flex_flow(build_info_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_size(build_info_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - - auto* esp_idf_version = lv_label_create(build_info_wrapper); - lv_label_set_text_fmt(esp_idf_version, "IDF version: %d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH); + auto* tactility_version = lv_label_create(about_tab); + lv_label_set_text_fmt(tactility_version, "Tactility v%s", TT_VERSION); +#ifdef ESP_PLATFORM + auto* esp_idf_version = lv_label_create(about_tab); + lv_label_set_text_fmt(esp_idf_version, "ESP-IDF v%d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH); #endif } }; diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index 95e4eb57..6a4aeb3c 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -26,6 +26,7 @@ public: void onShow(AppContext& app, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lvgl::toolbar_create(parent, app); diff --git a/Tactility/Source/app/timezone/TimeZone.cpp b/Tactility/Source/app/timezone/TimeZone.cpp index bbf54d0b..49c2dbb1 100644 --- a/Tactility/Source/app/timezone/TimeZone.cpp +++ b/Tactility/Source/app/timezone/TimeZone.cpp @@ -193,6 +193,8 @@ public: void onShow(AppContext& app, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + lvgl::toolbar_create(parent, app); auto* search_wrapper = lv_obj_create(parent); diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index 40f7f476..1ea8e3bc 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -64,6 +64,8 @@ class WifiApSettings : public App { std::string ssid = paremeters->getString("ssid"); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + lvgl::toolbar_create(parent, ssid); // Wrappers diff --git a/Tactility/Source/app/wificonnect/View.cpp b/Tactility/Source/app/wificonnect/View.cpp index 6b3e5616..5fd121bc 100644 --- a/Tactility/Source/app/wificonnect/View.cpp +++ b/Tactility/Source/app/wificonnect/View.cpp @@ -109,8 +109,9 @@ void View::createBottomButtons(lv_obj_t* parent) { // TODO: Standardize dialogs void View::init(AppContext& app, lv_obj_t* parent) { - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + lvgl::toolbar_create(parent, app); auto* wrapper = lv_obj_create(parent); diff --git a/Tactility/Source/app/wifimanage/View.cpp b/Tactility/Source/app/wifimanage/View.cpp index 6b2aa9a6..14055218 100644 --- a/Tactility/Source/app/wifimanage/View.cpp +++ b/Tactility/Source/app/wifimanage/View.cpp @@ -272,12 +272,15 @@ void View::updateEnableOnBootToggle() { // region Main void View::init(const AppContext& app, lv_obj_t* parent) { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + root = parent; paths = app.getPaths(); // Toolbar - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_t* toolbar = lvgl::toolbar_create(parent, app); scanning_spinner = lvgl::toolbar_add_spinner_action(toolbar); diff --git a/Tactility/Source/file/PropertiesFile.cpp b/Tactility/Source/file/PropertiesFile.cpp index 9a7a549d..01cac08d 100644 --- a/Tactility/Source/file/PropertiesFile.cpp +++ b/Tactility/Source/file/PropertiesFile.cpp @@ -22,17 +22,22 @@ bool loadPropertiesFile(const std::string& filePath, std::function(filePath, [&filePath, &callback] { TT_LOG_I(TAG, "Reading properties file %s", filePath.c_str()); uint16_t line_count = 0; - return readLines(filePath, true, [&line_count, &filePath, &callback](const std::string& line) { + std::string key_prefix = ""; + return readLines(filePath, true, [&key_prefix, &line_count, &filePath, &callback](const std::string& line) { line_count++; std::string key, value; auto trimmed_line = string::trim(line, " \t"); if (!trimmed_line.starts_with("#")) { - if (getKeyValuePair(trimmed_line, key, value)) { - std::string trimmed_key = string::trim(key, " \t"); - std::string trimmed_value = string::trim(value, " \t"); - callback(trimmed_key, trimmed_value); + if (trimmed_line.starts_with("[")) { + key_prefix = trimmed_line; } else { - TT_LOG_E(TAG, "Failed to parse line %d of %s", line_count, filePath.c_str()); + if (getKeyValuePair(trimmed_line, key, value)) { + std::string trimmed_key = key_prefix + string::trim(key, " \t"); + std::string trimmed_value = string::trim(value, " \t"); + callback(trimmed_key, trimmed_value); + } else { + TT_LOG_E(TAG, "Failed to parse line %d of %s", line_count, filePath.c_str()); + } } } }); diff --git a/Tactility/Source/service/ServiceRegistration.cpp b/Tactility/Source/service/ServiceRegistration.cpp index ffc6091a..0d1bc466 100644 --- a/Tactility/Source/service/ServiceRegistration.cpp +++ b/Tactility/Source/service/ServiceRegistration.cpp @@ -77,8 +77,15 @@ bool startService(const std::string& id) { instance_mutex.unlock(); service_instance->setState(State::Starting); - service_instance->getService()->onStart(*service_instance); - service_instance->setState(State::Started); + if (service_instance->getService()->onStart(*service_instance)) { + service_instance->setState(State::Started); + } else { + TT_LOG_E(TAG, "Starting %s failed", id.c_str()); + service_instance->setState(State::Stopped); + instance_mutex.lock(); + service_instance_map.erase(manifest->id); + instance_mutex.unlock(); + } TT_LOG_I(TAG, "Started %s", id.c_str()); diff --git a/Tactility/Source/service/development/DevelopmentService.cpp b/Tactility/Source/service/development/DevelopmentService.cpp index b4edf0f2..da8dea51 100644 --- a/Tactility/Source/service/development/DevelopmentService.cpp +++ b/Tactility/Source/service/development/DevelopmentService.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include namespace tt::service::development { @@ -25,7 +27,7 @@ extern const ServiceManifest manifest; constexpr const char* TAG = "DevService"; -void DevelopmentService::onStart(ServiceContext& service) { +bool DevelopmentService::onStart(ServiceContext& service) { auto lock = mutex.asScopedLock(); lock.lock(); @@ -39,6 +41,8 @@ void DevelopmentService::onStart(ServiceContext& service) { ); setEnabled(shouldEnableOnBoot()); + + return true; } void DevelopmentService::onStop(ServiceContext& service) { @@ -97,6 +101,7 @@ void DevelopmentService::startServer() { deviceResponse = stream.str(); httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.stack_size = 5120; config.server_port = 6666; config.uri_match_fn = httpd_uri_match_wildcard; @@ -154,6 +159,8 @@ void DevelopmentService::onNetworkDisconnected() { // region endpoints esp_err_t DevelopmentService::handleGetInfo(httpd_req_t* request) { + TT_LOG_I(TAG, "GET /device"); + if (httpd_resp_set_type(request, "application/json") != ESP_OK) { TT_LOG_W(TAG, "Failed to send header"); return ESP_FAIL; @@ -171,6 +178,8 @@ esp_err_t DevelopmentService::handleGetInfo(httpd_req_t* request) { } esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) { + TT_LOG_I(TAG, "POST /app/run"); + std::string query; if (!network::getQueryOrSendError(request, query)) { return ESP_FAIL; @@ -184,28 +193,15 @@ esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) { return ESP_FAIL; } - auto app_id = id_key_pos->second; - if (app_id.ends_with(".app.elf")) { - if (!file::isFile(app_id)) { - TT_LOG_W(TAG, "[400] /app/run cannot find app %s", app_id.c_str()); - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "app not found"); - return ESP_FAIL; - } - app::registerElfApp(app_id); - app_id = app::getElfAppId(app_id); - } else if (!app::findAppById(app_id.c_str())) { - TT_LOG_W(TAG, "[400] /app/run app not found"); - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "app not found"); - return ESP_FAIL; - } - - app::start(app_id); - TT_LOG_I(TAG, "[200] /app/run %s", app_id.c_str()); + app::start(id_key_pos->second); + TT_LOG_I(TAG, "[200] /app/run %s", id_key_pos->second.c_str()); httpd_resp_send(request, nullptr, 0); return ESP_OK; } esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) { + TT_LOG_I(TAG, "PUT /app/install"); + std::string boundary; if (!network::getMultiPartBoundaryOrSendError(request, boundary)) { return false; @@ -250,8 +246,19 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) { } content_left -= content_read; + const std::string tmp_path = app::getTempPath(); + auto lock = file::getLock(tmp_path)->asScopedLock(); + + lock.lock(); + if (!file::findOrCreateDirectory(tmp_path, 0777)) { + httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to save file"); + return ESP_FAIL; + } + lock.unlock(); + // Write file - auto file_path = std::format("/data/{}", filename_entry->second); + lock.lock(); + auto file_path = std::format("{}/{}", tmp_path, filename_entry->second); auto* file = fopen(file_path.c_str(), "wb"); auto file_bytes_written = fwrite(buffer.get(), 1, file_size, file); fclose(file); @@ -259,6 +266,8 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to save file"); return ESP_FAIL; } + lock.unlock(); + // Read and verify part if (!network::readAndDiscardOrSendError(request, part_after_file)) { @@ -270,9 +279,21 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) { TT_LOG_W(TAG, "We have more bytes at the end of the request parsing?!"); } + if (!app::install(file_path)) { + httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to install"); + return ESP_FAIL; + } + + lock.lock(); + if (remove(file_path.c_str()) != 0) { + TT_LOG_W(TAG, "Failed to delete %s", file_path.c_str()); + } + lock.unlock(); + TT_LOG_I(TAG, "[200] /app/install -> %s", file_path.c_str()); httpd_resp_send(request, nullptr, 0); + return ESP_OK; } diff --git a/Tactility/Source/service/espnow/EspNowService.cpp b/Tactility/Source/service/espnow/EspNowService.cpp index 58cc39b0..81ab0da8 100644 --- a/Tactility/Source/service/espnow/EspNowService.cpp +++ b/Tactility/Source/service/espnow/EspNowService.cpp @@ -20,11 +20,13 @@ static uint8_t BROADCAST_MAC[ESP_NOW_ETH_ALEN]; constexpr bool isBroadcastAddress(uint8_t address[ESP_NOW_ETH_ALEN]) { return memcmp(address, BROADCAST_MAC, ESP_NOW_ETH_ALEN) == 0; } -void EspNowService::onStart(ServiceContext& service) { +bool EspNowService::onStart(ServiceContext& service) { auto lock = mutex.asScopedLock(); lock.lock(); memset(BROADCAST_MAC, 0xFF, sizeof(BROADCAST_MAC)); + + return true; } void EspNowService::onStop(ServiceContext& service) { diff --git a/Tactility/Source/service/gps/GpsService.cpp b/Tactility/Source/service/gps/GpsService.cpp index 78e272ad..38b27bf2 100644 --- a/Tactility/Source/service/gps/GpsService.cpp +++ b/Tactility/Source/service/gps/GpsService.cpp @@ -12,8 +12,8 @@ namespace tt::service::gps { constexpr const char* TAG = "GpsService"; extern const ServiceManifest manifest; -constexpr inline bool hasTimeElapsed(TickType_t now, TickType_t timeInThePast, TickType_t expireTimeInTicks) { - return (TickType_t)(now - timeInThePast) >= expireTimeInTicks; +constexpr bool hasTimeElapsed(TickType_t now, TickType_t timeInThePast, TickType_t expireTimeInTicks) { + return (now - timeInThePast) >= expireTimeInTicks; } GpsService::GpsDeviceRecord* _Nullable GpsService::findGpsRecord(const std::shared_ptr& device) { @@ -58,14 +58,14 @@ void GpsService::removeGpsDevice(const std::shared_ptr& device) { }); } -void GpsService::onStart(tt::service::ServiceContext& serviceContext) { +bool GpsService::onStart(ServiceContext& serviceContext) { auto lock = mutex.asScopedLock(); lock.lock(); - paths = serviceContext.getPaths(); + return true; } -void GpsService::onStop(tt::service::ServiceContext& serviceContext) { +void GpsService::onStop(ServiceContext& serviceContext) { if (getState() == State::On) { stopReceiving(); } diff --git a/Tactility/Source/service/gui/GuiService.cpp b/Tactility/Source/service/gui/GuiService.cpp index 3408355d..8ab73918 100644 --- a/Tactility/Source/service/gui/GuiService.cpp +++ b/Tactility/Source/service/gui/GuiService.cpp @@ -112,7 +112,13 @@ void GuiService::redraw() { unlock(); } -void GuiService::onStart(TT_UNUSED ServiceContext& service) { +bool GuiService::onStart(TT_UNUSED ServiceContext& service) { + auto* screen_root = lv_screen_active(); + if (screen_root == nullptr) { + TT_LOG_E(TAG, "No display found"); + return false; + } + thread = new Thread( "gui", 4096, // Last known minimum was 2800 for launching desktop @@ -123,12 +129,9 @@ void GuiService::onStart(TT_UNUSED ServiceContext& service) { onLoaderEvent(event); }); + lvgl::lock(portMAX_DELAY); - tt_check(lvgl::lock(1000 / portTICK_PERIOD_MS)); keyboardGroup = lv_group_create(); - auto* screen_root = lv_screen_active(); - assert(screen_root != nullptr); - lvgl::obj_set_style_bg_blacken(screen_root); lv_obj_t* vertical_container = lv_obj_create(screen_root); @@ -156,6 +159,8 @@ void GuiService::onStart(TT_UNUSED ServiceContext& service) { thread->setPriority(THREAD_PRIORITY_SERVICE); thread->start(); + + return true; } void GuiService::onStop(TT_UNUSED ServiceContext& service) { diff --git a/Tactility/Source/service/loader/Loader.cpp b/Tactility/Source/service/loader/Loader.cpp index a8b08ed1..b1ade374 100644 --- a/Tactility/Source/service/loader/Loader.cpp +++ b/Tactility/Source/service/loader/Loader.cpp @@ -62,8 +62,9 @@ class LoaderService final : public Service { public: - void onStart(TT_UNUSED ServiceContext& service) override { + bool onStart(TT_UNUSED ServiceContext& service) override { dispatcherThread->start(); + return true; } void onStop(TT_UNUSED ServiceContext& service) override { diff --git a/Tactility/Source/service/screenshot/Screenshot.cpp b/Tactility/Source/service/screenshot/Screenshot.cpp index 37bb289b..18668d3a 100644 --- a/Tactility/Source/service/screenshot/Screenshot.cpp +++ b/Tactility/Source/service/screenshot/Screenshot.cpp @@ -2,16 +2,15 @@ #if TT_FEATURE_SCREENSHOT_ENABLED -#include "Tactility/service/screenshot/Screenshot.h" - -#include +#include #include #include +#include namespace tt::service::screenshot { -#define TAG "screenshot_service" +constexpr auto* TAG = "ScreenshotService"; extern const ServiceManifest manifest; @@ -51,6 +50,15 @@ void ScreenshotService::startTimed(const std::string& path, uint8_t delayInSecon } } +bool ScreenshotService::onStart(ServiceContext& serviceContext) { + if (lv_screen_active() == nullptr) { + TT_LOG_E(TAG, "No display found"); + return false; + } + + return true; +} + void ScreenshotService::stop() { auto lock = mutex.asScopedLock(); if (!lock.lock(50 / portTICK_PERIOD_MS)) { diff --git a/Tactility/Source/service/sdcard/Sdcard.cpp b/Tactility/Source/service/sdcard/Sdcard.cpp index a58cc83c..850f21a6 100644 --- a/Tactility/Source/service/sdcard/Sdcard.cpp +++ b/Tactility/Source/service/sdcard/Sdcard.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -52,13 +53,21 @@ class SdCardService final : public Service { public: - void onStart(ServiceContext& serviceContext) override { + bool onStart(ServiceContext& serviceContext) override { + if (hal::findFirstDevice(hal::Device::Type::SdCard) == nullptr) { + TT_LOG_W(TAG, "No SD card device found - not starting Service"); + return false; + } + auto service = findServiceById(manifest.id); updateTimer = std::make_unique(Timer::Type::Periodic, [service]() { service->update(); }); + // We want to try and scan more often in case of startup or scan lock failure updateTimer->start(1000); + + return true; } void onStop(ServiceContext& serviceContext) override { diff --git a/Tactility/Source/service/statusbar/Statusbar.cpp b/Tactility/Source/service/statusbar/Statusbar.cpp index caf5d6d6..f2b6c038 100644 --- a/Tactility/Source/service/statusbar/Statusbar.cpp +++ b/Tactility/Source/service/statusbar/Statusbar.cpp @@ -249,7 +249,12 @@ public: lvgl::statusbar_icon_remove(gps_icon_id); } - void onStart(ServiceContext& serviceContext) override { + bool onStart(ServiceContext& serviceContext) override { + if (lv_screen_active() == nullptr) { + TT_LOG_E(TAG, "No display found"); + return false; + } + paths = serviceContext.getPaths(); // TODO: Make thread-safe for LVGL @@ -265,6 +270,8 @@ public: // We want to try and scan more often in case of startup or scan lock failure updateTimer->start(1000); + + return true; } void onStop(ServiceContext& service) override{ diff --git a/Tactility/Source/service/wifi/WifiEsp.cpp b/Tactility/Source/service/wifi/WifiEsp.cpp index 79f0b6ea..9fb77076 100644 --- a/Tactility/Source/service/wifi/WifiEsp.cpp +++ b/Tactility/Source/service/wifi/WifiEsp.cpp @@ -414,10 +414,9 @@ static bool copy_scan_list(std::shared_ptr wifi) { } static bool find_auto_connect_ap(std::shared_ptr wifi, settings::WifiApSettings& settings) { + TT_LOG_I(TAG, "find_auto_connect_ap()"); auto lock = wifi->dataMutex.asScopedLock(); - if (lock.lock(10 / portTICK_PERIOD_MS)) { - TT_LOG_I(TAG, "auto_connect()"); for (int i = 0; i < wifi->scan_list_count; ++i) { auto ssid = reinterpret_cast(wifi->scan_list[i].ssid); if (settings::contains(ssid)) { @@ -897,7 +896,7 @@ class WifiService final : public Service { public: - void onStart(ServiceContext& service) override { + bool onStart(ServiceContext& service) override { assert(wifi_singleton == nullptr); wifi_singleton = std::make_shared(); @@ -913,6 +912,8 @@ public: TT_LOG_I(TAG, "Auto-enabling due to setting"); getMainDispatcher().dispatch([] { dispatchEnable(wifi_singleton); }); } + + return true; } void onStop(ServiceContext& service) override { diff --git a/Tactility/Source/service/wifi/WifiMock.cpp b/Tactility/Source/service/wifi/WifiMock.cpp index dfcd0eb5..ceaba01f 100644 --- a/Tactility/Source/service/wifi/WifiMock.cpp +++ b/Tactility/Source/service/wifi/WifiMock.cpp @@ -144,9 +144,10 @@ class WifiService final : public Service { public: - void onStart(TT_UNUSED ServiceContext& service) final { + bool onStart(TT_UNUSED ServiceContext& service) final { tt_check(wifi == nullptr); wifi = new Wifi(); + return true; } void onStop(TT_UNUSED ServiceContext& service) final { diff --git a/TactilityCore/Include/Tactility/file/File.h b/TactilityCore/Include/Tactility/file/File.h index a96a9f0c..dd84ffc1 100644 --- a/TactilityCore/Include/Tactility/file/File.h +++ b/TactilityCore/Include/Tactility/file/File.h @@ -67,7 +67,11 @@ bool writeString(const std::string& filepath, const std::string& content); * @param[in] mode the mode to use when creating directories * @return true when the specified path was found, or otherwise creates the directories recursively with the specified mode. */ -bool findOrCreateDirectory(std::string path, mode_t mode); +bool findOrCreateDirectory(const std::string& path, mode_t mode); + +bool findOrCreateParentDirectory(const std::string& path, mode_t mode); + +bool deleteRecursively(const std::string& path); /** * Concatenate a child path with a parent path, ensuring proper slash inbetween @@ -77,6 +81,8 @@ bool findOrCreateDirectory(std::string path, mode_t mode); */ std::string getChildPath(const std::string& basePath, const std::string& childPath); +std::string getLastPathSegment(const std::string& path); + typedef int (*ScandirFilter)(const dirent*); typedef bool (*ScandirSort)(const dirent&, const dirent&); @@ -91,6 +97,21 @@ bool isFile(const std::string& path); bool isDirectory(const std::string& path); +/** + * 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] onEntry a pointer to a function that accepts an entry + * @return true if the directory exists and listing its contents was successful + */ +bool listDirectory( + const std::string& path, + std::function onEntry +); + /** * A scandir()-like implementation that works on ESP32. * It does not return "." and ".." items but otherwise functions the same. @@ -106,8 +127,8 @@ bool isDirectory(const std::string& path); int scandir( const std::string& path, std::vector& outList, - ScandirFilter _Nullable filter, - ScandirSort _Nullable sort + ScandirFilter _Nullable filter = nullptr, + ScandirSort _Nullable sort = nullptr ); bool readLines(const std::string& filePath, bool stripNewLine, std::function callback); diff --git a/TactilityCore/Source/file/File.cpp b/TactilityCore/Source/file/File.cpp index b79fde56..1fb5bf76 100644 --- a/TactilityCore/Source/file/File.cpp +++ b/TactilityCore/Source/file/File.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace tt::hal::sdcard { class SdCardDevice; @@ -35,6 +36,28 @@ bool direntSortAlphaAndType(const dirent& left, const dirent& right) { } } +bool listDirectory( + const std::string& path, + std::function onEntry +) { + TT_LOG_I(TAG, "listDir start %s", path.c_str()); + DIR* dir = opendir(path.c_str()); + if (dir == nullptr) { + TT_LOG_E(TAG, "Failed to open dir %s", path.c_str()); + return false; + } + + dirent* current_entry; + while ((current_entry = readdir(dir)) != nullptr) { + onEntry(*current_entry); + } + + closedir(dir); + + TT_LOG_I(TAG, "listDir stop %s", path.c_str()); + return true; +} + int scandir( const std::string& path, std::vector& outList, @@ -50,7 +73,7 @@ int scandir( dirent* current_entry; while ((current_entry = readdir(dir)) != nullptr) { - if (filterMethod(current_entry) == 0) { + if (filterMethod == nullptr || filterMethod(current_entry) == 0) { outList.push_back(*current_entry); } } @@ -184,7 +207,16 @@ static bool findOrCreateDirectoryInternal(std::string path, mode_t mode) { return true; } -bool findOrCreateDirectory(std::string path, mode_t mode) { +std::string getLastPathSegment(const std::string& path) { + auto index = path.find_last_of('/'); + if (index != std::string::npos) { + return path.substr(index + 1); + } else { + return ""; + } +} + +bool findOrCreateDirectory(const std::string& path, mode_t mode) { if (path.empty()) { return true; } @@ -213,6 +245,45 @@ bool findOrCreateDirectory(std::string path, mode_t mode) { return true; } +bool findOrCreateParentDirectory(const std::string& path, mode_t mode) { + std::string parent; + if (!string::getPathParent(path, parent)) { + return false; + } + return findOrCreateDirectory(parent, mode); +} + +bool deleteRecursively(const std::string& path) { + if (path.empty()) { + return true; + } + + if (isDirectory(path)) { + std::vector entries; + if (scandir(path, entries) < 0) { + TT_LOG_E(TAG, "Failed to scan directory %s", path.c_str()); + return false; + } + for (const auto& entry : entries) { + auto child_path = path + "/" + entry.d_name; + if (!deleteRecursively(child_path)) { + return false; + } + } + TT_LOG_I(TAG, "Deleting %s", path.c_str()); + return rmdir(path.c_str()) == 0; + } else if (isFile(path)) { + TT_LOG_I(TAG, "Deleting %s", path.c_str()); + return remove(path.c_str()) == 0; + } else if (path == "/" || path == "." || path == "..") { + // No-op + return true; + } else { + TT_LOG_E(TAG, "Failed to delete \"%s\": unknown type", path.c_str()); + return false; + } +} + bool isFile(const std::string& path) { return access(path.c_str(), F_OK) == 0; }