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.
This commit is contained in:
Ken Van Hoeylandt 2025-09-14 13:37:34 +02:00 committed by GitHub
parent 7027da00b8
commit d5c94c7a8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 182 additions and 53 deletions

View File

@ -24,26 +24,6 @@ static DeviceVector createDevices() {
extern const Configuration m5stack_cardputer = { extern const Configuration m5stack_cardputer = {
.initBoot = initBoot, .initBoot = initBoot,
.createDevices = createDevices, .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 { .spi {
// Display // Display
spi::Configuration { spi::Configuration {

View File

@ -31,6 +31,7 @@
- Bug: Turn on WiFi (when testing it wasn't connected/connecting - just active). Open chat. Observe crash. - 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") - 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) - 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 ## Lower Priority

View File

@ -14,7 +14,7 @@ import shutil
import configparser import configparser
ttbuild_path = ".tactility" ttbuild_path = ".tactility"
ttbuild_version = "2.0.0" ttbuild_version = "2.1.0"
ttbuild_cdn = "https://cdn.tactility.one" ttbuild_cdn = "https://cdn.tactility.one"
ttbuild_sdk_json_validity = 3600 # seconds ttbuild_sdk_json_validity = 3600 # seconds
ttport = 6666 ttport = 6666
@ -60,8 +60,9 @@ def print_help():
print(" clean Clean the build folders") print(" clean Clean the build folders")
print(" clearcache Clear the SDK cache") print(" clearcache Clear the SDK cache")
print(" updateself Update this tool") print(" updateself Update this tool")
print(" run [ip] Run an application") print(" run [ip] Run the application")
print(" install [ip] Install an 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(" 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(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
print("") print("")
@ -544,6 +545,19 @@ def install_action(ip, platforms):
except IOError as e: except IOError as e:
print_error(f"File error: {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 #region Main
if __name__ == "__main__": if __name__ == "__main__":
@ -599,6 +613,11 @@ if __name__ == "__main__":
platform = sys.argv[3] platform = sys.argv[3]
platforms_to_install = [platform] platforms_to_install = [platform]
install_action(sys.argv[2], platforms_to_install) 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": elif action_arg == "bir":
if len(sys.argv) < 3: if len(sys.argv) < 3:
print_help() print_help()

View File

@ -14,7 +14,7 @@ import shutil
import configparser import configparser
ttbuild_path = ".tactility" ttbuild_path = ".tactility"
ttbuild_version = "2.0.0" ttbuild_version = "2.1.0"
ttbuild_cdn = "https://cdn.tactility.one" ttbuild_cdn = "https://cdn.tactility.one"
ttbuild_sdk_json_validity = 3600 # seconds ttbuild_sdk_json_validity = 3600 # seconds
ttport = 6666 ttport = 6666
@ -60,8 +60,9 @@ def print_help():
print(" clean Clean the build folders") print(" clean Clean the build folders")
print(" clearcache Clear the SDK cache") print(" clearcache Clear the SDK cache")
print(" updateself Update this tool") print(" updateself Update this tool")
print(" run [ip] Run an application") print(" run [ip] Run the application")
print(" install [ip] Install an 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(" 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(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
print("") print("")
@ -544,6 +545,19 @@ def install_action(ip, platforms):
except IOError as e: except IOError as e:
print_error(f"File error: {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 #region Main
if __name__ == "__main__": if __name__ == "__main__":
@ -599,6 +613,11 @@ if __name__ == "__main__":
platform = sys.argv[3] platform = sys.argv[3]
platforms_to_install = [platform] platforms_to_install = [platform]
install_action(sys.argv[2], platforms_to_install) 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": elif action_arg == "bir":
if len(sys.argv) < 3: if len(sys.argv) < 3:
print_help() print_help()

View File

@ -14,7 +14,7 @@ import shutil
import configparser import configparser
ttbuild_path = ".tactility" ttbuild_path = ".tactility"
ttbuild_version = "2.0.0" ttbuild_version = "2.1.0"
ttbuild_cdn = "https://cdn.tactility.one" ttbuild_cdn = "https://cdn.tactility.one"
ttbuild_sdk_json_validity = 3600 # seconds ttbuild_sdk_json_validity = 3600 # seconds
ttport = 6666 ttport = 6666
@ -60,8 +60,9 @@ def print_help():
print(" clean Clean the build folders") print(" clean Clean the build folders")
print(" clearcache Clear the SDK cache") print(" clearcache Clear the SDK cache")
print(" updateself Update this tool") print(" updateself Update this tool")
print(" run [ip] Run an application") print(" run [ip] Run the application")
print(" install [ip] Install an 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(" 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(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
print("") print("")
@ -544,6 +545,19 @@ def install_action(ip, platforms):
except IOError as e: except IOError as e:
print_error(f"File error: {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 #region Main
if __name__ == "__main__": if __name__ == "__main__":
@ -599,6 +613,11 @@ if __name__ == "__main__":
platform = sys.argv[3] platform = sys.argv[3]
platforms_to_install = [platform] platforms_to_install = [platform]
install_action(sys.argv[2], platforms_to_install) 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": elif action_arg == "bir":
if len(sys.argv) < 3: if len(sys.argv) < 3:
print_help() print_help()

View File

@ -100,4 +100,6 @@ std::string getInstallPath();
bool install(const std::string& path); bool install(const std::string& path);
bool uninstall(const std::string& appId);
} }

View File

@ -11,6 +11,9 @@ struct AppManifest;
/** Register an application with its manifest */ /** Register an application with its manifest */
void addApp(const AppManifest& 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 /** Find an application manifest by its id
* @param[in] id the manifest id * @param[in] id the manifest id
* @return the application manifest if it was found * @return the application manifest if it was found

View File

@ -41,6 +41,13 @@ class DevelopmentService final : public Service {
.user_ctx = this .user_ctx = this
}; };
httpd_uri_t appUninstallEndpoint = {
.uri = "/app/uninstall",
.method = HTTP_PUT,
.handler = handleAppUninstall,
.user_ctx = this
};
void onNetworkConnected(); void onNetworkConnected();
void onNetworkDisconnected(); void onNetworkDisconnected();
@ -50,6 +57,7 @@ class DevelopmentService final : public Service {
static esp_err_t handleGetInfo(httpd_req_t* request); static esp_err_t handleGetInfo(httpd_req_t* request);
static esp_err_t handleAppRun(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 handleAppInstall(httpd_req_t* request);
static esp_err_t handleAppUninstall(httpd_req_t* request);
public: public:

View File

@ -223,4 +223,25 @@ bool install(const std::string& path) {
return true; 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<bool>(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 } // namespace

View File

@ -29,6 +29,15 @@ void addApp(const AppManifest& manifest) {
hash_mutex.unlock(); 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<AppManifest> findAppById(const std::string& id) { _Nullable std::shared_ptr<AppManifest> findAppById(const std::string& id) {
hash_mutex.lock(); hash_mutex.lock();
auto result = app_manifest_map.find(id); auto result = app_manifest_map.find(id);

View File

@ -10,6 +10,7 @@
#include <unistd.h> #include <unistd.h>
#include <vector> #include <vector>
#include <dirent.h> #include <dirent.h>
#include <Tactility/file/FileLock.h>
namespace tt::app::filebrowser { namespace tt::app::filebrowser {
@ -56,18 +57,19 @@ bool State::setEntriesForPath(const std::string& path) {
return true; return true;
} else { } else {
dir_entries.clear(); dir_entries.clear();
// TODO: file Lock return file::withLock<bool>(path, [this, &path] {
int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType); int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType);
if (count >= 0) { if (count >= 0) {
TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count); TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count);
current_path = path; current_path = path;
selected_child_entry = ""; selected_child_entry = "";
action = ActionNone; action = ActionNone;
return true; return true;
} else { } else {
TT_LOG_E(TAG, "Failed to fetch entries for %s", path.c_str()); TT_LOG_E(TAG, "Failed to fetch entries for %s", path.c_str());
return false; return false;
} }
});
} }
} }

View File

@ -16,6 +16,7 @@
#include <cstring> #include <cstring>
#include <unistd.h> #include <unistd.h>
#include <Tactility/file/FileLock.h>
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include "Tactility/service/loader/Loader.h" #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"); 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); 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); lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
} }
@ -314,12 +317,18 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bu
switch (state->getPendingAction()) { switch (state->getPendingAction()) {
case State::ActionDelete: { case State::ActionDelete: {
if (alertdialog::getResultIndex(*bundle) == 0) { if (alertdialog::getResultIndex(*bundle) == 0) {
int delete_count = remove(filepath.c_str()); file::withLock<void>(filepath, [&filepath] {
if (delete_count > 0) { if (file::isDirectory(filepath)) {
TT_LOG_I(TAG, "Deleted %d items", delete_count); if (!file::deleteRecursively(filepath)) {
} else { TT_LOG_W(TAG, "Failed to delete %s", filepath.c_str());
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()); state->setEntriesForPath(state->getCurrentPath());
update(); update();
} }
@ -328,12 +337,15 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bu
case State::ActionRename: { case State::ActionRename: {
auto new_name = inputdialog::getResult(*bundle); auto new_name = inputdialog::getResult(*bundle);
if (!new_name.empty() && new_name != state->getSelectedChildEntry()) { if (!new_name.empty() && new_name != state->getSelectedChildEntry()) {
std::string rename_to = file::getChildPath(state->getCurrentPath(), new_name); file::withLock<void>(filepath, [this, &filepath, &new_name] {
if (rename(filepath.c_str(), rename_to.c_str())) { std::string rename_to = file::getChildPath(state->getCurrentPath(), new_name);
TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str()); if (rename(filepath.c_str(), rename_to.c_str())) {
} else { TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str());
TT_LOG_E(TAG, "Failed to rename \"%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()); state->setEntriesForPath(state->getCurrentPath());
update(); update();
} }

View File

@ -110,6 +110,7 @@ void DevelopmentService::startServer() {
httpd_register_uri_handler(server, &handleGetInfoEndpoint); httpd_register_uri_handler(server, &handleGetInfoEndpoint);
httpd_register_uri_handler(server, &appRunEndpoint); httpd_register_uri_handler(server, &appRunEndpoint);
httpd_register_uri_handler(server, &appInstallEndpoint); httpd_register_uri_handler(server, &appInstallEndpoint);
httpd_register_uri_handler(server, &appUninstallEndpoint);
TT_LOG_I(TAG, "Started on port %d", config.server_port); TT_LOG_I(TAG, "Started on port %d", config.server_port);
} else { } else {
TT_LOG_E(TAG, "Failed to start"); TT_LOG_E(TAG, "Failed to start");
@ -297,6 +298,39 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
return ESP_OK; 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 // endregion
std::shared_ptr<DevelopmentService> findService() { std::shared_ptr<DevelopmentService> findService() {