Various improvements (#461)

* **New Features**
  * Time and delay utilities added (ticks, ms, µs); SD card now uses an expansion-header CS pin; HTTP downloads warn when run on the GUI task and yield to avoid blocking.

* **Bug Fixes / Reliability**
  * Many hard-crash paths converted to guarded checks to reduce abrupt termination and improve stability.

* **Tests**
  * Unit tests added to validate time and delay accuracy.

* **Chores**
  * License header and build/macro updates.
This commit is contained in:
Ken Van Hoeylandt
2026-01-27 08:04:21 +01:00
committed by GitHub
parent 619b5aa53b
commit e6abd496f9
95 changed files with 433 additions and 284 deletions
@@ -59,7 +59,7 @@ class DisplayIdleService final : public Service {
}
public:
bool onStart(TT_UNUSED ServiceContext& service) override {
bool onStart(ServiceContext& service) override {
// Load settings once at startup and cache them
// This eliminates file I/O from timer callback (prevents watchdog timeout)
cachedDisplaySettings = settings::display::loadOrGetDefault();
@@ -73,7 +73,7 @@ public:
return true;
}
void onStop(TT_UNUSED ServiceContext& service) override {
void onStop(ServiceContext& service) override {
if (timer) {
timer->stop();
timer = nullptr;
+17 -4
View File
@@ -1,5 +1,7 @@
#include <Tactility/service/gui/GuiService.h>
#include <cstring>
#include <Tactility/app/AppInstance.h>
#include <Tactility/Logger.h>
#include <Tactility/LogMessages.h>
@@ -15,6 +17,15 @@ extern const ServiceManifest manifest;
static const auto LOGGER = Logger("GuiService");
using namespace loader;
constexpr auto* GUI_TASK_NAME = "gui";
void warnIfRunningOnGuiTask(const char* context) {
const char* task_name = pcTaskGetName(nullptr);
if (strcmp(GUI_TASK_NAME, task_name) == 0) {
LOGGER.warn("{} shouldn't run on the GUI task", context);
}
}
// region AppManifest
void GuiService::onLoaderEvent(LoaderService::Event event) {
@@ -114,7 +125,7 @@ void GuiService::redraw() {
unlock();
}
bool GuiService::onStart(TT_UNUSED ServiceContext& service) {
bool GuiService::onStart(ServiceContext& service) {
auto* screen_root = lv_screen_active();
if (screen_root == nullptr) {
LOGGER.error("No display found");
@@ -122,11 +133,13 @@ bool GuiService::onStart(TT_UNUSED ServiceContext& service) {
}
thread = new Thread(
"gui",
GUI_TASK_NAME,
4096, // Last known minimum was 2800 for launching desktop
[]() { return guiMain(); }
);
thread->setPriority(THREAD_PRIORITY_SERVICE);
const auto loader = findLoaderService();
assert(loader != nullptr);
loader_pubsub_subscription = loader->getPubsub()->subscribe([this](auto event) {
@@ -169,7 +182,7 @@ bool GuiService::onStart(TT_UNUSED ServiceContext& service) {
return true;
}
void GuiService::onStop(TT_UNUSED ServiceContext& service) {
void GuiService::onStop(ServiceContext& service) {
lock();
const auto loader = findLoaderService();
@@ -186,7 +199,7 @@ void GuiService::onStop(TT_UNUSED ServiceContext& service) {
unlock();
tt_check(lvgl::lock(1000 / portTICK_PERIOD_MS));
check(lvgl::lock(1000 / portTICK_PERIOD_MS));
lv_group_delete(keyboardGroup);
lvgl::unlock();
}
+2 -2
View File
@@ -17,7 +17,7 @@ static void show_keyboard(lv_event_t* event) {
}
}
static void hide_keyboard(TT_UNUSED lv_event_t* event) {
static void hide_keyboard(lv_event_t* event) {
auto service = findService();
if (service != nullptr) {
service->softwareKeyboardHide();
@@ -53,7 +53,7 @@ void GuiService::keyboardAddTextArea(lv_obj_t* textarea) {
lock();
if (isStarted) {
tt_check(lvgl::lock(0), "lvgl should already be locked before calling this method");
check(lvgl::lock(0), "lvgl should already be locked before calling this method");
if (softwareKeyboardIsEnabled()) {
lv_obj_add_event_cb(textarea, show_keyboard, LV_EVENT_FOCUSED, nullptr);
@@ -58,7 +58,7 @@ class KeyboardIdleService final : public Service {
}
public:
bool onStart(TT_UNUSED ServiceContext& service) override {
bool onStart(ServiceContext& service) override {
// Load settings once at startup and cache them
// This eliminates file I/O from timer callback (prevents watchdog timeout)
cachedKeyboardSettings = settings::keyboard::loadOrGetDefault();
@@ -72,7 +72,7 @@ public:
return true;
}
void onStop(TT_UNUSED ServiceContext& service) override {
void onStop(ServiceContext& service) override {
if (timer) {
timer->stop();
timer = nullptr;
+1 -1
View File
@@ -245,7 +245,7 @@ void LoaderService::transitionAppToState(const std::shared_ptr<app::AppInstance>
switch (state) {
using enum app::State;
case Initial:
tt_crash(LOG_MESSAGE_ILLEGAL_STATE);
check(false, LOG_MESSAGE_ILLEGAL_STATE);
case Created:
assert(app->getState() == app::State::Initial);
app->getApp()->onCreate(*app);
@@ -107,7 +107,7 @@ void ScreenshotTask::taskStart() {
return;
}
tt_check(thread == nullptr);
check(thread == nullptr);
thread = new Thread(
"screenshot",
8192,
@@ -1,5 +1,6 @@
#include <Tactility/lvgl/Statusbar.h>
#include <Tactility/Check.h>
#include <Tactility/hal/power/PowerDevice.h>
#include <Tactility/hal/sdcard/SdCardDevice.h>
#include <Tactility/Logger.h>
@@ -71,7 +72,7 @@ static const char* getWifiStatusIcon(wifi::RadioState state, bool secure) {
rssi = wifi::getRssi();
return getWifiStatusIconForRssi(rssi);
default:
tt_crash("not implemented");
check(false, "not implemented");
}
}
@@ -85,7 +86,7 @@ static const char* getSdCardStatusIcon(hal::sdcard::SdCardDevice::State state) {
case Timeout:
return STATUSBAR_ICON_SDCARD_ALERT;
default:
tt_crash("Unhandled SdCard state");
check(false, "Unhandled SdCard state");
}
}
@@ -1,5 +1,6 @@
#ifdef ESP_PLATFORM
#include <Tactility/Check.h>
#include <Tactility/service/webserver/WebServerService.h>
#include <Tactility/service/webserver/AssetVersion.h>
#include <Tactility/service/ServiceManifest.h>
@@ -93,7 +94,7 @@ static void publish_event(WebServerService* webserver, WebServerEvent event) {
std::shared_ptr<PubSub<WebServerEvent>> getPubsub() {
WebServerService* webserver = g_webServerInstance.load();
if (webserver == nullptr) {
tt_crash("Service not running");
check(false, "Service not running");
}
return webserver->getPubsub();
+2 -1
View File
@@ -1,6 +1,7 @@
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/Check.h>
#include <Tactility/CoreDefines.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
@@ -22,7 +23,7 @@ const char* radioStateToString(RadioState state) {
case Off:
return TT_STRINGIFY(Off);
}
tt_crash("not implemented");
check(false, "not implemented");
}
extern const ServiceManifest manifest;
+3 -2
View File
@@ -6,6 +6,7 @@
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/Check.h>
#include <Tactility/EventGroup.h>
#include <Tactility/Logger.h>
#include <Tactility/LogMessages.h>
@@ -140,7 +141,7 @@ static std::shared_ptr<Wifi> wifi_singleton;
std::shared_ptr<PubSub<WifiEvent>> getPubsub() {
auto wifi = wifi_singleton;
if (wifi == nullptr) {
tt_crash("Service not running");
check(false, "Service not running");
}
return wifi->pubsub;
@@ -472,7 +473,7 @@ static void dispatchAutoConnect(std::shared_ptr<Wifi> wifi) {
}
}
static void eventHandler(TT_UNUSED void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
static void eventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
auto wifi = wifi_singleton;
if (wifi == nullptr) {
LOGGER.error("eventHandler: no wifi instance");
+5 -5
View File
@@ -76,7 +76,7 @@ void setScanRecords(uint16_t records) {
}
std::vector<ApRecord> getScanResults() {
tt_check(wifi);
check(wifi);
std::vector<ApRecord> records;
records.push_back((ApRecord) {
@@ -145,14 +145,14 @@ class WifiService final : public Service {
public:
bool onStart(TT_UNUSED ServiceContext& service) override {
tt_check(wifi == nullptr);
bool onStart(ServiceContext& service) override {
check(wifi == nullptr);
wifi = new Wifi();
return true;
}
void onStop(TT_UNUSED ServiceContext& service) override {
tt_check(wifi != nullptr);
void onStop(ServiceContext& service) override {
check(wifi != nullptr);
delete wifi;
wifi = nullptr;
}