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 = {
.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 {

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -100,4 +100,6 @@ std::string getInstallPath();
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 */
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

View File

@ -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:

View File

@ -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<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

View File

@ -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<AppManifest> findAppById(const std::string& id) {
hash_mutex.lock();
auto result = app_manifest_map.find(id);

View File

@ -10,6 +10,7 @@
#include <unistd.h>
#include <vector>
#include <dirent.h>
#include <Tactility/file/FileLock.h>
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<bool>(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;
}
});
}
}

View File

@ -16,6 +16,7 @@
#include <cstring>
#include <unistd.h>
#include <Tactility/file/FileLock.h>
#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<Bundle> 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<void>(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<Bundle> 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<void>(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();
}

View File

@ -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<DevelopmentService> findService() {