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
4 changes: 3 additions & 1 deletion Apps/EspNowBridge/main/Source/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ int main(int argc, char* argv[]) {

window_manager_remove(window);
check(app_event_unsubscribe(&sub) == ERROR_NONE);
task_event_group_destruct(&event_group);
// Must unsubscribe from the WiFi event group (inside espNowBridgeTeardown()) before
// destructing it below - releasing a subscription's bit needs the group to still be alive.
espNowBridgeTeardown(ctx.get());
task_event_group_destruct(&event_group);

return 0;
}
Expand Down
4 changes: 2 additions & 2 deletions Apps/EspNowBridge/manifest.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32p4
app.id=tactility.espnowbridge
app.version.name=0.7.0
app.version.code=7
app.version.name=0.8.0
app.version.code=8
app.name=ESP-NOW Bridge
app.description=Companion app for updating P4 device C6 co-processor firmware to enable ESP-NOW bridge support.
114 changes: 58 additions & 56 deletions Apps/MediaKeys/main/Source/MediaKeys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ void onSwitchToggled(lv_event_t* event);
void onButtonPressed(lv_event_t* event);
void onKeyEvent(lv_event_t* event);
void onKeyHighlightTimer(lv_timer_t* t);
void btEventCallback(struct Device* device, void* context, struct BtEvent event);
void handleBtEvent(Context* ctx, const BtEvent& event);
void handleSwitchToggle(Context* ctx, bool enabled);

