Ken Van Hoeylandt 84049658db
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
2025-09-12 16:24:22 +02:00

295 lines
9.1 KiB
C++

#include "Tactility/lvgl/Toolbar.h"
#include <Tactility/Assets.h>
#include <Tactility/hal/Device.h>
#include <Tactility/Tactility.h>
#include <format>
#include <lvgl.h>
#include <utility>
#ifdef ESP_PLATFORM
#include <esp_vfs_fat.h>
#include <Tactility/MountPoints.h>
#endif
namespace tt::app::systeminfo {
constexpr auto* TAG = "SystemInfo";
static size_t getHeapFree() {
#ifdef ESP_PLATFORM
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
#else
return 4096 * 1024;
#endif
}
static size_t getHeapTotal() {
#ifdef ESP_PLATFORM
return heap_caps_get_total_size(MALLOC_CAP_INTERNAL);
#else
return 8192 * 1024;
#endif
}
static size_t getSpiFree() {
#ifdef ESP_PLATFORM
return heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
#else
return 4096 * 1024;
#endif
}
static size_t getSpiTotal() {
#ifdef ESP_PLATFORM
return heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
#else
return 8192 * 1024;
#endif
}
enum class StorageUnit {
Bytes,
Kilobytes,
Megabytes,
Gigabytes
};
static StorageUnit getStorageUnit(uint64_t value) {
using enum StorageUnit;
if (value / (1024 * 1024 * 1024) > 0) {
return Gigabytes;
} else if (value / (1024 * 1024) > 0) {
return Megabytes;
} else if (value / 1024 > 0) {
return Kilobytes;
} else {
return Bytes;
}
}
static std::string getStorageUnitString(StorageUnit unit) {
using enum StorageUnit;
switch (unit) {
case Bytes:
return "bytes";
case Kilobytes:
return "kB";
case Megabytes:
return "MB";
case Gigabytes:
return "GB";
default:
std::unreachable();
}
}
static std::string getStorageValue(StorageUnit unit, uint64_t bytes) {
using enum StorageUnit;
switch (unit) {
case Bytes:
return std::to_string(bytes);
case Kilobytes:
return std::to_string(bytes / 1024);
case Megabytes:
return std::format("{:.1f}", static_cast<float>(bytes) / 1024.f / 1024.f);
case Gigabytes:
return std::format("{:.1f}", static_cast<float>(bytes) / 1024.f / 1024.f / 1024.f);
default:
std::unreachable();
}
}
static void addMemoryBar(lv_obj_t* parent, const char* label, uint64_t free, uint64_t total) {
uint64_t used = total - free;
auto* container = lv_obj_create(parent);
lv_obj_set_size(container, LV_PCT(100), LV_SIZE_CONTENT);
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);
lv_obj_set_width(left_label, 60);
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, total_scaled);
} else {
lv_bar_set_range(bar, 0, 1);
}
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, "%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
static const char* getTaskState(const TaskStatus_t& task) {
switch (task.eCurrentState) {
case eRunning:
return "running";
case eReady:
return "ready";
case eBlocked:
return "blocked";
case eSuspended:
return "suspended";
case eDeleted:
return "deleted";
case eInvalid:
default:
return "invalid";
}
}
static void addRtosTask(lv_obj_t* parent, const TaskStatus_t& task) {
auto* label = lv_label_create(parent);
const char* name = (task.pcTaskName == nullptr || task.pcTaskName[0] == 0) ? "(unnamed)" : task.pcTaskName;
lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task));
}
static void addRtosTasks(lv_obj_t* parent) {
UBaseType_t count = uxTaskGetNumberOfTasks();
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);
}
free(tasks);
}
#endif
static void addDevice(lv_obj_t* parent, const std::shared_ptr<hal::Device>& device) {
auto* label = lv_label_create(parent);
lv_label_set_text(label, device->getName().c_str());
}
static void addDevices(lv_obj_t* parent) {
auto devices = hal::getDevices();
for (const auto& device: devices) {
addDevice(parent, device);
}
}
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
auto* wrapper = lv_obj_create(parent);
lv_obj_set_style_border_width(wrapper, 0, 0);
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);
auto* tabview = lv_tabview_create(wrapper);
lv_tabview_set_tab_bar_position(tabview, LV_DIR_LEFT);
lv_tabview_set_tab_bar_size(tabview, 80);
// 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_tab, "External", getSpiFree(), getSpiTotal());
}
#ifdef ESP_PLATFORM
// Wrapper for the memory usage bars
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_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_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_tab,
sdcard->getMountPath().c_str(),
storage_free,
storage_total
);
}
}
#endif
#if configUSE_TRACE_FACILITY
addRtosTasks(tasks_tab);
#endif
addDevices(devices_tab);
// Build info
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
}
};
extern const AppManifest manifest = {
.id = "SystemInfo",
.name = "System Info",
.icon = TT_ASSETS_APP_ICON_SYSTEM_INFO,
.type = Type::System,
.createApp = create<SystemInfoApp>
};
} // namespace