Improvements and fixes (#595)

- Fix BluetoothSettings app not updating when toggling on Bluetooth on
- Fix BluetoothSettings app crash when closing while scanning
- Fix BluetoothSettings app crash when turning BT off while scanning
- WebServer doesn't save config anymore as a side-effect of reading the config
- Add missing license headers
- Use TactilityKernel's time and delay functions instead of TactilityFreeRtos ones
This commit is contained in:
Ken Van Hoeylandt 2026-07-28 18:38:23 +02:00 committed by GitHub
parent e6e1dcd0ca
commit bd108cc3c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 215 additions and 112 deletions

View File

@ -1,4 +1,5 @@
#include <tactility/module.h> #include <tactility/module.h>
#include <tactility/delay.h>
#include <tactility/error.h> #include <tactility/error.h>
#include <tactility/log.h> #include <tactility/log.h>
@ -42,7 +43,7 @@ static error_t start() {
} }
// Avoids crash when no SD card is inserted. It's unknown why, but likely is related to power draw. // Avoids crash when no SD card is inserted. It's unknown why, but likely is related to power draw.
tt::kernel::delayMillis(100); delay_millis(100);
subscribe_events(); subscribe_events();

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#include "unphone_nav_buttons.h" #include "unphone_nav_buttons.h"
#include <tactility/delay.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/driver.h> #include <tactility/driver.h>
#include <tactility/drivers/gpio_controller.h> #include <tactility/drivers/gpio_controller.h>
@ -76,9 +77,9 @@ static int32_t nav_buttons_thread_main(UnphoneNavButtonsInternal* internal) {
// Debounce all events for a short period of time // Debounce all events for a short period of time
// This is easier than keeping track when each button was last pressed // This is easier than keeping track when each button was last pressed
tt::kernel::delayMillis(50); delay_millis(50);
xQueueReset(internal->event_queue); xQueueReset(internal->event_queue);
tt::kernel::delayMillis(50); delay_millis(50);
xQueueReset(internal->event_queue); xQueueReset(internal->event_queue);
} }
} }

View File

@ -2,7 +2,6 @@
## Before release ## Before release
- WebServer service shouldn't save webserver.properties at start, it slows the boot process
- Remove incubating flag from various devices - Remove incubating flag from various devices
- Add `// SPDX-License-Identifier: GPL-3.0-only` and `// SPDX-License-Identifier: Apache-2.0` to individual files in the project - Add `// SPDX-License-Identifier: GPL-3.0-only` and `// SPDX-License-Identifier: Apache-2.0` to individual files in the project
- Elecrow Basic & Advance 3.5" memory issue: not enough memory for App Hub - Elecrow Basic & Advance 3.5" memory issue: not enough memory for App Hub
@ -13,7 +12,6 @@
## Higher Priority ## Higher Priority
- Bluetooth app: when toggling BT on, it doesn't update the UI with discovered devices. It only works after re-opening the app.
- display.h API: get_backlight does not change ref counting, but it should - display.h API: get_backlight does not change ref counting, but it should
- bluetooth: various getters for child devices do not change ref counting, but they should - bluetooth: various getters for child devices do not change ref counting, but they should
- Improve kernel_init.cpp (and other modules): create driver_ensure_added() and driver_ensure_destructed() - Improve kernel_init.cpp (and other modules): create driver_ensure_added() and driver_ensure_destructed()

View File

@ -1,3 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once #pragma once
#define LVGL_ICON_LAUNCHER_APPS "\xEE\x97\x83" #define LVGL_ICON_LAUNCHER_APPS "\xEE\x97\x83"

View File

@ -1,3 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once #pragma once
#define LVGL_ICON_SHARED_ADD "\xEE\x85\x85" #define LVGL_ICON_SHARED_ADD "\xEE\x85\x85"

View File

@ -1,3 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once #pragma once
#define LVGL_ICON_STATUSBAR_LOCATION_ON "\xEF\x87\x9B" #define LVGL_ICON_STATUSBAR_LOCATION_ON "\xEF\x87\x9B"

View File

