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
+73 -3
View File
@@ -1,10 +1,11 @@
#include <Tactility/Tactility.h>
#include <Tactility/TactilityConfig.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/DispatcherThread.h>
#include <Tactility/MountPoints.h>
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/hal/HalPrivate.h>
#include <Tactility/hal/sdcard/SdCardMounting.h>
#include <Tactility/lvgl/LvglPrivate.h>
#include <Tactility/network/NtpPrivate.h>
#include <Tactility/service/ServiceManifest.h>
@@ -12,6 +13,9 @@
#include <Tactility/service/loader/Loader.h>
#include <Tactility/settings/TimePrivate.h>
#include <map>
#include <format>
#ifdef ESP_PLATFORM
#include <Tactility/InitEsp.h>
#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<std::string, std::string> 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<hal::sdcard::SdCardDevice>& 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::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
for (const auto& sdcard : sdcard_devices) {
if (sdcard->isMounted()) {
registerInstalledAppsFromSdCard(sdcard);
}
}
}
static void registerUserApps(const std::vector<const app::AppManifest*>& apps) {
TT_LOG_I(TAG, "Registering user apps");
for (auto* manifest : apps) {
@@ -176,8 +241,13 @@ static void registerAndStartUserServices(const std::vector<const service::Servic
void initFromBootApp() {
auto configuration = getConfiguration();
// Then we register system apps. They are not used/started yet.
// Then we register apps. They are not used/started yet.
registerSystemApps();
auto data_apps_path = std::format("{}/apps", file::MOUNT_POINT_DATA);
if (file::isDirectory(data_apps_path)) {
registerInstalledApps(data_apps_path);
}
registerInstalledAppsFromSdCards();
// Then we register and start user services. They are started after system app
// registration just in case they want to figure out which system apps are installed.
registerAndStartUserServices(configuration->services);
+1 -2
View File
@@ -1,5 +1,4 @@
#include "Tactility/app/App.h"
#include <Tactility/app/App.h>
#include <Tactility/service/loader/Loader.h>
namespace tt::app {
+226
View File
@@ -0,0 +1,226 @@
#include <Tactility/app/App.h>
#include <Tactility/MountPoints.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/file/File.h>
#include <Tactility/file/FileLock.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/hal/Device.h>
#include <Tactility/hal/sdcard/SdCardDevice.h>
#include <errno.h>
#include <fcntl.h>
#include <format>
#include <libgen.h>
#include <map>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <minitar.h>
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<char*>(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::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
bool is_set = false;
hal::findDevices<hal::sdcard::SdCardDevice>(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<std::string, std::string> 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
+4 -4
View File
@@ -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<AppManifest>(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<AppManifest>(manifest);
hash_mutex.unlock();
}
+6 -21
View File
@@ -38,7 +38,7 @@ static std::shared_ptr<Lock> elfManifestLock = std::make_shared<Mutex>();
class ElfApp : public App {
const std::string filePath;
const std::string appPath;
std::unique_ptr<uint8_t[]> 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<void>(filePath, [this, &size]{
elfFileData = file::readBinary(filePath, size);
file::withLock<void>(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<App> createElfApp(const std::shared_ptr<AppManifest>& manifest) {
TT_LOG_I(TAG, "createElfApp");
assert(manifest != nullptr);
+1
View File
@@ -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);
@@ -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<std::string>& buttonLabels) {
LaunchId start(const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels) {
std::string items_joined = string::join(buttonLabels, PARAMETER_ITEM_CONCATENATION_TOKEN);
auto bundle = std::make_shared<Bundle>();
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>();
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) {
+29 -11
View File
@@ -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();
}
};
@@ -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
+25 -57
View File
@@ -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<ChatApp*>(lv_event_get_user_data(e));
auto* btn = static_cast<lv_obj_t*>(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<ChatApp*>(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<const uint8_t*>(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<char*>(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<uint8_t*>(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);
@@ -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);
+1
View File
@@ -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);
@@ -28,7 +28,7 @@ public:
}
void onResult(AppContext& appContext, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
view->onResult(result, std::move(bundle));
view->onResult(launchId, result, std::move(bundle));
}
};
@@ -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) {
+16 -5
View File
@@ -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> bundle) {
void View::onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> 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());
@@ -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);
+1
View File
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
+2
View File
@@ -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);
+1
View File
@@ -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);
+1
View File
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
+57 -46
View File
@@ -4,6 +4,7 @@
#include <Tactility/hal/Device.h>
#include <Tactility/Tactility.h>
#include <format>
#include <lvgl.h>
#include <utility>
@@ -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<float>(bytes) / 1024.f / 1024.f);
case Gigabytes:
return bytes / 1024 / 1024 / 1024;
return std::format("{:.1f}", static_cast<float>(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<uint64_t>(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::sdcard::SdCardDevice>(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
}
};
@@ -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);
@@ -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);
@@ -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
+2 -1
View File
@@ -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);
+4 -1
View File
@@ -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);
+11 -6
View File
@@ -22,17 +22,22 @@ bool loadPropertiesFile(const std::string& filePath, std::function<void(const st
return file::withLock<bool>(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());
}
}
}
});
@@ -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());
@@ -18,6 +18,8 @@
#include <esp_wifi.h>
#include <ranges>
#include <sstream>
#include <Tactility/Tactility.h>
#include <Tactility/file/FileLock.h>
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;
}
@@ -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) {
+5 -5
View File
@@ -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<GpsDevice>& device) {
@@ -58,14 +58,14 @@ void GpsService::removeGpsDevice(const std::shared_ptr<GpsDevice>& 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();
}
+10 -5
View File
@@ -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) {
+2 -1
View File
@@ -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 {
@@ -2,16 +2,15 @@
#if TT_FEATURE_SCREENSHOT_ENABLED
#include "Tactility/service/screenshot/Screenshot.h"
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/screenshot/Screenshot.h>
#include <Tactility/service/ServiceRegistration.h>
#include <memory>
#include <lvgl.h>
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)) {
+10 -1
View File
@@ -2,6 +2,7 @@
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/Mutex.h>
#include <Tactility/Tactility.h>
#include <Tactility/Timer.h>
#include <Tactility/hal/sdcard/SdCardDevice.h>
@@ -52,13 +53,21 @@ class SdCardService final : public Service {
public:
void onStart(ServiceContext& serviceContext) override {
bool onStart(ServiceContext& serviceContext) override {
if (hal::findFirstDevice<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard) == nullptr) {
TT_LOG_W(TAG, "No SD card device found - not starting Service");
return false;
}
auto service = findServiceById<SdCardService>(manifest.id);
updateTimer = std::make_unique<Timer>(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 {
@@ -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{
+4 -3
View File
@@ -414,10 +414,9 @@ static bool copy_scan_list(std::shared_ptr<Wifi> wifi) {
}
static bool find_auto_connect_ap(std::shared_ptr<Wifi> 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<const char*>(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<Wifi>();
@@ -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 {
+2 -1
View File
@@ -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 {