From d5c94c7a8aa46336df48b0251925bb2d9cac727f Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 14 Sep 2025 13:37:34 +0200 Subject: [PATCH] Merge develop to main (#334) - `FileBrowser` app now supports deleting directories (recursively) - `DevelopmentService` and `tactility.py` now support the app `uninstall` action - Fix crash for `File` app: implement file locking in several places (SPI SD cards need it) - Remove I2C configuration from `M5stackCardputer.cpp` because we don't support the "Cardputer Adv" variant in that firmware. --- .../Source/M5stackCardputer.cpp | 20 ----------- Documentation/ideas.md | 1 + ExternalApps/Calculator/tactility.py | 25 +++++++++++-- ExternalApps/GraphicsDemo/tactility.py | 25 +++++++++++-- ExternalApps/HelloWorld/tactility.py | 25 +++++++++++-- Tactility/Include/Tactility/app/App.h | 2 ++ .../Include/Tactility/app/AppRegistration.h | 3 ++ .../service/development/DevelopmentService.h | 8 +++++ Tactility/Source/app/AppInstall.cpp | 21 +++++++++++ Tactility/Source/app/AppRegistration.cpp | 9 +++++ Tactility/Source/app/filebrowser/State.cpp | 26 +++++++------- Tactility/Source/app/filebrowser/View.cpp | 36 ++++++++++++------- .../development/DevelopmentService.cpp | 34 ++++++++++++++++++ 13 files changed, 182 insertions(+), 53 deletions(-) diff --git a/Boards/M5stackCardputer/Source/M5stackCardputer.cpp b/Boards/M5stackCardputer/Source/M5stackCardputer.cpp index 145a4202..227f30a6 100644 --- a/Boards/M5stackCardputer/Source/M5stackCardputer.cpp +++ b/Boards/M5stackCardputer/Source/M5stackCardputer.cpp @@ -24,26 +24,6 @@ static DeviceVector createDevices() { extern const Configuration m5stack_cardputer = { .initBoot = initBoot, .createDevices = createDevices, - .i2c = { - // Only available on Cardputer Adv (enabling it breaks the keyboard on a Cardputer v1.1) - i2c::Configuration { - .name = "Internal", - .port = I2C_NUM_0, - .initMode = i2c::InitMode::Disabled, - .isMutable = true, - .config = (i2c_config_t) { - .mode = I2C_MODE_MASTER, - .sda_io_num = GPIO_NUM_8, - .scl_io_num = GPIO_NUM_9, - .sda_pullup_en = true, - .scl_pullup_en = true, - .master = { - .clk_speed = 400000 - }, - .clk_flags = 0 - } - } - }, .spi { // Display spi::Configuration { diff --git a/Documentation/ideas.md b/Documentation/ideas.md index c0128538..8af0de7b 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -31,6 +31,7 @@ - Bug: Turn on WiFi (when testing it wasn't connected/connecting - just active). Open chat. Observe crash. - Toolbar: when the title doesn't fit, scroll the text instead of splitting it onto a new line (try on Waveshare 1.47") - UI: create UI size classification (e.g. "compact" for tiny screens without touch) +- Bug: Crash handling app cannot be exited with an EncoderDevice. (current work-around is to manually reset the device) ## Lower Priority diff --git a/ExternalApps/Calculator/tactility.py b/ExternalApps/Calculator/tactility.py index 267b459d..0f733e3a 100644 --- a/ExternalApps/Calculator/tactility.py +++ b/ExternalApps/Calculator/tactility.py @@ -14,7 +14,7 @@ import shutil import configparser ttbuild_path = ".tactility" -ttbuild_version = "2.0.0" +ttbuild_version = "2.1.0" ttbuild_cdn = "https://cdn.tactility.one" ttbuild_sdk_json_validity = 3600 # seconds ttport = 6666 @@ -60,8 +60,9 @@ def print_help(): 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(" run [ip] Run the application") + print(" install [ip] Install the application") + print(" uninstall [ip] Uninstall the 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("") @@ -544,6 +545,19 @@ def install_action(ip, platforms): except IOError as e: print_error(f"File error: {e}") +def uninstall_action(manifest, ip): + app_id = manifest["app"]["id"] + print(f"Uninstalling {app_id} on {ip}") + url = get_url(ip, "/app/uninstall") + params = {'id': app_id} + try: + response = requests.put(url, params=params) + if response.status_code != 200: + print_error("Uninstall failed") + else: + print(f"{shell_color_green}Uninstall successful ✅{shell_color_reset}") + except requests.RequestException as e: + print(f"Request failed: {e}") #region Main if __name__ == "__main__": @@ -599,6 +613,11 @@ if __name__ == "__main__": platform = sys.argv[3] platforms_to_install = [platform] install_action(sys.argv[2], platforms_to_install) + elif action_arg == "uninstall": + if len(sys.argv) < 3: + print_help() + exit_with_error("Commandline parameter missing") + uninstall_action(manifest, sys.argv[2]) elif action_arg == "bir": if len(sys.argv) < 3: print_help() diff --git a/ExternalApps/GraphicsDemo/tactility.py b/ExternalApps/GraphicsDemo/tactility.py index 267b459d..0f733e3a 100644 --- a/ExternalApps/GraphicsDemo/tactility.py +++ b/ExternalApps/GraphicsDemo/tactility.py @@ -14,7 +14,7 @@ import shutil import configparser ttbuild_path = ".tactility" -ttbuild_version = "2.0.0" +ttbuild_version = "2.1.0" ttbuild_cdn = "https://cdn.tactility.one" ttbuild_sdk_json_validity = 3600 # seconds ttport = 6666 @@ -60,8 +60,9 @@ def print_help(): 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(" run [ip] Run the application") + print(" install [ip] Install the application") + print(" uninstall [ip] Uninstall the 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("") @@ -544,6 +545,19 @@ def install_action(ip, platforms): except IOError as e: print_error(f"File error: {e}") +def uninstall_action(manifest, ip): + app_id = manifest["app"]["id"] + print(f"Uninstalling {app_id} on {ip}") + url = get_url(ip, "/app/uninstall") + params = {'id': app_id} + try: + response = requests.put(url, params=params) + if response.status_code != 200: + print_error("Uninstall failed") + else: + print(f"{shell_color_green}Uninstall successful ✅{shell_color_reset}") + except requests.RequestException as e: + print(f"Request failed: {e}") #region Main if __name__ == "__main__": @@ -599,6 +613,11 @@ if __name__ == "__main__": platform = sys.argv[3] platforms_to_install = [platform] install_action(sys.argv[2], platforms_to_install) + elif action_arg == "uninstall": + if len(sys.argv) < 3: + print_help() + exit_with_error("Commandline parameter missing") + uninstall_action(manifest, sys.argv[2]) elif action_arg == "bir": if len(sys.argv) < 3: print_help() diff --git a/ExternalApps/HelloWorld/tactility.py b/ExternalApps/HelloWorld/tactility.py index 267b459d..0f733e3a 100644 --- a/ExternalApps/HelloWorld/tactility.py +++ b/ExternalApps/HelloWorld/tactility.py @@ -14,7 +14,7 @@ import shutil import configparser ttbuild_path = ".tactility" -ttbuild_version = "2.0.0" +ttbuild_version = "2.1.0" ttbuild_cdn = "https://cdn.tactility.one" ttbuild_sdk_json_validity = 3600 # seconds ttport = 6666 @@ -60,8 +60,9 @@ def print_help(): 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(" run [ip] Run the application") + print(" install [ip] Install the application") + print(" uninstall [ip] Uninstall the 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("") @@ -544,6 +545,19 @@ def install_action(ip, platforms): except IOError as e: print_error(f"File error: {e}") +def uninstall_action(manifest, ip): + app_id = manifest["app"]["id"] + print(f"Uninstalling {app_id} on {ip}") + url = get_url(ip, "/app/uninstall") + params = {'id': app_id} + try: + response = requests.put(url, params=params) + if response.status_code != 200: + print_error("Uninstall failed") + else: + print(f"{shell_color_green}Uninstall successful ✅{shell_color_reset}") + except requests.RequestException as e: + print(f"Request failed: {e}") #region Main if __name__ == "__main__": @@ -599,6 +613,11 @@ if __name__ == "__main__": platform = sys.argv[3] platforms_to_install = [platform] install_action(sys.argv[2], platforms_to_install) + elif action_arg == "uninstall": + if len(sys.argv) < 3: + print_help() + exit_with_error("Commandline parameter missing") + uninstall_action(manifest, sys.argv[2]) elif action_arg == "bir": if len(sys.argv) < 3: print_help() diff --git a/Tactility/Include/Tactility/app/App.h b/Tactility/Include/Tactility/app/App.h index bcb11e7b..701a3b44 100644 --- a/Tactility/Include/Tactility/app/App.h +++ b/Tactility/Include/Tactility/app/App.h @@ -100,4 +100,6 @@ std::string getInstallPath(); bool install(const std::string& path); +bool uninstall(const std::string& appId); + } diff --git a/Tactility/Include/Tactility/app/AppRegistration.h b/Tactility/Include/Tactility/app/AppRegistration.h index 649b3eb8..ec6c122d 100644 --- a/Tactility/Include/Tactility/app/AppRegistration.h +++ b/Tactility/Include/Tactility/app/AppRegistration.h @@ -11,6 +11,9 @@ struct AppManifest; /** Register an application with its manifest */ void addApp(const AppManifest& manifest); +/** Remove an app from the registry */ +bool removeApp(const std::string& id); + /** Find an application manifest by its id * @param[in] id the manifest id * @return the application manifest if it was found diff --git a/Tactility/Private/Tactility/service/development/DevelopmentService.h b/Tactility/Private/Tactility/service/development/DevelopmentService.h index 3e45ca91..0eb5262a 100644 --- a/Tactility/Private/Tactility/service/development/DevelopmentService.h +++ b/Tactility/Private/Tactility/service/development/DevelopmentService.h @@ -41,6 +41,13 @@ class DevelopmentService final : public Service { .user_ctx = this }; + httpd_uri_t appUninstallEndpoint = { + .uri = "/app/uninstall", + .method = HTTP_PUT, + .handler = handleAppUninstall, + .user_ctx = this + }; + void onNetworkConnected(); void onNetworkDisconnected(); @@ -50,6 +57,7 @@ class DevelopmentService final : public Service { static esp_err_t handleGetInfo(httpd_req_t* request); static esp_err_t handleAppRun(httpd_req_t* request); static esp_err_t handleAppInstall(httpd_req_t* request); + static esp_err_t handleAppUninstall(httpd_req_t* request); public: diff --git a/Tactility/Source/app/AppInstall.cpp b/Tactility/Source/app/AppInstall.cpp index 20c9db9e..15b84b59 100644 --- a/Tactility/Source/app/AppInstall.cpp +++ b/Tactility/Source/app/AppInstall.cpp @@ -223,4 +223,25 @@ bool install(const std::string& path) { return true; } +bool uninstall(const std::string& appId) { + TT_LOG_I(TAG, "Uninstalling app %s", appId.c_str()); + auto app_path = getInstallPath() + "/" + appId; + return file::withLock(app_path, [&app_path, &appId]() { + if (!file::isDirectory(app_path)) { + TT_LOG_E(TAG, "App %s not found at ", app_path.c_str()); + return false; + } + + if (!file::deleteRecursively(app_path)) { + return false; + } + + if (!removeApp(appId)) { + TT_LOG_W(TAG, "Failed to remove app %d from registry", appId.c_str()); + } + + return true; + }); +} + } // namespace \ No newline at end of file diff --git a/Tactility/Source/app/AppRegistration.cpp b/Tactility/Source/app/AppRegistration.cpp index 5f2a4383..697dfa7d 100644 --- a/Tactility/Source/app/AppRegistration.cpp +++ b/Tactility/Source/app/AppRegistration.cpp @@ -29,6 +29,15 @@ void addApp(const AppManifest& manifest) { hash_mutex.unlock(); } +bool removeApp(const std::string& id) { + TT_LOG_I(TAG, "Removing manifest for %s", id.c_str()); + + auto lock = hash_mutex.asScopedLock(); + lock.lock(); + + return app_manifest_map.erase(id) == 1; +} + _Nullable std::shared_ptr findAppById(const std::string& id) { hash_mutex.lock(); auto result = app_manifest_map.find(id); diff --git a/Tactility/Source/app/filebrowser/State.cpp b/Tactility/Source/app/filebrowser/State.cpp index 352f274e..feda67a9 100644 --- a/Tactility/Source/app/filebrowser/State.cpp +++ b/Tactility/Source/app/filebrowser/State.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace tt::app::filebrowser { @@ -56,18 +57,19 @@ bool State::setEntriesForPath(const std::string& path) { return true; } else { dir_entries.clear(); - // TODO: file Lock - int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType); - if (count >= 0) { - TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count); - current_path = path; - selected_child_entry = ""; - action = ActionNone; - return true; - } else { - TT_LOG_E(TAG, "Failed to fetch entries for %s", path.c_str()); - return false; - } + return file::withLock(path, [this, &path] { + int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType); + if (count >= 0) { + TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count); + current_path = path; + selected_child_entry = ""; + action = ActionNone; + return true; + } else { + TT_LOG_E(TAG, "Failed to fetch entries for %s", path.c_str()); + return false; + } + }); } } diff --git a/Tactility/Source/app/filebrowser/View.cpp b/Tactility/Source/app/filebrowser/View.cpp index 2406c237..95983185 100644 --- a/Tactility/Source/app/filebrowser/View.cpp +++ b/Tactility/Source/app/filebrowser/View.cpp @@ -16,6 +16,7 @@ #include #include +#include #ifdef ESP_PLATFORM #include "Tactility/service/loader/Loader.h" @@ -215,6 +216,8 @@ void View::showActionsForDirectory() { auto* rename_button = lv_list_add_button(action_list, LV_SYMBOL_EDIT, "Rename"); lv_obj_add_event_cb(rename_button, onRenamePressedCallback, LV_EVENT_SHORT_CLICKED, this); + auto* delete_button = lv_list_add_button(action_list, LV_SYMBOL_TRASH, "Delete"); + lv_obj_add_event_cb(delete_button, onDeletePressedCallback, LV_EVENT_SHORT_CLICKED, this); lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN); } @@ -314,12 +317,18 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr bu switch (state->getPendingAction()) { case State::ActionDelete: { if (alertdialog::getResultIndex(*bundle) == 0) { - int delete_count = remove(filepath.c_str()); - if (delete_count > 0) { - TT_LOG_I(TAG, "Deleted %d items", delete_count); - } else { - TT_LOG_W(TAG, "Failed to delete %s", filepath.c_str()); - } + file::withLock(filepath, [&filepath] { + if (file::isDirectory(filepath)) { + if (!file::deleteRecursively(filepath)) { + TT_LOG_W(TAG, "Failed to delete %s", filepath.c_str()); + } + } else if (file::isFile(filepath)) { + if (remove(filepath.c_str()) <= 0) { + TT_LOG_W(TAG, "Failed to delete %s", filepath.c_str()); + } + } + }); + state->setEntriesForPath(state->getCurrentPath()); update(); } @@ -328,12 +337,15 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr bu case State::ActionRename: { auto new_name = inputdialog::getResult(*bundle); if (!new_name.empty() && new_name != state->getSelectedChildEntry()) { - std::string rename_to = file::getChildPath(state->getCurrentPath(), new_name); - if (rename(filepath.c_str(), rename_to.c_str())) { - TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str()); - } else { - TT_LOG_E(TAG, "Failed to rename \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str()); - } + file::withLock(filepath, [this, &filepath, &new_name] { + std::string rename_to = file::getChildPath(state->getCurrentPath(), new_name); + if (rename(filepath.c_str(), rename_to.c_str())) { + TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str()); + } else { + TT_LOG_E(TAG, "Failed to rename \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str()); + } + }); + state->setEntriesForPath(state->getCurrentPath()); update(); } diff --git a/Tactility/Source/service/development/DevelopmentService.cpp b/Tactility/Source/service/development/DevelopmentService.cpp index da8dea51..889ad15f 100644 --- a/Tactility/Source/service/development/DevelopmentService.cpp +++ b/Tactility/Source/service/development/DevelopmentService.cpp @@ -110,6 +110,7 @@ void DevelopmentService::startServer() { httpd_register_uri_handler(server, &handleGetInfoEndpoint); httpd_register_uri_handler(server, &appRunEndpoint); httpd_register_uri_handler(server, &appInstallEndpoint); + httpd_register_uri_handler(server, &appUninstallEndpoint); TT_LOG_I(TAG, "Started on port %d", config.server_port); } else { TT_LOG_E(TAG, "Failed to start"); @@ -297,6 +298,39 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) { return ESP_OK; } +esp_err_t DevelopmentService::handleAppUninstall(httpd_req_t* request) { + TT_LOG_I(TAG, "PUT /app/uninstall"); + + std::string query; + if (!network::getQueryOrSendError(request, query)) { + return ESP_FAIL; + } + + auto parameters = network::parseUrlQuery(query); + auto id_key_pos = parameters.find("id"); + if (id_key_pos == parameters.end()) { + TT_LOG_W(TAG, "[400] /app/uninstall id not specified"); + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified"); + return ESP_FAIL; + } + + if (!app::findAppById(id_key_pos->second)) { + TT_LOG_I(TAG, "[200] /app/uninstall %s (app wasn't installed)", id_key_pos->second.c_str()); + httpd_resp_send(request, nullptr, 0); + return ESP_OK; + } + + if (app::uninstall(id_key_pos->second)) { + TT_LOG_I(TAG, "[200] /app/uninstall %s", id_key_pos->second.c_str()); + httpd_resp_send(request, nullptr, 0); + return ESP_OK; + } else { + TT_LOG_W(TAG, "[500] /app/uninstall %s", id_key_pos->second.c_str()); + httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to uninstall"); + return ESP_FAIL; + } +} + // endregion std::shared_ptr findService() {