void onSwitchToggled(lv_event_t* event) {
Expand Down Expand Up @@ -164,10 +164,7 @@ void onKeyHighlightTimer(lv_timer_t* t) {
lv_timer_pause(t);
}

void btEventCallback(struct Device* /*device*/, void* context, struct BtEvent event) {
auto* ctx = static_cast<Context*>(context);
if (!ctx) return;

void handleBtEvent(Context* ctx, const BtEvent& event) {
if (event.type == BT_EVENT_RADIO_STATE_CHANGED) {
LOG_I(TAG, "BT radio state: %d", (int)event.radio_state);

Expand Down Expand Up @@ -207,17 +204,12 @@ void btEventCallback(struct Device* /*device*/, void* context, struct BtEvent ev

void startHid(Context* ctx) {
// Called once the BT radio is confirmed ON (either already was, or just came up).
// May be called from the BT event callback thread - LVGL must already be locked by caller.
// May be called from handleBtEvent() on the app's own task - LVGL must already be locked by caller.
ctx->radioEnabling = false;

ctx->hidDevice = bluetooth_hid_device_get_device();
if (!ctx->hidDevice) {
LOG_E(TAG, "BLE HID device unavailable after radio on");
if (ctx->btDevice) {
bluetooth_remove_event_callback(ctx->btDevice, btEventCallback);
device_put(ctx->btDevice);
ctx->btDevice = nullptr;
}
ctx->isEnabled = false;
if (ctx->switchWidget) lv_obj_remove_state(ctx->switchWidget, LV_STATE_CHECKED);
return;
Expand All @@ -226,11 +218,6 @@ void startHid(Context* ctx) {
error_t err = bluetooth_hid_device_start(ctx->hidDevice, BT_HID_DEVICE_MODE_KEYBOARD);
if (err != ERROR_NONE) {
LOG_E(TAG, "Failed to start HID device: %d", (int)err);
if (ctx->btDevice) {
bluetooth_remove_event_callback(ctx->btDevice, btEventCallback);
device_put(ctx->btDevice);
ctx->btDevice = nullptr;
}
ctx->hidDevice = nullptr;
ctx->isEnabled = false;
if (ctx->switchWidget) lv_obj_remove_state(ctx->switchWidget, LV_STATE_CHECKED);
Expand All @@ -241,56 +228,44 @@ void startHid(Context* ctx) {
if (device_has_active_by_type(&KEYBOARD_TYPE)) enterKeyMode(ctx);
}

// Restores the radio to the state we found it in when we turned it on ourselves. Does not touch
// the BT event subscription - that's app-lifetime, set up by mediaKeysInitBt()/torn down by
// teardownBt() at app exit, not per toggle.
void restoreRadioIfNeeded(Context* ctx) {
if (ctx->btDevice && ctx->radioWasOff) bluetooth_set_radio_enabled(ctx->btDevice, false);
ctx->radioWasOff = false;
}

void teardownBt(Context* ctx) {
// Remove callback FIRST - stops any in-flight BT events from firing against
// our (possibly already freed) UI widget pointers after this returns.
if (ctx->btDevice) bluetooth_remove_event_callback(ctx->btDevice, btEventCallback);
// Unsubscribe FIRST - stops any in-flight BT events from firing against our (possibly
// already freed) UI widget pointers after this returns.
if (ctx->btDevice) bluetooth_event_unsubscribe(ctx->btDevice, &ctx->btEventSub);
// Do NOT call bluetooth_hid_device_stop here: it calls ble_gatts_reset() /
// ble_gatts_start() which corrupts NimBLE heap while the host task is still
// running. HID device is a persistent kernel device; hid_device_start() cleans
// up stale context on next use. Explicit stop is handled by handleSwitchToggle.
// Restore the radio/device to the state we found them in.
if (ctx->btDevice && ctx->radioWasOff) bluetooth_set_radio_enabled(ctx->btDevice, false);
if (ctx->btDevice && ctx->deviceWasStarted) device_stop(ctx->btDevice);
// The device itself is never stopped, it's started for the process lifetime (see
// Documentation/bluetooth-app-migration.md) - just restore the radio and drop our ref.
restoreRadioIfNeeded(ctx);
if (ctx->btDevice) device_put(ctx->btDevice);
ctx->btDevice = nullptr;
ctx->hidDevice = nullptr;
ctx->radioWasOff = false;
ctx->deviceWasStarted = false;
}

void handleSwitchToggle(Context* ctx, bool enabled) {
LOG_I(TAG, "Switch: %s", enabled ? "ON" : "OFF");
ctx->isEnabled = enabled;

if (enabled) {
if (device_get_first_by_type(&BLUETOOTH_TYPE, &ctx->btDevice) != ERROR_NONE) {
if (!ctx->btDevice) {
LOG_E(TAG, "No Bluetooth device found");
ctx->btDevice = nullptr;
ctx->isEnabled = false;
if (ctx->switchWidget) lv_obj_remove_state(ctx->switchWidget, LV_STATE_CHECKED);
return;
}

// Device may not be started yet (BT disabled in DTS by default to save memory).
if (!device_is_ready(ctx->btDevice)) {
LOG_I(TAG, "BT device not started, starting now");
if (device_start(ctx->btDevice) != ERROR_NONE) {
LOG_E(TAG, "Failed to start BT device");
device_put(ctx->btDevice);
ctx->btDevice = nullptr;
ctx->isEnabled = false;
if (ctx->switchWidget) lv_obj_remove_state(ctx->switchWidget, LV_STATE_CHECKED);
return;
}
ctx->deviceWasStarted = true;
}

bluetooth_set_device_name(ctx->btDevice, "Tactility Media Keys");

// Register callback before enabling radio so we don't miss the state-change event.
bluetooth_add_event_callback(ctx->btDevice, ctx, btEventCallback);

enum BtRadioState radioState;
bluetooth_get_radio_state(ctx->btDevice, &radioState);

Expand All @@ -299,8 +274,10 @@ void handleSwitchToggle(Context* ctx, bool enabled) {
ctx->radioWasOff = false;
startHid(ctx);
} else {
// Turn the radio on; startHid() will be called from btEventCallback
// once BT_RADIO_STATE_ON fires.
// Turn the radio on; startHid() will be called from handleBtEvent()
// once BT_RADIO_STATE_ON fires. Our BT event subscription (mediaKeysInitBt())
// was already claimed before the app's main loop started waiting, so that
// wakeup won't be missed.
LOG_I(TAG, "BT radio not on (state=%d), enabling...", (int)radioState);
ctx->radioWasOff = true;
ctx->radioEnabling = true;
Expand All @@ -312,7 +289,8 @@ void handleSwitchToggle(Context* ctx, bool enabled) {
// Explicit user toggle-off: stop HID cleanly (safe here since we're on the
// LVGL task and the user intentionally disabled, so no race with app teardown).
if (ctx->hidDevice) bluetooth_hid_device_stop(ctx->hidDevice);
teardownBt(ctx);
ctx->hidDevice = nullptr;
restoreRadioIfNeeded(ctx);
if (ctx->mainWrapper) lv_obj_add_flag(ctx->mainWrapper, LV_OBJ_FLAG_HIDDEN);
}
}
Expand All @@ -336,6 +314,34 @@ void handleButtonPress(Context* ctx, uint32_t buttonId) {

} // namespace

bool mediaKeysInitBt(Context* ctx) {
// ble0 is started for the process lifetime once enabled in the devicetree - just look it up,
// no device_start() needed (see Documentation/bluetooth-app-migration.md).
if (device_get_first_by_type(&BLUETOOTH_TYPE, &ctx->btDevice) != ERROR_NONE) {
LOG_E(TAG, "No Bluetooth device found");
ctx->btDevice = nullptr;
return false;
}
// Must claim this subscription's bit before the app's main loop makes its first
// task_event_group_wait_any() call - a bit claimed mid-wait isn't included until the next
// call, so a radio-on event fired right after a late subscribe could go unnoticed forever.
if (bluetooth_event_subscribe(ctx->btDevice, &ctx->btEventSub, ctx->eventGroup) != ERROR_NONE) {
LOG_E(TAG, "Failed to subscribe to BT events");
device_put(ctx->btDevice);
ctx->btDevice = nullptr;
return false;
}
return true;
}

void mediaKeysProcessBtEvents(Context* ctx) {
if (ctx->btDevice == nullptr) return;
BtEvent event {};
while (bluetooth_event_poll(&ctx->btEventSub, &event) == ERROR_NONE) {
handleBtEvent(ctx, event);
}
}

void mediaKeysCreateWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData);

Expand Down Expand Up @@ -386,17 +392,13 @@ void mediaKeysCreateWidgets(lv_obj_t* parent, void* userData) {
lv_obj_add_flag(ctx->mainWrapper, LV_OBJ_FLAG_HIDDEN);

// Auto-enable if BT is already on (turned on via QuickPanel/Settings before opening app).
// Transient lookup only - handleSwitchToggle() acquires its own reference into ctx->btDevice.
struct Device* btDev = nullptr;
if (device_get_first_by_type(&BLUETOOTH_TYPE, &btDev) == ERROR_NONE) {
if (device_is_ready(btDev)) {
enum BtRadioState radioState;
if (bluetooth_get_radio_state(btDev, &radioState) == ERROR_NONE && radioState == BT_RADIO_STATE_ON) {
lv_obj_add_state(ctx->switchWidget, LV_STATE_CHECKED);
handleSwitchToggle(ctx, true);
}
// ctx->btDevice was already acquired by mediaKeysInitBt() before this ran.
if (ctx->btDevice && device_is_ready(ctx->btDevice)) {
enum BtRadioState radioState;
if (bluetooth_get_radio_state(ctx->btDevice, &radioState) == ERROR_NONE && radioState == BT_RADIO_STATE_ON) {
lv_obj_add_state(ctx->switchWidget, LV_STATE_CHECKED);
handleSwitchToggle(ctx, true);
}
device_put(btDev);
}
}

Expand Down
17 changes: 14 additions & 3 deletions Apps/MediaKeys/main/Source/MediaKeys.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
struct Context {
AppInstanceId appInstanceId = 0;
WindowId window = 0;
struct TaskEventGroup* eventGroup = nullptr; // borrowed from main(); outlives ctx

// UI elements
lv_obj_t* mainWrapper = nullptr;
Expand All @@ -24,17 +25,27 @@ struct Context {
// HAL device handles
struct Device* btDevice = nullptr;
struct Device* hidDevice = nullptr;
struct BtEventSubscription btEventSub {};

// State - accessed from both LVGL thread and BT callback thread
// State - accessed from both LVGL thread and the app's own task (BT event poll loop)
std::atomic<bool> isEnabled {false};
std::atomic<bool> radioEnabling {false}; // true while waiting for radio to come ON
std::atomic<bool> radioWasOff {false}; // true if we turned the radio on (restore on exit)
std::atomic<bool> deviceWasStarted{false}; // true if we called device_start (restore on exit)
};

/** window_manager_create()'s WindowCreateWidgetsFn - @a userData is the Context* for this instance. */
void mediaKeysCreateWidgets(lv_obj_t* parent, void* userData);

/** Removes the BT callback, stops HID, restores radio/device state, releases widget-tracking
/** Looks up the BT device and subscribes to its events, claiming a bit in ctx->eventGroup.
* Must be called once, before the app's main loop starts blocking on that event group (its bit
* has to already be claimed by the first task_event_group_wait_any() call - see that function's
* warning about bits claimed mid-wait). @return true on success. */
bool mediaKeysInitBt(Context* ctx);

/** Drains any BT events queued for ctx and reacts to them (radio state, HID profile state).
* Call from the app's main loop after task_event_group_wait_any() returns. */
void mediaKeysProcessBtEvents(Context* ctx);

/** Removes the BT event subscription, stops HID, restores radio state, releases widget-tracking
* state. Call once, after the window has been torn down. */
void mediaKeysTeardown(Context* ctx);
17 changes: 13 additions & 4 deletions Apps/MediaKeys/main/Source/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,23 @@ extern "C" {
int main(int argc, char* argv[]) {
AppInstanceId app_instance_id = app_scheduler_current_app_id();

// Heap-allocated: the BT event callback (bluetooth_add_event_callback) captures ctx's
// address for a background BT-stack thread to call back into, so it can't be a stack frame
// that goes away while that callback might still fire.
// Heap-allocated: BT event subscription/HID background work holds a raw Context* across the
// whole app instance lifetime, well past any single stack frame here.
auto ctx = std::make_unique<Context>();
ctx->appInstanceId = app_instance_id;

struct TaskEventGroup event_group {};
task_event_group_construct(&event_group);
ctx->eventGroup = &event_group;

struct AppEventSubscription sub {};
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);

// Must happen before window_manager_create() (which synchronously builds widgets and may
// auto-enable BT) and before the wait loop below makes its first task_event_group_wait_any()
// call - see mediaKeysInitBt()'s doc comment.
mediaKeysInitBt(ctx.get());

WindowId window = window_manager_create(app_instance_id, mediaKeysCreateWidgets, ctx.get());
ctx->window = window;

Expand All @@ -41,12 +46,16 @@ int main(int argc, char* argv[]) {
}
if (should_close) break;
}

mediaKeysProcessBtEvents(ctx.get());
}

window_manager_remove(window);
check(app_event_unsubscribe(&sub) == ERROR_NONE);
task_event_group_destruct(&event_group);
// Must unsubscribe from the BT event group (inside mediaKeysTeardown()) before destructing
// it below - releasing a subscription's bit needs the group to still be alive.
mediaKeysTeardown(ctx.get());
task_event_group_destruct(&event_group);

return 0;
}
Expand Down
4 changes: 2 additions & 2 deletions Apps/MediaKeys/manifest.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32s3,esp32p4
app.id=tactility.mediakeys
app.version.name=0.10.0
app.version.code=10
app.version.name=0.11.0
app.version.code=11
app.name=Media Keys
app.description=Bluetooth media keys. Touch or Physical Keyboard control\nB - previous, P - play/pause, N - next, M - mute, D - volume down, U - volume up.\nQ or ESC to exit focus.
Loading