@ -194,10 +194,22 @@ void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) {
size_t i = start_idx; size_t i = start_idx;
while (true) { while (true) {
ble_addr_t addr = {}; ble_addr_t addr = {};
bool found = false; bool found = false;
{ bool radio_on = false;
xSemaphoreTake(ctx->scan_mutex, portMAX_DELAY); int rc = -1;
// Don't start (or chain into) a new GAP connection once the radio is going down
// (or is already off) — ble_gap_connect() racing nimble_port_stop() can block the
// NimBLE host task and hang the stop, same class of bug as the OFF_PENDING guard
// on advertising restart in gap_event_handler's BLE_GAP_EVENT_DISCONNECT case.
// The check must be re-done on every iteration (not just once on entry) since a
// failed ble_gap_connect() loops back for the next peer, and it must happen under
// scan_mutex together with the connect call itself so a concurrent dispatch_disable()
// can't flip radio_state between the check and the call.
xSemaphoreTake(ctx->scan_mutex, portMAX_DELAY);
radio_on = ctx->radio_state.load() == BT_RADIO_STATE_ON;
if (radio_on) {
while (i < ctx->scan_count) { while (i < ctx->scan_count) {
if (ctx->scan_results[i].name[0] == '\0') { if (ctx->scan_results[i].name[0] == '\0') {
addr = ctx->scan_addrs[i]; addr = ctx->scan_addrs[i];
@ -206,7 +218,23 @@ void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) {
} }
++i; ++i;
} }
xSemaphoreGive(ctx->scan_mutex); if (found) {
uint8_t own_addr_type;
ble_hs_id_infer_auto(0, &own_addr_type);
void* idx_arg = (void*)(uintptr_t)i;
rc = ble_gap_connect(own_addr_type, &addr, 1500, nullptr,
name_res_gap_callback, idx_arg);
}
}
xSemaphoreGive(ctx->scan_mutex);
if (!radio_on) {
LOG_I(TAG, "Name resolution: aborting (radio not on)");
ble_set_scan_active(device, false);
struct BtEvent e = {};
e.type = BT_EVENT_SCAN_FINISHED;
ble_publish_event(device, e);
return;
} }
if (!found) { if (!found) {
@ -218,12 +246,6 @@ void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) {
return; return;
} }
uint8_t own_addr_type;
ble_hs_id_infer_auto(0, &own_addr_type);
void* idx_arg = (void*)(uintptr_t)i;
int rc = ble_gap_connect(own_addr_type, &addr, 1500, nullptr,
name_res_gap_callback, idx_arg);
if (rc == 0) { if (rc == 0) {
return; // name_res_gap_callback continues the chain return; // name_res_gap_callback continues the chain
} }

View File

@ -8,6 +8,9 @@
#include <Tactility/bluetooth/Bluetooth.h> #include <Tactility/bluetooth/Bluetooth.h>
#include <tactility/drivers/bluetooth.h> #include <tactility/drivers/bluetooth.h>
#include <atomic>
#include <memory>
namespace tt::app::btmanage { namespace tt::app::btmanage {
class BtManage final : public App { class BtManage final : public App {
@ -18,6 +21,15 @@ class BtManage final : public App {
View view = View(&bindings, &state); View view = View(&bindings, &state);
bool isViewEnabled = false; bool isViewEnabled = false;
Device* btDevice = nullptr; Device* btDevice = nullptr;
bool callbackRegistered = false;
// Bumped by onHide() to invalidate any BT event already dispatched to the main
// task for this show/hide session (BtManage is reused across hide/show cycles -
// e.g. launching BtPeerSettings pushes it on top and hides this instance without
// destroying it). Kept in its own heap allocation, independent of BtManage's
// lifetime, so a dispatched callback can check it without touching a possibly
// already-destroyed `this`.
std::shared_ptr<std::atomic<int>> generation = std::make_shared<std::atomic<int>>(0);
public: public:
@ -35,6 +47,20 @@ public:
State& getState() { return state; } State& getState() { return state; }
void requestViewUpdate(); void requestViewUpdate();
std::shared_ptr<std::atomic<int>> getGeneration() const { return generation; }
// Re-attempts registering the device event callback. Needed because the BLE driver
// only allocates its callback list while the device is started/on: a registration
// attempted while the radio is off silently no-ops, so this must be called again
// right after a successful bluetooth::start(). Idempotent: no-ops if already
// registered for this device, so it's safe to call from both onShow() and here.
void registerDeviceCallback(Device* dev);
// Call after bluetooth::stop(): the driver frees its callback list on stop, so the
// registration state must be cleared here too, without touching the (now-dangling)
// driver-side list.
void forgetCallbackRegistration();
}; };
} // namespace tt::app::btmanage } // namespace tt::app::btmanage

View File

@ -1,10 +1,11 @@
#include <Tactility/CoreDefines.h>
#include <Tactility/Mutex.h>
#include <Tactility/SystemEvents.h> #include <Tactility/SystemEvents.h>
#include <Tactility/Mutex.h>
#include <tactility/check.h> #include <tactility/check.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/time.h>
#include <Tactility/CoreDefines.h>
#include <list> #include <list>
namespace tt::kernel { namespace tt::kernel {

View File

@ -1,13 +1,13 @@
#include "Tactility/app/alertdialog/AlertDialog.h" #include "Tactility/app/alertdialog/AlertDialog.h"
#include <lvgl/widgets/toolbar.h> #include <Tactility/service/loader/Loader.h>
#include "Tactility/service/loader/Loader.h"
#include <Tactility/StringUtils.h> #include <Tactility/StringUtils.h>
#include <lvgl.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <lvgl.h>
#include <lvgl/widgets/toolbar.h>
namespace tt::app::alertdialog { namespace tt::app::alertdialog {
#define PARAMETER_BUNDLE_KEY_TITLE "title" #define PARAMETER_BUNDLE_KEY_TITLE "title"

View File

@ -1,6 +1,8 @@
#include "Tactility/lvgl/Lvgl.h" #include <tactility/delay.h>
#include "tactility/drivers/backlight.h" #include <tactility/drivers/backlight.h>
#include "tactility/drivers/display.h" #include <tactility/drivers/display.h>
#include <tactility/log.h>
#include <tactility/time.h>
#include <Tactility/CpuAffinity.h> #include <Tactility/CpuAffinity.h>
#include <Tactility/Paths.h> #include <Tactility/Paths.h>
@ -10,13 +12,13 @@
#include <Tactility/app/AppPaths.h> #include <Tactility/app/AppPaths.h>
#include <Tactility/app/alertdialog/AlertDialog.h> #include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/hal/usb/Usb.h> #include <Tactility/hal/usb/Usb.h>
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/lvgl/Style.h> #include <Tactility/lvgl/Style.h>
#include <Tactility/service/loader/Loader.h> #include <Tactility/service/loader/Loader.h>
#include <Tactility/settings/BootSettings.h> #include <Tactility/settings/BootSettings.h>
#include <Tactility/settings/DisplaySettings.h> #include <Tactility/settings/DisplaySettings.h>
#include <lvgl.h> #include <lvgl.h>
#include <tactility/log.h>
#include <atomic> #include <atomic>
@ -108,23 +110,23 @@ class BootApp : public App {
} }
static void waitForMinimalSplashDuration(TickType_t startTime) { static void waitForMinimalSplashDuration(TickType_t startTime) {
const auto end_time = kernel::getTicks(); const auto end_time = get_ticks();
const auto ticks_passed = end_time - startTime; const auto ticks_passed = end_time - startTime;
constexpr auto minimum_ticks = (CONFIG_TT_SPLASH_DURATION / portTICK_PERIOD_MS); constexpr auto minimum_ticks = (CONFIG_TT_SPLASH_DURATION / portTICK_PERIOD_MS);
if (minimum_ticks > ticks_passed) { if (minimum_ticks > ticks_passed) {
kernel::delayTicks(minimum_ticks - ticks_passed); delay_ticks(minimum_ticks - ticks_passed);
} }
} }
static int32_t bootThreadCallback() { static int32_t bootThreadCallback() {
LOG_I(TAG, "Starting boot thread"); LOG_I(TAG, "Starting boot thread");
const auto start_time = kernel::getTicks(); const auto start_time = get_ticks();
// Give the UI some time to redraw // 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 // 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 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 // This works with 5 ms on a T-Lora Pager, so we give it 10 ms to be safe
kernel::delayMillis(10); delay_millis(10);
// TODO: Support for multiple displays // TODO: Support for multiple displays
LOG_I(TAG, "Setup display"); LOG_I(TAG, "Setup display");

View File

@ -23,10 +23,20 @@ static void onBtToggled(bool requestOn) {
bool radio_on = bluetooth::isRadioOnOrPending(dev); bool radio_on = bluetooth::isRadioOnOrPending(dev);
if (requestOn && !radio_on) { if (requestOn && !radio_on) {
LOG_I(TAG, "Turning on"); LOG_I(TAG, "Turning on");
bluetooth::start(dev); if (bluetooth::start(dev)) {
// The driver only allocates its callback list once the device is started,
// so the registration attempted in onShow() (while radio was off) was a
// no-op. Register again now that the device is actually up.
auto bt = std::static_pointer_cast<BtManage>(getCurrentApp());
bt->registerDeviceCallback(dev);
}
} else if (!requestOn && radio_on) { } else if (!requestOn && radio_on) {
LOG_I(TAG, "Turning off"); LOG_I(TAG, "Turning off");
bluetooth::stop(dev); if (bluetooth::stop(dev)) {
// A completed stop frees the driver's callback list.
auto bt = std::static_pointer_cast<BtManage>(getCurrentApp());
bt->forgetCallbackRegistration();
}
} }
device_put(dev); device_put(dev);
} else { } else {
@ -90,13 +100,19 @@ void BtManage::unlock() {
} }
void BtManage::requestViewUpdate() { void BtManage::requestViewUpdate() {
// Lock order must match onShow()/onHide(): both run under GuiService's lvgl_lock()
// and then take `mutex` internally. Taking `mutex` before lvgl_lock() here would
// invert that order and deadlock against a concurrent onHide()/onShow() (GUI task
// holding LVGL lock, waiting on `mutex`; this task holding `mutex`, waiting on LVGL
// lock) - exactly what happens when BT events fire rapidly (e.g. during scanning)
// while the app is being hidden.
lvgl_lock();
lock(); lock();
if (isViewEnabled) { if (isViewEnabled) {
lvgl_lock();
view.update(); view.update();
lvgl_unlock();
} }
unlock(); unlock();
lvgl_unlock();
} }
void BtManage::onBtEvent(const BtEvent& event) { void BtManage::onBtEvent(const BtEvent& event) {
@ -148,17 +164,45 @@ static void onKernelBtEvent(Device* /*device*/, void* context, BtEvent event) {
// nimble_port_stop), creating a permanent deadlock. Dispatch to the main task so // nimble_port_stop), creating a permanent deadlock. Dispatch to the main task so
// the NimBLE host task is never blocked by BtManage's state updates or LVGL lock. // the NimBLE host task is never blocked by BtManage's state updates or LVGL lock.
auto* self = static_cast<BtManage*>(context); auto* self = static_cast<BtManage*>(context);
getMainDispatcher().dispatch([self, event] { // Captured while `self` is still guaranteed valid (the callback is only invoked
// while registered, i.e. before onHide() removes it). Comparing this later - without
// dereferencing `self` - lets the dispatched lambda detect a stale event from a
// session that has since been hidden (and possibly destroyed) without a UAF.
auto generation = self->getGeneration();
int expectedGeneration = generation->load();
getMainDispatcher().dispatch([self, generation, expectedGeneration, event] {
if (generation->load() != expectedGeneration) {
return;
}
self->onBtEvent(event); self->onBtEvent(event);
}); });
} }
void BtManage::registerDeviceCallback(Device* dev) {
lock();
if (btDevice == dev && !callbackRegistered) {
// Only latch the flag on success: while the radio is off the driver has no
// callback list yet, so this add is a silent no-op and must be retried once
// bluetooth::start() actually brings the device up.
if (bluetooth_add_event_callback(dev, this, onKernelBtEvent) == ERROR_NONE) {
callbackRegistered = true;
}
}
unlock();
}
void BtManage::forgetCallbackRegistration() {
lock();
callbackRegistered = false;
unlock();
}
void BtManage::onShow(AppContext& app, lv_obj_t* parent) { void BtManage::onShow(AppContext& app, lv_obj_t* parent) {
// Initialise state and view before subscribing to avoid incoming events // Initialise state and view before subscribing to avoid incoming events
// racing with state initialisation. // racing with state initialisation.
state.setRadioState(bluetooth::getRadioState()); state.setRadioState(bluetooth::getRadioState());
Device* dev = nullptr; Device* dev = nullptr;
device_get_first_active_by_type(&BLUETOOTH_TYPE, &dev); device_get_first_by_type(&BLUETOOTH_TYPE, &dev);
state.setScanning(dev ? bluetooth_is_scanning(dev) : false); state.setScanning(dev ? bluetooth_is_scanning(dev) : false);
state.updateScanResults(); state.updateScanResults();
@ -177,7 +221,7 @@ void BtManage::onShow(AppContext& app, lv_obj_t* parent) {
btDevice = dev; btDevice = dev;
if (btDevice) { if (btDevice) {
bluetooth_add_event_callback(btDevice, this, onKernelBtEvent); registerDeviceCallback(btDevice);
} }
auto radio_state = bluetooth::getRadioState(); auto radio_state = bluetooth::getRadioState();
@ -192,9 +236,17 @@ void BtManage::onShow(AppContext& app, lv_obj_t* parent) {
} }
void BtManage::onHide(AppContext& app) { void BtManage::onHide(AppContext& app) {
// Invalidate any BT event dispatched-but-not-yet-run for this session before doing
// anything else, so it can't race a subsequent destruction of this instance (see
// onKernelBtEvent()/getGeneration()).
generation->fetch_add(1);
lock(); lock();
if (btDevice) { if (btDevice) {
bluetooth_remove_event_callback(btDevice, onKernelBtEvent); if (callbackRegistered) {
bluetooth_remove_event_callback(btDevice, onKernelBtEvent);
callbackRegistered = false;
}
device_put(btDevice); device_put(btDevice);
btDevice = nullptr; btDevice = nullptr;
} }

View File

@ -8,6 +8,7 @@
#include <Tactility/lvgl/Toolbar.h> #include <Tactility/lvgl/Toolbar.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/time.h>
#include <atomic> #include <atomic>
#include <cstring> #include <cstring>
@ -211,7 +212,7 @@ public:
GpsSettingsApp() { GpsSettingsApp() {
// Runs while the screen is shown - there's no push notification for GPS device state // Runs while the screen is shown - there's no push notification for GPS device state
// changes, so this is the only way this screen finds out about them. // changes, so this is the only way this screen finds out about them.
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::secondsToTicks(1), [this] { timer = std::make_unique<Timer>(Timer::Type::Periodic, seconds_to_ticks(1), [this] {
updateDeviceStates(); updateDeviceStates();
}); });
} }

View File

@ -6,6 +6,7 @@
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/drivers/power_supply.h> #include <tactility/drivers/power_supply.h>
#include <tactility/time.h>
#include <lvgl/lvgl.h> #include <lvgl/lvgl.h>
#include <lvgl/icons/shared.h> #include <lvgl/icons/shared.h>
@ -53,7 +54,7 @@ struct DeviceEntry {
class PowerApp : public App { class PowerApp : public App {
Timer update_timer = Timer(Timer::Type::Periodic, kernel::millisToTicks(1000),[]() { onTimer(); }); Timer update_timer = Timer(Timer::Type::Periodic, millis_to_ticks(1000),[]() { onTimer(); });
std::vector<DeviceEntry> entries; std::vector<DeviceEntry> entries;

View File

@ -1,8 +1,11 @@
#include <Tactility/TactilityConfig.h> #include "tactility/time.h"
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/Tactility.h>
#include <Tactility/Timer.h>
#include <Tactility/Paths.h> #include <Tactility/Paths.h>
#include <Tactility/Tactility.h>
#include <Tactility/TactilityConfig.h>
#include <Tactility/Timer.h>
#include <Tactility/lvgl/Toolbar.h>
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
@ -242,7 +245,7 @@ static std::shared_ptr<SystemInfoApp> optApp() {
} }
class SystemInfoApp final : public App { class SystemInfoApp final : public App {
Timer memoryTimer = Timer(Timer::Type::Periodic, kernel::millisToTicks(10000), [] { Timer memoryTimer = Timer(Timer::Type::Periodic, millis_to_ticks(10000), [] {
auto app = optApp(); auto app = optApp();
if (app) { if (app) {
lvgl_lock(); lvgl_lock();
@ -251,7 +254,7 @@ class SystemInfoApp final : public App {
} }
}); });
Timer tasksTimer = Timer(Timer::Type::Periodic, kernel::millisToTicks(15000), [] { Timer tasksTimer = Timer(Timer::Type::Periodic, millis_to_ticks(15000), [] {
auto app = optApp(); auto app = optApp();
if (app) { if (app) {
lvgl_lock(); lvgl_lock();

View File

@ -11,6 +11,7 @@
#include <tactility/check.h> #include <tactility/check.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/time.h>
#include <lvgl/fonts.h> #include <lvgl/fonts.h>
#include <lvgl/lvgl.h> #include <lvgl/lvgl.h>
@ -182,7 +183,7 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
obj_set_style_bg_invisible(left_spacer); obj_set_style_bg_invisible(left_spacer);
lv_obj_set_flex_grow(left_spacer, 1); lv_obj_set_flex_grow(left_spacer, 1);
statusbar_data.mutex.lock(kernel::MAX_TICKS); statusbar_data.mutex.lock(MAX_TICKS);
for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) {
auto* image = lv_image_create(obj); auto* image = lv_image_create(obj);
lv_obj_set_size(image, icon_size, icon_size); // regular padding doesn't work lv_obj_set_size(image, icon_size, icon_size); // regular padding doesn't work

View File

@ -1,6 +1,8 @@
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include <Tactility/service/displayidle/DisplayIdleService.h> #include <Tactility/service/displayidle/DisplayIdleService.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include "BouncingBallsScreensaver.h" #include "BouncingBallsScreensaver.h"
#include "MatrixRainScreensaver.h" #include "MatrixRainScreensaver.h"
@ -8,14 +10,14 @@
#include "Screensaver.h" #include "Screensaver.h"
#include "StackChanScreensaver.h" #include "StackChanScreensaver.h"
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <cstdlib> #include <cstdlib>
#include <ctime> #include <ctime>
#include <tactility/delay.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/drivers/display.h> #include <tactility/drivers/display.h>
#include <tactility/drivers/backlight.h> #include <tactility/drivers/backlight.h>
#include <lvgl/lvgl.h> #include <lvgl/lvgl.h>
namespace tt::service::displayidle { namespace tt::service::displayidle {
@ -221,7 +223,7 @@ bool DisplayIdleService::onStart(ServiceContext& service) {
cachedDisplaySettings = settings::display::loadOrGetDefault(); cachedDisplaySettings = settings::display::loadOrGetDefault();
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::millisToTicks(TICK_INTERVAL_MS), [this]{ this->tick(); }); timer = std::make_unique<Timer>(Timer::Type::Periodic, millis_to_ticks(TICK_INTERVAL_MS), [this]{ this->tick(); });
timer->setCallbackPriority(Thread::Priority::Lower); timer->setCallbackPriority(Thread::Priority::Lower);
timer->start(); timer->start();
return true; return true;
@ -238,7 +240,7 @@ void DisplayIdleService::onStop(ServiceContext& service) {
for (int i = 0; i < maxRetries && screensaverOverlay; ++i) { for (int i = 0; i < maxRetries && screensaverOverlay; ++i) {
stopScreensaver(); stopScreensaver();
if (screensaverOverlay && i < maxRetries - 1) { if (screensaverOverlay && i < maxRetries - 1) {
kernel::delayMillis(50); // Brief delay before retry delay_millis(50); // Brief delay before retry
} }
} }
if (screensaverOverlay) { if (screensaverOverlay) {

View File

@ -2,6 +2,7 @@
#include <display/lv_display.h> #include <display/lv_display.h>
#include <lvgl/lvgl.h>
#include <Tactility/Timer.h> #include <Tactility/Timer.h>
#include <Tactility/service/ServiceContext.h> #include <Tactility/service/ServiceContext.h>
@ -9,10 +10,10 @@
#include <Tactility/service/ServiceRegistration.h> #include <Tactility/service/ServiceRegistration.h>
#include <Tactility/settings/KeyboardSettings.h> #include <Tactility/settings/KeyboardSettings.h>
#include <lvgl/lvgl.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/drivers/backlight.h> #include <tactility/drivers/backlight.h>
#include <tactility/drivers/keyboard.h> #include <tactility/drivers/keyboard.h>
#include <tactility/time.h>
namespace tt::service::keyboardidle { namespace tt::service::keyboardidle {
@ -92,7 +93,7 @@ public:
// Note: Settings changes require service restart to take effect // Note: Settings changes require service restart to take effect
// TODO: Add KeyboardSettingsChanged events for dynamic updates // TODO: Add KeyboardSettingsChanged events for dynamic updates
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::millisToTicks(250), [this]{ this->tick(); }); timer = std::make_unique<Timer>(Timer::Type::Periodic, millis_to_ticks(250), [this]{ this->tick(); });
timer->setCallbackPriority(Thread::Priority::Lower); timer->setCallbackPriority(Thread::Priority::Lower);
timer->start(); timer->start();
return true; return true;

View File

@ -8,6 +8,7 @@
#include <Tactility/service/loader/Loader.h> #include <Tactility/service/loader/Loader.h>
#include <Tactility/service/screenshot/ScreenshotTask.h> #include <Tactility/service/screenshot/ScreenshotTask.h>
#include <tactility/delay.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <lvgl/lvgl.h> #include <lvgl/lvgl.h>
@ -71,7 +72,7 @@ void ScreenshotTask::taskMain() {
if (work.type == TASK_WORK_TYPE_DELAY) { if (work.type == TASK_WORK_TYPE_DELAY) {
// Splitting up the delays makes it easier to stop the service // Splitting up the delays makes it easier to stop the service
for (int i = 0; i < (work.delay_in_seconds * 10) && !isInterrupted(); ++i){ for (int i = 0; i < (work.delay_in_seconds * 10) && !isInterrupted(); ++i){
kernel::delayMillis(100); delay_millis(100);
} }
if (!isInterrupted()) { if (!isInterrupted()) {
@ -88,14 +89,14 @@ void ScreenshotTask::taskMain() {
if (appContext != nullptr) { if (appContext != nullptr) {
const app::AppManifest& manifest = appContext->getManifest(); const app::AppManifest& manifest = appContext->getManifest();
if (manifest.appId != last_app_id) { if (manifest.appId != last_app_id) {
kernel::delayMillis(100); delay_millis(100);
last_app_id = manifest.appId; last_app_id = manifest.appId;
auto filename = std::format("{}/screenshot-{}.png", work.path, manifest.appId); auto filename = std::format("{}/screenshot-{}.png", work.path, manifest.appId);
makeScreenshot(filename); makeScreenshot(filename);
} }
} }
// Ensure the LVGL widgets are rendered as the app just started // Ensure the LVGL widgets are rendered as the app just started
kernel::delayMillis(250); delay_millis(250);
} }
} }

View File

@ -11,12 +11,12 @@
#include <Tactility/service/ServiceRegistration.h> #include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/wifi/WifiBootSplashInit.h> #include <Tactility/service/wifi/WifiBootSplashInit.h>
#include <Tactility/service/wifi/WifiGlobals.h> #include <Tactility/service/wifi/WifiGlobals.h>
#include <Tactility/service/wifi/WifiSettings.h>
#include <tactility/check.h> #include <tactility/check.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/drivers/wifi.h> #include <tactility/drivers/wifi.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/time.h>
#include <tactility/wifi_auto_scan.h> #include <tactility/wifi_auto_scan.h>
#include <algorithm> #include <algorithm>
@ -76,7 +76,7 @@ struct WifiServiceState {
bool connectionTargetRemember = false; bool connectionTargetRemember = false;
settings::WifiApSettings connectionTarget; settings::WifiApSettings connectionTarget;
uint16_t scanRecordLimit = TT_WIFI_SCAN_RECORD_LIMIT; uint16_t scanRecordLimit = TT_WIFI_SCAN_RECORD_LIMIT;
TickType_t lastScanTime = kernel::MAX_TICKS; TickType_t lastScanTime = MAX_TICKS;
std::unique_ptr<Timer> autoConnectTimer; std::unique_ptr<Timer> autoConnectTimer;
kernel::SystemEventSubscription bootEventSubscription = kernel::NoSystemEventSubscription; kernel::SystemEventSubscription bootEventSubscription = kernel::NoSystemEventSubscription;
}; };
@ -165,7 +165,7 @@ void dispatchScan() {
LOG_I(TAG, "dispatchScan()"); LOG_I(TAG, "dispatchScan()");
if (!started || state.device == nullptr || !device_is_ready(state.device)) return; if (!started || state.device == nullptr || !device_is_ready(state.device)) return;
state.lastScanTime = kernel::getTicks(); state.lastScanTime = get_ticks();
error_t result = wifi_scan(state.device); error_t result = wifi_scan(state.device);
if (result != ERROR_NONE) { if (result != ERROR_NONE) {
@ -263,7 +263,7 @@ bool shouldScanForAutoConnect() {
!state.pauseAutoConnect && !state.externalScanPause.load(); !state.pauseAutoConnect && !state.externalScanPause.load();
if (!radio_scannable) return false; if (!radio_scannable) return false;
TickType_t current_time = kernel::getTicks(); TickType_t current_time = get_ticks();
bool scan_time_has_looped = current_time < state.lastScanTime; bool scan_time_has_looped = current_time < state.lastScanTime;
bool no_recent_scan = (current_time - state.lastScanTime) > (AUTO_SCAN_INTERVAL / portTICK_PERIOD_MS); bool no_recent_scan = (current_time - state.lastScanTime) > (AUTO_SCAN_INTERVAL / portTICK_PERIOD_MS);
return scan_time_has_looped || no_recent_scan; return scan_time_has_looped || no_recent_scan;

View File

@ -144,23 +144,17 @@ bool load(WebServerSettings& settings) {
? static_cast<uint8_t>(parseInt(ap_channel->second, 1, 13, 1)) ? static_cast<uint8_t>(parseInt(ap_channel->second, 1, 13, 1))
: 1; : 1;
// Security: If AP password is empty, generate a strong random password. // Security: If AP password is empty, generate a strong random password in memory.
// Skip this if user explicitly wants an open network. // Skip this if user explicitly wants an open network.
// Note: We only auto-generate for EMPTY passwords, not user-set ones. // Note: We only auto-generate for EMPTY passwords, not user-set ones.
// This is a read-only function: the generated password is NOT persisted here —
// callers that want it saved must call save() explicitly.
if (!settings.apOpenNetwork && isEmptyCredential(settings.apPassword)) { if (!settings.apOpenNetwork && isEmptyCredential(settings.apPassword)) {
LOG_I(TAG, "AP password is empty - generating secure random password"); LOG_I(TAG, "AP password is empty - generating secure random password (not persisted)");
// Generate 12-character random password (alphanumeric, ~71 bits of entropy) // Generate 12-character random password (alphanumeric, ~71 bits of entropy)
// WPA2 requires 8-63 characters, so 12 is well within range // WPA2 requires 8-63 characters, so 12 is well within range
settings.apPassword = generateRandomCredential(12); settings.apPassword = generateRandomCredential(12);
// Persist the generated password immediately
map[KEY_AP_PASSWORD] = settings.apPassword;
if (file::savePropertiesFile(getSettingsFilePath(), map)) {
LOG_I(TAG, "Generated and saved new secure AP password");
} else {
LOG_E(TAG, "Failed to save generated AP password");
}
} }
// Web server settings // Web server settings
@ -181,27 +175,20 @@ bool load(WebServerSettings& settings) {
settings.webServerUsername = (webserver_username != map.end()) ? webserver_username->second : ""; settings.webServerUsername = (webserver_username != map.end()) ? webserver_username->second : "";
settings.webServerPassword = (webserver_password != map.end()) ? webserver_password->second : ""; settings.webServerPassword = (webserver_password != map.end()) ? webserver_password->second : "";
// Security: If auth is enabled but credentials are empty, // Security: If auth is enabled but credentials are empty, generate strong random
// generate strong random credentials and persist them immediately. // credentials in memory. Note: We only auto-generate for EMPTY credentials, allowing
// Note: We only auto-generate for EMPTY credentials, allowing users to set their own. // users to set their own.
// This is a read-only function: the generated credentials are NOT persisted here —
// callers that want them saved (so they're consistent across reboots) must call
// save() explicitly.
if (settings.webServerAuthEnabled && if (settings.webServerAuthEnabled &&
(isEmptyCredential(settings.webServerUsername) || isEmptyCredential(settings.webServerPassword))) { (isEmptyCredential(settings.webServerUsername) || isEmptyCredential(settings.webServerPassword))) {
LOG_I(TAG, "Auth enabled with empty credentials - generating secure random credentials"); LOG_I(TAG, "Auth enabled with empty credentials - generating secure random credentials (not persisted)");
// Generate 12-character random credentials (alphanumeric, ~71 bits of entropy each) // Generate 12-character random credentials (alphanumeric, ~71 bits of entropy each)
settings.webServerUsername = generateRandomCredential(12); settings.webServerUsername = generateRandomCredential(12);
settings.webServerPassword = generateRandomCredential(12); settings.webServerPassword = generateRandomCredential(12);
// Persist the generated credentials immediately
// We need to save these to the file so they're consistent across reboots
map[KEY_WEBSERVER_USERNAME] = settings.webServerUsername;
map[KEY_WEBSERVER_PASSWORD] = settings.webServerPassword;
if (file::savePropertiesFile(getSettingsFilePath(), map)) {
LOG_I(TAG, "Generated and saved new secure credentials");
} else {
LOG_E(TAG, "Failed to save generated credentials - auth may be inconsistent across reboots");
}
} }
return true; return true;
@ -228,14 +215,10 @@ WebServerSettings loadOrGetDefault() {
bool loadedFromFlash = load(settings); bool loadedFromFlash = load(settings);
if (!loadedFromFlash) { if (!loadedFromFlash) {
// First boot - use defaults (WiFi OFF, WebServer OFF) // No properties file yet (e.g. first boot) - use defaults in memory (WiFi OFF,
// WebServer OFF). Read-only function: does NOT persist these defaults — callers
// that want them saved must call save() explicitly.
settings = getDefault(); settings = getDefault();
// Save defaults to flash so toggle states persist
if (save(settings)) {
LOG_I(TAG, "First boot - saved default settings (WiFi OFF WebServer OFF)");
} else {
LOG_W(TAG, "First boot - failed to save default settings to flash");
}
} }
return settings; return settings;

View File

@ -57,7 +57,7 @@ public:
* @param[in] timeout lock acquisition timeout * @param[in] timeout lock acquisition timeout
* @return true if dispatching was successful (timeout not reached) * @return true if dispatching was successful (timeout not reached)
*/ */
bool dispatch(Function function, TickType_t timeout = kernel::MAX_TICKS) { bool dispatch(Function function, TickType_t timeout = kernel::FREERTOS_MAX_TICKS) {
// Mutate // Mutate
if (!mutex.lock(timeout)) { if (!mutex.lock(timeout)) {
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
@ -92,7 +92,7 @@ public:
* @param[in] timeout the ticks to wait for a message * @param[in] timeout the ticks to wait for a message
* @return the amount of messages that were consumed * @return the amount of messages that were consumed
*/ */
uint32_t consume(TickType_t timeout = kernel::MAX_TICKS) { uint32_t consume(TickType_t timeout = kernel::FREERTOS_MAX_TICKS) {
// Wait for signal // Wait for signal
if (!eventFlag.wait(WAIT_FLAG, false, true, nullptr, timeout)) { if (!eventFlag.wait(WAIT_FLAG, false, true, nullptr, timeout)) {
return 0; return 0;

View File

@ -46,7 +46,7 @@ public:
/** /**
* Dispatch a message. * Dispatch a message.
*/ */
bool dispatch(const Dispatcher::Function& function, TickType_t timeout = kernel::MAX_TICKS) { bool dispatch(const Dispatcher::Function& function, TickType_t timeout = kernel::FREERTOS_MAX_TICKS) {
return dispatcher.dispatch(function, timeout); return dispatcher.dispatch(function, timeout);
} }

View File

@ -100,7 +100,7 @@ public:
bool awaitAll = false, bool awaitAll = false,
bool clearOnExit = true, bool clearOnExit = true,
uint32_t* outFlags = nullptr, uint32_t* outFlags = nullptr,
TickType_t timeout = kernel::MAX_TICKS TickType_t timeout = kernel::FREERTOS_MAX_TICKS
) const { ) const {
assert(xPortInIsrContext() == pdFALSE); assert(xPortInIsrContext() == pdFALSE);

View File

@ -20,7 +20,7 @@ public:
virtual bool lock(TickType_t timeout) const = 0; virtual bool lock(TickType_t timeout) const = 0;
bool lock() const { return lock(kernel::MAX_TICKS); } bool lock() const { return lock(kernel::FREERTOS_MAX_TICKS); }
virtual void unlock() const = 0; virtual void unlock() const = 0;
@ -40,9 +40,9 @@ public:
} }
} }
void withLock(const std::function<void()>& onLockAcquired) const { withLock(kernel::MAX_TICKS, onLockAcquired); } void withLock(const std::function<void()>& onLockAcquired) const { withLock(kernel::FREERTOS_MAX_TICKS, onLockAcquired); }
void withLock(const std::function<void()>& onLockAcquired, const std::function<void()>& onLockFailed) const { withLock(kernel::MAX_TICKS, onLockAcquired, onLockFailed); } void withLock(const std::function<void()>& onLockAcquired, const std::function<void()>& onLockFailed) const { withLock(kernel::FREERTOS_MAX_TICKS, onLockAcquired, onLockFailed); }
ScopedLock asScopedLock() const; ScopedLock asScopedLock() const;
}; };

View File

@ -43,7 +43,7 @@ public:
} }
// Wait for Mutex usage // Wait for Mutex usage
if (mutex.lock(kernel::MAX_TICKS)) { if (mutex.lock(kernel::FREERTOS_MAX_TICKS)) {
// TODO: Fix the case where the mutex might be immediately locked after this point and then crashes when deleted // TODO: Fix the case where the mutex might be immediately locked after this point and then crashes when deleted
mutex.unlock(); mutex.unlock();
} }

View File

@ -224,7 +224,7 @@ public:
/** /**
* @warning If this blocks forever, it might be because of the Thread, but it could also be because another task is blocking the CPU. * @warning If this blocks forever, it might be because of the Thread, but it could also be because another task is blocking the CPU.
*/ */
bool join(TickType_t timeout = kernel::MAX_TICKS, TickType_t pollInterval = 10) { bool join(TickType_t timeout = kernel::FREERTOS_MAX_TICKS, TickType_t pollInterval = 10) {
assert(getCurrent() != this); assert(getCurrent() != this);
TickType_t start_ticks = kernel::getTicks(); TickType_t start_ticks = kernel::getTicks();

View File

@ -29,7 +29,7 @@ private:
struct TimerHandleDeleter { struct TimerHandleDeleter {
void operator()(TimerHandle_t handleToDelete) const { void operator()(TimerHandle_t handleToDelete) const {
xTimerDelete(handleToDelete, kernel::MAX_TICKS); xTimerDelete(handleToDelete, kernel::FREERTOS_MAX_TICKS);
} }
}; };
@ -75,7 +75,7 @@ public:
*/ */
bool start() const { bool start() const {
assert(xPortInIsrContext() == pdFALSE); assert(xPortInIsrContext() == pdFALSE);
return xTimerStart(handle.get(), kernel::MAX_TICKS) == pdPASS; return xTimerStart(handle.get(), kernel::FREERTOS_MAX_TICKS) == pdPASS;
} }
/** Stop the timer /** Stop the timer
@ -84,7 +84,7 @@ public:
*/ */
bool stop() const { bool stop() const {
assert(xPortInIsrContext() == pdFALSE); assert(xPortInIsrContext() == pdFALSE);
return xTimerStop(handle.get(), kernel::MAX_TICKS) == pdPASS; return xTimerStop(handle.get(), kernel::FREERTOS_MAX_TICKS) == pdPASS;
} }
/** /**
@ -94,8 +94,8 @@ public:
*/ */
bool reset(TickType_t interval) const { bool reset(TickType_t interval) const {
assert(xPortInIsrContext() == pdFALSE); assert(xPortInIsrContext() == pdFALSE);
return xTimerChangePeriod(handle.get(), interval, kernel::MAX_TICKS) == pdPASS && return xTimerChangePeriod(handle.get(), interval, kernel::FREERTOS_MAX_TICKS) == pdPASS &&
xTimerReset(handle.get(), kernel::MAX_TICKS) == pdPASS; xTimerReset(handle.get(), kernel::FREERTOS_MAX_TICKS) == pdPASS;
} }
/** /**
@ -104,7 +104,7 @@ public:
*/ */
bool reset() const { bool reset() const {
assert(xPortInIsrContext() == pdFALSE); assert(xPortInIsrContext() == pdFALSE);
return xTimerReset(handle.get(), kernel::MAX_TICKS) == pdPASS; return xTimerReset(handle.get(), kernel::FREERTOS_MAX_TICKS) == pdPASS;
} }
/** @return true when the timer is running */ /** @return true when the timer is running */

View File

@ -17,7 +17,7 @@
namespace tt::kernel { namespace tt::kernel {
constexpr TickType_t MAX_TICKS = ~static_cast<TickType_t>(0); constexpr TickType_t FREERTOS_MAX_TICKS = ~static_cast<TickType_t>(0);
/** @return the frequency at which the kernel task schedulers operate */ /** @return the frequency at which the kernel task schedulers operate */
constexpr uint32_t getTickFrequency() { constexpr uint32_t getTickFrequency() {

View File

@ -10,6 +10,7 @@
#endif #endif
#include <tactility/freertos/freertos.h> #include <tactility/freertos/freertos.h>
#include <tactility/freertos/task.h>
#include <tactility/check.h> #include <tactility/check.h>
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -11,7 +11,8 @@
#include <stdint.h> #include <stdint.h>
#include "defines.h" #include "defines.h"
#include "tactility/freertos/task.h" #include <tactility/freertos/port.h>
#include <tactility/freertos/task.h>
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include <esp_timer.h> #include <esp_timer.h>
@ -31,6 +32,8 @@ static_assert(configTICK_RATE_HZ == 1000);
static_assert(configTICK_RATE_HZ == 1000, "configTICK_RATE_HZ must be 1000"); static_assert(configTICK_RATE_HZ == 1000, "configTICK_RATE_HZ must be 1000");
#endif #endif
#define MAX_TICKS (~(TickType_t)0)
static inline uint32_t get_tick_frequency() { static inline uint32_t get_tick_frequency() {
return configTICK_RATE_HZ; return configTICK_RATE_HZ;
} }

View File

@ -7,13 +7,13 @@ using namespace tt;
TEST_CASE("a Mutex can block a thread") { TEST_CASE("a Mutex can block a thread") {
auto mutex = Mutex(); auto mutex = Mutex();
CHECK_EQ(mutex.lock(kernel::MAX_TICKS), true); CHECK_EQ(mutex.lock(kernel::FREERTOS_MAX_TICKS), true);
Thread thread = Thread( Thread thread = Thread(
"thread", "thread",
1024, 1024,
[&mutex] { [&mutex] {
mutex.lock(kernel::MAX_TICKS); mutex.lock(kernel::FREERTOS_MAX_TICKS);
return 0; return 0;
} }
); );

View File

@ -7,13 +7,13 @@ using namespace tt;
TEST_CASE("a RecursiveMutex can block a thread") { TEST_CASE("a RecursiveMutex can block a thread") {
auto mutex = RecursiveMutex(); auto mutex = RecursiveMutex();
CHECK_EQ(mutex.lock(kernel::MAX_TICKS), true); CHECK_EQ(mutex.lock(kernel::FREERTOS_MAX_TICKS), true);
Thread thread = Thread( Thread thread = Thread(
"thread", "thread",
1024, 1024,
[&mutex] { [&mutex] {
mutex.lock(kernel::MAX_TICKS); mutex.lock(kernel::FREERTOS_MAX_TICKS);
return 0; return 0;
} }
); );