app-module events & callstack config, crash diagnostics (#630)
- Apps can specify task stack depth and preferred memory placement in their manifests. - App identifiers are validated against length and character requirements. - Crash diagnostics now show the crash cause, reason, fault address, call stack, and program-counter details, with logs saved for review. - App closing is more consistent across built-in screens. There's now a dedicated function, and the old _emit() function is made private. - Crash diagnostics no longer display a QR code without a call stack. - Improved memory allocation for unrestricted requests.
This commit is contained in:
committed by
GitHub
parent
c656ee9ffd
commit
92ca046681
@@ -11,8 +11,10 @@
|
||||
|
||||
## Higher Priority
|
||||
|
||||
- CrashDiagnostics shouldn't show a QR when there's no callstack
|
||||
- lvgl file lock won't work with display vs sdcard when lvgl is stopped (external app bug risk)
|
||||
- Apps should be able to specify stack size in their manifest
|
||||
- Apps should be able to specify stack size in their manifest, per architecture.
|
||||
Use thread_get_stack_space() to find out the unused bytes
|
||||
- Apps currently have a `Context` object with an `appInstanceId` in it, purely for being able to close the app.
|
||||
Change it so that the app has its own termination signal that it waits for in the loop, it should subscribe to the event group.
|
||||
- stopAppFromToolbar() in Tactility.cpp stops the top-most app. Change it so the toolbar knows for which app id it is created, so it can rely on that.
|
||||
|
||||
@@ -114,16 +114,6 @@ error_t app_event_subscribe_with_app_id(struct AppEventSubscription* sub, struct
|
||||
*/
|
||||
error_t app_event_unsubscribe(struct AppEventSubscription* sub);
|
||||
|
||||
/**
|
||||
* Deliver @a event to every subscription registered for @a app_instance_id (normally exactly one).
|
||||
* @warning Does not work in ISR context.
|
||||
* @retval ERROR_NONE delivered to at least one subscription
|
||||
* @retval ERROR_NOT_FOUND no subscription is registered for @a app_instance_id
|
||||
* @retval ERROR_RESOURCE at least one matching subscription's queue was full; the event was
|
||||
* dropped for that subscription (still delivered to any other matching subscription)
|
||||
*/
|
||||
error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event);
|
||||
|
||||
/**
|
||||
* Non-blocking: pop the next event for @a sub if one is already queued.
|
||||
* @warning Never blocks. To wait for an event, block in task_event_group_wait()/
|
||||
@@ -134,6 +124,13 @@ error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* eve
|
||||
*/
|
||||
error_t app_event_poll(struct AppEventSubscription* sub, struct AppEvent* out_event);
|
||||
|
||||
/**
|
||||
* Emits a close event to the specified app.
|
||||
* @param[in] instance_id
|
||||
* @return ERROR_NONE when event was successfully emitted
|
||||
*/
|
||||
error_t app_event_emit_close(AppInstanceId instance_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
|
||||
#include "location.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Character count, excluding null terminator
|
||||
#define APP_ID_LENGTH 32
|
||||
|
||||
/** Broad classification of an app, used for grouping/launcher presentation. */
|
||||
enum AppCategory {
|
||||
APP_CATEGORY_SYSTEM,
|
||||
@@ -24,6 +28,21 @@ enum AppManifestFlags {
|
||||
APP_MANIFEST_FLAG_HIDDEN = 1 >> 0,
|
||||
};
|
||||
|
||||
/** Largest stack depth (in words) an app may request. Keeps `depth * sizeof(StackType_t)` safely
|
||||
* bounded and stops one app from claiming an unreasonable share of available RAM. A depth beyond
|
||||
* this must be rejected outright, not silently truncated or clamped. */
|
||||
#define APP_STACK_SIZE_MAX 16384
|
||||
|
||||
struct AppStackConfig {
|
||||
/** Stack depth (in words, matching FreeRTOS's configSTACK_DEPTH_TYPE) for this app's task.
|
||||
* 0 uses the scheduler's default. Must not exceed APP_STACK_SIZE_MAX. */
|
||||
uint16_t depth;
|
||||
/** Desired memory capability.
|
||||
* 0 means default.
|
||||
* Combine one or more of \a MemoryCapability from <tactility/memory.h> with a bitwise OR.*/
|
||||
uint16_t desired_memory_capability;
|
||||
};
|
||||
|
||||
/** Describes a registrable app. One manifest exists per app id. */
|
||||
struct AppManifest {
|
||||
/** Unique app identifier. Should never be NULL. */
|
||||
@@ -34,8 +53,12 @@ struct AppManifest {
|
||||
struct AppLocation location;
|
||||
/** Bitmask of AppManifestFlags. Most apps should leave this 0. */
|
||||
uint8_t flags;
|
||||
/** Stack allocation config for this app's task. */
|
||||
struct AppStackConfig stack;
|
||||
};
|
||||
|
||||
bool app_id_is_valid(const char* id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -50,6 +50,12 @@ struct AppMetadata {
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char requires_device_id[APP_METADATA_REQUIRES_DEVICE_ID_LENGTH + 1];
|
||||
|
||||
/**
|
||||
* Stack depth (in words) for the app's task. Optional; 0 means scheduler default.
|
||||
* @warning Avoid default values: the default is conservative, which wastes memory.
|
||||
*/
|
||||
uint32_t stack_depth;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/instance.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Deliver @a event to every subscription registered for @a app_instance_id (normally exactly one).
|
||||
* @warning Does not work in ISR context.
|
||||
* @retval ERROR_NONE delivered to at least one subscription
|
||||
* @retval ERROR_NOT_FOUND no subscription is registered for @a app_instance_id
|
||||
* @retval ERROR_RESOURCE at least one matching subscription's queue was full; the event was
|
||||
* dropped for that subscription (still delivered to any other matching subscription)
|
||||
*/
|
||||
error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+3
-1
@@ -13,10 +13,10 @@
|
||||
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value);
|
||||
|
||||
bool app_metadata_is_valid_format_version(const std::string& version);
|
||||
bool app_metadata_is_valid_id(const std::string& id);
|
||||
bool app_metadata_is_valid_name(const std::string& name);
|
||||
bool app_metadata_is_valid_version_name(const std::string& version);
|
||||
bool app_metadata_is_valid_version_code(const std::string& version);
|
||||
bool app_metadata_is_valid_stack_size(const std::string& value);
|
||||
|
||||
/** Validates a comma-separated list of device ids (alphanumeric + '-' items, matching Devices/<id> folder names). */
|
||||
bool app_metadata_is_valid_device_id_list(const std::string& value);
|
||||
@@ -31,3 +31,5 @@ bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties,
|
||||
|
||||
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map into @a out_metadata. */
|
||||
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
|
||||
|
||||
bool app_metadata_validate_string(const std::string& value, bool (*is_valid_char)(char));
|
||||
+2
-1
@@ -21,12 +21,13 @@ extern "C" {
|
||||
* AppLoaderApi::load()/run(), marking the instance ACTIVE for the duration of run().
|
||||
* @param[in] app_instance_id id already allocated in the ledger for this instance
|
||||
* @param[in] location the location of the app
|
||||
* @param[in] stack stack allocation config for the app's task
|
||||
* @param[in] argc the amount of arguments to pass to the app's main function
|
||||
* @param[in] argv the array of arguments to pass to the app's main function - ownership is
|
||||
* taken by the scheduler regardless of outcome (freed once the spawned task's run() returns, or
|
||||
* immediately on a failure to start it)
|
||||
*/
|
||||
error_t app_scheduler_start(AppInstanceId app_instance_id, struct AppLocation location, int argc, char* argv[]);
|
||||
error_t app_scheduler_start(AppInstanceId app_instance_id, struct AppLocation location, struct AppStackConfig stack, int argc, char* argv[]);
|
||||
|
||||
/**
|
||||
* Permanently stops an app instance (APP_EVENT_CLOSE if it was running), bound-waits for its
|
||||
@@ -4,8 +4,8 @@
|
||||
#include <app/manager.h>
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <app/private/app_fs.h>
|
||||
#include <app/private/app_ledger.h>
|
||||
#include <app/private/fs.h>
|
||||
#include <app/private/ledger.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/filesystem/file_mutex.h>
|
||||
@@ -180,6 +180,7 @@ error_t register_installed_app_locked(const std::string& app_dir_path, const App
|
||||
.category = APP_CATEGORY_USER,
|
||||
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
|
||||
.flags = 0,
|
||||
.stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
// Belt-and-braces: app_install()'s earlier app_manager_remove() call is meant to have
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "tactility/filesystem/file_mutex.h"
|
||||
|
||||
#include <tactility/filesystem/file_mutex.h>
|
||||
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <app/private/app_metadata_parsing_internal.h>
|
||||
#include <app/private/metadata_parsing_internal.h>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
@@ -16,8 +14,19 @@
|
||||
|
||||
constexpr auto* TAG = "app_metadata";
|
||||
|
||||
bool app_metadata_validate_string(const std::string& value, bool (*is_valid_char)(char)) {
|
||||
for (char c: value) {
|
||||
if (!is_valid_char(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
#define validate_string app_metadata_validate_string
|
||||
|
||||
std::string trim(const std::string& value) {
|
||||
constexpr auto* whitespace = " \t\r\n";
|
||||
auto start = value.find_first_not_of(whitespace);
|
||||
@@ -28,15 +37,6 @@ std::string trim(const std::string& value) {
|
||||
return value.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
bool validate_string(const std::string& value, bool (*is_valid_char)(char)) {
|
||||
for (char c: value) {
|
||||
if (!is_valid_char(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Validates a comma-separated list: non-empty, no leading/trailing/double commas (which would
|
||||
* produce an empty item), and every item passing @a is_valid_item. */
|
||||
bool validate_csv_list(const std::string& value, bool (*is_valid_item)(const std::string&)) {
|
||||
@@ -125,12 +125,6 @@ bool app_metadata_is_valid_format_version(const std::string& version) {
|
||||
});
|
||||
}
|
||||
|
||||
bool app_metadata_is_valid_id(const std::string& id) {
|
||||
return id.size() >= 5 && id.size() <= APP_METADATA_APP_ID_LENGTH && validate_string(id, [](char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
|
||||
});
|
||||
}
|
||||
|
||||
bool app_metadata_is_valid_name(const std::string& name) {
|
||||
return name.size() >= 2 && name.size() <= APP_METADATA_APP_NAME_LENGTH && validate_string(name, [](char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == ' ' || c == '-';
|
||||
@@ -150,6 +144,13 @@ bool app_metadata_is_valid_version_code(const std::string& version) {
|
||||
});
|
||||
}
|
||||
|
||||
bool app_metadata_is_valid_stack_size(const std::string& value) {
|
||||
// 10 digits is the maximum decimal width of uint32_t.
|
||||
return !value.empty() && value.size() <= 10 && validate_string(value, [](char c) {
|
||||
return std::isdigit(static_cast<unsigned char>(c)) != 0;
|
||||
});
|
||||
}
|
||||
|
||||
bool app_metadata_is_valid_device_id_list(const std::string& value) {
|
||||
return validate_csv_list(value, [](const std::string& item) {
|
||||
bool has_alnum = false;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "app/manifest.h"
|
||||
|
||||
|
||||
#include <app/metadata.h>
|
||||
#include <app/private/app_metadata_parsing_internal.h>
|
||||
#include <app/private/metadata_parsing_internal.h>
|
||||
|
||||
#include <charconv>
|
||||
|
||||
@@ -28,7 +31,7 @@ bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!app_metadata_is_valid_id(id)) {
|
||||
if (!app_id_is_valid(id.c_str())) {
|
||||
LOG_E(TAG, "Invalid app id");
|
||||
return false;
|
||||
}
|
||||
@@ -97,5 +100,7 @@ bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties,
|
||||
return false;
|
||||
}
|
||||
|
||||
out_metadata.stack_depth = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "app/manifest.h"
|
||||
|
||||
|
||||
#include <app/metadata.h>
|
||||
#include <app/private/app_metadata_parsing_internal.h>
|
||||
#include <app/private/metadata_parsing_internal.h>
|
||||
|
||||
#include <charconv>
|
||||
|
||||
@@ -28,7 +31,7 @@ bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!app_metadata_is_valid_id(id)) {
|
||||
if (!app_id_is_valid(id.c_str())) {
|
||||
LOG_E(TAG, "Invalid app id");
|
||||
return false;
|
||||
}
|
||||
@@ -114,5 +117,33 @@ bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties,
|
||||
}
|
||||
}
|
||||
|
||||
// app.stack.depth (optional; if present, must be a valid unsigned decimal fitting uint32_t)
|
||||
|
||||
auto stack_size_iterator = properties.find("app.stack.depth");
|
||||
if (stack_size_iterator != properties.end()) {
|
||||
const std::string& stack_size_string = stack_size_iterator->second;
|
||||
if (!app_metadata_is_valid_stack_size(stack_size_string)) {
|
||||
LOG_E(TAG, "Invalid app.stack.depth");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t stack_size = 0;
|
||||
const auto* stack_size_first = stack_size_string.data();
|
||||
const auto* stack_size_last = stack_size_first + stack_size_string.size();
|
||||
if (std::from_chars(stack_size_first, stack_size_last, stack_size).ec != std::errc {}) {
|
||||
LOG_E(TAG, "App stack depth out of range");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject outright rather than truncating/clamping into AppStackConfig::depth (uint16_t) -
|
||||
// a value like 1073741825 would otherwise silently narrow to 1, handing the app a
|
||||
// catastrophically undersized stack instead of the huge one it declared.
|
||||
if (stack_size > APP_STACK_SIZE_MAX) {
|
||||
LOG_E(TAG, "App stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", stack_size, APP_STACK_SIZE_MAX);
|
||||
return false;
|
||||
}
|
||||
out_metadata.stack_depth = stack_size;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -120,4 +120,9 @@ error_t app_event_poll(AppEventSubscription* sub, AppEvent* out_event) {
|
||||
return try_pop(sub, out_event) ? ERROR_NONE : ERROR_TIMEOUT;
|
||||
}
|
||||
|
||||
error_t app_event_emit_close(AppInstanceId instance_id) {
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
return app_event_emit(instance_id, &event);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/manager.h>
|
||||
#include <app/metadata.h>
|
||||
#include <app/private/app_fs.h>
|
||||
#include <app/private/app_ledger.h>
|
||||
#include <app/private/app_scheduler.h>
|
||||
#include <app/private/fs.h>
|
||||
#include <app/private/ledger.h>
|
||||
#include <app/private/scheduler.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/error.h>
|
||||
@@ -103,17 +103,20 @@ error_t start_internal(const char* id, AppInstanceId parent_instance_id, int arg
|
||||
const AppManifest* manifest = manifest_iterator->second;
|
||||
|
||||
AppInstanceId target_id = ledger.next_instance_id++;
|
||||
AppInstanceRecord record { target_id, manifest, APP_INSTANCE_STATE_STARTING, nullptr };
|
||||
AppInstanceRecord record { .id = target_id, .manifest = manifest, .state = APP_INSTANCE_STATE_STARTING, .task = nullptr };
|
||||
record.parent_id = parent_instance_id;
|
||||
ledger.instances[target_id] = record;
|
||||
mutex_unlock(&ledger.mutex);
|
||||
|
||||
error_t result = app_scheduler_start(target_id, manifest->location, argc, argv);
|
||||
if (result != ERROR_NONE) {
|
||||
LOG_I(TAG, "[instance %d] starting %s with parent %d", target_id, manifest->id, parent_instance_id);
|
||||
|
||||
error_t error = app_scheduler_start(target_id, manifest->location, manifest->stack, argc, argv);
|
||||
if (error != ERROR_NONE) {
|
||||
mutex_lock(&ledger.mutex);
|
||||
ledger.instances.erase(target_id);
|
||||
mutex_unlock(&ledger.mutex);
|
||||
return result;
|
||||
LOG_I(TAG, "[instance %d] Failed to start: %s", target_id, error_to_string(error));
|
||||
return error;
|
||||
}
|
||||
|
||||
*out_app_instance_id = target_id;
|
||||
@@ -289,6 +292,7 @@ void app_manager_install_path_scan(void) {
|
||||
.category = APP_CATEGORY_USER,
|
||||
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
|
||||
.flags = 0,
|
||||
.stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 },
|
||||
};
|
||||
new_records.push_back(std::move(record));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#include <app/manifest.h>
|
||||
#include <app/private/metadata_parsing_internal.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
bool app_id_is_valid(const char* id) {
|
||||
auto size = strlen(id);
|
||||
return size >= 5 && size <= APP_ID_LENGTH && app_metadata_validate_string(id, [](char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+150
-16
@@ -1,9 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/private/app_ledger.h>
|
||||
#include <app/private/app_scheduler.h>
|
||||
#include <app/event.h>
|
||||
#include <app/instance.h>
|
||||
#include <app/loader.h>
|
||||
#include <app/private/event.h>
|
||||
#include <app/private/ledger.h>
|
||||
#include <app/private/scheduler.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <service/instance.h>
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/memory.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -27,6 +28,15 @@ constexpr size_t APP_INSTANCE_ID_THREAD_SLOT_INDEX = 1;
|
||||
// Matches TactilityKernel's Thread wrapper's THREAD_PRIORITY_NORMAL.
|
||||
constexpr UBaseType_t APP_TASK_PRIORITY = 4;
|
||||
|
||||
// Used when an app's manifest doesn't request a specific stack depth (0). 8192 bytes' worth.
|
||||
constexpr size_t APP_DEFAULT_STACK_DEPTH = 8192 / sizeof(StackType_t);
|
||||
|
||||
// Task control blocks must stay in internal RAM; only the stack itself may live in external memory.
|
||||
constexpr MemoryPolicy APP_TASK_TCB_POLICY = { MEMORY_CAPABILITY_INTERNAL, 0, 0 };
|
||||
|
||||
constexpr auto* APP_REAPER_TASK_NAME = "app_reaper";
|
||||
constexpr size_t APP_REAPER_STACK_DEPTH = 2048 / sizeof(StackType_t);
|
||||
|
||||
namespace {
|
||||
|
||||
struct TaskContext {
|
||||
@@ -36,8 +46,42 @@ struct TaskContext {
|
||||
int argc;
|
||||
char** argv;
|
||||
AppCompletionSignal* completion;
|
||||
StackType_t* stackBuffer;
|
||||
StaticTask_t* taskTcb;
|
||||
};
|
||||
|
||||
struct ReaperContext {
|
||||
TaskHandle_t target;
|
||||
StackType_t* stackBuffer;
|
||||
StaticTask_t* taskTcb;
|
||||
};
|
||||
|
||||
void reaper_task_main(void* context) {
|
||||
auto* ctx = static_cast<ReaperContext*>(context);
|
||||
|
||||
while (eTaskGetState(ctx->target) == eRunning) {
|
||||
taskYIELD();
|
||||
}
|
||||
vTaskDelete(ctx->target);
|
||||
memory_free(ctx->stackBuffer);
|
||||
memory_free(ctx->taskTcb);
|
||||
delete ctx;
|
||||
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
// Hands off this task's own statically-allocated stack/TCB (which it can never safely free
|
||||
// itself - a task can't free the stack it's still running on, see UsbHidInput.cpp for the same
|
||||
// constraint) to a short-lived helper task, then suspends forever. Never returns.
|
||||
void reap_self(StackType_t* stack_buffer, StaticTask_t* task_tcb) {
|
||||
auto* reaper_ctx = new (std::nothrow) ReaperContext { xTaskGetCurrentTaskHandle(), stack_buffer, task_tcb };
|
||||
if (reaper_ctx == nullptr || xTaskCreate(reaper_task_main, APP_REAPER_TASK_NAME, APP_REAPER_STACK_DEPTH, reaper_ctx, tskIDLE_PRIORITY, nullptr) != pdPASS) {
|
||||
LOG_E(TAG, "Failed to create app reaper task; leaking stack buffer");
|
||||
delete reaper_ctx;
|
||||
}
|
||||
vTaskSuspend(nullptr);
|
||||
}
|
||||
|
||||
void set_state(AppInstanceId app_instance_id, AppInstanceState state) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
@@ -144,7 +188,7 @@ void app_task_main(void* context) {
|
||||
check(pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX) == nullptr);
|
||||
vTaskSetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX, reinterpret_cast<void*>(static_cast<uintptr_t>(ctx->app_instance_id)));
|
||||
|
||||
LOG_I(TAG, "Thread for %d started", ctx->app_instance_id);
|
||||
LOG_I(TAG, "[instance %lu] Task started", ctx->app_instance_id);
|
||||
|
||||
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_ACTIVE);
|
||||
|
||||
@@ -165,9 +209,11 @@ void app_task_main(void* context) {
|
||||
|
||||
AppInstanceId app_instance_id = ctx->app_instance_id;
|
||||
AppCompletionSignal* completion = ctx->completion;
|
||||
StackType_t* stack_buffer = ctx->stackBuffer;
|
||||
StaticTask_t* task_tcb = ctx->taskTcb;
|
||||
delete ctx;
|
||||
|
||||
LOG_I(TAG, "Thread for %d finished", app_instance_id);
|
||||
LOG_I(TAG, "[instance %lu] Task finished", app_instance_id);
|
||||
|
||||
// Erase the ledger entry before self-deleting - see "Reap self-terminated app tasks":
|
||||
// nothing else is guaranteed to ever call app_scheduler_stop() for this instance (the
|
||||
@@ -187,17 +233,23 @@ void app_task_main(void* context) {
|
||||
xSemaphoreGive(completion->semaphore);
|
||||
release_completion_signal(completion); // releases app_task_main()'s own reference
|
||||
|
||||
LOG_I(TAG, "[instance %lu] minimum free stack space: %d bytes", app_instance_id, uxTaskGetStackHighWaterMark(nullptr));
|
||||
#ifdef ESP_PLATFORM
|
||||
// Statically-allocated stack/TCB (see app_scheduler_start()) - can't self-delete, see reap_self().
|
||||
reap_self(stack_buffer, task_tcb);
|
||||
#else
|
||||
vTaskDelete(nullptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, int argc, char* argv[]) {
|
||||
error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, AppStackConfig stack, int argc, char* argv[]) {
|
||||
const AppLoaderApi* loader = find_loader_api(location.type);
|
||||
if (loader == nullptr) {
|
||||
LOG_E(TAG, "No app loader is registered (service '%s' not found)", loader_service_id_for(location.type));
|
||||
LOG_E(TAG, "[instance %lu] No app loader is registered (service '%s' not found)", app_instance_id, loader_service_id_for(location.type));
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
@@ -205,30 +257,96 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
|
||||
void* runtime = nullptr;
|
||||
error_t load_result = loader->load(location, &runtime);
|
||||
if (load_result != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to load app: %s", error_to_string(load_result));
|
||||
LOG_E(TAG, "[instance %lu] Failed to load app: %s", app_instance_id, error_to_string(load_result));
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return load_result;
|
||||
}
|
||||
|
||||
auto* completion = new (std::nothrow) AppCompletionSignal();
|
||||
if (completion == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate app");
|
||||
LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
completion->semaphore = xSemaphoreCreateBinary();
|
||||
if (completion->semaphore == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate app");
|
||||
LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
|
||||
delete completion;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
auto* context = new (std::nothrow) TaskContext { loader, runtime, app_instance_id, argc, argv, completion };
|
||||
// Same bound app_metadata_parse() enforces on manifest.properties-declared depths - a
|
||||
// manifest built directly in C++ (not parsed from a file) must be held to it too.
|
||||
if (stack.depth > APP_STACK_SIZE_MAX) {
|
||||
LOG_E(TAG, "[instance %lu] stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", app_instance_id, stack.depth, APP_STACK_SIZE_MAX);
|
||||
vSemaphoreDelete(completion->semaphore);
|
||||
delete completion;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (stack.depth == 0) {
|
||||
LOG_W(TAG, "[instance %lu] using default stack depth", app_instance_id);
|
||||
}
|
||||
size_t effective_stack_depth = stack.depth != 0 ? stack.depth : APP_DEFAULT_STACK_DEPTH;
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
// ESP-IDF's FreeRTOS port has configSUPPORT_STATIC_ALLOCATION, POSIX doesn't
|
||||
// Try the desired capability first (if any). If it fails or isn't specified, use the fallback/default alloc behaviour (use internal memory).
|
||||
StackType_t* stack_buffer = nullptr;
|
||||
if (stack.desired_memory_capability != 0) {
|
||||
MemoryPolicy requested_policy = { .required = stack.desired_memory_capability, .desired = 0, .alignment = 0 };
|
||||
stack_buffer = static_cast<StackType_t*>(memory_alloc_with_policy(effective_stack_depth * sizeof(StackType_t), &requested_policy));
|
||||
}
|
||||
if (stack_buffer == nullptr) {
|
||||
MemoryPolicy internal_policy = { .required = MEMORY_CAPABILITY_INTERNAL, .desired = 0, .alignment = 0 };
|
||||
stack_buffer = static_cast<StackType_t*>(memory_alloc_with_policy(effective_stack_depth * sizeof(StackType_t), &internal_policy));
|
||||
}
|
||||
if (stack_buffer == nullptr) {
|
||||
LOG_E(TAG, "[instance %lu] Failed to allocate app stack", app_instance_id);
|
||||
vSemaphoreDelete(completion->semaphore);
|
||||
delete completion;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
auto* task_tcb = static_cast<StaticTask_t*>(memory_alloc_with_policy(sizeof(StaticTask_t), &APP_TASK_TCB_POLICY));
|
||||
if (task_tcb == nullptr) {
|
||||
LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
|
||||
memory_free(stack_buffer);
|
||||
vSemaphoreDelete(completion->semaphore);
|
||||
delete completion;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
#else
|
||||
StackType_t* stack_buffer = nullptr;
|
||||
StaticTask_t* task_tcb = nullptr;
|
||||
#endif
|
||||
|
||||
auto* context = new (std::nothrow) TaskContext {
|
||||
.loader = loader,
|
||||
.runtime = runtime,
|
||||
.app_instance_id = app_instance_id,
|
||||
.argc = argc,
|
||||
.argv = argv,
|
||||
.completion = completion,
|
||||
.stackBuffer = stack_buffer,
|
||||
.taskTcb = task_tcb
|
||||
};
|
||||
|
||||
if (context == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate app");
|
||||
LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
|
||||
#ifdef ESP_PLATFORM
|
||||
memory_free(task_tcb);
|
||||
memory_free(stack_buffer);
|
||||
#endif
|
||||
vSemaphoreDelete(completion->semaphore);
|
||||
delete completion;
|
||||
loader->unload(runtime);
|
||||
@@ -239,14 +357,23 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
|
||||
char task_name[16];
|
||||
snprintf(task_name, sizeof(task_name), "app_%lu", static_cast<unsigned long>(app_instance_id));
|
||||
|
||||
TaskHandle_t task_handle = nullptr;
|
||||
// 8192 bytes -> stack depth in words, matching what TactilityKernel's Thread wrapper does with the stack size it's given.
|
||||
// Created at idle priority so it can't preempt us before vTaskSuspend() below runs, then suspended immediately -
|
||||
// the ledger must record the handle (set_task()) before the task can possibly observe or erase its own entry.
|
||||
// (see app_scheduler_stop()'s liveness check and app_task_main()'s exit path)
|
||||
BaseType_t create_result = xTaskCreate(app_task_main, task_name, 8192 / sizeof(StackType_t), context, tskIDLE_PRIORITY, &task_handle);
|
||||
if (create_result != pdPASS) {
|
||||
#ifdef ESP_PLATFORM
|
||||
TaskHandle_t task_handle = xTaskCreateStatic(app_task_main, task_name, effective_stack_depth, context, tskIDLE_PRIORITY, stack_buffer, task_tcb);
|
||||
#else
|
||||
TaskHandle_t task_handle = nullptr;
|
||||
if (xTaskCreate(app_task_main, task_name, effective_stack_depth, context, tskIDLE_PRIORITY, &task_handle) != pdPASS) {
|
||||
task_handle = nullptr;
|
||||
}
|
||||
#endif
|
||||
if (task_handle == nullptr) {
|
||||
delete context;
|
||||
#ifdef ESP_PLATFORM
|
||||
memory_free(task_tcb);
|
||||
memory_free(stack_buffer);
|
||||
#endif
|
||||
vSemaphoreDelete(completion->semaphore);
|
||||
delete completion;
|
||||
loader->unload(runtime);
|
||||
@@ -260,10 +387,17 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
|
||||
vTaskPrioritySet(task_handle, APP_TASK_PRIORITY);
|
||||
vTaskResume(task_handle);
|
||||
|
||||
memory_print_stats();
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t app_scheduler_stop(AppInstanceId app_instance_id, TickType_t join_timeout) {
|
||||
if (app_scheduler_current_app_id() == app_instance_id) {
|
||||
LOG_E(TAG, "Can't call app_scheduler_stop() from the owning task");
|
||||
return ERROR_NOT_ALLOWED;
|
||||
}
|
||||
|
||||
AppCompletionSignal* completion = acquire_completion_signal(app_instance_id);
|
||||
if (completion != nullptr) {
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <app/event.h>
|
||||
#include <app/install.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/metadata.h>
|
||||
#include <app/paths.h>
|
||||
#include <app/scheduler.h>
|
||||
@@ -21,7 +22,6 @@ const ModuleSymbol app_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(app_event_subscribe),
|
||||
DEFINE_MODULE_SYMBOL(app_event_subscribe_with_app_id),
|
||||
DEFINE_MODULE_SYMBOL(app_event_unsubscribe),
|
||||
DEFINE_MODULE_SYMBOL(app_event_emit),
|
||||
DEFINE_MODULE_SYMBOL(app_event_poll),
|
||||
// app/install
|
||||
DEFINE_MODULE_SYMBOL(app_get_install_path),
|
||||
@@ -42,6 +42,8 @@ const ModuleSymbol app_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(app_manager_install_path_add),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_install_path_scan),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_install_path_uninstall),
|
||||
// app/manifest
|
||||
DEFINE_MODULE_SYMBOL(app_id_is_valid),
|
||||
// app/metadata
|
||||
DEFINE_MODULE_SYMBOL(app_metadata_parse),
|
||||
// app/paths
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
// app_event_emit() is declared in app-module's private app/private/event.h (PRIV_INCLUDE_DIRS,
|
||||
// not exposed to this test target) - declared directly here, same as app_manager_test.cpp does
|
||||
// for app_internal_loader_service_manifest.
|
||||
extern "C" error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event);
|
||||
|
||||
TEST_CASE("app_event_subscribe/_poll deliver events in FIFO order") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
@@ -451,6 +451,42 @@ TEST_CASE("app_manager_get_topmost_instance_id returns NOT_FOUND when nothing is
|
||||
app_manager_remove("test.app.top_b");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start honors a custom AppManifest::stack.depth") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest manifest { "test.app.stack.custom", "Stack Custom", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
manifest.stack.depth = 4096;
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t instance_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.stack.custom", &instance_id), ERROR_NONE);
|
||||
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE);
|
||||
CHECK_EQ(app_manager_get_state(instance_id), APP_INSTANCE_STATE_STOPPED);
|
||||
|
||||
app_manager_remove("test.app.stack.custom");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start still works when AppManifest::stack is left at its zero-value default") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
// stack.depth == 0 - app_scheduler_start() must fall back to its own default stack depth
|
||||
// rather than fail or create a zero-sized stack.
|
||||
AppManifest manifest { "test.app.stack.default", "Stack Default", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(manifest.stack.depth, 0);
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t instance_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.stack.default", &instance_id), ERROR_NONE);
|
||||
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE);
|
||||
CHECK_EQ(app_manager_get_state(instance_id), APP_INSTANCE_STATE_STOPPED);
|
||||
|
||||
app_manager_remove("test.app.stack.default");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_get_topmost_app_id returns BUFFER_OVERFLOW for a too-small buffer, NOT_FOUND when nothing is active") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
#include <tactility/system_event.h>
|
||||
|
||||
// app_event_emit() is declared in app-module's private app/private/event.h (PRIV_INCLUDE_DIRS,
|
||||
// not exposed to this test target) - declared directly here, same as app_manager_test.cpp does
|
||||
// for app_internal_loader_service_manifest.
|
||||
extern "C" error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event);
|
||||
|
||||
// Regression coverage for the primary motivation behind TaskEventGroup: a task subscribed to
|
||||
// both an app_event and a system_event must be able to block once and wake for either, without
|
||||
// losing an event or regressing either subsystem's own delivery semantics (FIFO for app_event,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#define CRASH_DATA_CALLSTACK_LIMIT 64
|
||||
#define CRASH_DATA_INCLUDES_SP false
|
||||
#define CRASH_DATA_REASON_LENGTH 128
|
||||
|
||||
/** Represents a single frame on the callstack. */
|
||||
struct CallstackFrame {
|
||||
@@ -15,11 +16,25 @@ struct CallstackFrame {
|
||||
#endif
|
||||
};
|
||||
|
||||
/** Broad category of what caused the panic (mirrors ESP-IDF's panic_exception_t). */
|
||||
enum class CrashCause : uint8_t {
|
||||
Unknown,
|
||||
Debug,
|
||||
WatchdogInterrupt,
|
||||
WatchdogTask,
|
||||
Abort,
|
||||
Fault,
|
||||
};
|
||||
|
||||
/** Callstack-related crash data. */
|
||||
struct CrashData {
|
||||
bool callstackCorrupted = false;
|
||||
uint8_t callstackLength = 0;
|
||||
CallstackFrame callstack[CRASH_DATA_CALLSTACK_LIMIT];
|
||||
|
||||
CrashCause cause = CrashCause::Unknown;
|
||||
uint32_t faultAddress = 0;
|
||||
char reason[CRASH_DATA_REASON_LENGTH] = { 0 };
|
||||
};
|
||||
|
||||
/** @return the crash data */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <Tactility/PanicHandler.h>
|
||||
|
||||
std::string getUrlFromCrashData();
|
||||
std::string getUrlFromCrashData(const CrashData& data);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include <Tactility/DeprecatedPaths.h>
|
||||
|
||||
#include "../../Modules/app-module/private/app/private/app_metadata_parsing_internal.h"
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <Tactility/MountPoints.h>
|
||||
|
||||
@@ -72,12 +72,12 @@ std::string getUserHomePath() {
|
||||
}
|
||||
|
||||
std::string getAppInstallPath(const std::string& appId) {
|
||||
assert(app_metadata_is_valid_id(appId.c_str()));
|
||||
assert(app_id_is_valid(appId.c_str()));
|
||||
return std::format("{}/{}", getAppInstallPath(), appId);
|
||||
}
|
||||
|
||||
std::string getAppUserPath(const std::string& appId) {
|
||||
assert(app_metadata_is_valid_id(appId.c_str()));
|
||||
assert(app_id_is_valid(appId.c_str()));
|
||||
return std::format("{}/app/{}", getUserHomePath(), appId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
#if defined(ESP_PLATFORM)
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#if defined(ESP_PLATFORM) && defined(CONFIG_IDF_TARGET_ARCH_XTENSA)
|
||||
|
||||
#include "Tactility/kernel/PanicHandler.h"
|
||||
#include <Tactility/PanicHandler.h>
|
||||
|
||||
#include <esp_debug_helpers.h>
|
||||
#include <esp_attr.h>
|
||||
#include <esp_memory_utils.h>
|
||||
#include <esp_cpu.h>
|
||||
#include <esp_cpu_utils.h>
|
||||
#include <esp_debug_helpers.h>
|
||||
#include <esp_memory_utils.h>
|
||||
#include <esp_private/panic_internal.h>
|
||||
#include <xtensa/xtruntime.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
|
||||
/**
|
||||
@@ -28,7 +35,34 @@ void __wrap_esp_panic_handler(void* info) {
|
||||
.exc_frame = nullptr
|
||||
};
|
||||
|
||||
const auto* panic_info = static_cast<const panic_info_t*>(info);
|
||||
|
||||
switch (panic_info->exception) {
|
||||
// Watchdag timer issues are not consider real crashes: they trigger relatively often
|
||||
// and could cause a previous real crash to be overwritten by a watchdog timer warning during reboot.
|
||||
case PANIC_EXCEPTION_IWDT: crashData.cause = CrashCause::WatchdogInterrupt; return;
|
||||
case PANIC_EXCEPTION_TWDT: crashData.cause = CrashCause::WatchdogTask; return;
|
||||
// We also don't care about debugger errors:
|
||||
case PANIC_EXCEPTION_DEBUG: crashData.cause = CrashCause::Debug; return;
|
||||
// We only care about 'real' crashes:
|
||||
case PANIC_EXCEPTION_ABORT: crashData.cause = CrashCause::Abort; break;
|
||||
case PANIC_EXCEPTION_FAULT:
|
||||
default: crashData.cause = CrashCause::Fault; break;
|
||||
}
|
||||
|
||||
crashData.callstackLength = 0;
|
||||
crashData.faultAddress = reinterpret_cast<uint32_t>(panic_info->addr);
|
||||
|
||||
// g_panic_abort_details carries the actual assert()/abort() message when present; panic_info->reason
|
||||
// is ESP-IDF's generic description otherwise (e.g. "IllegalInstruction").
|
||||
const char* reason = (panic_info->exception == PANIC_EXCEPTION_ABORT && g_panic_abort_details != nullptr)
|
||||
? g_panic_abort_details
|
||||
: panic_info->reason;
|
||||
crashData.reason[0] = '\0';
|
||||
if (reason != nullptr) {
|
||||
strncpy(crashData.reason, reason, sizeof(crashData.reason) - 1);
|
||||
crashData.reason[sizeof(crashData.reason) - 1] = '\0';
|
||||
}
|
||||
|
||||
esp_backtrace_get_start(&frame.pc, &frame.sp, &frame.next_pc);
|
||||
crashData.callstack[0].pc = frame.pc;
|
||||
|
||||
@@ -355,12 +355,8 @@ static void stopAppFromToolbar(lv_event_t*) {
|
||||
// every not-yet-converted app's toolbar still relies on).
|
||||
AppInstanceId topmost = 0;
|
||||
check(app_manager_get_topmost_instance_id(&topmost) == ERROR_NONE);
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that
|
||||
// bound-waits (thread_join) for the app's own thread to finish, which needs the LVGL
|
||||
// lock to clean up - but this callback runs ON the LVGL task, which would deadlock
|
||||
// against itself.
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(topmost, &event);
|
||||
|
||||
app_event_emit_close(topmost);
|
||||
}
|
||||
|
||||
// The on-screen keyboard widget itself, constructed during windowManagerScreenInit
|
||||
|
||||
@@ -54,12 +54,7 @@ std::vector<std::string> getModelNames() {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onAddGpsPressed(lv_event_t* event) {
|
||||
|
||||
@@ -51,13 +51,7 @@ void onButtonPressed(lv_event_t* e) {
|
||||
auto* btnCtx = static_cast<ButtonContext*>(lv_event_get_user_data(e));
|
||||
LOG_I(TAG, "Selected item at index %d", (int)btnCtx->index);
|
||||
btnCtx->ctx->result = btnCtx->index;
|
||||
// Async, non-blocking - just wakes this dialog's own thread. Must NOT call
|
||||
// app_manager_stop() here: that bound-waits (thread_join) for the dialog's thread to
|
||||
// finish, which needs the LVGL lock (window_manager_remove()) - but this callback is
|
||||
// running ON the LVGL task, which would deadlock against itself. The caller reaps this
|
||||
// instance via app_manager_stop() after it receives the APP_EVENT_RESULT instead.
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(btnCtx->ctx->appInstanceId, &event);
|
||||
app_event_emit_close(btnCtx->ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createButton(Context* ctx, lv_obj_t* parent, const std::string& text, int32_t index) {
|
||||
|
||||
@@ -50,12 +50,7 @@ void onPressUninstall(lv_event_t* event) {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
@@ -51,8 +51,7 @@ void refresh(Context* ctx);
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
AppEvent closeEvent {.type = APP_EVENT_CLOSE, .timestamp = 0, .result = {}};
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onAppPressed(lv_event_t* e) {
|
||||
|
||||
@@ -63,8 +63,7 @@ uint32_t showConfirmDialog(Context* ctx, const char* action) {
|
||||
|
||||
void onBackPressed(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onInstallPressed(lv_event_t* e) {
|
||||
|
||||
@@ -20,7 +20,9 @@ namespace tt::app::applist {
|
||||
|
||||
namespace {
|
||||
|
||||
uint32_t appListInstanceId = 0;
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
};
|
||||
|
||||
void onAppPressed(lv_event_t* e) {
|
||||
// Fire-and-forget top-level navigation, same as Launcher's own app-launch buttons.
|
||||
@@ -29,15 +31,9 @@ void onAppPressed(lv_event_t* e) {
|
||||
app_manager_start(manifest->id, &instanceId);
|
||||
}
|
||||
|
||||
void onBackPressed(lv_event_t*) {
|
||||
// The global toolbar nav callback (ToolbarConfig.nav_action_callback, set once in
|
||||
// Tactility.cpp) only knows how to stop old-model apps, so this new-model app overrides
|
||||
// its own toolbar's nav action to close itself instead. Async, non-blocking - must NOT
|
||||
// call app_manager_stop() directly here: that bound-waits (thread_join) for this app's
|
||||
// own thread to finish, which needs the LVGL lock (window_manager_remove()) - but this
|
||||
// callback runs ON the LVGL task, which would deadlock against itself.
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(appListInstanceId, &event);
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createAppWidget(const ::AppManifest* manifest, lv_obj_t* list) {
|
||||
@@ -54,9 +50,11 @@ void collectManifest(const ::AppManifest* manifest, void* context) {
|
||||
manifests->push_back(manifest);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void*) {
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
auto* toolbar = lvgl_toolbar_create(parent, "Apps");
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, nullptr);
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
lv_obj_t* list = lv_list_create(parent);
|
||||
@@ -83,7 +81,7 @@ void createWidgets(lv_obj_t* parent, void*) {
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
uint32_t appInstanceId = app_scheduler_current_app_id();
|
||||
appListInstanceId = appInstanceId;
|
||||
Context ctx { appInstanceId };
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
@@ -91,7 +89,7 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
AppEventSubscription sub {};
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||
|
||||
while (true) {
|
||||
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||
@@ -119,8 +117,9 @@ extern const ::AppManifest manifest = {
|
||||
.id = "tactility.applist",
|
||||
.name = "Apps",
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN,
|
||||
.stack = { .depth = 2400, .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -24,21 +24,18 @@ extern const ::AppManifest manifest;
|
||||
|
||||
namespace {
|
||||
|
||||
// Set by appMain() right before window_manager_create(), read by onBackPressed().
|
||||
uint32_t appSettingsInstanceId = 0;
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
};
|
||||
|
||||
void onAppPressed(lv_event_t* e) {
|
||||
const auto* target_manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
|
||||
appdetails::start(target_manifest->id);
|
||||
}
|
||||
|
||||
void onBackPressed(lv_event_t*) {
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(appSettingsInstanceId, &event);
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createAppWidget(const ::AppManifest* target_manifest, lv_obj_t* list) {
|
||||
@@ -55,10 +52,12 @@ void collectManifest(const ::AppManifest* manifest, void* context) {
|
||||
manifests->push_back(manifest);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void*) {
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
auto* toolbar = lvgl_toolbar_create(parent, "Installed Apps");
|
||||
// The global toolbar nav callback only knows how to stop old-model apps.
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, nullptr);
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
lv_obj_t* list = lv_list_create(parent);
|
||||
@@ -92,7 +91,7 @@ void createWidgets(lv_obj_t* parent, void*) {
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
uint32_t appInstanceId = app_scheduler_current_app_id();
|
||||
appSettingsInstanceId = appInstanceId;
|
||||
Context ctx { appInstanceId };
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
@@ -100,7 +99,7 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
AppEventSubscription sub {};
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||
|
||||
bool shouldClose = false;
|
||||
while (!shouldClose) {
|
||||
@@ -132,7 +131,9 @@ extern const ::AppManifest manifest = {
|
||||
.id = "tactility.appsettings",
|
||||
.name = "Apps",
|
||||
.category = APP_CATEGORY_SETTINGS,
|
||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = 0,
|
||||
.stack = { .depth = 2400, .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -37,12 +37,7 @@ struct Context {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
@@ -37,12 +37,7 @@ struct Context {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onInputEnabledSwitch(lv_event_t* event) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "tactility/memory.h"
|
||||
#include "tactility/system_event.h"
|
||||
|
||||
#include <tactility/check.h>
|
||||
@@ -274,16 +275,27 @@ void runBootSequence(TickType_t startTime) {
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!setupUsbBootMode()) {
|
||||
registerApps();
|
||||
waitForMinimalSplashDuration(startTime);
|
||||
startNextApp();
|
||||
if (setupUsbBootMode()) {
|
||||
// Stay open: the splash's "Return to OS" button is this app's only way to leave mass
|
||||
// storage mode, so it must not self-close here like the normal boot path does below.
|
||||
return;
|
||||
}
|
||||
|
||||
registerApps();
|
||||
waitForMinimalSplashDuration(startTime);
|
||||
startNextApp();
|
||||
|
||||
if (sdCardMissing) {
|
||||
// Stay open: the error screen's "Reboot" button is this app's only way to leave here.
|
||||
return;
|
||||
}
|
||||
|
||||
// This event will likely block as other systems are initialized
|
||||
// e.g. Wi-Fi reads AP configs from SD card
|
||||
LOG_I(TAG, "Publish event");
|
||||
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
|
||||
|
||||
app_event_emit_close(app_scheduler_current_app_id());
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
@@ -337,8 +349,9 @@ extern const ::AppManifest manifest = {
|
||||
.id = "tactility.boot",
|
||||
.name = "Boot",
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN,
|
||||
.stack = { .depth = 4096, .desired_memory_capability = MEMORY_CAPABILITY_INTERNAL }
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -133,8 +133,7 @@ void onBtEvent(Context* ctx, const BtEvent& event) {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
@@ -20,12 +20,7 @@ namespace tt::app::btmanage {
|
||||
|
||||
static void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
static void onEnableSwitchChanged(lv_event_t* event) {
|
||||
|
||||
@@ -93,12 +93,7 @@ void onPressForget(lv_event_t* event) {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onToggleAutoConnect(lv_event_t* event) {
|
||||
|
||||
@@ -148,12 +148,7 @@ void ChatView::createChannelPanel(lv_obj_t* parent) {
|
||||
|
||||
void ChatView::onBackPressed(lv_event_t* e) {
|
||||
auto* self = static_cast<ChatView*>(lv_event_get_user_data(e));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(self->app->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(self->app->appInstanceId);
|
||||
}
|
||||
|
||||
void ChatView::init(lv_obj_t* parent) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "Tactility/PanicHandler.h"
|
||||
|
||||
|
||||
#include <Tactility/app/crashdiagnostics/QrHelpers.h>
|
||||
#include <Tactility/app/crashdiagnostics/QrUrl.h>
|
||||
#include <Tactility/app/launcher/Launcher.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/lvgl/Statusbar.h>
|
||||
|
||||
#include <app/event.h>
|
||||
@@ -17,8 +21,19 @@
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/paths.h>
|
||||
|
||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||
#include <esp_cpu_utils.h>
|
||||
#else
|
||||
#include <esp_cpu.h>
|
||||
#endif
|
||||
|
||||
#include <sdkconfig.h>
|
||||
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
|
||||
namespace tt::app::crashdiagnostics {
|
||||
|
||||
@@ -41,16 +56,70 @@ struct Context {
|
||||
};
|
||||
|
||||
|
||||
const char* crashCauseToString(CrashCause cause) {
|
||||
switch (cause) {
|
||||
case CrashCause::Debug: return "Debug";
|
||||
case CrashCause::WatchdogInterrupt: return "Watchdog (interrupt)";
|
||||
case CrashCause::WatchdogTask: return "Watchdog (task)";
|
||||
case CrashCause::Abort: return "Abort";
|
||||
case CrashCause::Fault: return "Fault";
|
||||
case CrashCause::Unknown:
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string formatCrashData(const CrashData& crashData) {
|
||||
std::stringstream stream;
|
||||
|
||||
stream << "Cause: " << crashCauseToString(crashData.cause) << "\n";
|
||||
|
||||
stream << "Reason: ";
|
||||
if (crashData.reason[0] != '\0') {
|
||||
stream << crashData.reason;
|
||||
} else {
|
||||
stream << "unknown";
|
||||
}
|
||||
stream << "\n";
|
||||
|
||||
stream << "Fault address: " << std::hex << std::setw(8) << std::setfill('0') << crashData.faultAddress << std::dec << "\n";
|
||||
|
||||
stream << "Callstack" << (crashData.callstackCorrupted ? " (corrupted)" : "") << ":";
|
||||
if (crashData.callstackLength > 0) {
|
||||
stream << "\n";
|
||||
for (uint8_t i = 0; i < crashData.callstackLength; i++) {
|
||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||
uint32_t pc = esp_cpu_process_stack_pc(crashData.callstack[i].pc);
|
||||
#else
|
||||
uint32_t pc = crashData.callstack[i].pc; // No processing needed on RISC-V
|
||||
#endif
|
||||
stream << std::hex << std::setw(8) << std::setfill('0') << pc << std::dec << " ";
|
||||
}
|
||||
} else {
|
||||
stream << " empty" << "\n";
|
||||
}
|
||||
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
// Best-effort: crash.txt is a convenience for offline inspection, not required for the app to work.
|
||||
void writeCrashLogFile(const CrashData& crashData) {
|
||||
char root[128];
|
||||
if (paths_get_data_path(root, sizeof(root)) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to resolve data path for crash.txt");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string path = std::string(root) + "/crash.txt";
|
||||
file::FileMutexGuard guard(path);
|
||||
if (!file::writeString(path, formatCrashData(crashData))) {
|
||||
LOG_E(TAG, "Failed to write %s", path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void onContinuePressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
ctx->continuePressed = true;
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself. launcher::start() is deferred to appMain(), after this app's
|
||||
// own thread has finished cleaning up.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
@@ -72,7 +141,9 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
}
|
||||
lv_obj_align(bottom_label, LV_ALIGN_BOTTOM_MID, 0, -2);
|
||||
|
||||
std::string url = getUrlFromCrashData();
|
||||
const auto& crash_data = getRtcCrashData();
|
||||
|
||||
std::string url = getUrlFromCrashData(crash_data);
|
||||
LOG_I(TAG, "%s", url.c_str());
|
||||
size_t url_length = url.length();
|
||||
|
||||
@@ -156,6 +227,8 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
Context ctx {};
|
||||
ctx.appInstanceId = appInstanceId;
|
||||
|
||||
writeCrashLogFile(getRtcCrashData());
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
@@ -207,7 +280,7 @@ extern const ::AppManifest manifest = {
|
||||
.id = "tactility.crashdiagnostics",
|
||||
.name = "Crash Diagnostics",
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/app/crashdiagnostics/QrUrl.h>
|
||||
#include <Tactility/PanicHandler.h>
|
||||
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
@@ -14,8 +13,7 @@
|
||||
|
||||
#include <sdkconfig.h>
|
||||
|
||||
std::string getUrlFromCrashData() {
|
||||
auto crash_data = getRtcCrashData();
|
||||
std::string getUrlFromCrashData(const CrashData& crash_data) {
|
||||
std::vector<uint32_t> stack_buffer(crash_data.callstackLength * 2);
|
||||
for (int i = 0; i < crash_data.callstackLength; ++i) {
|
||||
const CallstackFrame&frame = crash_data.callstack[i];
|
||||
|
||||
@@ -46,12 +46,7 @@ void updateViewState(Context* ctx);
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onEnableSwitchChanged(lv_event_t* event) {
|
||||
|
||||
@@ -61,12 +61,7 @@ Device* getBacklightDevice() {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onBacklightSliderEvent(lv_event_t* event) {
|
||||
|
||||
@@ -376,12 +376,7 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
|
||||
}
|
||||
|
||||
void View::onBackPressed() {
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(appInstanceId, &event);
|
||||
app_event_emit_close(appInstanceId);
|
||||
}
|
||||
|
||||
void View::onNavigateUpPressed() {
|
||||
|
||||
@@ -62,8 +62,7 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
// app_manager_stop() after it receives the APP_EVENT_RESULT instead.
|
||||
lastPath = path;
|
||||
ctx.result = 0;
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(appInstanceId, &closeEvent);
|
||||
app_event_emit_close(appInstanceId);
|
||||
});
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
|
||||
@@ -41,12 +41,7 @@ static void onNavigateUpPressedCallback(lv_event_t* event) {
|
||||
|
||||
void View::onBackPressedCallback(lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(view->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(view->appInstanceId);
|
||||
}
|
||||
|
||||
void View::onTapFile(const std::string& path, const std::string& filename) {
|
||||
|
||||
@@ -64,12 +64,7 @@ void createWidgets(lv_obj_t* parent, void* userData);
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onAddGpsPressed(lv_event_t* event) {
|
||||
|
||||
@@ -45,12 +45,7 @@ void onModeChanged(lv_event_t* e) {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
@@ -284,12 +284,7 @@ void selectBus(Context* ctx, int32_t selected) {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onSelectBus(lv_event_t* event) {
|
||||
|
||||
@@ -32,12 +32,7 @@ struct Context {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
@@ -59,10 +59,7 @@ void onButtonPressed(lv_event_t* e) {
|
||||
LOG_I(TAG, "Cancel pressed");
|
||||
btnCtx->ctx->result = 1;
|
||||
}
|
||||
// Async, non-blocking - see AlertDialog.cpp's onButtonPressed() for why this must not
|
||||
// call app_manager_stop() directly (would deadlock against the LVGL lock).
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(btnCtx->ctx->appInstanceId, &event);
|
||||
app_event_emit_close(btnCtx->ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createButton(Context* ctx, lv_obj_t* parent, const std::string& text, lv_obj_t* textarea) {
|
||||
|
||||
@@ -59,12 +59,7 @@ struct Context {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onBacklightSwitch(lv_event_t* e) {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#include "tactility/drivers/pointer.h"
|
||||
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
@@ -17,8 +14,10 @@
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/drivers/power_supply.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/memory.h>
|
||||
|
||||
#include <Tactility/app/setup/Setup.h>
|
||||
#include <Tactility/settings/BootSettings.h>
|
||||
@@ -273,8 +272,10 @@ extern const ::AppManifest manifest = {
|
||||
.id = "tactility.launcher",
|
||||
.name = "Launcher",
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN,
|
||||
// No file IO, so callstack can be in external RAM
|
||||
.stack = { .depth = 3072 , .desired_memory_capability = MEMORY_CAPABILITY_EXTERNAL }
|
||||
};
|
||||
|
||||
// Kept for Tactility/Private/Tactility/app/launcher/Launcher.h's existing declaration (still
|
||||
|
||||
@@ -99,12 +99,7 @@ void onLanguageSet(lv_event_t* event) {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
@@ -104,12 +104,7 @@ void updateUi(Context* ctx) {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onPowerEnabledChanged(lv_event_t* event) {
|
||||
|
||||
@@ -99,12 +99,7 @@ void onYesPressed(lv_event_t* /*event*/) {
|
||||
|
||||
void onNoPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
@@ -66,12 +66,7 @@ void updateScreenshotMode(Context* ctx) {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onStartPressed(lv_event_t* event) {
|
||||
|
||||
@@ -47,13 +47,7 @@ void onItemSelected(lv_event_t* e) {
|
||||
auto* itemCtx = static_cast<ItemContext*>(lv_event_get_user_data(e));
|
||||
LOG_I(TAG, "Selected item at index %d", (int)itemCtx->index);
|
||||
itemCtx->ctx->result = itemCtx->index;
|
||||
// Async, non-blocking - just wakes this dialog's own thread. Must NOT call
|
||||
// app_manager_stop() here: that bound-waits (thread_join) for the dialog's thread to
|
||||
// finish, which needs the LVGL lock (window_manager_remove()) - but this callback is
|
||||
// running ON the LVGL task, which would deadlock against itself. The caller reaps this
|
||||
// instance via app_manager_stop() after it receives the APP_EVENT_RESULT instead.
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(itemCtx->ctx->appInstanceId, &event);
|
||||
app_event_emit_close(itemCtx->ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createChoiceItem(Context* ctx, lv_obj_t* list, const std::string& title, int32_t index) {
|
||||
@@ -67,8 +61,7 @@ void createChoiceItem(Context* ctx, lv_obj_t* list, const std::string& title, in
|
||||
// mirrors the original's 0-items (error) and 1-item (auto-select) shortcuts.
|
||||
void closeWithResult(Context* ctx, int32_t result) {
|
||||
ctx->result = result;
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &event);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
@@ -20,7 +20,9 @@ namespace tt::app::settings {
|
||||
|
||||
namespace {
|
||||
|
||||
uint32_t settingsInstanceId = 0;
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
};
|
||||
|
||||
void onAppPressed(lv_event_t* e) {
|
||||
// Fire-and-forget top-level navigation, same as AppList's own app-launch buttons.
|
||||
@@ -29,13 +31,9 @@ void onAppPressed(lv_event_t* e) {
|
||||
app_manager_start(manifest->id, &instanceId);
|
||||
}
|
||||
|
||||
void onBackPressed(lv_event_t*) {
|
||||
// The global toolbar nav callback only knows how to stop old-model apps, so this
|
||||
// new-model app overrides its own toolbar's nav action to close itself instead. Async,
|
||||
// non-blocking - see AppList.cpp's onBackPressed() for why this must not call
|
||||
// app_manager_stop() directly (would deadlock against the LVGL lock).
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(settingsInstanceId, &event);
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidget(const ::AppManifest* manifest, lv_obj_t* list) {
|
||||
@@ -53,12 +51,14 @@ void collectManifest(const ::AppManifest* manifest, void* context) {
|
||||
manifests->push_back(manifest);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void*) {
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* toolbar = lvgl_toolbar_create(parent, "Settings");
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, nullptr);
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
auto* list = lv_list_create(parent);
|
||||
lv_obj_set_width(list, LV_PCT(100));
|
||||
@@ -79,7 +79,7 @@ void createWidgets(lv_obj_t* parent, void*) {
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
uint32_t appInstanceId = app_scheduler_current_app_id();
|
||||
settingsInstanceId = appInstanceId;
|
||||
Context ctx { appInstanceId };
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
@@ -87,7 +87,7 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
AppEventSubscription sub {};
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||
|
||||
while (true) {
|
||||
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||
@@ -115,8 +115,9 @@ extern const ::AppManifest manifest = {
|
||||
.id = "tactility.settings",
|
||||
.name = "Settings",
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN,
|
||||
.stack = { .depth = 2400, .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -159,11 +159,7 @@ void onContinueClicked(lv_event_t* event) {
|
||||
break;
|
||||
case Phase::Done: {
|
||||
markCompleted();
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: this
|
||||
// callback runs ON the LVGL task, and app-lifecycle transitions must happen on this
|
||||
// app's own thread (woken via app_event_poll()), which closes by returning.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,12 +305,7 @@ void updateTasks(Context* ctx) {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
@@ -34,12 +34,7 @@ struct Context {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onTimeFormatChanged(lv_event_t* event) {
|
||||
|
||||
@@ -109,8 +109,7 @@ void createListItem(Context* ctx, lv_obj_t* list, const std::string& title, size
|
||||
lastCode = entry.code;
|
||||
|
||||
ctx->result = 0; // Ok
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}, LV_EVENT_SHORT_CLICKED, buttonCtx);
|
||||
}
|
||||
|
||||
@@ -186,12 +185,7 @@ void updateList(Context* ctx) {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
@@ -174,12 +174,7 @@ void onPress(lv_event_t* event) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: this callback runs
|
||||
// ON the LVGL task, and app-lifecycle transitions must happen on this app's own thread
|
||||
// (woken up via app_event_poll() below), which closes by returning. The result (Ok/Error)
|
||||
// is reported by appMain() itself when it returns, based on ctx.calibrationApplied.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
@@ -72,12 +72,7 @@ struct Context {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void applyLive(Context* ctx) {
|
||||
|
||||
@@ -27,12 +27,7 @@ struct Context {
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onRebootMassStorageSdmmc(lv_event_t* event) {
|
||||
|
||||
@@ -54,12 +54,7 @@ void createWidgets(lv_obj_t* parent, void* userData);
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onWifiModeChanged(lv_event_t* e) {
|
||||
|
||||
@@ -47,11 +47,7 @@ void updateViews(Context* ctx);
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// but this callback runs ON the LVGL task, which would deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onPressForget(lv_event_t* event) {
|
||||
|
||||
@@ -60,11 +60,7 @@ void setLoading(Context* ctx, bool loading);
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// but this callback runs ON the LVGL task, which would deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onWifiEvent(Context* ctx, WifiEvent event) {
|
||||
@@ -89,8 +85,7 @@ void onWifiEvent(Context* ctx, WifiEvent event) {
|
||||
lvgl_unlock();
|
||||
|
||||
if (shouldClose) {
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(ctx->appInstanceId, &closeEvent);
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <Tactility/app/wifimanage/View.h>
|
||||
#include <Tactility/app/wifimanage/WifiManagePrivate.h>
|
||||
#include <Tactility/lvgl/Style.h>
|
||||
#include <Tactility/lvgl/Toolbar.h>
|
||||
#include <Tactility/service/wifi/Wifi.h>
|
||||
#include <Tactility/service/wifi/WifiSettings.h>
|
||||
#include <Tactility/Tactility.h>
|
||||
@@ -23,12 +22,7 @@ constexpr auto* TAG = "WifiManageView";
|
||||
|
||||
static void onBackPressed(lv_event_t* event) {
|
||||
auto* appInstanceId = static_cast<uint32_t*>(lv_event_get_user_data(event));
|
||||
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
|
||||
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
|
||||
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
|
||||
// deadlock against itself.
|
||||
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(*appInstanceId, &closeEvent);
|
||||
app_event_emit_close(*appInstanceId);
|
||||
}
|
||||
|
||||
static uint8_t mapRssiToPercentage(int rssi) {
|
||||
@@ -40,7 +34,7 @@ static uint8_t mapRssiToPercentage(int rssi) {
|
||||
}
|
||||
|
||||
auto percentage = (float)(90U - abs_rssi) / 60.f * 100.f;
|
||||
return (uint8_t)percentage;
|
||||
return static_cast<uint8_t>(percentage);
|
||||
}
|
||||
|
||||
static void onEnableSwitchChanged(lv_event_t* event) {
|
||||
|
||||
@@ -50,7 +50,7 @@ Thread* thread_alloc(void);
|
||||
/**
|
||||
* @brief Creates a new thread instance with specified parameters.
|
||||
* @param[in] name The name of the thread.
|
||||
* @param[in] stack_size The size of the thread stack in bytes.
|
||||
* @param[in] stack_size The size of the task stack in bytes.
|
||||
* @param[in] function The main function to be executed by the thread.
|
||||
* @param[in] function_context A pointer to the context to be passed to the main function.
|
||||
* @param[in] affinity The CPU core affinity for the thread (e.g., tskNO_AFFINITY).
|
||||
@@ -58,7 +58,7 @@ Thread* thread_alloc(void);
|
||||
*/
|
||||
Thread* thread_alloc_full(
|
||||
const char* name,
|
||||
configSTACK_DEPTH_TYPE stack_size,
|
||||
size_t stack_size,
|
||||
thread_main_fn_t function,
|
||||
void* function_context,
|
||||
portBASE_TYPE affinity
|
||||
@@ -82,7 +82,7 @@ void thread_set_name(Thread* thread, const char* name);
|
||||
/**
|
||||
* @brief Sets the stack size for the thread.
|
||||
* @param[in] thread The thread instance.
|
||||
* @param[in] stack_size The stack size in bytes. Must be a multiple of 4.
|
||||
* @param[in] stack_size The stack size in bytes, must be a multiple of StackType_t, which varies per platform.
|
||||
* @note Can only be called when the thread is in the STOPPED state.
|
||||
*/
|
||||
void thread_set_stack_size(Thread* thread, size_t stack_size);
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
@@ -23,9 +25,9 @@ struct Thread {
|
||||
thread_state_callback_t stateCallback = nullptr;
|
||||
void* stateCallbackContext = nullptr;
|
||||
std::string name = "unnamed";
|
||||
enum ThreadPriority priority = THREAD_PRIORITY_NORMAL;
|
||||
struct Mutex mutex = { 0 };
|
||||
configSTACK_DEPTH_TYPE stackSize = 4096;
|
||||
ThreadPriority priority = THREAD_PRIORITY_NORMAL;
|
||||
Mutex mutex = { 0 };
|
||||
configSTACK_DEPTH_TYPE stackDepth = 4096;
|
||||
portBASE_TYPE affinity = -1;
|
||||
|
||||
Thread() {
|
||||
@@ -94,7 +96,7 @@ Thread* thread_alloc(void) {
|
||||
|
||||
Thread* thread_alloc_full(
|
||||
const char* name,
|
||||
configSTACK_DEPTH_TYPE stack_size,
|
||||
size_t stack_size,
|
||||
thread_main_fn_t function,
|
||||
void* function_context,
|
||||
portBASE_TYPE affinity
|
||||
@@ -128,7 +130,8 @@ void thread_set_stack_size(Thread* thread, size_t stack_size) {
|
||||
thread->lock();
|
||||
check(stack_size > 0);
|
||||
check(thread->state == THREAD_STATE_STOPPED);
|
||||
thread->stackSize = stack_size;
|
||||
|
||||
thread->stackDepth = stack_size / sizeof(StackType_t);
|
||||
thread->unlock();
|
||||
}
|
||||
|
||||
@@ -176,14 +179,13 @@ error_t thread_start(Thread* thread) {
|
||||
thread->lock();
|
||||
check(thread->mainFunction != nullptr);
|
||||
check(thread->state == THREAD_STATE_STOPPED);
|
||||
check(thread->stackSize);
|
||||
check(thread->stackDepth);
|
||||
thread->unlock();
|
||||
|
||||
thread_set_state_internal(thread, THREAD_STATE_STARTING);
|
||||
|
||||
thread->lock();
|
||||
uint32_t stack_depth = thread->stackSize / sizeof(StackType_t);
|
||||
enum ThreadPriority priority = thread->priority;
|
||||
auto priority = static_cast<UBaseType_t>(thread->priority);
|
||||
portBASE_TYPE affinity = thread->affinity;
|
||||
thread->unlock();
|
||||
|
||||
@@ -193,9 +195,9 @@ error_t thread_start(Thread* thread) {
|
||||
result = xTaskCreatePinnedToCore(
|
||||
thread_main_body,
|
||||
thread->name.c_str(),
|
||||
stack_depth,
|
||||
thread->stackDepth,
|
||||
thread,
|
||||
(UBaseType_t)priority,
|
||||
priority,
|
||||
&thread->taskHandle,
|
||||
affinity
|
||||
);
|
||||
@@ -203,9 +205,9 @@ error_t thread_start(Thread* thread) {
|
||||
result = xTaskCreate(
|
||||
thread_main_body,
|
||||
thread->name.c_str(),
|
||||
stack_depth,
|
||||
thread->stackDepth,
|
||||
thread,
|
||||
(UBaseType_t)priority,
|
||||
priority,
|
||||
&thread->taskHandle
|
||||
);
|
||||
#endif
|
||||
@@ -213,9 +215,9 @@ error_t thread_start(Thread* thread) {
|
||||
result = xTaskCreate(
|
||||
thread_main_body,
|
||||
thread->name.c_str(),
|
||||
stack_depth,
|
||||
thread->stackDepth,
|
||||
thread,
|
||||
(UBaseType_t)priority,
|
||||
priority,
|
||||
&thread->taskHandle
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,20 +34,25 @@ void* memory_alloc_with_policy(size_t size, const struct MemoryPolicy* policy) {
|
||||
uint32_t required_caps = toHeapCaps(policy->required);
|
||||
uint32_t desired_caps = toHeapCaps(policy->desired);
|
||||
|
||||
// heap_caps matches heaps via (heap->caps[prio] & caps) != 0 - a caps value of 0 (e.g.
|
||||
// required_caps when policy->required wasn't set) can never match any heap, so the fallback
|
||||
// must OR in MALLOC_CAP_DEFAULT to actually reach a general-purpose heap, same as ESP-IDF's
|
||||
// own heap_caps_malloc_default() does.
|
||||
// heap_caps_match() tests (heap->caps & caps) == caps - a caps value of 0 is trivially true
|
||||
// for every heap, not none, so an unconstrained request must be steered to MALLOC_CAP_DEFAULT
|
||||
// explicitly (same as ESP-IDF's own heap_caps_malloc_default()) or it can land on a heap
|
||||
// that's unsuitable for the caller's actual use (e.g. not valid as a FreeRTOS task stack).
|
||||
uint32_t combined_caps = required_caps | desired_caps;
|
||||
if (combined_caps == 0) {
|
||||
combined_caps = MALLOC_CAP_DEFAULT;
|
||||
}
|
||||
|
||||
void* ptr;
|
||||
if (policy->alignment > 0) {
|
||||
ptr = heap_caps_aligned_alloc(policy->alignment, size, required_caps | desired_caps);
|
||||
ptr = heap_caps_aligned_alloc(policy->alignment, size, combined_caps);
|
||||
if (ptr == nullptr && desired_caps != 0) {
|
||||
// Desired caps couldn't be satisfied alongside the required ones - retry with
|
||||
// required only, since desired is explicitly optional.
|
||||
ptr = heap_caps_aligned_alloc(policy->alignment, size, required_caps | MALLOC_CAP_DEFAULT);
|
||||
}
|
||||
} else {
|
||||
ptr = heap_caps_malloc(size, required_caps | desired_caps);
|
||||
ptr = heap_caps_malloc(size, combined_caps);
|
||||
if (ptr == nullptr && desired_caps != 0) {
|
||||
ptr = heap_caps_malloc(size, required_caps | MALLOC_CAP_DEFAULT);
|
||||
}
|
||||
@@ -59,9 +64,14 @@ void* memory_realloc_with_policy(void* ptr, size_t size, const struct MemoryPoli
|
||||
uint32_t required_caps = toHeapCaps(policy->required);
|
||||
uint32_t desired_caps = toHeapCaps(policy->desired);
|
||||
|
||||
uint32_t combined_caps = required_caps | desired_caps;
|
||||
if (combined_caps == 0) {
|
||||
combined_caps = MALLOC_CAP_DEFAULT;
|
||||
}
|
||||
|
||||
// No aligned-realloc counterpart in the heap_caps API - policy->alignment is only honored
|
||||
// on fresh allocations (memory_alloc_with_policy/memory_calloc_with_policy).
|
||||
void* result = heap_caps_realloc(ptr, size, required_caps | desired_caps);
|
||||
void* result = heap_caps_realloc(ptr, size, combined_caps);
|
||||
if (result == nullptr && desired_caps != 0) {
|
||||
result = heap_caps_realloc(ptr, size, required_caps | MALLOC_CAP_DEFAULT);
|
||||
}
|
||||
@@ -72,14 +82,19 @@ void* memory_calloc_with_policy(size_t count, size_t size, const struct MemoryPo
|
||||
uint32_t required_caps = toHeapCaps(policy->required);
|
||||
uint32_t desired_caps = toHeapCaps(policy->desired);
|
||||
|
||||
uint32_t combined_caps = required_caps | desired_caps;
|
||||
if (combined_caps == 0) {
|
||||
combined_caps = MALLOC_CAP_DEFAULT;
|
||||
}
|
||||
|
||||
void* ptr;
|
||||
if (policy->alignment > 0) {
|
||||
ptr = heap_caps_aligned_calloc(policy->alignment, count, size, required_caps | desired_caps);
|
||||
ptr = heap_caps_aligned_calloc(policy->alignment, count, size, combined_caps);
|
||||
if (ptr == nullptr && desired_caps != 0) {
|
||||
ptr = heap_caps_aligned_calloc(policy->alignment, count, size, required_caps | MALLOC_CAP_DEFAULT);
|
||||
}
|
||||
} else {
|
||||
ptr = heap_caps_calloc(count, size, required_caps | desired_caps);
|
||||
ptr = heap_caps_calloc(count, size, combined_caps);
|
||||
if (ptr == nullptr && desired_caps != 0) {
|
||||
ptr = heap_caps_calloc(count, size, required_caps | MALLOC_CAP_DEFAULT);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user