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
This commit is contained in:
Ken Van Hoeylandt
2025-09-12 16:24:22 +02:00
committed by GitHub
parent 068600f98c
commit 84049658db
87 changed files with 1490 additions and 537 deletions
+24 -3
View File
@@ -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<void(const dirent&)> 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<dirent>& 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<void(const char* line)> callback);
+73 -2
View File
@@ -3,6 +3,7 @@
#include <cstring>
#include <fstream>
#include <unistd.h>
#include <Tactility/StringUtils.h>
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<void(const dirent&)> 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<dirent>& 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<dirent> 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;
}