Various improvements (#614)
- Auto-select widgets in Launcher and apps with toolbars on devices without touch. - Improved USB HID input reliability, cleanup - Updated PSRAM settings to improve boot stability on supported devices. - Prevented duplicate Wi-Fi event subscriptions during screen rebuilds. - Updated docs - Fixes in WifiManage and WifiConnect - Reduced main task stack size - Moved USB HID stack size to PSRAM when available - app_manager_find_manifest() now returns a copy instead of a pointer
This commit is contained in:
committed by
GitHub
parent
cc8be3faef
commit
d6b1d15e56
@@ -109,7 +109,11 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
||||
Context ctx {};
|
||||
ctx.appInstanceId = appInstanceId;
|
||||
ctx.targetAppId = (argc > 0) ? argv[0] : std::string();
|
||||
ctx.targetManifest = *app_manager_find_manifest(ctx.targetAppId.c_str());
|
||||
if (app_manager_find_manifest(ctx.targetAppId.c_str(), &ctx.targetManifest) != ERROR_NONE) {
|
||||
LOG_W(TAG, "App %s not found", ctx.targetAppId.c_str());
|
||||
app_manager_finish(appInstanceId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = appInstanceId;
|
||||
|
||||
@@ -99,7 +99,8 @@ void showApps(Context* ctx) {
|
||||
for (int i = 0; i < ctx->entries.size(); i++) {
|
||||
auto& entry = ctx->entries[i];
|
||||
LOG_I(TAG, "Adding %s", entry.appName.c_str());
|
||||
const char* icon = app_manager_find_manifest(entry.appId.c_str()) != nullptr ? LV_SYMBOL_OK : nullptr;
|
||||
AppManifest manifest;
|
||||
const char* icon = app_manager_find_manifest(entry.appId.c_str(), &manifest) == ERROR_NONE ? LV_SYMBOL_OK : nullptr;
|
||||
auto* entry_button = lv_list_add_button(list, icon, entry.appName.c_str());
|
||||
auto int_as_voidptr = reinterpret_cast<void*>(i);
|
||||
lv_obj_set_user_data(entry_button, int_as_voidptr);
|
||||
|
||||
@@ -159,7 +159,8 @@ void updateApp(Context* ctx) {
|
||||
void updateViews(Context* ctx) {
|
||||
lvgl_toolbar_clear_actions(ctx->toolbar);
|
||||
auto app_id = ctx->entry.appId.c_str();
|
||||
const auto manifest = app_manager_find_manifest(app_id);
|
||||
AppManifest manifest;
|
||||
bool is_installed = app_manager_find_manifest(app_id, &manifest) == ERROR_NONE;
|
||||
ctx->spinner = lvgl_toolbar_add_spinner_action(ctx->toolbar);
|
||||
lv_obj_add_flag(ctx->spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(ctx->updateLabel, LV_OBJ_FLAG_HIDDEN);
|
||||
@@ -177,7 +178,7 @@ void updateViews(Context* ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (manifest != nullptr) {
|
||||
if (is_installed) {
|
||||
if (metadata.app_version_code < ctx->entry.appVersionCode) {
|
||||
ctx->updateButton = lvgl_toolbar_add_image_button_action(ctx->toolbar, LV_SYMBOL_DOWNLOAD, onUpdatePressed, ctx);
|
||||
lv_obj_remove_flag(ctx->updateLabel, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
@@ -149,7 +149,8 @@ std::string getLauncherAppId() {
|
||||
}
|
||||
|
||||
// If the app in the boot.properties does not exist, return default
|
||||
if (app_manager_find_manifest(boot_properties.launcherAppId.c_str()) == nullptr) {
|
||||
AppManifest manifest;
|
||||
if (app_manager_find_manifest(boot_properties.launcherAppId.c_str(), &manifest) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Launcher app %s not found", boot_properties.launcherAppId.c_str());
|
||||
return CONFIG_TT_LAUNCHER_APP_ID;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
# Chat App
|
||||
|
||||
ESP-NOW-based chat application with channel-based messaging. Devices with the same encryption key can communicate in real-time without requiring a WiFi access point or internet connection.
|
||||
|
||||
## Features
|
||||
|
||||
- **Channel-based messaging**: Join named channels (e.g. `#general`, `#random`) to organize conversations
|
||||
- **Broadcast support**: Messages with empty target are visible in all channels
|
||||
- **Configurable nickname**: Identify yourself with a custom name (max 23 characters)
|
||||
- **Unique sender ID**: Each device gets a random 32-bit ID on first launch for future DM support
|
||||
- **Encryption key**: Optional shared key for private group communication
|
||||
- **Persistent settings**: Sender ID, nickname, key, and current chat channel are saved across reboots
|
||||
|
||||
## Requirements
|
||||
|
||||
- ESP32 with WiFi support (not available on ESP32-P4)
|
||||
- ESP-NOW service enabled
|
||||
|
||||
## UI Layout
|
||||
|
||||
```text
|
||||
+------------------------------------------+
|
||||
| [Back] Chat: #general [List] [Gear] |
|
||||
+------------------------------------------+
|
||||
| alice: hello everyone |
|
||||
| bob: hey alice! |
|
||||
| You: hi there |
|
||||
| (scrollable message list) |
|
||||
+------------------------------------------+
|
||||
| [____input textarea____] [Send] |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
- **Toolbar title**: Shows `Chat: <channel>` with the current channel name
|
||||
- **List icon**: Opens channel selector to switch channels
|
||||
- **Gear icon**: Opens settings panel (nickname, encryption key)
|
||||
- **Message list**: Shows messages matching the current channel or broadcast messages
|
||||
- **Input bar**: Type and send messages to the current channel
|
||||
|
||||
## Channel Selector
|
||||
|
||||
Tap the list icon to change channels. Enter a channel name (e.g. `#general`, `#team1`) and press OK. The message list refreshes to show only messages matching the new channel.
|
||||
|
||||
Messages are sent with the current channel as the target. Only devices viewing the same channel will display the message. Broadcast messages (empty target) appear in all channels.
|
||||
|
||||
## First Launch
|
||||
|
||||
On first launch (when no settings file exists), the settings panel opens automatically so users can configure their nickname before chatting. A unique sender ID is also generated using the hardware RNG.
|
||||
|
||||
## Settings
|
||||
|
||||
Tap the gear icon to configure:
|
||||
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| Nickname | Your display name (max 23 chars) | `Device` |
|
||||
| Key | Encryption key as 32 hex characters (16 bytes) | All zeros (empty field) |
|
||||
|
||||
Settings are stored in `/data/settings/chat.properties`. The encryption key is stored encrypted using AES-256-CBC. The sender ID is stored as a decimal number.
|
||||
|
||||
When the key field is left empty, the default all-zeros key is used. All devices using the default key can communicate without configuration.
|
||||
|
||||
Changing the encryption key causes ESP-NOW to restart with the new configuration.
|
||||
|
||||
## Wire Protocol v2
|
||||
|
||||
Compact variable-length packets broadcast over ESP-NOW:
|
||||
|
||||
### Header (16 bytes)
|
||||
|
||||
```text
|
||||
Offset Size Field
|
||||
------ ---- -----
|
||||
0 4 magic (0x54435432 "TCT2")
|
||||
4 2 protocol_version (2)
|
||||
6 4 from (sender ID, random uint32)
|
||||
10 4 to (recipient ID, 0 = broadcast/channel)
|
||||
14 1 payload_type (1 = TextMessage)
|
||||
15 1 payload_size (length of payload)
|
||||
```
|
||||
|
||||
### Text Message Payload (variable)
|
||||
|
||||
```text
|
||||
[nickname\0][target\0][message bytes]
|
||||
```
|
||||
|
||||
- `nickname`: Null-terminated sender display name (2-23 chars + null; single-letter names rejected)
|
||||
- `target`: Null-terminated channel or empty for broadcast (0-23 chars + null)
|
||||
- Empty string (`\0`): broadcast to all channels
|
||||
- Channel name (e.g. `#general`): visible only when viewing that channel
|
||||
- `message`: Remaining bytes, NOT null-terminated, minimum 1 byte (length = `payload_size - strlen(nickname) - 1 - strlen(target) - 1`)
|
||||
|
||||
**Minimum packet size for TextMessage:** 16 (header) + 2 (min nickname) + 1 (null) + 0 (empty target) + 1 (null) + 1 (min message) = **21 bytes**
|
||||
|
||||
**Example calculation:** If nickname is "Alice" (5 chars) and target is "#general" (8 chars):
|
||||
- Overhead: 5 + 1 + 8 + 1 = 15 bytes
|
||||
- Max message: 255 - 15 = 240 bytes
|
||||
|
||||
### Example
|
||||
|
||||
"Alice" sends "Hi!" to #general:
|
||||
- Header: 16 bytes
|
||||
- Payload: `Alice\0#general\0Hi!` = 18 bytes
|
||||
- **Total: 34 bytes**
|
||||
|
||||
### Size Limits
|
||||
|
||||
| Constraint | Min | Max |
|
||||
|------------|-----|-----|
|
||||
| Header size | 16 bytes | 16 bytes |
|
||||
| Payload (uint8_t) | 5 bytes | 255 bytes |
|
||||
| Nickname | 2 characters | 23 characters |
|
||||
| Channel/target | 0 (broadcast) | 23 characters |
|
||||
| Message (wire) | 1 byte | up to 251 bytes (varies by overhead) |
|
||||
| Message (UI) | 1 character | 200 characters |
|
||||
| Total packet (TextMessage) | 21 bytes | 271 bytes |
|
||||
|
||||
### Payload Types
|
||||
|
||||
| Type | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| TextMessage | 1 | Chat message with nickname, target, and text |
|
||||
| (reserved) | 2+ | Future: Position, Telemetry, etc. |
|
||||
|
||||
### Target Field Semantics
|
||||
|
||||
| `to` Value | `target` Field | Meaning |
|
||||
|------------|----------------|---------|
|
||||
| 0 | `""` (empty) | Broadcast - visible in all channels |
|
||||
| 0 | `#channel` | Channel message - visible only when viewing that channel |
|
||||
| non-zero | `nickname` | Direct message (future - requires address discovery protocol) |
|
||||
|
||||
Messages with incorrect magic/version or invalid payload are silently discarded.
|
||||
|
||||
> **Note:** Direct messaging (non-zero `to`) will require an address discovery mechanism, such as periodic broadcasts announcing nickname→sender_id mappings, before devices can address each other directly.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
ChatApp - App lifecycle, ESP-NOW send/receive, settings management
|
||||
ChatState - Message storage (deque, max 100), channel filtering, mutex-protected
|
||||
ChatView - LVGL UI: toolbar, message list, input bar, settings/channel panels
|
||||
ChatProtocol - MessageHeader struct, serialize/deserialize, PayloadType enum
|
||||
ChatSettings - Properties file load/save with encrypted key storage, sender ID generation
|
||||
```
|
||||
|
||||
All files are guarded with `#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)` to exclude from P4 builds.
|
||||
|
||||
## Message Flow
|
||||
|
||||
### Sending
|
||||
|
||||
1. User types message and taps Send
|
||||
2. `serializeTextMessage()` builds compact packet with sender ID, nickname, channel, message
|
||||
3. Broadcast via ESP-NOW to nearby devices
|
||||
4. Own message stored and displayed locally
|
||||
|
||||
### Receiving
|
||||
|
||||
1. ESP-NOW callback fires with raw data
|
||||
2. Validate packet:
|
||||
- Minimum size: 21 bytes (16 header + 2 min nickname + 1 null + 0 min target + 1 null + 1 min message)
|
||||
- Magic bytes: must be `0x54435432` ("TCT2")
|
||||
- Protocol version: must be 2
|
||||
- Payload size: `header.payload_size` must equal `received_length - 16`
|
||||
3. Parse null-terminated nickname and target from payload
|
||||
4. Validate minimum lengths: nickname >= 2 chars, message >= 1 byte
|
||||
5. Extract message from remaining bytes (length derived from payload_size)
|
||||
6. Store in message deque with sender ID
|
||||
7. Display if target matches current channel or is broadcast (empty)
|
||||
|
||||
## Limitations
|
||||
|
||||
- Maximum 100 stored messages (oldest discarded when full)
|
||||
- Nickname: 23 characters max
|
||||
- Channel name: 23 characters max
|
||||
- Message text: 200 characters max (UI limit; actual wire limit varies by nickname/target length)
|
||||
- No message persistence across app restarts (messages are in-memory only)
|
||||
- All communication is broadcast; channel filtering is client-side only
|
||||
- Sender ID collisions: 32-bit random IDs have ~50% collision probability at ~77,000 active devices (birthday paradox); no collision detection/resolution implemented
|
||||
|
||||
## Security Considerations
|
||||
|
||||
The chat protocol relies on ESP-NOW's built-in encryption (when configured) but has additional security limitations:
|
||||
|
||||
- **No message authentication**: No MAC/HMAC to verify message integrity or sender authenticity beyond the sender ID
|
||||
- **No replay protection**: No sequence numbers or timestamps; messages can be replayed
|
||||
- **Sender ID spoofing**: Any device knowing the encryption key can forge messages with arbitrary sender IDs
|
||||
- **No forward secrecy**: Compromise of the shared key exposes all past and future messages
|
||||
|
||||
These tradeoffs are acceptable for casual local communication but should be understood before using for sensitive applications.
|
||||
@@ -1,3 +1,6 @@
|
||||
#include "tactility/drivers/pointer.h"
|
||||
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
@@ -165,7 +168,7 @@ void createWidgets(lv_obj_t* parent, void*) {
|
||||
? computeButtonMargin(lv_display_get_horizontal_resolution(display), total_button_size)
|
||||
: computeButtonMargin(lv_display_get_vertical_resolution(display), total_button_size);
|
||||
|
||||
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_APPS, "AppList", margin, is_landscape_display);
|
||||
auto* app_list_button = createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_APPS, "AppList", margin, is_landscape_display);
|
||||
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_FOLDER, "Files", margin, is_landscape_display);
|
||||
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_SETTINGS, "Settings", margin, is_landscape_display);
|
||||
|
||||
@@ -189,14 +192,24 @@ void createWidgets(lv_obj_t* parent, void*) {
|
||||
lv_label_set_text(power_label, LV_SYMBOL_POWER);
|
||||
lv_obj_set_style_text_color(power_label, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
// If we don't have a touch device, we assume there's some other kind of input like a keyboard, an encoder or button control
|
||||
// In that scenario we want to automatically have the app list button selected so the user doesn't have to press the widget selection
|
||||
// an extra time.
|
||||
if (!device_has_active_by_type(&POINTER_TYPE)) {
|
||||
// lv_obj_update_layout(parent); // Resolve flex layout first, so focus/state invalidate against final coords
|
||||
lv_group_focus_obj(app_list_button);
|
||||
lv_obj_add_state(app_list_button, LV_STATE_FOCUS_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
void runAutoStart() {
|
||||
settings::BootSettings boot_properties;
|
||||
AppManifest manifest;
|
||||
if (
|
||||
// Auto-start due to built-in requirement
|
||||
strcmp(CONFIG_TT_AUTO_START_APP_ID, "") != 0 &&
|
||||
app_manager_find_manifest(CONFIG_TT_AUTO_START_APP_ID) != nullptr
|
||||
app_manager_find_manifest(CONFIG_TT_AUTO_START_APP_ID, &manifest) == ERROR_NONE
|
||||
) {
|
||||
LOG_I(TAG, "Starting %s", CONFIG_TT_AUTO_START_APP_ID);
|
||||
uint32_t app_launch_id;
|
||||
@@ -205,7 +218,7 @@ void runAutoStart() {
|
||||
// Auto-start due to user configuration
|
||||
settings::loadBootSettings(boot_properties) &&
|
||||
!boot_properties.autoStartAppId.empty() &&
|
||||
app_manager_find_manifest(boot_properties.autoStartAppId.c_str()) != nullptr
|
||||
app_manager_find_manifest(boot_properties.autoStartAppId.c_str(), &manifest) == ERROR_NONE
|
||||
) {
|
||||
LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str());
|
||||
uint32_t app_launch_id;
|
||||
|
||||
@@ -105,6 +105,10 @@ void updateBusySpinner(Context* ctx) {
|
||||
}
|
||||
|
||||
void updateViews(Context* ctx) {
|
||||
if (ctx->connectButton == nullptr) {
|
||||
// Buried (e.g. the forget confirmation dialog opened on top) - see destroyWidgets().
|
||||
return;
|
||||
}
|
||||
updateConnectButton(ctx);
|
||||
updateBusySpinner(ctx);
|
||||
}
|
||||
@@ -115,13 +119,18 @@ void requestViewUpdate(Context* ctx) {
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
// Runs with the LVGL lock already held, possibly on another app's thread - see
|
||||
// WindowDestroyWidgetsFn's warnings. Must stay lock-free: only nulls pointers.
|
||||
void destroyWidgets(void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
ctx->busySpinner = nullptr;
|
||||
ctx->connectButton = nullptr;
|
||||
ctx->disconnectButton = nullptr;
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
ctx->wifiSubscription = service::wifi::getPubsub()->subscribe([ctx](auto) {
|
||||
requestViewUpdate(ctx);
|
||||
});
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
@@ -202,7 +211,14 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
||||
sub.app_instance_id = appInstanceId;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||
// Subscribed once here, not in createWidgets(): that callback re-runs on every
|
||||
// burial/resurface rebuild, and re-subscribing there would leak the previous subscription
|
||||
// (and its captured ctx pointer) every time, only the last of which shutdown ever cleans up.
|
||||
ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto) {
|
||||
requestViewUpdate(&ctx);
|
||||
});
|
||||
|
||||
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||
|
||||
bool shouldClose = false;
|
||||
while (!shouldClose) {
|
||||
|
||||
@@ -120,6 +120,11 @@ void setLoading(Context* ctx, bool loading) {
|
||||
}
|
||||
|
||||
void updateView(Context* ctx) {
|
||||
if (ctx->connect_button == nullptr) {
|
||||
// Buried (e.g. this window's own connecting state closed it, or a future dialog opens
|
||||
// on top) - see destroyWidgets().
|
||||
return;
|
||||
}
|
||||
if (ctx->connectionError) {
|
||||
setLoading(ctx, false);
|
||||
resetErrors(ctx);
|
||||
@@ -194,14 +199,24 @@ void createBottomButtons(Context* ctx, lv_obj_t* parent) {
|
||||
lv_obj_add_event_cb(ctx->connect_button, onConnectPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
}
|
||||
|
||||
// Runs with the LVGL lock already held, possibly on another app's thread - see
|
||||
// WindowDestroyWidgetsFn's warnings. Must stay lock-free: only nulls pointers.
|
||||
void destroyWidgets(void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
ctx->ssid_textarea = nullptr;
|
||||
ctx->ssid_error = nullptr;
|
||||
ctx->password_textarea = nullptr;
|
||||
ctx->password_error = nullptr;
|
||||
ctx->connect_button = nullptr;
|
||||
ctx->remember_switch = nullptr;
|
||||
ctx->connecting_spinner = nullptr;
|
||||
ctx->connection_error = nullptr;
|
||||
}
|
||||
|
||||
// TODO: Standardize dialogs
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
ctx->wifiSubscription = service::wifi::getPubsub()->subscribe([ctx](auto event) {
|
||||
onWifiEvent(ctx, event);
|
||||
});
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
@@ -302,7 +317,14 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
||||
sub.app_instance_id = appInstanceId;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||
// Subscribed once here, not in createWidgets(): that callback re-runs on every
|
||||
// burial/resurface rebuild, and re-subscribing there would leak the previous subscription
|
||||
// (and its captured ctx pointer) every time, only the last of which shutdown ever cleans up.
|
||||
ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto event) {
|
||||
onWifiEvent(&ctx, event);
|
||||
});
|
||||
|
||||
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||
|
||||
bool shouldClose = false;
|
||||
while (!shouldClose) {
|
||||
|
||||
@@ -323,10 +323,23 @@ void View::init(uint32_t newAppInstanceId, lv_obj_t* parent) {
|
||||
}
|
||||
|
||||
void View::update() {
|
||||
if (root == nullptr) {
|
||||
// Buried (or not yet built) - see reset().
|
||||
return;
|
||||
}
|
||||
updateWifiToggle();
|
||||
updateScanning();
|
||||
updateNetworkList();
|
||||
updateConnectToHidden();
|
||||
}
|
||||
|
||||
void View::reset() {
|
||||
root = nullptr;
|
||||
enable_switch = nullptr;
|
||||
enable_on_boot_switch = nullptr;
|
||||
scanning_spinner = nullptr;
|
||||
networks_list = nullptr;
|
||||
connect_to_hidden = nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -65,6 +65,8 @@ static void onConnectToHidden() {
|
||||
void requestViewUpdate(Context* ctx) {
|
||||
ctx->lock();
|
||||
lvgl_lock();
|
||||
// Safe even while buried (e.g. WifiApSettings/WifiConnect opened on top): destroyWidgets()
|
||||
// nulls the view's widget pointers before they're deleted, and update() no-ops on that.
|
||||
ctx->view.update();
|
||||
lvgl_unlock();
|
||||
ctx->unlock();
|
||||
@@ -103,6 +105,13 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
ctx->unlock();
|
||||
}
|
||||
|
||||
// Runs with the LVGL lock already held, possibly on another app's thread - see
|
||||
// WindowDestroyWidgetsFn's warnings. Must stay lock-free: View::reset() only nulls pointers.
|
||||
void destroyWidgets(void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
ctx->view.reset();
|
||||
}
|
||||
|
||||
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
||||
Context ctx;
|
||||
ctx.appInstanceId = appInstanceId;
|
||||
@@ -127,7 +136,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
|
||||
sub.app_instance_id = appInstanceId;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||
|
||||
service::wifi::RadioState radio_state = service::wifi::getRadioState();
|
||||
bool can_scan = radio_state == service::wifi::RadioState::On ||
|
||||
|
||||
Reference in New Issue
Block a user