Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Devices/lilygo-tdeck/source/module.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <tactility/module.h>
#include <tactility/delay.h>
#include <tactility/error.h>
#include <tactility/log.h>

Expand Down Expand Up @@ -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.
tt::kernel::delayMillis(100);
delay_millis(100);

subscribe_events();

Expand Down
5 changes: 3 additions & 2 deletions Devices/unphone/source/drivers/unphone_nav_buttons.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#include "unphone_nav_buttons.h"

#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/gpio_controller.h>
Expand Down Expand Up @@ -76,9 +77,9 @@ static int32_t nav_buttons_thread_main(UnphoneNavButtonsInternal* internal) {

// Debounce all events for a short period of time
// This is easier than keeping track when each button was last pressed
tt::kernel::delayMillis(50);
delay_millis(50);
xQueueReset(internal->event_queue);
tt::kernel::delayMillis(50);
delay_millis(50);
xQueueReset(internal->event_queue);
}
}
Expand Down
2 changes: 0 additions & 2 deletions Documentation/ideas.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

## Before release

- WebServer service shouldn't save webserver.properties at start, it slows the boot process
- 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
- Elecrow Basic & Advance 3.5" memory issue: not enough memory for App Hub
Expand All @@ -13,7 +12,6 @@

## 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
- 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()
Expand Down
1 change: 1 addition & 0 deletions Modules/lvgl-module/include/lvgl/icons/launcher.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once

#define LVGL_ICON_LAUNCHER_APPS "\xEE\x97\x83"
Expand Down
1 change: 1 addition & 0 deletions Modules/lvgl-module/include/lvgl/icons/shared.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once

#define LVGL_ICON_SHARED_ADD "\xEE\x85\x85"
Expand Down
1 change: 1 addition & 0 deletions Modules/lvgl-module/include/lvgl/icons/statusbar.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once

#define LVGL_ICON_STATUSBAR_LOCATION_ON "\xEF\x87\x9B"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,22 @@ void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) {

size_t i = start_idx;
while (true) {
ble_addr_t addr = {};
bool found = false;
{
xSemaphoreTake(ctx->scan_mutex, portMAX_DELAY);
ble_addr_t addr = {};
bool found = false;
bool radio_on = false;
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) {
if (ctx->scan_results[i].name[0] == '\0') {
addr = ctx->scan_addrs[i];
Expand All @@ -206,7 +218,23 @@ void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) {
}
++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) {
Expand All @@ -218,12 +246,6 @@ void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) {
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) {
return; // name_res_gap_callback continues the chain
}
Expand Down
26 changes: 26 additions & 0 deletions Tactility/Private/Tactility/app/btmanage/BtManagePrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
#include <Tactility/bluetooth/Bluetooth.h>
#include <tactility/drivers/bluetooth.h>

#include <atomic>
#include <memory>

namespace tt::app::btmanage {

class BtManage final : public App {
Expand All @@ -18,6 +21,15 @@ class BtManage final : public App {
View view = View(&bindings, &state);
bool isViewEnabled = false;
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:

Expand All @@ -35,6 +47,20 @@ class BtManage final : public App {
State& getState() { return state; }

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
5 changes: 3 additions & 2 deletions Tactility/Source/SystemEvents.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#include <Tactility/CoreDefines.h>
#include <Tactility/Mutex.h>
#include <Tactility/SystemEvents.h>

#include <Tactility/Mutex.h>
#include <tactility/check.h>
#include <tactility/log.h>
#include <tactility/time.h>

#include <Tactility/CoreDefines.h>
#include <list>

namespace tt::kernel {
Expand Down
8 changes: 4 additions & 4 deletions Tactility/Source/app/alertdialog/AlertDialog.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#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 <lvgl.h>
#include <tactility/log.h>

#include <lvgl.h>
#include <lvgl/widgets/toolbar.h>

namespace tt::app::alertdialog {

#define PARAMETER_BUNDLE_KEY_TITLE "title"
Expand Down
18 changes: 10 additions & 8 deletions Tactility/Source/app/boot/Boot.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "Tactility/lvgl/Lvgl.h"
#include "tactility/drivers/backlight.h"
#include "tactility/drivers/display.h"
#include <tactility/delay.h>
#include <tactility/drivers/backlight.h>
#include <tactility/drivers/display.h>
#include <tactility/log.h>
#include <tactility/time.h>

#include <Tactility/CpuAffinity.h>
#include <Tactility/Paths.h>
Expand All @@ -10,13 +12,13 @@
#include <Tactility/app/AppPaths.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/hal/usb/Usb.h>
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/settings/BootSettings.h>
#include <Tactility/settings/DisplaySettings.h>

#include <lvgl.h>
#include <tactility/log.h>

#include <atomic>

Expand Down Expand Up @@ -108,23 +110,23 @@ class BootApp : public App {
}

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;
constexpr auto minimum_ticks = (CONFIG_TT_SPLASH_DURATION / portTICK_PERIOD_MS);
if (minimum_ticks > ticks_passed) {
kernel::delayTicks(minimum_ticks - ticks_passed);
delay_ticks(minimum_ticks - ticks_passed);
}
}

static int32_t bootThreadCallback() {
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
// 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
kernel::delayMillis(10);
delay_millis(10);

// TODO: Support for multiple displays
LOG_I(TAG, "Setup display");
Expand Down
68 changes: 60 additions & 8 deletions Tactility/Source/app/btmanage/BtManage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,20 @@ static void onBtToggled(bool requestOn) {
bool radio_on = bluetooth::isRadioOnOrPending(dev);
if (requestOn && !radio_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);
Comment thread
KenVanHoeylandt marked this conversation as resolved.
}
} else if (!requestOn && radio_on) {
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);
} else {
Expand Down Expand Up @@ -90,13 +100,19 @@ void BtManage::unlock() {
}

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();
if (isViewEnabled) {
lvgl_lock();
view.update();
lvgl_unlock();
}
unlock();
lvgl_unlock();
}

void BtManage::onBtEvent(const BtEvent& event) {
Expand Down Expand Up @@ -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
// the NimBLE host task is never blocked by BtManage's state updates or LVGL lock.
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);
});
}

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) {
// Initialise state and view before subscribing to avoid incoming events
// racing with state initialisation.
state.setRadioState(bluetooth::getRadioState());
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.updateScanResults();
Expand All @@ -177,7 +221,7 @@ void BtManage::onShow(AppContext& app, lv_obj_t* parent) {

btDevice = dev;
if (btDevice) {
bluetooth_add_event_callback(btDevice, this, onKernelBtEvent);
registerDeviceCallback(btDevice);
}

auto radio_state = bluetooth::getRadioState();
Expand All @@ -192,9 +236,17 @@ void BtManage::onShow(AppContext& app, lv_obj_t* parent) {
}

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();
if (btDevice) {
bluetooth_remove_event_callback(btDevice, onKernelBtEvent);
if (callbackRegistered) {
bluetooth_remove_event_callback(btDevice, onKernelBtEvent);
callbackRegistered = false;
}
device_put(btDevice);
btDevice = nullptr;
}
Expand Down
Loading
Loading