Improve Terminal, App List, app starting, and more (#658)

app-module:

- Implement `AppStartContext` to reduce amount of `app_start_` and `app_exec` functions: replace them with `app_start_with_context()`
- Add support for environment variables when starting apps
- Implement support for environment variables for apps

graphics-module:

- Add this new module
- Move font-module code into this module
- Implement pixel_buffer

apps:

- Terminal app: reduce memory usage by only buffering a single row of characters, and try to buffer in IRAM when possible. Implement new pixel_buffer.
- App List: Mark apps as favourite to pin them on top. Use touch or press "x" key.
- App List: Add toolbar button that shows alert dialog with info about using favourites
This commit is contained in:
Ken Van Hoeylandt 2026-09-24 00:44:28 +02:00 committed by GitHub
parent 4279a8ed72
commit 86010186d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
101 changed files with 2317 additions and 583 deletions

View File

@ -7,3 +7,4 @@ http-module
lvgl-module
lvgl-window-manager-module
service-module
graphics-module

View File

@ -129,9 +129,9 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Modules/cjson-module)
add_subdirectory(Modules/cpp-symbols-module)
add_subdirectory(Modules/crypt-module)
add_subdirectory(Modules/font-module)
add_subdirectory(Modules/freertos-module)
add_subdirectory(Modules/gps-module)
add_subdirectory(Modules/graphics-module)
add_subdirectory(Modules/http-module)
add_subdirectory(Modules/mbedtls-module)
add_subdirectory(Modules/posix-symbols-module)

View File

@ -9,8 +9,8 @@
## Higher Priority
- App List: Close when app was started
- App List: Support marking apps as favourite
- DisplayApi::get_frame_buffer should be nullable and have a default behaviour of assigning NULL to the output pointer.
get_frame_buffer_count should be NULLable too, document it explicitly
- Shell interpreter works recursive-descent algorithm, which requires a big stack size. Can we optimize this?
- CrashDiagnostics shouldn't show a QR when there's no callstack
- 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.

View File

@ -260,7 +260,7 @@ static uint8_t esp_epaper_get_frame_buffer_count(Device*) {
}
static const DisplayApi esp_epaper_display_api = {
.capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH,
.capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH | DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME,
.reset = esp_epaper_reset,
.init = esp_epaper_init,
.draw_bitmap = esp_epaper_draw_bitmap,

View File

@ -26,7 +26,7 @@ constexpr auto* TAG = "GDEQ031T10";
#define GET_CONFIG(device) (static_cast<const Gdeq031t10Config*>((device)->config))
/** Capabilities every instance has; see gdeq031t10_has_capability() for the per-board ones. */
#define GDEQ031T10_STATIC_CAPABILITIES (DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH)
#define GDEQ031T10_STATIC_CAPABILITIES (DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH | DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME)
static constexpr int WIDTH = 240;
static constexpr int HEIGHT = 320;

View File

@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#include <stdbool.h>
/**
* Per-app-instance environment variables, as "NAME=VALUE" strings scoped to the calling app instance.
* Seeded from AppStartContext's own `environment` (app/start.h) at start, and mutable at runtime.
* Every function here acts on app_scheduler_current_app_id()'s own instance; there is no way to read
* or change another instance's environment.
*
* Matches POSIX semantics.
*/
#ifdef __cplusplus
extern "C" {
#endif
/**
* Sets @a name to @a value in the calling app instance's environment, adding it if not already present.
* @param[in] name must be non-empty and not contain '='
* @param[in] overwrite if false and @a name is already set, the existing value is left untouched
* (not an error)
* @retval ERROR_INVALID_ARGUMENT @a name or @a value is NULL, @a name is empty, or @a name
* contains '='
* @retval ERROR_NOT_FOUND the calling task isn't a running app instance
* @retval ERROR_NONE on success
*/
error_t app_env_set(const char* name, const char* value, bool overwrite);
/**
* Removes @a name from the calling app instance's environment, if present. Not an error if it wasn't set.
* @param[in] name must be non-empty and not contain '='
* @retval ERROR_INVALID_ARGUMENT @a name is NULL, empty, or contains '='
* @retval ERROR_NOT_FOUND the calling task isn't a running app instance
* @retval ERROR_NONE on success
*/
error_t app_env_unset(const char* name);
/**
* @param[in] name must be non-empty and not contain '='
* @return the value of @a name in the calling app instance's environment, or NULL if unset, @a name is invalid,
* or the calling task isn't a running app instance.
* @warning As with POSIX getenv(), the returned pointer is only valid until the next app_env_*
* call on this instance (from any task); copy it if it needs to outlive that.
*/
const char* app_env_get(const char* name);
/**
* Sets an environment variable for the calling app instance from a single "NAME=VALUE" string.
* @param[in] string must contain '=' with a non-empty name before it
* @retval ERROR_INVALID_ARGUMENT @a string is NULL, has no '=', or the name before it is empty
* @retval ERROR_NOT_FOUND the calling task isn't a running app instance
* @retval ERROR_NONE on success
*/
error_t app_env_put(const char* string);
#ifdef __cplusplus
}
#endif

View File

@ -25,6 +25,7 @@ extern "C" {
* @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type
* @retval ERROR_NONE on success
*/
[[deprecated("Use app_start_with_context()")]]
error_t app_execute(
struct AppLocation location,
struct AppStackConfig stack,
@ -39,6 +40,7 @@ error_t app_execute(
* @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type
* @retval ERROR_NONE on success
*/
[[deprecated("Use app_start_with_context()")]]
error_t app_execute_for_result(
struct AppLocation location,
struct AppStackConfig stack,
@ -57,6 +59,7 @@ error_t app_execute_for_result(
* @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim
* @retval ERROR_NONE on success
*/
[[deprecated("Use app_start_with_context()")]]
error_t app_execute_with_streams(
struct AppLocation location,
struct AppStackConfig stack,
@ -75,6 +78,7 @@ error_t app_execute_with_streams(
* @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim
* @retval ERROR_NONE on success
*/
[[deprecated("Use app_start_with_context()")]]
error_t app_execute_for_result_with_streams(
struct AppLocation location,
struct AppStackConfig stack,

View File

@ -4,14 +4,87 @@
#include <app/manager.h>
/**
* This file contains functions to start and run apps that were registered to the app manager.
* It differs from execute.h which runs executables from a specific path.
* This file contains functions to start and run apps.
* When an AppManifest is provided, it can start apps that were registered with the app manager.
* It can also start app binaries directly.
*
* Steps:
* - Create an AppStartContext by using one of the helper functions:
* - app_start_context_for_manifest() for manager-registered apps
* - app_start_context_for_location() for plain binaries
* - Optionally modify AppStartContext with with one of the helper functions. (e.g. to add parameters)
* - Call app_start_with_context() to start the execution.
*/
#ifdef __cplusplus
extern "C" {
#endif
/**
* Identifies what app_manager_start_internal() should launch and how.
* Construct via app_start_context_from_manifest()/app_start_context_from_location()
* @warning Fields are for internal use only. Do not read or write them directly.
*/
struct AppStartContext {
const struct AppManifest* manifest;
struct AppLocation location;
struct AppStackConfig stack;
int argc;
const char* const* argv;
const struct AppStreamBinding* bindings;
size_t binding_count;
AppInstanceId parent_id;
/** NULL-terminated array of "KEY=VALUE" strings, like POSIX environ. */
char* const* environment;
};
/** Builds a context for starting a manifest already registered via app_manager_add(). */
struct AppStartContext app_start_context_for_manifest(const struct AppManifest* manifest);
/** Builds a context for starting @a location directly, with no registered manifest.
* Stack defaults to zeroed (the scheduler's default). */
struct AppStartContext app_start_context_for_location(struct AppLocation location);
/**
* Looks up @a id in the manifest registry and builds a context for it (see
* app_start_context_from_manifest()).
* @retval ERROR_NOT_FOUND no manifest with this id is registered
* @retval ERROR_NONE on success
*/
error_t app_start_context_from_id(const char* id, struct AppStartContext* out_context);
/** Overrides the task's stack allocation config (defaults to the manifest's own, or zeroed/
* scheduler-default for a location-based context). See AppStackConfig. */
void app_start_context_set_stack(struct AppStartContext* context, struct AppStackConfig stack);
/** @param[in] argv @a argc strings, borrowed only until app_manager_start_internal() returns. It makes its own deep copy. */
void app_start_context_set_arguments_ext(struct AppStartContext* context, int argc, const char* const argv[]);
/** @param[in] arguments null-terminated string array, borrowed only until app_manager_start_internal() returns. It makes its own deep copy. */
void app_start_context_set_arguments(struct AppStartContext* context, const char* const arguments[]);
/** @param[in] bindings @a binding_count entries; see app_start_with_streams() for ownership. */
void app_start_context_set_streams(struct AppStartContext* context, const struct AppStreamBinding* bindings, size_t binding_count);
/** See app_start_for_result() for the parent/result-delivery contract. */
void app_start_context_set_parent(struct AppStartContext* context, AppInstanceId parent_id);
/** @param[in] environment NULL-terminated array of "KEY=VALUE" strings, borrowed only until
* app_start_with_context() returns. */
void app_start_context_set_environment(struct AppStartContext* context, char* const environment[]);
/**
* Starts an app from @a context, built via app_start_context_for_manifest()/
* app_start_context_for_location()/app_start_context_from_id() and app_start_context_set_*().
* Replaces app_start()/app_start_for_result()/app_start_with_streams()/
* app_start_for_result_with_streams().
* @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a context->location.type
* @retval ERROR_OUT_OF_RANGE a binding's producer_fd is out of range
* @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim
* @retval ERROR_NONE on success
*/
error_t app_start_with_context(struct AppStartContext* context, AppInstanceId* out_app_instance_id);
/**
* Starts @a id, a manifest already registered via app_manager_add(), passing @a argc/@a argv to
* the new instance's own main function (see app/loader.h's AppMainFn), modelled on a C program's
@ -23,6 +96,7 @@ extern "C" {
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
* @retval ERROR_NONE on success
*/
[[deprecated("Use app_start_with_context()")]]
error_t app_start(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
/**
@ -40,6 +114,7 @@ error_t app_start(const char* id, int argc, const char* const argv[], AppInstanc
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
* @retval ERROR_NONE on success
*/
[[deprecated("Use app_start_with_context()")]]
error_t app_start_for_result(const char* id, int argc, const char* const argv[], AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id);
/**
@ -54,6 +129,7 @@ error_t app_start_for_result(const char* id, int argc, const char* const argv[],
* @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim
* @retval ERROR_NONE on success
*/
[[deprecated("Use app_start_with_context()")]]
error_t app_start_with_streams(const char* id, const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id);
/**
@ -69,6 +145,7 @@ error_t app_start_with_streams(const char* id, const struct AppStreamBinding* bi
* @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim
* @retval ERROR_NONE on success
*/
[[deprecated("Use app_start_with_context()")]]
error_t app_start_for_result_with_streams(const char* id, int argc, const char* const argv[], const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id);
#ifdef __cplusplus

View File

@ -5,8 +5,10 @@
#include <new>
/**
* Deep-copies @a argv (@a argc <= 0 => NULL, matching "no parameters"). Caller passes the result
* to app_scheduler_start(), which takes ownership regardless of outcome.
* Deep-copies @a argv (@a argc <= 0 => NULL, matching "no parameters"). Used internally by
* app_scheduler_start(), which takes ownership of the result regardless of outcome (freed via
* app_arguments_free() once the spawned task's run() returns, or immediately on a failure to
* start it).
* @return NULL if @a argc <= 0 (no parameters), or if allocation failed. For @a argc > 0,
* these are the only cases that produce NULL, so a caller can tell them apart by its own
* already-known @a argc: NULL back from a positive @a argc always means allocation failed.
@ -58,3 +60,33 @@ inline void app_arguments_free(int count, char** values) {
}
delete[] values;
}
/**
* Counts entries in a NULL-terminated array, e.g. TaskContext::argv/env (see app_arguments_copy(),
* which always NULL-terminates its result) or an AppStartContext's own argv/environment before a
* caller-given count is known.
*/
inline int app_arguments_count_null_terminated(const char* const* values) {
int count = 0;
if (values != nullptr) {
while (values[count] != nullptr) {
count++;
}
}
return count;
}
/**
* Same as app_arguments_free(), for a deep copy whose count wasn't kept around separately (e.g.
* TaskContext::env) - walks until the NULL terminator app_arguments_copy() always leaves in
* place, instead of taking an explicit count. Safe to call with values == nullptr (no-op).
*/
inline void app_arguments_free_null_terminated(char** values) {
if (values == nullptr) {
return;
}
for (char** p = values; *p != nullptr; p++) {
delete[] *p;
}
delete[] values;
}

View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <string>
#include <vector>
/**
* Applies @a overlay (a NULL-terminated array of "NAME=VALUE" strings, e.g.
* AppStartContext::environment) onto @a env in place: each entry either replaces the existing
* same-name entry in @a env or is appended. Used by app_manager_start_internal() to combine a new
* app instance's inherited-from-parent environment (the base) with its own explicit environment
* (the overlay).
*/
void app_env_apply(std::vector<std::string>& env, const char* const* overlay);

View File

@ -59,6 +59,11 @@ struct AppInstanceRecord {
/** This instance's fd table. Constructed by app_manager_start_internal() before insertion
* into AppLedger::instances, torn down (every open fd closed) when the instance's task exits. */
AppFdTable fd_table {};
/** This instance's environment, as "NAME=VALUE" strings - seeded from AppStartContext's own
* `environment` by app_manager_start_internal(), then mutable at runtime via app_env_*()
* (app/env.h). Protected by AppLedger::mutex, same as every other field here. */
std::vector<std::string> env {};
};
/** A registered installed package - see app_manager_add_package() (app/manager.h). */

View File

@ -2,6 +2,7 @@
#pragma once
#include <app/manager.h>
#include <app/start.h>
#ifdef __cplusplus
extern "C" {
@ -9,16 +10,16 @@ extern "C" {
/**
* Shared core behind every app_start*() (app/manager.h) and app_execute*()
* (app/execute.h) entry point: deep-copies @a argv, allocates an instance id, installs
* @a bindings into the new instance's fd table before app_scheduler_start() is called, then
* starts it. @a manifest may be NULL for a location-based start with no manifest at all.
* @retval ERROR_INVALID_ARGUMENT @a binding_count is nonzero but @a bindings is NULL
* @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type
* (app/execute.h) entry point: allocates an instance id, installs @a context->bindings into the
* new instance's fd table, then starts it via app_scheduler_start() (which deep-copies @a
* context->argv itself). @a context->manifest may be NULL for a location-based start with no manifest.
* @retval ERROR_INVALID_ARGUMENT @a context->binding_count is nonzero but @a context->bindings is NULL
* @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a context->location.type
* @retval ERROR_OUT_OF_RANGE a binding's producer_fd is out of range
* @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim
* @retval ERROR_NONE on success
*/
error_t app_manager_start_internal(const struct AppManifest* manifest, struct AppLocation location, struct AppStackConfig stack, AppInstanceId parent_instance_id, int argc, const char* const argv[], const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id);
error_t app_manager_start_internal(const struct AppStartContext* context, AppInstanceId* out_app_instance_id);
#ifdef __cplusplus
}

View File

@ -2,6 +2,7 @@
#pragma once
#include <app/manifest.h>
#include <app/start.h>
#include <tactility/error.h>
@ -20,14 +21,11 @@ extern "C" {
* Loads and starts an app instance: spawns a dedicated task that calls
* 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)
* @param[in] context describes what to start and how (location/stack/argc/argv). @a
* context->argv is deep-copied internally (see app_arguments_copy()), so it and the strings it
* points to may be freed/go out of scope immediately after this call returns.
*/
error_t app_scheduler_start(AppInstanceId app_instance_id, struct AppLocation location, struct AppStackConfig stack, int argc, char* argv[]);
error_t app_scheduler_start(AppInstanceId app_instance_id, const struct AppStartContext* context);
/**
* Permanently stops an app instance (APP_EVENT_CLOSE if it was running), bound-waits for its

View File

@ -0,0 +1,134 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/env.h>
#include <app/private/env_internal.h>
#include <app/private/ledger.h>
#include <app/scheduler.h>
#include <tactility/concurrent/mutex.h>
#include <algorithm>
#include <cstring>
#include <string>
#include <vector>
namespace {
bool is_valid_name(const char* name) {
return name != nullptr && name[0] != '\0' && strchr(name, '=') == nullptr;
}
// "NAME=..." prefix match, not just a prefix: requires the '=' right after it, so "FOO" doesn't match "FOOBAR=x".
bool entry_matches_name(const std::string& entry, const char* name, size_t name_len) {
return entry.size() > name_len && entry[name_len] == '=' && entry.compare(0, name_len, name) == 0;
}
// Finds the calling app instance's environment under the ledger lock.
// On a non-NULL return, the lock is left held for the caller to keep mutating/reading it,
// and must be released via mutex_unlock(&app_ledger().mutex) before returning to app_env_*()'s own caller.
std::vector<std::string>* lock_current_env() {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_scheduler_current_app_id());
if (iterator == ledger.instances.end()) {
mutex_unlock(&ledger.mutex);
return nullptr;
}
return &iterator->second.env;
}
std::vector<std::string>::iterator find_entry(std::vector<std::string>& env, const char* name, size_t name_len) {
return std::find_if(env.begin(), env.end(), [&](const std::string& entry) {
return entry_matches_name(entry, name, name_len);
});
}
} // namespace
void app_env_apply(std::vector<std::string>& env, const char* const* overlay) {
for (int i = 0; overlay != nullptr && overlay[i] != nullptr; i++) {
const char* separator = strchr(overlay[i], '=');
if (separator == nullptr || separator == overlay[i]) {
continue; // matches app_env_put()'s own rejection of a nameless/equals-less entry
}
const size_t name_len = static_cast<size_t>(separator - overlay[i]);
const std::string name(overlay[i], name_len);
auto iterator = find_entry(env, name.c_str(), name_len);
if (iterator == env.end()) {
env.emplace_back(overlay[i]);
} else {
*iterator = overlay[i];
}
}
}
extern "C" {
error_t app_env_set(const char* name, const char* value, bool overwrite) {
if (!is_valid_name(name) || value == nullptr) {
return ERROR_INVALID_ARGUMENT;
}
std::vector<std::string>* env = lock_current_env();
if (env == nullptr) {
return ERROR_NOT_FOUND;
}
const size_t name_len = strlen(name);
auto iterator = find_entry(*env, name, name_len);
if (iterator == env->end()) {
env->emplace_back(std::string(name) + "=" + value);
} else if (overwrite) {
*iterator = std::string(name) + "=" + value;
}
mutex_unlock(&app_ledger().mutex);
return ERROR_NONE;
}
error_t app_env_unset(const char* name) {
if (!is_valid_name(name)) {
return ERROR_INVALID_ARGUMENT;
}
std::vector<std::string>* env = lock_current_env();
if (env == nullptr) {
return ERROR_NOT_FOUND;
}
auto iterator = find_entry(*env, name, strlen(name));
if (iterator != env->end()) {
env->erase(iterator);
}
mutex_unlock(&app_ledger().mutex);
return ERROR_NONE;
}
const char* app_env_get(const char* name) {
if (!is_valid_name(name)) {
return nullptr;
}
std::vector<std::string>* env = lock_current_env();
if (env == nullptr) {
return nullptr;
}
const size_t name_len = strlen(name);
auto iterator = find_entry(*env, name, name_len);
const char* result = iterator != env->end() ? iterator->c_str() + name_len + 1 : nullptr;
mutex_unlock(&app_ledger().mutex);
return result;
}
error_t app_env_put(const char* string) {
if (string == nullptr) {
return ERROR_INVALID_ARGUMENT;
}
const char* separator = strchr(string, '=');
if (separator == nullptr || separator == string) {
return ERROR_INVALID_ARGUMENT;
}
const std::string name(string, separator - string);
return app_env_set(name.c_str(), separator + 1, true);
}
} // extern "C"

View File

@ -3,6 +3,7 @@
#include <app/loader.h>
#include <app/location.h>
#include <app/start.h>
#include <app/private/manager_internal.h>
#include <service/instance.h>
@ -29,19 +30,35 @@ const AppLoaderApi* find_loader_api(AppLocationType type) {
extern "C" {
error_t app_execute(AppLocation location, AppStackConfig stack, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) {
return app_manager_start_internal(nullptr, location, stack, 0, argc, argv, nullptr, 0, out_app_instance_id);
AppStartContext context = app_start_context_for_location(location);
context.stack = stack;
app_start_context_set_arguments_ext(&context, argc, argv);
return app_manager_start_internal(&context, out_app_instance_id);
}
error_t app_execute_for_result(AppLocation location, AppStackConfig stack, int argc, const char* const argv[], AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id) {
return app_manager_start_internal(nullptr, location, stack, parent_instance_id, argc, argv, nullptr, 0, out_app_instance_id);
AppStartContext context = app_start_context_for_location(location);
context.stack = stack;
app_start_context_set_arguments_ext(&context, argc, argv);
app_start_context_set_parent(&context, parent_instance_id);
return app_manager_start_internal(&context, out_app_instance_id);
}
error_t app_execute_with_streams(AppLocation location, AppStackConfig stack, int argc, const char* const argv[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) {
return app_manager_start_internal(nullptr, location, stack, 0, argc, argv, bindings, binding_count, out_app_instance_id);
AppStartContext context = app_start_context_for_location(location);
context.stack = stack;
app_start_context_set_arguments_ext(&context, argc, argv);
app_start_context_set_streams(&context, bindings, binding_count);
return app_manager_start_internal(&context, out_app_instance_id);
}
error_t app_execute_for_result_with_streams(AppLocation location, AppStackConfig stack, int argc, const char* const argv[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id) {
return app_manager_start_internal(nullptr, location, stack, parent_instance_id, argc, argv, bindings, binding_count, out_app_instance_id);
AppStartContext context = app_start_context_for_location(location);
context.stack = stack;
app_start_context_set_arguments_ext(&context, argc, argv);
app_start_context_set_streams(&context, bindings, binding_count);
app_start_context_set_parent(&context, parent_instance_id);
return app_manager_start_internal(&context, out_app_instance_id);
}
bool app_is_executable_path(const char* path) {

View File

@ -1,8 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manager.h>
#include <app/package_manifest.h>
#include <app/private/arguments.h>
#include <app/private/binary_path.h>
#include <app/private/env_internal.h>
#include <app/private/fd_table.h>
#include <app/private/fs.h>
#include <app/private/ledger.h>
@ -137,14 +137,13 @@ void app_manager_for_each_package(AppPackageVisitorFn visitor, void* context) {
mutex_unlock(&ledger.mutex);
}
error_t app_manager_start_internal(const AppManifest* manifest, AppLocation location, AppStackConfig stack, AppInstanceId parent_instance_id, int argc, const char* const argv_in[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) {
char** argv = app_arguments_copy(argc, argv_in);
if (argc > 0 && argv == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
error_t app_manager_start_internal(const AppStartContext* context, AppInstanceId* out_app_instance_id) {
const AppManifest* manifest = context->manifest;
const AppStreamBinding* bindings = context->bindings;
size_t binding_count = context->binding_count;
AppInstanceId parent_instance_id = context->parent_id;
if (binding_count != 0 && bindings == nullptr) {
app_arguments_free(argc, argv);
return ERROR_INVALID_ARGUMENT;
}
@ -152,8 +151,22 @@ error_t app_manager_start_internal(const AppManifest* manifest, AppLocation loca
mutex_lock(&ledger.mutex);
AppInstanceId target_id = ledger.next_instance_id++;
AppInstanceRecord record { .id = target_id, .manifest = manifest, .state = APP_INSTANCE_STATE_STARTING, .task = nullptr };
AppInstanceRecord record {
.id = target_id,
.manifest = manifest,
.state = APP_INSTANCE_STATE_STARTING,
.task = nullptr,
};
record.parent_id = parent_instance_id;
// Inherit the parent environment
if (parent_instance_id != 0) {
auto parent_iterator = ledger.instances.find(parent_instance_id);
if (parent_iterator != ledger.instances.end()) {
record.env = parent_iterator->second.env;
}
}
app_env_apply(record.env, context->environment);
ledger.instances[target_id] = record;
// Construct on the map-resident copy, not `record`: fds[] point into slots[] by address
// (fd_table.h), so constructing on the stack-local record would leave them dangling.
@ -175,12 +188,11 @@ error_t app_manager_start_internal(const AppManifest* manifest, AppLocation loca
app_fd_table_teardown(&ledger.instances[target_id].fd_table);
ledger.instances.erase(target_id);
mutex_unlock(&ledger.mutex);
app_arguments_free(argc, argv);
return bind_result;
}
}
error_t error = app_scheduler_start(target_id, location, stack, argc, argv);
error_t error = app_scheduler_start(target_id, context);
if (error != ERROR_NONE) {
for (size_t j = 0; j < binding_count; j++) {
app_stream_unsubscribe(bindings[j].stream);

View File

@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/env.h>
#include <app/event.h>
#include <app/execute.h>
#include <app/install.h>
@ -21,6 +22,11 @@ extern "C" {
extern ServiceManifest app_internal_loader_service_manifest;
static const ModuleSymbol SYMBOLS[] = {
// app/env
DEFINE_MODULE_SYMBOL(app_env_set),
DEFINE_MODULE_SYMBOL(app_env_unset),
DEFINE_MODULE_SYMBOL(app_env_get),
DEFINE_MODULE_SYMBOL(app_env_put),
// app/event
DEFINE_MODULE_SYMBOL(app_event_subscribe),
DEFINE_MODULE_SYMBOL(app_event_subscribe_with_app_id),
@ -74,6 +80,16 @@ static const ModuleSymbol SYMBOLS[] = {
// app/scheduler
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id),
// app/start
DEFINE_MODULE_SYMBOL(app_start_context_for_manifest),
DEFINE_MODULE_SYMBOL(app_start_context_for_location),
DEFINE_MODULE_SYMBOL(app_start_context_from_id),
DEFINE_MODULE_SYMBOL(app_start_context_set_stack),
DEFINE_MODULE_SYMBOL(app_start_context_set_arguments_ext),
DEFINE_MODULE_SYMBOL(app_start_context_set_arguments),
DEFINE_MODULE_SYMBOL(app_start_context_set_streams),
DEFINE_MODULE_SYMBOL(app_start_context_set_parent),
DEFINE_MODULE_SYMBOL(app_start_context_set_environment),
DEFINE_MODULE_SYMBOL(app_start_with_context),
DEFINE_MODULE_SYMBOL(app_start),
DEFINE_MODULE_SYMBOL(app_start_for_result),
DEFINE_MODULE_SYMBOL(app_start_with_streams),

View File

@ -47,7 +47,7 @@ struct TaskContext {
const AppLoaderApi* loader;
void* runtime;
AppInstanceId app_instance_id;
int argc;
/** Nullable, null-terminated. */
char** argv;
AppCompletionSignal* completion;
StackType_t* stackBuffer;
@ -207,7 +207,7 @@ void app_task_main(void* context) {
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_ACTIVE);
int32_t result = ctx->loader->run(ctx->runtime, ctx->app_instance_id, ctx->argc, ctx->argv);
int32_t result = ctx->loader->run(ctx->runtime, ctx->app_instance_id, app_arguments_count_null_terminated(ctx->argv), ctx->argv);
// The platform might buffer stdout (e.g. esp-idf with newlib)
// Do a manual flush to ensure data has been written:
@ -224,7 +224,7 @@ void app_task_main(void* context) {
// response to APP_EVENT_CLOSE.
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_STOPPED);
app_arguments_free(ctx->argc, ctx->argv);
app_arguments_free_null_terminated(ctx->argv);
AppInstanceId app_instance_id = ctx->app_instance_id;
AppCompletionSignal* completion = ctx->completion;
@ -271,11 +271,14 @@ void app_task_main(void* context) {
extern "C" {
error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, AppStackConfig stack, int argc, char* argv[]) {
error_t app_scheduler_start(AppInstanceId app_instance_id, const AppStartContext* start_context) {
const AppLocation location = start_context->location;
const AppStackConfig stack = start_context->stack;
const int argc = start_context->argc;
const AppLoaderApi* loader = find_loader_api(location.type);
if (loader == nullptr) {
LOG_E(TAG, "[instance %lu] No app loader is registered (service '%s' not found)", app_instance_id, loader_service_id_for(location.type));
app_arguments_free(argc, argv);
return ERROR_NOT_FOUND;
}
@ -283,7 +286,6 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
error_t load_result = loader->load(location, &runtime);
if (load_result != ERROR_NONE) {
LOG_E(TAG, "[instance %lu] Failed to load app: %s", app_instance_id, error_to_string(load_result));
app_arguments_free(argc, argv);
return load_result;
}
@ -291,34 +293,31 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
if (completion == nullptr) {
LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
loader->unload(runtime);
app_arguments_free(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
completion->semaphore = xSemaphoreCreateBinary();
if (completion->semaphore == nullptr) {
LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
delete completion;
loader->unload(runtime);
app_arguments_free(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
// Same bound package_manifest_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_arguments_free(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;
char** argv = app_arguments_copy(argc, start_context->argv);
if (argc > 0 && argv == nullptr) {
LOG_W(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
vSemaphoreDelete(completion->semaphore);
delete completion;
loader->unload(runtime);
return ERROR_OUT_OF_MEMORY;
}
#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).
@ -332,7 +331,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
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);
LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
vSemaphoreDelete(completion->semaphore);
delete completion;
loader->unload(runtime);
@ -359,11 +358,10 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
.loader = loader,
.runtime = runtime,
.app_instance_id = app_instance_id,
.argc = argc,
.argv = argv,
.completion = completion,
.stackBuffer = stack_buffer,
.taskTcb = task_tcb
.taskTcb = task_tcb,
};
if (context == nullptr) {

View File

@ -1,17 +1,31 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/start.h>
#include <app/manifest.h>
#include <app/private/ledger.h>
#include <app/private/manager_internal.h>
#include <tactility/concurrent/mutex.h>
namespace {
extern "C" {
// Looks @a id up in the manifest registry, then delegates to app_manager_start_internal(). The
// only path that requires a registered manifest; app_execute*() (app/execute.h) bypasses this
// entirely.
error_t start_internal_by_id(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) {
AppStartContext app_start_context_for_manifest(const AppManifest* manifest) {
AppStartContext context = {};
context.manifest = manifest;
context.location = manifest->location;
context.stack = manifest->stack;
return context;
}
AppStartContext app_start_context_for_location(AppLocation location) {
AppStartContext context = {};
context.manifest = nullptr;
context.location = location;
context.stack = {};
return context;
}
error_t app_start_context_from_id(const char* id, AppStartContext* out_context) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
@ -23,27 +37,88 @@ error_t start_internal_by_id(const char* id, AppInstanceId parent_instance_id, i
const AppManifest* manifest = manifest_iterator->second;
mutex_unlock(&ledger.mutex);
return app_manager_start_internal(manifest, manifest->location, manifest->stack, parent_instance_id, argc, argv, bindings, binding_count, out_app_instance_id);
*out_context = app_start_context_for_manifest(manifest);
return ERROR_NONE;
}
} // namespace
void app_start_context_set_stack(AppStartContext* context, AppStackConfig stack) {
context->stack = stack;
}
extern "C" {
void app_start_context_set_arguments_ext(AppStartContext* context, int argc, const char* const argv[]) {
context->argc = argc;
context->argv = argv;
}
void app_start_context_set_arguments(AppStartContext* context, const char* const arguments[]) {
int argc = 0;
if (arguments != nullptr) {
while (arguments[argc] != nullptr) {
argc++;
}
}
context->argc = argc;
context->argv = arguments;
}
void app_start_context_set_streams(AppStartContext* context, const AppStreamBinding* bindings, size_t binding_count) {
context->bindings = bindings;
context->binding_count = binding_count;
}
void app_start_context_set_parent(AppStartContext* context, AppInstanceId parent_id) {
context->parent_id = parent_id;
}
void app_start_context_set_environment(AppStartContext* context, char* const environment[]) {
context->environment = environment;
}
error_t app_start_with_context(AppStartContext* context, AppInstanceId* out_app_instance_id) {
return app_manager_start_internal(context, out_app_instance_id);
}
error_t app_start(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) {
return start_internal_by_id(id, 0, argc, argv, nullptr, 0, out_app_instance_id);
AppStartContext context;
error_t lookup_result = app_start_context_from_id(id, &context);
if (lookup_result != ERROR_NONE) {
return lookup_result;
}
app_start_context_set_arguments_ext(&context, argc, argv);
return app_manager_start_internal(&context, out_app_instance_id);
}
error_t app_start_for_result(const char* id, int argc, const char* const argv[], AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id) {
return start_internal_by_id(id, parent_instance_id, argc, argv, nullptr, 0, out_app_instance_id);
AppStartContext context;
error_t lookup_result = app_start_context_from_id(id, &context);
if (lookup_result != ERROR_NONE) {
return lookup_result;
}
app_start_context_set_arguments_ext(&context, argc, argv);
app_start_context_set_parent(&context, parent_instance_id);
return app_manager_start_internal(&context, out_app_instance_id);
}
error_t app_start_with_streams(const char* id, const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) {
return start_internal_by_id(id, 0, 0, nullptr, bindings, binding_count, out_app_instance_id);
AppStartContext context;
error_t lookup_result = app_start_context_from_id(id, &context);
if (lookup_result != ERROR_NONE) {
return lookup_result;
}
app_start_context_set_streams(&context, bindings, binding_count);
return app_manager_start_internal(&context, out_app_instance_id);
}
error_t app_start_for_result_with_streams(const char* id, int argc, const char* const argv[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id) {
return start_internal_by_id(id, parent_instance_id, argc, argv, bindings, binding_count, out_app_instance_id);
AppStartContext context;
error_t lookup_result = app_start_context_from_id(id, &context);
if (lookup_result != ERROR_NONE) {
return lookup_result;
}
app_start_context_set_arguments_ext(&context, argc, argv);
app_start_context_set_streams(&context, bindings, binding_count);
app_start_context_set_parent(&context, parent_instance_id);
return app_manager_start_internal(&context, out_app_instance_id);
}
} // extern "C"

View File

@ -0,0 +1,314 @@
#include "doctest.h"
#include <app/env.h>
#include <app/event.h>
#include <app/loader.h>
#include <app/manager.h>
#include <app/private/env_internal.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <service/manager.h>
#include <tactility/delay.h>
#include <tactility/freertos/task.h>
#include <algorithm>
#include <atomic>
#include <cstring>
#include <string>
#include <vector>
extern ServiceManifest app_internal_loader_service_manifest;
namespace {
// See manager_test.cpp's own copy of this helper for why this checks the registry directly
// rather than a per-translation-unit static bool, and why APP_LOCATION_MEMORY (the real,
// already-shared app_internal_loader_service_manifest) is used here instead of a second
// competing fake APP_LOCATION_PATH loader.
void ensure_memory_loader_registered() {
if (service_manager_find_instance(APP_LOADER_MEMORY_SERVICE_ID) == nullptr) {
service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
}
}
// Set by env_test_app_main() (below) once it has finished exercising app_env_*() - a test thread
// waits on this before reading the results_* variables, since reaching
// APP_INSTANCE_STATE_ACTIVE only means the instance's task has started, not that
// env_test_app_main() has gotten this far.
std::atomic<bool> results_ready { false };
bool result_get_before_set_is_null;
bool result_set_returns_ok;
bool result_get_after_set_matches;
bool result_no_overwrite_returns_ok;
bool result_no_overwrite_value_unchanged;
bool result_overwrite_returns_ok;
bool result_overwrite_value_changed;
bool result_put_returns_ok;
bool result_get_after_put_matches;
bool result_unset_returns_ok;
bool result_get_after_unset_is_null;
bool result_seeded_value_matches;
// Runs entirely on the app's own task, so app_scheduler_current_app_id() (which every
// app_env_*() call resolves against) actually names this instance - exercising the same call
// path a real app's own main() would use, not the test runner's task.
int32_t env_test_app_main(int, char**) {
result_get_before_set_is_null = app_env_get("TEST_VAR") == nullptr;
result_set_returns_ok = app_env_set("TEST_VAR", "one", true) == ERROR_NONE;
const char* after_set = app_env_get("TEST_VAR");
result_get_after_set_matches = after_set != nullptr && strcmp(after_set, "one") == 0;
result_no_overwrite_returns_ok = app_env_set("TEST_VAR", "two", false) == ERROR_NONE;
const char* after_no_overwrite = app_env_get("TEST_VAR");
result_no_overwrite_value_unchanged = after_no_overwrite != nullptr && strcmp(after_no_overwrite, "one") == 0;
result_overwrite_returns_ok = app_env_set("TEST_VAR", "three", true) == ERROR_NONE;
const char* after_overwrite = app_env_get("TEST_VAR");
result_overwrite_value_changed = after_overwrite != nullptr && strcmp(after_overwrite, "three") == 0;
result_put_returns_ok = app_env_put("PUT_VAR=put_value") == ERROR_NONE;
const char* after_put = app_env_get("PUT_VAR");
result_get_after_put_matches = after_put != nullptr && strcmp(after_put, "put_value") == 0;
result_unset_returns_ok = app_env_unset("TEST_VAR") == ERROR_NONE;
result_get_after_unset_is_null = app_env_get("TEST_VAR") == nullptr;
const char* seeded = app_env_get("SEEDED_VAR");
result_seeded_value_matches = seeded != nullptr && strcmp(seeded, "seeded_value") == 0;
results_ready.store(true, std::memory_order_release);
// Same subscribe-until-close contract as every other fake app in this test suite.
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
app_event_subscribe(&sub, &event_group);
while (true) {
if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
break; // safety net so a bug here can't hang the test suite
}
bool done = false;
AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
done = true;
break;
}
}
if (done) break;
}
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}
// Set by env_child_main() (below) once it has checked its own inherited/overridden environment.
std::atomic<bool> child_results_ready { false };
bool result_child_inherited_var_matches;
bool result_child_override_var_matches;
bool result_child_only_var_matches;
// The child in the parent/child inheritance test below - checks what it sees, stashes the
// result, and returns immediately (no subscribe loop needed: nothing sends it APP_EVENT_CLOSE).
int32_t env_child_main(int, char**) {
const char* inherited = app_env_get("INHERITED_VAR");
result_child_inherited_var_matches = inherited != nullptr && strcmp(inherited, "parent_value") == 0;
const char* overridden = app_env_get("OVERRIDE_VAR");
result_child_override_var_matches = overridden != nullptr && strcmp(overridden, "child_value") == 0;
const char* child_only = app_env_get("CHILD_ONLY_VAR");
result_child_only_var_matches = child_only != nullptr && strcmp(child_only, "child_value") == 0;
child_results_ready.store(true, std::memory_order_release);
return 0;
}
// The parent in the parent/child inheritance test below: sets its own environment, then starts
// env_child_main() as a modal child (app_start_context_set_parent()) with its own explicit
// environment that overrides one of the parent's variables and adds a new one.
int32_t env_parent_main(int, char**) {
// Subscribed before the child is started, not after: env_child_main() can finish and set
// child_results_ready before this task gets back around to subscribing, so the test thread
// is free to call app_manager_stop() (APP_EVENT_CLOSE) the moment it observes that - which
// would otherwise have no subscriber to deliver to, leaving this task stuck in its wait loop
// until the stop call's own timeout.
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
app_event_subscribe(&sub, &event_group);
app_env_set("INHERITED_VAR", "parent_value", true);
app_env_set("OVERRIDE_VAR", "parent_value", true);
AppLocation child_location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(env_child_main) };
AppStartContext child_context = app_start_context_for_location(child_location);
const char* child_env[] = { "OVERRIDE_VAR=child_value", "CHILD_ONLY_VAR=child_value", nullptr };
app_start_context_set_environment(&child_context, const_cast<char**>(child_env));
app_start_context_set_parent(&child_context, app_scheduler_current_app_id());
uint32_t child_id = 0;
app_start_with_context(&child_context, &child_id);
// Same subscribe-until-close contract as every other fake app in this test suite; the test
// thread stops this (the parent) once it has observed child_results_ready.
while (true) {
if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
break; // safety net so a bug here can't hang the test suite
}
bool done = false;
AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
done = true;
break;
}
}
if (done) break;
}
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}
bool wait_for_state(uint32_t instance_id, AppInstanceState target, uint32_t timeout_ms) {
uint32_t waited = 0;
while (waited < timeout_ms) {
if (app_manager_get_state(instance_id) == target) {
return true;
}
delay_millis(10);
waited += 10;
}
return app_manager_get_state(instance_id) == target;
}
bool wait_for_results(uint32_t timeout_ms) {
uint32_t waited = 0;
while (waited < timeout_ms) {
if (results_ready.load(std::memory_order_acquire)) {
return true;
}
delay_millis(10);
waited += 10;
}
return results_ready.load(std::memory_order_acquire);
}
bool wait_for_child_results(uint32_t timeout_ms) {
uint32_t waited = 0;
while (waited < timeout_ms) {
if (child_results_ready.load(std::memory_order_acquire)) {
return true;
}
delay_millis(10);
waited += 10;
}
return child_results_ready.load(std::memory_order_acquire);
}
} // namespace
TEST_CASE("app_env_* operate on the calling app instance's own environment") {
ensure_memory_loader_registered();
AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(env_test_app_main) };
AppStartContext context = app_start_context_for_location(location);
const char* seeded_env[] = { "SEEDED_VAR=seeded_value", nullptr };
app_start_context_set_environment(&context, const_cast<char**>(seeded_env));
results_ready.store(false, std::memory_order_release);
uint32_t instance_id = 0;
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
REQUIRE(wait_for_results(1000));
CHECK(result_get_before_set_is_null);
CHECK(result_set_returns_ok);
CHECK(result_get_after_set_matches);
CHECK(result_no_overwrite_returns_ok);
CHECK(result_no_overwrite_value_unchanged);
CHECK(result_overwrite_returns_ok);
CHECK(result_overwrite_value_changed);
CHECK(result_put_returns_ok);
CHECK(result_get_after_put_matches);
CHECK(result_unset_returns_ok);
CHECK(result_get_after_unset_is_null);
CHECK(result_seeded_value_matches);
app_manager_stop(instance_id);
}
TEST_CASE("app_env_* reject invalid names/strings without touching any instance") {
CHECK_EQ(app_env_set(nullptr, "value", true), ERROR_INVALID_ARGUMENT);
CHECK_EQ(app_env_set("", "value", true), ERROR_INVALID_ARGUMENT);
CHECK_EQ(app_env_set("NAME=X", "value", true), ERROR_INVALID_ARGUMENT);
CHECK_EQ(app_env_set("NAME", nullptr, true), ERROR_INVALID_ARGUMENT);
CHECK_EQ(app_env_unset(nullptr), ERROR_INVALID_ARGUMENT);
CHECK_EQ(app_env_unset(""), ERROR_INVALID_ARGUMENT);
CHECK_EQ(app_env_unset("NAME=X"), ERROR_INVALID_ARGUMENT);
CHECK_EQ(app_env_get(nullptr), nullptr);
CHECK_EQ(app_env_get(""), nullptr);
CHECK_EQ(app_env_get("NAME=X"), nullptr);
CHECK_EQ(app_env_put(nullptr), ERROR_INVALID_ARGUMENT);
CHECK_EQ(app_env_put("NO_EQUALS_SIGN"), ERROR_INVALID_ARGUMENT);
CHECK_EQ(app_env_put("=NO_NAME"), ERROR_INVALID_ARGUMENT);
}
TEST_CASE("app_env_* report ERROR_NOT_FOUND when the calling task isn't a running app instance") {
CHECK_EQ(app_env_set("NAME", "value", true), ERROR_NOT_FOUND);
CHECK_EQ(app_env_unset("NAME"), ERROR_NOT_FOUND);
CHECK_EQ(app_env_get("NAME"), nullptr);
}
TEST_CASE("app_env_apply overlays NAME=VALUE entries, overriding existing names and appending new ones") {
std::vector<std::string> env = { "INHERITED=parent", "OVERRIDE=parent" };
const char* overlay[] = { "OVERRIDE=child", "NEW=child", nullptr };
app_env_apply(env, overlay);
CHECK_EQ(env.size(), 3);
CHECK(std::find(env.begin(), env.end(), "INHERITED=parent") != env.end());
CHECK(std::find(env.begin(), env.end(), "OVERRIDE=child") != env.end());
CHECK(std::find(env.begin(), env.end(), "NEW=child") != env.end());
}
TEST_CASE("app_env_apply is a no-op for a NULL overlay") {
std::vector<std::string> env = { "A=1" };
app_env_apply(env, nullptr);
CHECK_EQ(env.size(), 1);
CHECK_EQ(env[0], "A=1");
}
TEST_CASE("app_env_apply skips a malformed overlay entry") {
std::vector<std::string> env = { "A=1" };
const char* overlay[] = { "NOEQUALS", "=NONAME", nullptr };
app_env_apply(env, overlay);
CHECK_EQ(env.size(), 1);
}
TEST_CASE("a child started with a parent inherits the parent's environment, with its own overriding") {
ensure_memory_loader_registered();
AppLocation parent_location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(env_parent_main) };
AppStartContext parent_context = app_start_context_for_location(parent_location);
child_results_ready.store(false, std::memory_order_release);
uint32_t parent_id = 0;
REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE);
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
REQUIRE(wait_for_child_results(1000));
CHECK(result_child_inherited_var_matches);
CHECK(result_child_override_var_matches);
CHECK(result_child_only_var_matches);
app_manager_stop(parent_id);
}

View File

@ -141,7 +141,8 @@ TEST_CASE("app_execute runs a location with no manifest at all, and reports no t
AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(location_app_main) };
uint32_t instance_id = 0;
REQUIRE_EQ(app_execute(location, AppStackConfig {}, 0, nullptr, &instance_id), ERROR_NONE);
AppStartContext context = app_start_context_for_location(location);
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
CHECK_EQ(topmost_instance_id(), instance_id);
@ -158,13 +159,15 @@ TEST_CASE("app_execute doesn't disturb app_manager_get_topmost_app_id for a norm
AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(location_app_main) };
uint32_t unregistered_id = 0;
REQUIRE_EQ(app_execute(location, AppStackConfig {}, 0, nullptr, &unregistered_id), ERROR_NONE);
AppStartContext context = app_start_context_for_location(location);
REQUIRE_EQ(app_start_with_context(&context, &unregistered_id), ERROR_NONE);
CHECK(wait_for_state(unregistered_id, APP_INSTANCE_STATE_ACTIVE, 1000));
AppManifest manifest { "test.app.execute.after", "After", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(location_app_main) } };
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
uint32_t registered_id = 0;
REQUIRE_EQ(app_start("test.app.execute.after", 0, nullptr, &registered_id), ERROR_NONE);
AppStartContext registered_context = app_start_context_for_manifest(&manifest);
REQUIRE_EQ(app_start_with_context(&registered_context, &registered_id), ERROR_NONE);
CHECK(wait_for_state(registered_id, APP_INSTANCE_STATE_ACTIVE, 1000));
char buffer[64];
@ -183,7 +186,9 @@ TEST_CASE("app_execute_for_result delivers APP_EVENT_RESULT to the parent, with
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
uint32_t parent_id = 0;
REQUIRE_EQ(app_start("test.app.execute.parent", 0, nullptr, &parent_id), ERROR_NONE);
AppStartContext parent_context;
REQUIRE_EQ(app_start_context_from_id("test.app.execute.parent", &parent_context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE);
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
TaskEventGroup parent_event_group {};
@ -195,7 +200,10 @@ TEST_CASE("app_execute_for_result delivers APP_EVENT_RESULT to the parent, with
AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(location_app_main) };
const char* argv[] = { "42" }; // location_app_main's single-arg shortcut - returns 42 immediately
uint32_t child_id = 0;
REQUIRE_EQ(app_execute_for_result(location, AppStackConfig {}, 1, argv, parent_id, &child_id), ERROR_NONE);
AppStartContext context = app_start_context_for_location(location);
app_start_context_set_arguments_ext(&context, 1, argv);
app_start_context_set_parent(&context, parent_id);
REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE);
REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
AppEvent event {};
@ -224,7 +232,9 @@ TEST_CASE("app_execute_with_streams pipes a manifest-less child's app_io_write()
AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group };
AppInstanceId child_id = 0;
REQUIRE_EQ(app_execute_with_streams(location, AppStackConfig {}, 0, nullptr, &binding, 1, &child_id), ERROR_NONE);
AppStartContext context = app_start_context_for_location(location);
app_start_context_set_streams(&context, &binding, 1);
REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE);
std::vector<uint8_t> received;
while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) {
@ -258,7 +268,10 @@ TEST_CASE("app_execute_with_streams passes argv through to the started app") {
const char* argv[] = { "hello" };
AppInstanceId child_id = 0;
REQUIRE_EQ(app_execute_with_streams(location, AppStackConfig {}, 1, argv, &binding, 1, &child_id), ERROR_NONE);
AppStartContext context = app_start_context_for_location(location);
app_start_context_set_arguments_ext(&context, 1, argv);
app_start_context_set_streams(&context, &binding, 1);
REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE);
std::vector<uint8_t> received;
while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) {
@ -285,7 +298,9 @@ TEST_CASE("app_execute_for_result_with_streams delivers both the stream data and
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
uint32_t parent_id = 0;
REQUIRE_EQ(app_start("test.app.execute.parent_streams", 0, nullptr, &parent_id), ERROR_NONE);
AppStartContext parent_context;
REQUIRE_EQ(app_start_context_from_id("test.app.execute.parent_streams", &parent_context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE);
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
TaskEventGroup parent_event_group {};
@ -299,7 +314,10 @@ TEST_CASE("app_execute_for_result_with_streams delivers both the stream data and
AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(stream_writer_app_main) };
uint32_t child_id = 0;
REQUIRE_EQ(app_execute_for_result_with_streams(location, AppStackConfig {}, 0, nullptr, &binding, 1, parent_id, &child_id), ERROR_NONE);
AppStartContext context = app_start_context_for_location(location);
app_start_context_set_streams(&context, &binding, 1);
app_start_context_set_parent(&context, parent_id);
REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE);
std::vector<uint8_t> received;
while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) {
@ -335,7 +353,9 @@ TEST_CASE("app_execute_for_result_with_streams pipes a child's plain printf() ca
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
uint32_t parent_id = 0;
REQUIRE_EQ(app_start("test.app.execute.printf_parent", 0, nullptr, &parent_id), ERROR_NONE);
AppStartContext parent_context;
REQUIRE_EQ(app_start_context_from_id("test.app.execute.printf_parent", &parent_context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE);
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
TaskEventGroup parent_event_group {};
@ -349,7 +369,10 @@ TEST_CASE("app_execute_for_result_with_streams pipes a child's plain printf() ca
AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(printf_stream_writer_app_main) };
uint32_t child_id = 0;
REQUIRE_EQ(app_execute_for_result_with_streams(location, AppStackConfig {}, 0, nullptr, &binding, 1, parent_id, &child_id), ERROR_NONE);
AppStartContext context = app_start_context_for_location(location);
app_start_context_set_streams(&context, &binding, 1);
app_start_context_set_parent(&context, parent_id);
REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE);
std::vector<uint8_t> received;
while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) {

View File

@ -214,7 +214,9 @@ TEST_CASE("an app's stdio fds default to the null device: write succeeds and dis
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
AppInstanceId instance_id = 0;
REQUIRE_EQ(app_start("test.io.unbound", 0, nullptr, &instance_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.io.unbound", &context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 1000));
CHECK_EQ(g_stdio_write_result.load(std::memory_order_acquire), 1);
@ -237,7 +239,10 @@ TEST_CASE("app_start_with_streams pipes a child's app_io_write() calls into a pa
AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group };
AppInstanceId child_id = 0;
REQUIRE_EQ(app_start_with_streams("test.io.writer", &binding, 1, &child_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.io.writer", &context), ERROR_NONE);
app_start_context_set_streams(&context, &binding, 1);
REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE);
std::vector<uint8_t> received;
while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) {
@ -275,7 +280,10 @@ TEST_CASE("app_io_await blocks until the bound stream becomes readable or times
AppStream child_stdin {};
AppStreamBinding binding { STDIN_FILENO, &child_stdin, storage, sizeof(storage), &event_group };
AppInstanceId child_id = 0;
REQUIRE_EQ(app_start_with_streams("test.io.await", &binding, 1, &child_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.io.await", &context), ERROR_NONE);
app_start_context_set_streams(&context, &binding, 1);
REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE);
// Wait for the child's first await (50ms, on an empty stream) to actually return before
// writing: a fixed delay doesn't prove that, and under slow scheduling the write could land
@ -310,7 +318,10 @@ TEST_CASE("a write blocked on a full stream wakes with an error once the consume
AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group };
AppInstanceId child_id = 0;
REQUIRE_EQ(app_start_with_streams("test.io.blocked", &binding, 1, &child_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.io.blocked", &context), ERROR_NONE);
app_start_context_set_streams(&context, &binding, 1);
REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE);
// Never drained: the child fills the 4-byte buffer and blocks awaiting space for the rest.
delay_millis(200);
@ -346,7 +357,10 @@ TEST_CASE("app_stream_unsubscribe is safe to call while a write is actively bloc
AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group };
AppInstanceId child_id = 0;
REQUIRE_EQ(app_start_with_streams("test.io.unsub_race", &binding, 1, &child_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.io.unsub_race", &context), ERROR_NONE);
app_start_context_set_streams(&context, &binding, 1);
REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE);
// Give the child time to fill the 4-byte buffer and block inside app_io_write(), already
// dispatched through app_fd_table_get_and_retain() and currently waiting in
@ -377,7 +391,9 @@ TEST_CASE("app_io_read/write/close pass through a real file fd app-module never
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
AppInstanceId instance_id = 0;
REQUIRE_EQ(app_start("test.io.real_file", 0, nullptr, &instance_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.io.real_file", &context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 1000));
CHECK_EQ(g_real_file_write_result.load(std::memory_order_acquire), 2);
@ -400,7 +416,9 @@ TEST_CASE("app_io_bind_self installs a custom AppFileOps used by subsequent app_
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
AppInstanceId instance_id = 0;
REQUIRE_EQ(app_start("test.io.self_bind", 0, nullptr, &instance_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.io.self_bind", &context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 1000));
CHECK_EQ(g_self_bind_result.load(std::memory_order_acquire), ERROR_NONE);
@ -428,7 +446,9 @@ TEST_CASE("closing an already-closed app fd reports EBADF instead of falling thr
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
AppInstanceId instance_id = 0;
REQUIRE_EQ(app_start("test.io.double_close", 0, nullptr, &instance_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.io.double_close", &context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 1000));
CHECK_EQ(g_double_close_first_result.load(std::memory_order_acquire), 0);

View File

@ -185,7 +185,9 @@ TEST_CASE("app_start activates an app instance, app_manager_stop terminates it")
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
uint32_t instance_id = 0;
REQUIRE_EQ(app_start("test.app.a", 0, nullptr, &instance_id), ERROR_NONE);
AppStartContext context_a;
REQUIRE_EQ(app_start_context_from_id("test.app.a", &context_a), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context_a, &instance_id), ERROR_NONE);
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE);
@ -203,11 +205,15 @@ TEST_CASE("app_start never touches another already-running app - every instance
REQUIRE_EQ(app_manager_add(&manifest_c), ERROR_NONE);
uint32_t id_b = 0;
REQUIRE_EQ(app_start("test.app.b", 0, nullptr, &id_b), ERROR_NONE);
AppStartContext context_b;
REQUIRE_EQ(app_start_context_from_id("test.app.b", &context_b), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context_b, &id_b), ERROR_NONE);
CHECK(wait_for_state(id_b, APP_INSTANCE_STATE_ACTIVE, 1000));
uint32_t id_c = 0;
REQUIRE_EQ(app_start("test.app.c", 0, nullptr, &id_c), ERROR_NONE);
AppStartContext context_c;
REQUIRE_EQ(app_start_context_from_id("test.app.c", &context_c), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context_c, &id_c), ERROR_NONE);
CHECK(wait_for_state(id_c, APP_INSTANCE_STATE_ACTIVE, 1000));
// b is untouched by c starting - both stay Active at once, each with its own task.
@ -226,11 +232,15 @@ TEST_CASE("app_start always creates a fresh instance, even for the same manifest
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
uint32_t id_first = 0;
REQUIRE_EQ(app_start("test.app.twice", 0, nullptr, &id_first), ERROR_NONE);
AppStartContext context_first;
REQUIRE_EQ(app_start_context_from_id("test.app.twice", &context_first), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context_first, &id_first), ERROR_NONE);
CHECK(wait_for_state(id_first, APP_INSTANCE_STATE_ACTIVE, 1000));
uint32_t id_second = 0;
REQUIRE_EQ(app_start("test.app.twice", 0, nullptr, &id_second), ERROR_NONE);
AppStartContext context_second;
REQUIRE_EQ(app_start_context_from_id("test.app.twice", &context_second), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context_second, &id_second), ERROR_NONE);
CHECK(wait_for_state(id_second, APP_INSTANCE_STATE_ACTIVE, 1000));
CHECK_NE(id_first, id_second);
@ -259,7 +269,10 @@ TEST_CASE("app_start_with_parameters deep-copies argv before the app instance re
std::string ssid = "MyNetwork";
std::string password = "hunter2";
const char* argv[] = { ssid.c_str(), password.c_str() };
REQUIRE_EQ(app_start("test.app.args", 2, argv, &instance_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.app.args", &context), ERROR_NONE);
app_start_context_set_arguments_ext(&context, 2, argv);
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
}
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
REQUIRE(wait_for_arguments_stashed(1000));
@ -306,7 +319,8 @@ TEST_CASE("app_manager_for_each_manifest visits every registered manifest, inclu
TEST_CASE("app_start fails for an unregistered manifest id") {
uint32_t instance_id = 0;
CHECK_EQ(app_start("test.app.nonexistent", 0, nullptr, &instance_id), ERROR_NOT_FOUND);
AppStartContext context;
CHECK_EQ(app_start_context_from_id("test.app.nonexistent", &context), ERROR_NOT_FOUND);
}
TEST_CASE("app_start runs an APP_LOCATION_MEMORY app via its function pointer, through the real internal loader") {
@ -321,7 +335,9 @@ TEST_CASE("app_start runs an APP_LOCATION_MEMORY app via its function pointer, t
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
uint32_t instance_id = 0;
REQUIRE_EQ(app_start("test.app.memory", 0, nullptr, &instance_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.app.memory", &context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE);
@ -339,7 +355,9 @@ TEST_CASE("app_start_for_result delivers APP_EVENT_RESULT to the parent, which s
REQUIRE_EQ(app_manager_add(&child_manifest), ERROR_NONE);
uint32_t parent_id = 0;
REQUIRE_EQ(app_start("test.app.parent", 0, nullptr, &parent_id), ERROR_NONE);
AppStartContext parent_context;
REQUIRE_EQ(app_start_context_from_id("test.app.parent", &parent_context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE);
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
TaskEventGroup parent_event_group {};
@ -350,7 +368,11 @@ TEST_CASE("app_start_for_result delivers APP_EVENT_RESULT to the parent, which s
const char* argv[] = { "42" };
uint32_t child_id = 0;
REQUIRE_EQ(app_start_for_result("test.app.child", 1, argv, parent_id, &child_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.app.child", &context), ERROR_NONE);
app_start_context_set_arguments_ext(&context, 1, argv);
app_start_context_set_parent(&context, parent_id);
REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE);
// Launching a modal child never touches the parent's own task/state.
CHECK_EQ(app_manager_get_state(parent_id), APP_INSTANCE_STATE_ACTIVE);
@ -379,7 +401,9 @@ TEST_CASE("app_start_for_result delivers the child's own return value as the res
REQUIRE_EQ(app_manager_add(&child_manifest), ERROR_NONE);
uint32_t parent_id = 0;
REQUIRE_EQ(app_start("test.app.parent2", 0, nullptr, &parent_id), ERROR_NONE);
AppStartContext parent_context;
REQUIRE_EQ(app_start_context_from_id("test.app.parent2", &parent_context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE);
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
TaskEventGroup parent_event_group {};
@ -391,7 +415,10 @@ TEST_CASE("app_start_for_result delivers the child's own return value as the res
uint32_t child_id = 0;
// No parameters - fake_run falls through to its normal CLOSE loop instead of acting as a
// dialog.
REQUIRE_EQ(app_start_for_result("test.app.child2", 0, nullptr, parent_id, &child_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.app.child2", &context), ERROR_NONE);
app_start_context_set_parent(&context, parent_id);
REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE);
CHECK(wait_for_state(child_id, APP_INSTANCE_STATE_ACTIVE, 1000));
app_manager_stop(child_id); // force-close
@ -421,14 +448,18 @@ TEST_CASE("app_manager_get_topmost_instance_id returns NOT_FOUND when nothing is
REQUIRE_EQ(app_manager_add(&manifest_b), ERROR_NONE);
uint32_t id_a = 0;
REQUIRE_EQ(app_start("test.app.top_a", 0, nullptr, &id_a), ERROR_NONE);
AppStartContext context_a;
REQUIRE_EQ(app_start_context_from_id("test.app.top_a", &context_a), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context_a, &id_a), ERROR_NONE);
CHECK(wait_for_state(id_a, APP_INSTANCE_STATE_ACTIVE, 1000));
CHECK_EQ(topmost_instance_id(), id_a);
// a stays Active - b just has a higher (more recently allocated) instance id, so it becomes
// topmost without a superseding/saving.
uint32_t id_b = 0;
REQUIRE_EQ(app_start("test.app.top_b", 0, nullptr, &id_b), ERROR_NONE);
AppStartContext context_b;
REQUIRE_EQ(app_start_context_from_id("test.app.top_b", &context_b), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context_b, &id_b), ERROR_NONE);
CHECK(wait_for_state(id_b, APP_INSTANCE_STATE_ACTIVE, 1000));
CHECK_EQ(topmost_instance_id(), id_b);
@ -441,7 +472,10 @@ TEST_CASE("app_manager_get_topmost_instance_id returns NOT_FOUND when nothing is
// its persistent CLOSE loop branch instead of instantly resolving like a real dialog would -
// needed here so there's a reliable window to observe it as topmost.
uint32_t id_c = 0;
REQUIRE_EQ(app_start_for_result("test.app.top_a", 0, nullptr, id_b, &id_c), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.app.top_a", &context), ERROR_NONE);
app_start_context_set_parent(&context, id_b);
REQUIRE_EQ(app_start_with_context(&context, &id_c), ERROR_NONE);
CHECK(wait_for_state(id_c, APP_INSTANCE_STATE_ACTIVE, 1000));
CHECK_EQ(topmost_instance_id(), id_c);
@ -462,7 +496,9 @@ TEST_CASE("app_start honors a custom AppManifest::stack.depth") {
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
uint32_t instance_id = 0;
REQUIRE_EQ(app_start("test.app.stack.custom", 0, nullptr, &instance_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.app.stack.custom", &context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE);
@ -481,7 +517,9 @@ TEST_CASE("app_start still works when AppManifest::stack is left at its zero-val
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
uint32_t instance_id = 0;
REQUIRE_EQ(app_start("test.app.stack.default", 0, nullptr, &instance_id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.app.stack.default", &context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE);
@ -501,7 +539,9 @@ TEST_CASE("app_manager_get_topmost_app_id returns BUFFER_OVERFLOW for a too-smal
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
uint32_t id = 0;
REQUIRE_EQ(app_start("test.app.top_overflow", 0, nullptr, &id), ERROR_NONE);
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.app.top_overflow", &context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context, &id), ERROR_NONE);
CHECK(wait_for_state(id, APP_INSTANCE_STATE_ACTIVE, 1000));
// "test.app.top_overflow" doesn't fit in a 4-byte buffer.

View File

@ -80,7 +80,8 @@ AppInstanceId start_idle_app(const char* id) {
manifest.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(idle_app_main) };
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
AppInstanceId instance_id = 0;
REQUIRE_EQ(app_start(id, 0, nullptr, &instance_id), ERROR_NONE);
AppStartContext context = app_start_context_for_manifest(&manifest);
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
return instance_id;
}

View File

@ -80,7 +80,9 @@ int32_t parent_app_main(int, char*[]) {
app_manager_add(&fixture_manifest);
AppInstanceId fixture_id = 0;
app_start_for_result("test.posix.fixture", 0, nullptr, self_id, &fixture_id);
AppStartContext context = app_start_context_for_manifest(&fixture_manifest);
app_start_context_set_parent(&context, self_id);
app_start_with_context(&context, &fixture_id);
while (true) {
if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
@ -132,7 +134,10 @@ int32_t printf_parent_app_main(int, char*[]) {
AppLocation location { APP_LOCATION_PATH, const_cast<char*>(PRINTF_FIXTURE_APP_PATH) };
AppInstanceId childId = 0;
app_execute_for_result_with_streams(location, AppStackConfig {}, 0, nullptr, bindings, 2, self_id, &childId);
AppStartContext context = app_start_context_for_location(location);
app_start_context_set_streams(&context, bindings, 2);
app_start_context_set_parent(&context, self_id);
app_start_with_context(&context, &childId);
uint8_t drain[256];
bool childDone = false;
@ -181,7 +186,9 @@ TEST_CASE("app-posix-module's loader-path service dlopen()s a .so whose printf()
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
AppInstanceId parent_id = 0;
REQUIRE_EQ(app_start("test.posix.printf_parent", 0, nullptr, &parent_id), ERROR_NONE);
AppStartContext parent_context;
REQUIRE_EQ(app_start_context_from_id("test.posix.printf_parent", &parent_context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE);
REQUIRE(wait_for_state(parent_id, APP_INSTANCE_STATE_STOPPED, 3000));
CHECK_EQ(g_printf_fixture_output, "hello from dlopen'd app\n");
@ -201,7 +208,9 @@ TEST_CASE("app-posix-module's loader-path service dlopen()s a .so and calls its
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
AppInstanceId parent_id = 0;
REQUIRE_EQ(app_start("test.posix.parent", 0, nullptr, &parent_id), ERROR_NONE);
AppStartContext parent_context;
REQUIRE_EQ(app_start_context_from_id("test.posix.parent", &parent_context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE);
REQUIRE(wait_for_state(parent_id, APP_INSTANCE_STATE_STOPPED, 3000));
CHECK(g_fixture_result_received.load(std::memory_order_acquire));

View File

@ -1,11 +0,0 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(font-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel
)

View File

@ -1,34 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <font/font.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Renders one character's glyph into buffer, a caller-owned RGB565 pixel buffer laid out as
* `stride` pixels per row. The glyph occupies a font->glyph_width x font->glyph_height box
* starting at buffer's first pixel - buffer must already point at that box's top-left corner
* within the larger destination image. A character outside [first_codepoint, last_codepoint]
* renders as a space.
*/
void font_render_char(const FixedWidthFont* font, char ch, uint16_t fg, uint16_t bg, uint16_t* buffer, int stride);
/**
* Renders a null-terminated string left to right, one font_render_char() call per character,
* each advanced by font->glyph_width pixels from the last. buffer must point at the first
* character's top-left corner; the string occupies strlen(str) * font->glyph_width x
* font->glyph_height pixels in total.
*/
void font_render_string(const FixedWidthFont* font, const char* str, uint16_t fg, uint16_t bg, uint16_t* buffer, int stride);
/** Pixel width font_render_string() would occupy for str: strlen(str) * font->glyph_width. */
int font_get_string_length(const FixedWidthFont* font, const char* str);
#ifdef __cplusplus
}
#endif

View File

@ -1,19 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include <font/module.h>
#include <font/render.h>
static const struct ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(font_render_char),
DEFINE_MODULE_SYMBOL(font_render_string),
DEFINE_MODULE_SYMBOL(font_get_string_length),
MODULE_SYMBOL_TERMINATOR
};
struct Module font_module = {
.name = "font",
.start = NULL,
.stop = NULL,
.drivers = NULL,
.symbols = SYMBOLS,
.internal = NULL,
};

View File

@ -1,34 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include <font/render.h>
#include <cstring>
void font_render_char(const FixedWidthFont* font, char ch, uint16_t fg, uint16_t bg, uint16_t* buffer, int stride) {
// Anything outside the font's range renders as blank rather than as garbage. The comparison
// uses an unsigned value so that a byte above 0x7F cannot come out negative and pass the
// lower-bound test.
const auto raw = static_cast<unsigned char>(ch);
const unsigned char glyphChar = (raw >= font->first_codepoint && raw <= font->last_codepoint) ? raw : ' ';
const uint8_t* glyph = &font->glyph_bitmap[(glyphChar - font->first_codepoint) * font->glyph_height * font->glyph_bytes_per_row];
for (int y = 0; y < font->glyph_height; y++) {
const uint8_t* bits = &glyph[y * font->glyph_bytes_per_row];
uint16_t* out = &buffer[y * stride];
for (int x = 0; x < font->glyph_width; x++) {
// The font stores the leftmost pixel of each row byte in the most significant bit.
out[x] = (bits[x / 8] & (0x80U >> (x % 8))) ? fg : bg;
}
}
}
void font_render_string(const FixedWidthFont* font, const char* str, uint16_t fg, uint16_t bg, uint16_t* buffer, int stride) {
for (const char* p = str; *p != '\0'; p++) {
font_render_char(font, *p, fg, bg, buffer, stride);
buffer += font->glyph_width;
}
}
int font_get_string_length(const FixedWidthFont* font, const char* str) {
return static_cast<int>(strlen(str)) * font->glyph_width;
}

View File

@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
list(APPEND REQUIRES_LIST
TactilityKernel
)
if (DEFINED ENV{ESP_IDF_VERSION})
list(APPEND REQUIRES_LIST
esp_mm # esp_cache.h, used by pixel_buffer_msync()
)
endif ()
tactility_add_module(graphics-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES ${REQUIRES_LIST}
)

View File

@ -0,0 +1,43 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <font/font.h>
#include <graphics/pixel_buffer.h>
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Renders one character's glyph into a PixelBuffer, in whatever pixel format it holds.
*
* The glyph occupies a font->glyph_width x font->glyph_height box at (x, y). A character outside
* [first_codepoint, last_codepoint] renders as a space.
*
* @param[out] buffer destination, in any pixel format
* @param[in] x pixel x of the glyph's top-left corner
* @param[in] y pixel y of the glyph's top-left corner
* @param[in] font the font to render with
* @param[in] character the character to render
* @param[in] fgColor colour for covered glyph pixels, as RGB565
* @param[in] bgColor colour for uncovered glyph pixels, as RGB565
* @param[in] conversion how fg/bg reduce into buffer's native format where that's ambiguous (see
* PixelBufferConversion)
*/
void font_render_char_pixel_buffer_rgb565(
struct PixelBuffer* buffer,
int x,
int y,
const FixedWidthFont* font,
char character,
uint16_t fgColor,
uint16_t bgColor,
enum PixelBufferConversion conversion
);
#ifdef __cplusplus
}
#endif

View File

@ -7,7 +7,7 @@
extern "C" {
#endif
extern struct Module font_module;
extern struct Module graphics_module;
#ifdef __cplusplus
}

View File

@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/drivers/display.h>
#include <tactility/memory.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct PixelBuffer;
/** Allocates a new buffer, width x height pixels, in the given pixel format. External-RAM
* preferred, 64-byte aligned. Returns NULL on allocation failure or invalid dimensions. */
struct PixelBuffer* pixel_buffer_create(enum DisplayColorFormat format, int width, int height);
/** Like pixel_buffer_create(), but allocates via the given policy instead of the external-RAM-
* preferred, 64-byte-aligned default. */
struct PixelBuffer* pixel_buffer_create_with_policy(enum DisplayColorFormat format, int width, int height, const struct MemoryPolicy* policy);
/**
* Wraps existing memory (e.g. a driver's own hw frame buffer, or a sub-rect of one) without
* owning it - pixel_buffer_free() releases only the handle, not this memory.
* @param[in] stride_bytes 0 for the tightly-packed default, or an explicit value when wrapping a
* sub-rectangle of something larger
*/
struct PixelBuffer* pixel_buffer_wrap(void* data, enum DisplayColorFormat format, int width, int height, size_t stride_bytes);
/** Frees the handle; also frees the underlying memory if it was allocated by pixel_buffer_create(). */
void pixel_buffer_free(struct PixelBuffer* buffer);
enum DisplayColorFormat pixel_buffer_get_format(const struct PixelBuffer* buffer);
int pixel_buffer_get_width(const struct PixelBuffer* buffer);
int pixel_buffer_get_height(const struct PixelBuffer* buffer);
size_t pixel_buffer_get_stride_bytes(const struct PixelBuffer* buffer);
void* pixel_buffer_get_data(struct PixelBuffer* buffer);
/** Pointer to the start of row y, for bulk/row-based writes (e.g. font glyph blitting). */
void* pixel_buffer_get_row(struct PixelBuffer* buffer, int y);
/** Zeroes the whole buffer. */
void pixel_buffer_clear(struct PixelBuffer* buffer);
/**
* Flushes CPU cache writes for the width x height pixel rect at (x, y) out to memory (cache-to-
* memory writeback) so DMA/hardware reading the buffer sees the latest data. Always returns true
* (no-op) when not built for ESP-IDF. Pass (0, 0, pixel_buffer_get_width(buffer),
* pixel_buffer_get_height(buffer)) to sync the whole buffer. For MONOCHROME buffers, x and width
* must be multiples of 8.
* @return false if the underlying esp_cache_msync() call failed (logged); the caller should treat
* the buffer's contents as possibly stale to hardware in that case.
*/
bool pixel_buffer_msync(struct PixelBuffer* buffer, int x, int y, int width, int height);
/**
* How a richer colour reduces to a lossy target format (matters for MONOCHROME destinations, and
* for GRAYSCALE8's choice of averaging formula - the RGB565/BGR565/RGB565_SWAPPED/BGR565_SWAPPED/
* RGB888 conversions are all direct/unambiguous and ignore this).
*/
enum PixelBufferConversion {
/** MONOCHROME: only exact black (RGB565 0x0000) is ink-off, everything else is ink-on.
* GRAYSCALE8: same as PIXEL_BUFFER_CONVERSION_LUMA_THRESHOLD. */
PIXEL_BUFFER_CONVERSION_EXACT_BLACK,
/** Perceptual luminance (299/587/114-weighted). MONOCHROME: thresholded at 128 for ink
* on/off. GRAYSCALE8: kept as the full byte. */
PIXEL_BUFFER_CONVERSION_LUMA_THRESHOLD,
/** Unweighted (r+g+b)/3. MONOCHROME: thresholded at 128 for ink on/off. GRAYSCALE8: kept as
* the full byte. */
PIXEL_BUFFER_CONVERSION_AVERAGE,
};
/** Sets one pixel from an RGB565 colour, converting into the buffer's native format per
* `conversion` where that's ambiguous (see PixelBufferConversion). Out-of-bounds is a no-op. */
void pixel_buffer_set_pixel_rgb565(struct PixelBuffer* buffer, int x, int y, uint16_t rgb565, enum PixelBufferConversion conversion);
/** Reads one pixel back as RGB565, regardless of the buffer's native format. Always unambiguous
* (e.g. MONOCHROME ink/off map directly to white/black), so no conversion flag needed.
* Out-of-bounds returns 0. */
uint16_t pixel_buffer_get_pixel_rgb565(const struct PixelBuffer* buffer, int x, int y);
/**
* Copies a width x height region from src (src_x, src_y) into dst (dst_x, dst_y), converting
* between their native formats as needed (per `conversion` where ambiguous) - e.g. moving
* glyph/sprite data rendered in one format into a scratch buffer of another, or into a driver's
* own hw frame buffer.
*/
void pixel_buffer_blit(struct PixelBuffer* dst, int dst_x, int dst_y,
const struct PixelBuffer* src, int src_x, int src_y, int width, int height,
enum PixelBufferConversion conversion);
/** Bytes per row for `width` pixels in `format`: (width+7)/8 for MONOCHROME, width for
* GRAYSCALE8, width*2 for the RGB565/BGR565 family, width*3 for RGB888. */
size_t pixel_buffer_row_stride_bytes(enum DisplayColorFormat format, int width);
#ifdef __cplusplus
}
#endif

View File

@ -118,14 +118,14 @@ def write_c_file(path: str, output: str, width: int, height: int, bytes_per_row:
config_macro = f"CONFIG_TT_FONT_{name.upper()}_{size_digits}"
with open(path, "w") as f:
f.write(f"/*\n * {os.path.basename(path)} - {output} FixedWidthFont\n")
f.write(f" *\n * Rasterized from a TTF with Modules/font-module/scripts/generate.py.\n")
f.write(f" *\n * Rasterized from a TTF with Modules/graphics-module/scripts/generate.py.\n")
f.write(f" * Each glyph is {width} pixels wide and {height} pixels tall, stored as {height} rows of\n")
f.write(f" * {bytes_per_row} byte{'s' if bytes_per_row != 1 else ''} each (bit 7 of the first byte = leftmost pixel).\n */\n\n")
f.write(f"#include <font/{output}.h>\n\n")
f.write("#ifdef ESP_PLATFORM\n")
f.write("#include <sdkconfig.h>\n")
f.write("#endif\n\n")
f.write(f"// See Modules/font-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating\n")
f.write(f"// See Modules/graphics-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating\n")
f.write(f"// this font's Kconfig entry) is treated the same as enabled.\n")
f.write(f"#if !defined({config_macro}) || {config_macro}\n\n")
f.write(f"// Glyph bitmap data: {glyph_count} characters (0x20-0x7E), {glyph_bytes} bytes each = {total_bytes} bytes\n")

View File

@ -1,7 +1,7 @@
/*
* ibmplexmono12.c - ibmplexmono12 FixedWidthFont
*
* Rasterized from a TTF with Modules/font-module/scripts/generate.py.
* Rasterized from a TTF with Modules/graphics-module/scripts/generate.py.
* Each glyph is 7 pixels wide and 12 pixels tall, stored as 12 rows of
* 1 byte each (bit 7 of the first byte = leftmost pixel).
*/
@ -12,7 +12,7 @@
#include <sdkconfig.h>
#endif
// See Modules/font-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// See Modules/graphics-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// this font's Kconfig entry) is treated the same as enabled.
#if !defined(CONFIG_TT_FONT_IBMPLEXMONO_12) || CONFIG_TT_FONT_IBMPLEXMONO_12

View File

@ -1,7 +1,7 @@
/*
* ibmplexmono14.c - ibmplexmono14 FixedWidthFont
*
* Rasterized from a TTF with Modules/font-module/scripts/generate.py.
* Rasterized from a TTF with Modules/graphics-module/scripts/generate.py.
* Each glyph is 8 pixels wide and 14 pixels tall, stored as 14 rows of
* 1 byte each (bit 7 of the first byte = leftmost pixel).
*/
@ -12,7 +12,7 @@
#include <sdkconfig.h>
#endif
// See Modules/font-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// See Modules/graphics-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// this font's Kconfig entry) is treated the same as enabled.
#if !defined(CONFIG_TT_FONT_IBMPLEXMONO_14) || CONFIG_TT_FONT_IBMPLEXMONO_14

View File

@ -1,7 +1,7 @@
/*
* ibmplexmono16.c - ibmplexmono16 FixedWidthFont
*
* Rasterized from a TTF with Modules/font-module/scripts/generate.py.
* Rasterized from a TTF with Modules/graphics-module/scripts/generate.py.
* Each glyph is 10 pixels wide and 16 pixels tall, stored as 16 rows of
* 2 bytes each (bit 7 of the first byte = leftmost pixel).
*/
@ -12,7 +12,7 @@
#include <sdkconfig.h>
#endif
// See Modules/font-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// See Modules/graphics-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// this font's Kconfig entry) is treated the same as enabled.
#if !defined(CONFIG_TT_FONT_IBMPLEXMONO_16) || CONFIG_TT_FONT_IBMPLEXMONO_16

View File

@ -1,7 +1,7 @@
/*
* ibmplexmono18.c - ibmplexmono18 FixedWidthFont
*
* Rasterized from a TTF with Modules/font-module/scripts/generate.py.
* Rasterized from a TTF with Modules/graphics-module/scripts/generate.py.
* Each glyph is 11 pixels wide and 18 pixels tall, stored as 18 rows of
* 2 bytes each (bit 7 of the first byte = leftmost pixel).
*/
@ -12,7 +12,7 @@
#include <sdkconfig.h>
#endif
// See Modules/font-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// See Modules/graphics-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// this font's Kconfig entry) is treated the same as enabled.
#if !defined(CONFIG_TT_FONT_IBMPLEXMONO_18) || CONFIG_TT_FONT_IBMPLEXMONO_18

View File

@ -1,7 +1,7 @@
/*
* ibmplexmono24.c - ibmplexmono24 FixedWidthFont
*
* Rasterized from a TTF with Modules/font-module/scripts/generate.py.
* Rasterized from a TTF with Modules/graphics-module/scripts/generate.py.
* Each glyph is 14 pixels wide and 24 pixels tall, stored as 24 rows of
* 2 bytes each (bit 7 of the first byte = leftmost pixel).
*/
@ -12,7 +12,7 @@
#include <sdkconfig.h>
#endif
// See Modules/font-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// See Modules/graphics-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// this font's Kconfig entry) is treated the same as enabled.
#if !defined(CONFIG_TT_FONT_IBMPLEXMONO_24) || CONFIG_TT_FONT_IBMPLEXMONO_24

View File

@ -1,7 +1,7 @@
/*
* ibmplexmono28.c - ibmplexmono28 FixedWidthFont
*
* Rasterized from a TTF with Modules/font-module/scripts/generate.py.
* Rasterized from a TTF with Modules/graphics-module/scripts/generate.py.
* Each glyph is 17 pixels wide and 28 pixels tall, stored as 28 rows of
* 3 bytes each (bit 7 of the first byte = leftmost pixel).
*/
@ -12,7 +12,7 @@
#include <sdkconfig.h>
#endif
// See Modules/font-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// See Modules/graphics-module/Kconfig: undefined (POSIX, or an ESP-IDF build predating
// this font's Kconfig entry) is treated the same as enabled.
#if !defined(CONFIG_TT_FONT_IBMPLEXMONO_28) || CONFIG_TT_FONT_IBMPLEXMONO_28

View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0
#include <font/render.h>
#include <graphics/module.h>
#include <graphics/pixel_buffer.h>
static const struct ModuleSymbol SYMBOLS[] = {
// font
DEFINE_MODULE_SYMBOL(font_render_char_pixel_buffer_rgb565),
// pixel_buffer
DEFINE_MODULE_SYMBOL(pixel_buffer_create),
DEFINE_MODULE_SYMBOL(pixel_buffer_create_with_policy),
DEFINE_MODULE_SYMBOL(pixel_buffer_wrap),
DEFINE_MODULE_SYMBOL(pixel_buffer_free),
DEFINE_MODULE_SYMBOL(pixel_buffer_get_format),
DEFINE_MODULE_SYMBOL(pixel_buffer_get_width),
DEFINE_MODULE_SYMBOL(pixel_buffer_get_height),
DEFINE_MODULE_SYMBOL(pixel_buffer_get_stride_bytes),
DEFINE_MODULE_SYMBOL(pixel_buffer_get_data),
DEFINE_MODULE_SYMBOL(pixel_buffer_get_row),
DEFINE_MODULE_SYMBOL(pixel_buffer_clear),
DEFINE_MODULE_SYMBOL(pixel_buffer_msync),
DEFINE_MODULE_SYMBOL(pixel_buffer_set_pixel_rgb565),
DEFINE_MODULE_SYMBOL(pixel_buffer_get_pixel_rgb565),
DEFINE_MODULE_SYMBOL(pixel_buffer_blit),
DEFINE_MODULE_SYMBOL(pixel_buffer_row_stride_bytes),
MODULE_SYMBOL_TERMINATOR,
};
struct Module graphics_module = {
.name = "graphics",
.start = NULL,
.stop = NULL,
.drivers = NULL,
.symbols = SYMBOLS,
.internal = NULL,
};

View File

@ -0,0 +1,334 @@
// SPDX-License-Identifier: Apache-2.0
#include <graphics/pixel_buffer.h>
#include <tactility/log.h>
#include <tactility/memory.h>
#include <stdlib.h>
#include <string.h>
#ifdef ESP_PLATFORM
#include <esp_cache.h>
static const char* TAG = "PixelBuffer";
#endif
struct PixelBuffer {
enum DisplayColorFormat format;
int width;
int height;
size_t stride_bytes;
uint8_t* data;
bool owns_data;
};
size_t pixel_buffer_row_stride_bytes(enum DisplayColorFormat format, int width) {
switch (format) {
case DISPLAY_COLOR_FORMAT_MONOCHROME:
return ((size_t)width + 7) / 8;
case DISPLAY_COLOR_FORMAT_GRAYSCALE8:
return (size_t)width;
case DISPLAY_COLOR_FORMAT_RGB888:
return (size_t)width * 3;
case DISPLAY_COLOR_FORMAT_BGR565:
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED:
case DISPLAY_COLOR_FORMAT_RGB565:
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
default:
return (size_t)width * 2;
}
}
struct PixelBuffer* pixel_buffer_create(enum DisplayColorFormat format, int width, int height) {
const struct MemoryPolicy policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 64 };
return pixel_buffer_create_with_policy(format, width, height, &policy);
}
struct PixelBuffer* pixel_buffer_create_with_policy(enum DisplayColorFormat format, int width, int height, const struct MemoryPolicy* policy) {
if (width <= 0 || height <= 0) {
return NULL;
}
struct PixelBuffer* buffer = malloc(sizeof(struct PixelBuffer));
if (buffer == NULL) {
return NULL;
}
const size_t stride = pixel_buffer_row_stride_bytes(format, width);
const size_t logical_bytes = stride * (size_t)height;
// Padded up to a cache-line multiple (64), not just the logical size: a caller that msyncs
// this buffer (e.g. before DMA) needs both the address and the size of the sync region to be
// a multiple of the cache line - rounding the logical size up for that sync call is only safe
// if the allocation itself has that much room.
const size_t bytes = (logical_bytes + 63) & ~(size_t)63;
uint8_t* data = memory_alloc_with_policy(bytes, policy);
if (data == NULL) {
free(buffer);
return NULL;
}
memset(data, 0, bytes);
buffer->format = format;
buffer->width = width;
buffer->height = height;
buffer->stride_bytes = stride;
buffer->data = data;
buffer->owns_data = true;
return buffer;
}
struct PixelBuffer* pixel_buffer_wrap(void* data, enum DisplayColorFormat format, int width, int height, size_t stride_bytes) {
if (data == NULL || width <= 0 || height <= 0) {
return NULL;
}
struct PixelBuffer* buffer = malloc(sizeof(struct PixelBuffer));
if (buffer == NULL) {
return NULL;
}
buffer->format = format;
buffer->width = width;
buffer->height = height;
buffer->stride_bytes = stride_bytes != 0 ? stride_bytes : pixel_buffer_row_stride_bytes(format, width);
buffer->data = (uint8_t*)data;
buffer->owns_data = false;
return buffer;
}
void pixel_buffer_free(struct PixelBuffer* buffer) {
if (buffer == NULL) {
return;
}
if (buffer->owns_data) {
memory_free(buffer->data);
}
free(buffer);
}
enum DisplayColorFormat pixel_buffer_get_format(const struct PixelBuffer* buffer) {
return buffer->format;
}
int pixel_buffer_get_width(const struct PixelBuffer* buffer) {
return buffer->width;
}
int pixel_buffer_get_height(const struct PixelBuffer* buffer) {
return buffer->height;
}
size_t pixel_buffer_get_stride_bytes(const struct PixelBuffer* buffer) {
return buffer->stride_bytes;
}
void* pixel_buffer_get_data(struct PixelBuffer* buffer) {
return buffer->data;
}
void* pixel_buffer_get_row(struct PixelBuffer* buffer, int y) {
return buffer->data + (size_t)y * buffer->stride_bytes;
}
void pixel_buffer_clear(struct PixelBuffer* buffer) {
memset(buffer->data, 0, buffer->stride_bytes * (size_t)buffer->height);
}
#ifdef ESP_PLATFORM
// esp_cache_msync() requires both the address and size to be cache-line (64-byte) aligned unless
// ESP_CACHE_MSYNC_FLAG_UNALIGNED is passed. An owned buffer's allocation is padded to a 64-byte
// multiple (see pixel_buffer_create()), so [addr, addr+bytes) can be widened out to the nearest
// cache-line boundaries and still stay inside the allocation. A wrapped (non-owned) buffer's true
// bounds beyond its own logical size are unknown, so its range is left as-is and
// ESP_CACHE_MSYNC_FLAG_UNALIGNED is passed instead, letting the cache API handle the misalignment
// without this code guessing at memory it doesn't own.
static esp_err_t pixel_buffer_msync_range(struct PixelBuffer* buffer, uint8_t* addr, size_t bytes) {
if (buffer->owns_data) {
const size_t offset = (size_t)(addr - buffer->data);
const size_t aligned_offset = offset & ~(size_t)63;
size_t aligned_end = (offset + bytes + 63) & ~(size_t)63;
const size_t capacity = (buffer->stride_bytes * (size_t)buffer->height + 63) & ~(size_t)63;
if (aligned_end > capacity) {
aligned_end = capacity;
}
return esp_cache_msync(buffer->data + aligned_offset, aligned_end - aligned_offset, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
}
return esp_cache_msync(addr, bytes, ESP_CACHE_MSYNC_FLAG_DIR_C2M | ESP_CACHE_MSYNC_FLAG_UNALIGNED);
}
#endif
bool pixel_buffer_msync(struct PixelBuffer* buffer, int x, int y, int width, int height) {
#ifdef ESP_PLATFORM
bool ok = true;
const bool contiguous = x == 0 && width == buffer->width;
if (contiguous) {
uint8_t* addr = pixel_buffer_get_row(buffer, y);
const size_t bytes = buffer->stride_bytes * (size_t)height;
ok = pixel_buffer_msync_range(buffer, addr, bytes) == ESP_OK;
} else {
const size_t rowBytes = pixel_buffer_row_stride_bytes(buffer->format, width);
const size_t xBytes = pixel_buffer_row_stride_bytes(buffer->format, x);
for (int row = 0; row < height; row++) {
uint8_t* rowPtr = (uint8_t*)pixel_buffer_get_row(buffer, y + row) + xBytes;
if (pixel_buffer_msync_range(buffer, rowPtr, rowBytes) != ESP_OK) {
ok = false;
}
}
}
if (!ok) {
LOG_E(TAG, "esp_cache_msync failed for %dx%d region at (%d, %d)", width, height, x, y);
}
return ok;
#else
(void)buffer;
(void)x;
(void)y;
(void)width;
(void)height;
return true;
#endif
}
// ============ RGB565 conversion helpers ============
static inline uint16_t rgb565_byteswap(uint16_t v) {
return (uint16_t)((v >> 8) | (v << 8));
}
// Swapping the 5-bit R and B fields is its own inverse, so this also converts BGR565 -> RGB565.
static inline uint16_t rgb565_swap_r_b(uint16_t v) {
const uint16_t r = (v >> 11) & 0x1F;
const uint16_t g = (v >> 5) & 0x3F;
const uint16_t b = v & 0x1F;
return (uint16_t)((b << 11) | (g << 5) | r);
}
static inline void rgb565_to_rgb888(uint16_t v, uint8_t out[3]) {
const uint32_t r5 = (v >> 11) & 0x1F;
const uint32_t g6 = (v >> 5) & 0x3F;
const uint32_t b5 = v & 0x1F;
out[0] = (uint8_t)((r5 << 3) | (r5 >> 2));
out[1] = (uint8_t)((g6 << 2) | (g6 >> 4));
out[2] = (uint8_t)((b5 << 3) | (b5 >> 2));
}
static inline uint16_t rgb888_to_rgb565(const uint8_t rgb[3]) {
return (uint16_t)(((rgb[0] >> 3) << 11) | ((rgb[1] >> 2) << 5) | (rgb[2] >> 3));
}
static inline uint8_t rgb565_to_luma8(uint16_t v) {
uint8_t rgb[3];
rgb565_to_rgb888(v, rgb);
const uint32_t luma = (uint32_t)rgb[0] * 299 + (uint32_t)rgb[1] * 587 + (uint32_t)rgb[2] * 114;
return (uint8_t)(luma / 1000);
}
static inline uint8_t rgb565_to_average8(uint16_t v) {
uint8_t rgb[3];
rgb565_to_rgb888(v, rgb);
return (uint8_t)(((uint16_t)rgb[0] + (uint16_t)rgb[1] + (uint16_t)rgb[2]) / 3);
}
static inline uint8_t rgb565_to_gray8(uint16_t v, enum PixelBufferConversion conversion) {
return conversion == PIXEL_BUFFER_CONVERSION_AVERAGE ? rgb565_to_average8(v) : rgb565_to_luma8(v);
}
static inline bool rgb565_to_ink(uint16_t v, enum PixelBufferConversion conversion) {
if (conversion == PIXEL_BUFFER_CONVERSION_EXACT_BLACK) {
return v != 0x0000;
}
return rgb565_to_gray8(v, conversion) >= 128; // LUMA_THRESHOLD or AVERAGE
}
// ============ Pixel access ============
void pixel_buffer_set_pixel_rgb565(struct PixelBuffer* buffer, int x, int y, uint16_t rgb565, enum PixelBufferConversion conversion) {
if (x < 0 || y < 0 || x >= buffer->width || y >= buffer->height) {
return;
}
uint8_t* row = pixel_buffer_get_row(buffer, y);
switch (buffer->format) {
case DISPLAY_COLOR_FORMAT_RGB565:
((uint16_t*)row)[x] = rgb565;
break;
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
((uint16_t*)row)[x] = rgb565_byteswap(rgb565);
break;
case DISPLAY_COLOR_FORMAT_BGR565:
((uint16_t*)row)[x] = rgb565_swap_r_b(rgb565);
break;
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED:
((uint16_t*)row)[x] = rgb565_byteswap(rgb565_swap_r_b(rgb565));
break;
case DISPLAY_COLOR_FORMAT_RGB888:
rgb565_to_rgb888(rgb565, &row[x * 3]);
break;
case DISPLAY_COLOR_FORMAT_GRAYSCALE8:
row[x] = rgb565_to_gray8(rgb565, conversion);
break;
case DISPLAY_COLOR_FORMAT_MONOCHROME:
default: {
const uint8_t mask = (uint8_t)(0x80U >> (x % 8));
if (rgb565_to_ink(rgb565, conversion)) {
row[x / 8] |= mask;
} else {
row[x / 8] &= (uint8_t)~mask;
}
break;
}
}
}
uint16_t pixel_buffer_get_pixel_rgb565(const struct PixelBuffer* buffer, int x, int y) {
if (x < 0 || y < 0 || x >= buffer->width || y >= buffer->height) {
return 0;
}
const uint8_t* row = &buffer->data[(size_t)y * buffer->stride_bytes];
switch (buffer->format) {
case DISPLAY_COLOR_FORMAT_RGB565:
return ((const uint16_t*)row)[x];
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
return rgb565_byteswap(((const uint16_t*)row)[x]);
case DISPLAY_COLOR_FORMAT_BGR565:
return rgb565_swap_r_b(((const uint16_t*)row)[x]);
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED:
return rgb565_swap_r_b(rgb565_byteswap(((const uint16_t*)row)[x]));
case DISPLAY_COLOR_FORMAT_RGB888:
return rgb888_to_rgb565(&row[x * 3]);
case DISPLAY_COLOR_FORMAT_GRAYSCALE8: {
const uint8_t rgb[3] = { row[x], row[x], row[x] };
return rgb888_to_rgb565(rgb);
}
case DISPLAY_COLOR_FORMAT_MONOCHROME:
default: {
const uint8_t mask = (uint8_t)(0x80U >> (x % 8));
return (row[x / 8] & mask) ? 0xFFFF : 0x0000;
}
}
}
void pixel_buffer_blit(struct PixelBuffer* dst, int dst_x, int dst_y,
const struct PixelBuffer* src, int src_x, int src_y, int width, int height,
enum PixelBufferConversion conversion) {
if (dst->format == src->format) {
// Same native format: a straight byte copy is both correct and much cheaper than
// converting every pixel through RGB565 and back.
const size_t row_bytes = pixel_buffer_row_stride_bytes(dst->format, width);
const size_t dst_x_bytes = pixel_buffer_row_stride_bytes(dst->format, dst_x);
const size_t src_x_bytes = pixel_buffer_row_stride_bytes(src->format, src_x);
for (int row = 0; row < height; row++) {
uint8_t* dst_row = dst->data + (size_t)(dst_y + row) * dst->stride_bytes + dst_x_bytes;
const uint8_t* src_row = src->data + (size_t)(src_y + row) * src->stride_bytes + src_x_bytes;
memcpy(dst_row, src_row, row_bytes);
}
return;
}
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
const uint16_t rgb565 = pixel_buffer_get_pixel_rgb565(src, src_x + col, src_y + row);
pixel_buffer_set_pixel_rgb565(dst, dst_x + col, dst_y + row, rgb565, conversion);
}
}
}

View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
#include <font/render.h>
#include <font/font.h>
#include <graphics/pixel_buffer.h>
extern "C" {
void font_render_char_pixel_buffer_rgb565(
PixelBuffer* buffer,
int x,
int y,
const FixedWidthFont* font,
char character,
uint16_t fg,
uint16_t bg,
PixelBufferConversion conversion
) {
const auto raw = static_cast<unsigned char>(character);
const unsigned char glyphChar = (raw >= font->first_codepoint && raw <= font->last_codepoint) ? raw : ' ';
const uint8_t* glyph = &font->glyph_bitmap[(glyphChar - font->first_codepoint) * font->glyph_height * font->glyph_bytes_per_row];
for (int row = 0; row < font->glyph_height; row++) {
const uint8_t* bits = &glyph[row * font->glyph_bytes_per_row];
for (int col = 0; col < font->glyph_width; col++) {
// The font stores the leftmost pixel of each row byte in the most significant bit.
const bool covered = (bits[col / 8] & (0x80U >> (col % 8))) != 0;
pixel_buffer_set_pixel_rgb565(buffer, x + col, y + row, covered ? fg : bg, conversion);
}
}
}
}

View File

@ -34,9 +34,9 @@ list(APPEND REQUIRES_LIST
cjson-module
cpp-symbols-module
crypt-module
font-module
freertos-module
gps-module
graphics-module
http-module
mbedtls-module
posix-symbols-module
@ -76,7 +76,6 @@ if (DEFINED ENV{ESP_IDF_VERSION})
fatfs
lwip
spi_flash
esp_mm # esp_cache.h, used by the terminal renderers
)
if ("${IDF_TARGET}" STREQUAL "esp32s3" OR "${IDF_TARGET}" STREQUAL "esp32p4")

View File

@ -0,0 +1,91 @@
#pragma once
#include <Tactility/file/File.h>
#include <app/paths.h>
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
namespace tt::app::applist {
/** Favourited app ids, persisted one per line under the data root. */
class Favourites final {
public:
std::vector<std::string> load() const {
std::vector<std::string> ids;
const std::string path = filePath();
if (!path.empty()) {
file::readLines(path, /* stripNewLine */ true, [&ids](const char* line) {
if (line[0] != '\0') {
ids.emplace_back(line);
}
});
}
return ids;
}
/** @return false if the data root is unavailable or the write itself failed - the previous
* file, if any, is left untouched either way. */
bool save(const std::vector<std::string>& ids) const {
const std::string path = filePath();
if (path.empty()) {
return false;
}
file::findOrCreateParentDirectory(path, 0777);
std::string content;
for (const auto& id : ids) {
content += id;
content += '\n';
}
// Written to a temp file and renamed into place, so a write that fails partway through
// can't truncate or corrupt the existing file.
const std::string tempPath = path + ".tmp";
if (!file::writeString(tempPath, content)) {
remove(tempPath.c_str());
return false;
}
// FatFs (ESP32's internal "/data" filesystem) rejects rename() onto an existing
// destination instead of replacing it, unlike POSIX - remove it first so this works the
// second and every later time, not just the first.
remove(path.c_str());
if (rename(tempPath.c_str(), path.c_str()) != 0) {
remove(tempPath.c_str());
return false;
}
return true;
}
static bool contains(const std::vector<std::string>& ids, const char* id) {
return std::ranges::find(ids, id) != ids.end();
}
/** @return false if the new list failed to persist (see save()) - the caller should tell the
* user, since the next load() will not reflect this toggle. */
bool toggle(const char* id) const {
auto ids = load();
auto it = std::ranges::find(ids, id);
if (it != ids.end()) {
ids.erase(it);
} else {
ids.emplace_back(id);
}
return save(ids);
}
private:
static std::string filePath() {
char path[128];
if (app_paths_get_user_data_path("tactility.applist", "favourites", path, sizeof(path)) != ERROR_NONE) {
return "";
}
return path;
}
};
}

View File

@ -1,22 +1,22 @@
#pragma once
#include <cstddef>
#include <cstdint>
struct Device;
struct PixelBuffer;
/**
* Draws the active vterm's cell grid to a display device.
*
* Owns everything about the terminal grid that doesn't depend on how pixels actually reach the
* panel: glyph painting, the shadow buffer used to skip unchanged cells, and cursor blink state.
* A subclass supplies the panel/frame dimension relationship and how the off-screen frame buffer
* gets onto the display: see TerminalRendererPpa (hardware rotate, for a landscape terminal on a
* portrait panel) and TerminalRendererGeneric (direct blit, panel's native orientation, no extra
* hardware needed).
* Glyph painting, the shadow buffer, cursor blink, and per-row dirty-tracking live here. A
* subclass supplies the panel/frame relationship and how a row reaches the display: see
* TerminalRendererPpa (hardware rotate) and TerminalRendererGeneric (direct blit).
*
* Because presenting a frame is a whole-buffer operation, redraws are frame-based rather than
* per-row: changed cells are painted into the off-screen buffer, and the whole buffer is only
* pushed when something actually changed.
* Painted and presented one row at a time, so frameBuffer only ever holds a single row.
* presentRegion()/finishFrame() accumulate rows and flip/push the whole buffer once per frame - a
* per-row push would show a mix of stale and fresh rows, since a hw double buffer's zero-copy
* path flips on any write regardless of the sub-rect given.
*
* Must be used only while LVGL is stopped, since it writes to the display directly.
*/
@ -24,13 +24,7 @@ class TerminalRenderer {
public:
virtual ~TerminalRenderer() = default;
/**
* Allocates buffers.
*
* @param display the display device to draw to
*
* Glyphs are drawn at their native size, never scaled up or down.
*/
/** Allocates buffers. Glyphs are drawn at native size, never scaled. */
virtual bool begin(Device* display) = 0;
/** Releases all resources. */
@ -38,9 +32,7 @@ public:
/**
* Repaints changed cells and presents the frame. Pass true to redraw everything.
*
* Also drives the cursor blink, so this must be called regularly even when nothing has changed.
* The blink phase advances on wall-clock time rather than on frame count.
* Also drives the cursor blink, so must be called regularly even when nothing has changed.
*/
void render(bool force = false);
@ -52,43 +44,96 @@ protected:
void paintCell(int row, int col, char ch, uint8_t attr);
void paintCursor(int row, int col);
/** Pushes frameBuffer (frameWidth x frameHeight) onto the display. */
virtual void present() = 0;
/**
* Turns frameBuffer's current row (pixels [yStart, yEnd)) into panel-oriented pixels and
* hands them to presentRegion() - or writes directly into the hw double buffer and sets
* hwBufferDirty itself, skipping presentRegion().
*/
virtual void present(int yStart, int yEnd) = 0;
/**
* Common begin() work: computes the cell grid from frameWidth/frameHeight and allocates
* frameBuffer + the shadow grid. The subclass must set display/panelWidth/panelHeight/
* frameWidth/frameHeight first.
* Common begin() work: computes the cell grid and allocates frameBuffer (one row) + shadow.
* The subclass must set display/panelWidth/panelHeight/frameWidth/frameHeight first.
*/
bool allocateCommon(Device* display);
/** Frees frameBuffer, the shadow grid, and the borrowed hw double buffer, if any. */
/** Frees frameBuffer, fullFrameBuffer, shadow, and the borrowed hw double buffer, if any. */
void freeCommon();
/**
* If the display reports two panel-sized frame buffers, borrows them as hwFrameBuffers[0/1]
* and sets usingHwFrameBuffer, so the display controller can scan out one while the next frame
* is written into the other. Both buffers are panelWidth x panelHeight regardless of whether
* the subclass rotates into them.
* Zeroes frameBuffer and presents it across the full panel height once, blanking the
* letterbox margins. Call from begin(), after allocateCommon() and any subclass output buffer
* are ready.
*/
void clearPanelOnce();
/**
* Queries DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME and DISPLAY_COLOR_FORMAT_MONOCHROME
* (monochrome always implies full-frame here, regardless of the driver's own flag). If either
* applies and there's no hw double buffer, allocates a panel-sized buffer for
* presentRegion()/finishFrame() to accumulate into, since such a display can't take a partial
* display_draw_bitmap() rect. Call from begin(), after acquireHwDoubleBuffer() and any
* subclass output buffer are ready.
* @retval false allocation failed; the subclass's begin() should fail too
*/
bool allocateFullFrameBufferIfNeeded();
/**
* Places src's full regionW x regionH extent at (regionX, regionY) via pixel_buffer_blit().
* Accumulates into hwFrameBuffers or fullFrameBuffer (see finishFrame()) when either applies,
* otherwise pushes directly via display_draw_bitmap() as a genuine partial update.
*/
void presentRegion(struct PixelBuffer* src, int regionX, int regionY, int regionW, int regionH);
/**
* Seeds hwFrameBuffers[backBufferIndex] from the other buffer on the first write since the
* last flip, since only rows touched this frame get repainted afterward. A subclass writing
* directly into hwFrameBuffers (bypassing presentRegion()) must call this itself first.
*/
void ensureBackBufferSeeded();
/**
* Call once after render()'s row loop. Pushes whichever buffer accumulated writes this
* frame, whole, and flips backBufferIndex in the hw case.
*/
void finishFrame();
/** Wraps the display's two panel-sized frame buffers as hwFrameBuffers[0/1] (non-owning - see
* pixel_buffer_wrap()) and sets usingHwFrameBuffer, if it reports any. */
bool acquireHwDoubleBuffer();
Device* display = nullptr;
// Off-screen buffer glyphs are painted into, before being pushed to the display.
uint16_t* frameBuffer = nullptr;
// True for a monochrome display. frameBuffer/hwFrameBuffers/fullFrameBuffer format-handling
// all lives in graphics-module now (see paintCell()/presentRegion()).
bool monochrome = false;
// Scratch buffer glyphs are painted into before being pushed. Sized for one text row, not the
// whole frame.
PixelBuffer* frameBuffer = nullptr;
int frameWidth = 0;
int frameHeight = 0;
// Pixel y of the row currently being painted; paintCell() subtracts it to index into the
// small, reused frameBuffer.
int currentRowYOffset = 0;
// Panel dimensions in its own native orientation.
int panelWidth = 0;
int panelHeight = 0;
// Either the panel's own double buffers (see acquireHwDoubleBuffer()), or left unset if a
// subclass needs its own single output buffer instead.
uint16_t* hwFrameBuffers[2] = { nullptr, nullptr };
// The panel's own double buffers (acquireHwDoubleBuffer()), wrapped non-owning, or unset if a
// subclass uses its own output buffer instead.
PixelBuffer* hwFrameBuffers[2] = { nullptr, nullptr };
int backBufferIndex = 1;
bool usingHwFrameBuffer = false;
// Set when hwFrameBuffers[backBufferIndex] has unflushed writes; finishFrame() flips only then.
bool hwBufferDirty = false;
// See allocateFullFrameBufferIfNeeded()/presentRegion()/finishFrame().
bool fullFrameRequired = false;
PixelBuffer* fullFrameBuffer = nullptr;
bool fullFrameDirty = false;
// Copy of what has been painted, used to skip unchanged cells.
struct Cell {

View File

@ -3,9 +3,8 @@
#include <Tactility/app/terminal/TerminalRenderer.h>
/**
* Draws the terminal directly in the display's native orientation: no rotation, so no PPA (or
* other rotation hardware) needed. Works on any display; see TerminalRendererPpa for the
* PPA-accelerated alternative used on panels (e.g. Tab5) that are natively portrait but want a
* Draws the terminal directly in the display's native orientation: no rotation, so no PPA needed.
* See TerminalRendererPpa for the PPA-accelerated alternative for a portrait panel that wants a
* landscape terminal.
*/
class TerminalRendererGeneric : public TerminalRenderer {
@ -16,5 +15,5 @@ public:
void end() override;
protected:
void present() override;
void present(int yStart, int yEnd) override;
};

View File

@ -17,13 +17,13 @@ public:
void end() override;
protected:
void present() override;
void present(int yStart, int yEnd) override;
private:
// ppa_client_handle_t, kept opaque here so driver/ppa.h stays confined to the .cpp.
void* ppaClient = nullptr;
// Output side when the panel has no hw double buffer: one PSRAM buffer rotated into and
// pushed manually.
uint16_t* rotatedBuffer = nullptr;
// No-hw-double-buffer fallback: one small PSRAM buffer, sized for one row's rotated
// footprint, rotated into and pushed manually per row.
PixelBuffer* rotatedRowBuffer = nullptr;
};

View File

@ -59,6 +59,12 @@ void vterm_get_size(int *rows, int *cols);
void vterm_set_size_override(int rows, int cols);
void vterm_clear_size_override(void);
void vterm_get_cursor(int vt_id, int *col, int *row, int *visible);
// True if the active VT's cells or cursor changed since the last call to this function (any
// vterm_write() into it, or a vterm_switch() that made it active) - consumes (clears) the flag.
// A renderer's own blink-timer-driven cursor toggle is not tracked here; that stays its own
// responsibility, since it happens without vterm ever being written to.
bool vterm_take_dirty(void);
void vterm_set_switch_callback(void (*cb)(int new_vt));
// Tactility addition: called just before a line scrolls off the top, while it is still readable

View File

@ -59,12 +59,12 @@
#include <cjson_symbols/module.h>
#include <cpp_symbols/module.h>
#include <crypt/module.h>
#include <font/module.h>
#include <freertos/module.h>
#include <gps/module.h>
#include <gps_generic/module.h>
#include <gps_meshtastic/module.h>
#include <graphics/module.h>
#include <http/module.h>
#include <mbedtls/module.h>
#include <pthread/module.h>
@ -530,7 +530,7 @@ void run(Module* const dtsModules[], const DtsDevice dtsDevices[]) {
check(module_ensure_started(&pthread_module) == ERROR_NONE);
// Other libraries
check(module_ensure_started(&http_module) == ERROR_NONE);
check(module_ensure_started(&font_module) == ERROR_NONE);
check(module_ensure_started(&graphics_module) == ERROR_NONE);
check(module_ensure_started(&app_module) == ERROR_NONE);
check(module_ensure_started(&crypt_module) == ERROR_NONE);
check(module_ensure_started(&audio_decoder_module) == ERROR_NONE);
@ -579,7 +579,8 @@ void run(Module* const dtsModules[], const DtsDevice dtsDevices[]) {
// It's a new-model (app-module + window-manager) app now, replacing the old app::start().
app_manager_add(&app::boot::manifest);
uint32_t boot_instance_id = 0;
app_start(app::boot::manifest.id, 0, nullptr, &boot_instance_id);
AppStartContext boot_context = app_start_context_for_manifest(&app::boot::manifest);
app_start_with_context(&boot_context, &boot_instance_id);
LOG_I(TAG, "Main dispatcher ready");
while (true) {

View File

@ -133,7 +133,7 @@ int32_t appMain(int argc, char* argv[]) {
namespace {
// Builds argv = [title, message, buttonLabels...] for app_start_for_result().
// Builds argv = [title, message, buttonLabels...] for app_start_with_context().
std::vector<const char*> buildArgv(const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels) {
std::vector<const char*> argv { title.c_str(), message.c_str() };
for (const auto& label: buttonLabels) {
@ -147,7 +147,10 @@ std::vector<const char*> buildArgv(const std::string& title, const std::string&
uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels) {
auto argv = buildArgv(title, message, buttonLabels);
uint32_t instanceId = 0;
app_start_for_result(manifest.id, static_cast<int>(argv.size()), argv.data(), callerAppInstanceId, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, static_cast<int>(argv.size()), argv.data());
app_start_context_set_parent(&context, callerAppInstanceId);
app_start_with_context(&context, &instanceId);
return instanceId;
}

View File

@ -352,7 +352,10 @@ void start(const apphub::AppHubEntry& entry) {
argv.push_back(platform.c_str());
}
uint32_t instanceId = 0;
app_start_for_result(manifest.id, static_cast<int>(argv.size()), argv.data(), /*parent_instance_id=*/0, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, static_cast<int>(argv.size()), argv.data());
app_start_context_set_parent(&context, /*parent_instance_id=*/0);
app_start_with_context(&context, &instanceId);
}
extern const ::AppManifest manifest = {

View File

@ -6,9 +6,12 @@
#include <lvgl_window_manager/window_manager.h>
#include <tactility/check.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/app/applist/Favourites.h>
#include <tactility/check.h>
#include <tactility/log.h>
#include <lvgl.h>
#include <algorithm>
#include <cstring>
#include <vector>
@ -19,17 +22,64 @@
namespace tt::app::applist {
constexpr auto* TAG = "AppList";
namespace {
struct Context {
uint32_t appInstanceId;
Favourites favourites;
};
void populateList(lv_obj_t* list);
// Named (not a lambda) so lv_async_call_cancel() below has a stable function pointer to match against.
void deferredRepopulate(void* userData) {
populateList(static_cast<lv_obj_t*>(userData));
}
void onListDeleted(lv_event_t* e) {
// Cancels a still-pending deferredRepopulate() scheduled by a long-press/key-press right
// before the list itself was deleted (e.g. the app closing)
// Otherwise it would run later against this now-dangling lv_obj_t*.
lv_async_call_cancel(deferredRepopulate, lv_event_get_target_obj(e));
}
void onAppPressed(lv_event_t* e) {
// Fire-and-forget top-level navigation, same as Launcher's own app-launch buttons.
const auto* manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
uint32_t instanceId = 0;
app_start(manifest->id, 0, nullptr, &instanceId);
AppStartContext context = app_start_context_for_manifest(manifest);
if (app_start_with_context(&context, &instanceId) == ERROR_NONE) {
lv_obj_t* list = lv_obj_get_parent(lv_event_get_target_obj(e));
auto* ctx = static_cast<Context*>(lv_obj_get_user_data(list));
app_event_emit_close(ctx->appInstanceId);
}
}
void onAppLongPressed(lv_event_t* e) {
const auto* manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
lv_obj_t* list = lv_obj_get_parent(lv_event_get_target_obj(e));
auto* ctx = static_cast<Context*>(lv_obj_get_user_data(list));
if (!ctx->favourites.toggle(manifest->id)) {
LOG_E(TAG, "Failed to save favourites");
}
// Deferred: populateList() deletes this button via lv_obj_clean() while its own long-press is still dispatching.
lv_async_call(deferredRepopulate, list);
}
void onAppKeyPressed(lv_event_t* e) {
const uint32_t key = lv_event_get_key(e);
if (key != 'f' && key != 'F') {
return;
}
const auto* manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
lv_obj_t* list = lv_obj_get_parent(lv_event_get_target_obj(e));
auto* ctx = static_cast<Context*>(lv_obj_get_user_data(list));
if (!ctx->favourites.toggle(manifest->id)) {
LOG_E(TAG, "Failed to save favourites");
}
// Deferred: populateList() deletes this button via lv_obj_clean() while its own key event is still dispatching.
lv_async_call(deferredRepopulate, list);
}
void onBackPressed(lv_event_t* event) {
@ -37,13 +87,24 @@ void onBackPressed(lv_event_t* event) {
app_event_emit_close(ctx->appInstanceId);
}
void createAppWidget(const ::AppManifest* manifest, lv_obj_t* list) {
// The new AppManifest has no per-app icon - use a shared generic one for every entry,
// same fallback the old model used for apps that didn't provide one.
lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_TOOLBAR, manifest->name);
void onHelpPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
alertdialog::start(ctx->appInstanceId, "Help", "Long-press an app, or press F, to toggle it as a favorite.");
}
lv_obj_t* createAppWidget(const ::AppManifest* manifest, lv_obj_t* list, bool favourite) {
// Plain "*" prefix, not an icon: shared Material Symbols font is subsetted and has no star glyph.
const std::string label = favourite ? (std::string("* ") + manifest->name) : manifest->name;
lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_TOOLBAR, label.c_str());
lv_obj_t* image = lv_obj_get_child(btn, 0);
lv_obj_set_style_text_font(image, lvgl_get_shared_icon_font(), LV_PART_MAIN);
// Not read by any event here (those get the manifest via their own callback user data) - lets
// populateList() identify a button by app id after a rebuild, to restore focus onto it.
lv_obj_set_user_data(btn, const_cast<::AppManifest*>(manifest));
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, const_cast<::AppManifest*>(manifest));
lv_obj_add_event_cb(btn, &onAppLongPressed, LV_EVENT_LONG_PRESSED, const_cast<::AppManifest*>(manifest));
lv_obj_add_event_cb(btn, &onAppKeyPressed, LV_EVENT_KEY, const_cast<::AppManifest*>(manifest));
return btn;
}
void collectManifest(const ::AppManifest* manifest, void* context) {
@ -51,34 +112,73 @@ void collectManifest(const ::AppManifest* manifest, void* context) {
manifests->push_back(manifest);
}
void populateList(lv_obj_t* list) {
// Captured before lv_obj_clean() deletes the currently focused button below: LVGL moves
// focus elsewhere as it leaves the group, and creating replacement buttons doesn't restore
// it, so the app id is remembered here and re-focused on its new button once rebuilt.
std::string focusedAppId;
lv_group_t* group = lv_group_get_default();
if (group != nullptr) {
lv_obj_t* focused = lv_group_get_focused(group);
if (focused != nullptr && lv_obj_get_parent(focused) == list) {
const auto* focusedManifest = static_cast<const ::AppManifest*>(lv_obj_get_user_data(focused));
if (focusedManifest != nullptr) {
focusedAppId = focusedManifest->id;
}
}
}
lv_obj_clean(list);
auto* ctx = static_cast<Context*>(lv_obj_get_user_data(list));
const std::vector<std::string> favouriteIds = ctx->favourites.load();
std::vector<const ::AppManifest*> manifests;
app_manager_for_each_manifest(collectManifest, &manifests);
std::ranges::sort(manifests, [&](const ::AppManifest* a, const ::AppManifest* b) {
const bool aFavourite = Favourites::contains(favouriteIds, a->id);
const bool bFavourite = Favourites::contains(favouriteIds, b->id);
if (aFavourite != bFavourite) {
return aFavourite;
}
return strcmp(a->name, b->name) < 0;
});
lv_obj_t* focusedButton = nullptr;
for (const auto* manifest: manifests) {
bool is_valid_category = (manifest->category == APP_CATEGORY_USER) || (manifest->category == APP_CATEGORY_SYSTEM);
if (is_valid_category && (manifest->flags & APP_MANIFEST_FLAG_HIDDEN) == 0) {
lv_obj_t* btn = createAppWidget(manifest, list, Favourites::contains(favouriteIds, manifest->id));
if (!focusedAppId.empty() && focusedAppId == manifest->id) {
focusedButton = btn;
}
}
}
if (focusedButton != nullptr) {
lv_group_focus_obj(focusedButton);
lv_obj_add_state(focusedButton, LV_STATE_FOCUS_KEY);
}
}
void createWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData);
// Flex column + flex_grow so LVGL recomputes the toolbar/list split on every layout pass,
// rather than a fixed height computed once from lv_obj_get_content_height(parent) that would
// go stale after a later display resolution/rotation change.
// Flex column + flex_grow so the toolbar/list split recomputes on layout instead of going stale after a resize.
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, "Apps");
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
lvgl_toolbar_add_text_button_action(toolbar, "?", onHelpPressed, ctx);
lv_obj_t* list = lv_list_create(parent);
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
lv_obj_set_user_data(list, ctx);
lv_obj_add_event_cb(list, &onListDeleted, LV_EVENT_DELETE, nullptr);
std::vector<const ::AppManifest*> manifests;
app_manager_for_each_manifest(collectManifest, &manifests);
std::ranges::sort(manifests, [](const ::AppManifest* a, const ::AppManifest* b) {
return strcmp(a->name, b->name) < 0;
});
for (const auto* manifest: manifests) {
bool is_valid_category = (manifest->category == APP_CATEGORY_USER) || (manifest->category == APP_CATEGORY_SYSTEM);
if (is_valid_category && (manifest->flags & APP_MANIFEST_FLAG_HIDDEN) == 0) {
createAppWidget(manifest, list);
}
}
populateList(list);
}
int32_t appMain(int argc, char* argv[]) {
@ -99,10 +199,17 @@ int32_t appMain(int argc, char* argv[]) {
bool shouldClose = false;
AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
switch (event.type) {
case APP_EVENT_CLOSE:
shouldClose = true;
break;
case APP_EVENT_RESULT:
app_manager_stop(event.result.launch_id);
break;
default:
break;
}
if (shouldClose) break;
}
if (shouldClose) break;
}
@ -121,7 +228,7 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 2400, .desired_memory_capability = 0 },
.stack = { .depth = 4000, .desired_memory_capability = 0 },
};
} // namespace

View File

@ -183,7 +183,9 @@ int32_t appMain(int argc, char* argv[]) {
void start(const std::string& packageId) {
const char* argv[] = { packageId.c_str() };
uint32_t instanceId = 0;
app_start(manifest.id, 1, argv, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, 1, argv);
app_start_with_context(&context, &instanceId);
}
extern const ::AppManifest manifest = {

View File

@ -291,7 +291,10 @@ void startNextApp() {
auto launcher_app_id = getLauncherAppId();
uint32_t launcher_instance_id = 0;
app_start(launcher_app_id.c_str(), 0, nullptr, &launcher_instance_id);
AppStartContext context;
if (app_start_context_from_id(launcher_app_id.c_str(), &context) == ERROR_NONE) {
app_start_with_context(&context, &launcher_instance_id);
}
}
void runBootSequence(TickType_t startTime) {

View File

@ -237,7 +237,8 @@ int32_t appMain(int argc, char* argv[]) {
uint32_t start() {
uint32_t instanceId = 0;
app_start(manifest.id, 0, nullptr, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_with_context(&context, &instanceId);
return instanceId;
}

View File

@ -263,7 +263,9 @@ int32_t appMain(int argc, char* argv[]) {
void start(const std::string& addrHex) {
const char* argv[] = { addrHex.c_str() };
uint32_t instanceId = 0;
app_start(manifest.id, 1, argv, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, 1, argv);
app_start_with_context(&context, &instanceId);
}
extern const ::AppManifest manifest = {

View File

@ -273,7 +273,8 @@ int32_t appMain(int argc, char* argv[]) {
void start() {
uint32_t instanceId = 0;
app_start(manifest.id, 0, nullptr, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_with_context(&context, &instanceId);
}
extern const ::AppManifest manifest = {

View File

@ -1,6 +1,7 @@
#include <app/event.h>
#include <app/execute.h>
#include <app/install.h>
#include <app/start.h>
#include <app/stream.h>
#include <lvgl/lvgl.h>
@ -224,7 +225,8 @@ void View::runFile(const std::string& file_path) {
AppLocation location { APP_LOCATION_PATH, const_cast<char*>(file_path.c_str()) };
AppInstanceId instance_id = 0;
if (app_execute(location, AppStackConfig {}, 0, nullptr, &instance_id) != ERROR_NONE) {
AppStartContext context = app_start_context_for_location(location);
if (app_start_with_context(&context, &instance_id) != ERROR_NONE) {
LOG_W(TAG, "Failed to run %s", file_path.c_str());
alertdialog::start(appInstanceId, "Run failed", "Could not run \"" + file::getLastPathSegment(file_path) + "\".");
}

View File

@ -107,7 +107,11 @@ uint32_t startWithMode(const char* modeArg, uint32_t callerAppInstanceId, AppStr
.event_group = eventGroup,
};
uint32_t instanceId = 0;
app_start_for_result_with_streams(manifest.id, 1, argv, &binding, 1, callerAppInstanceId, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, 1, argv);
app_start_context_set_streams(&context, &binding, 1);
app_start_context_set_parent(&context, callerAppInstanceId);
app_start_with_context(&context, &instanceId);
return instanceId;
}

View File

@ -74,7 +74,8 @@ void onAddGpsPressed(lv_event_t* event) {
// this app; rebuildDeviceList() runs fresh whenever this app is resumed regardless).
(void)ctx;
uint32_t instanceId = 0;
app_start(addgps::manifest.id, 0, nullptr, &instanceId);
AppStartContext context = app_start_context_for_manifest(&addgps::manifest);
app_start_with_context(&context, &instanceId);
}
void onDeviceButtonPressed(lv_event_t* event) {

View File

@ -433,7 +433,8 @@ extern const ::AppManifest manifest = {
uint32_t start() {
uint32_t instanceId = 0;
app_start(manifest.id, 0, nullptr, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_with_context(&context, &instanceId);
return instanceId;
}

View File

@ -124,7 +124,9 @@ int32_t appMain(int argc, char* argv[]) {
void start(const std::string& file) {
const char* argv[] = { file.c_str() };
uint32_t instanceId = 0;
app_start(manifest.id, 1, argv, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, 1, argv);
app_start_with_context(&context, &instanceId);
}
extern const ::AppManifest manifest = {

View File

@ -156,7 +156,11 @@ uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std
.event_group = eventGroup,
};
uint32_t instanceId = 0;
app_start_for_result_with_streams(manifest.id, 3, argv, &binding, 1, callerAppInstanceId, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, 3, argv);
app_start_context_set_streams(&context, &binding, 1);
app_start_context_set_parent(&context, callerAppInstanceId);
app_start_with_context(&context, &instanceId);
return instanceId;
}

View File

@ -48,7 +48,10 @@ int32_t computeButtonMargin(int32_t available_span, int32_t total_button_size) {
void onAppPressed(lv_event_t* e) {
auto* appId = static_cast<const char*>(lv_event_get_user_data(e));
uint32_t instance_id = 0;
app_start(appId, 0, nullptr, &instance_id);
AppStartContext context;
if (app_start_context_from_id(appId, &context) == ERROR_NONE) {
app_start_with_context(&context, &instance_id);
}
}
lv_obj_t* createAppButton(lv_obj_t* parent, UiDensity uiDensity, const char* imageFile, const char* appId, int32_t itemMargin, bool isLandscape) {
@ -217,7 +220,8 @@ void runAutoStart() {
) {
LOG_I(TAG, "Starting %s", CONFIG_TT_AUTO_START_APP_ID);
uint32_t app_launch_id;
app_start(CONFIG_TT_AUTO_START_APP_ID, 0, nullptr, &app_launch_id);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_with_context(&context, &app_launch_id);
} else if (
// Auto-start due to user configuration
settings::loadBootSettings(boot_properties) &&
@ -226,7 +230,8 @@ void runAutoStart() {
) {
LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str());
uint32_t app_launch_id;
app_start(boot_properties.autoStartAppId.c_str(), 0, nullptr, &app_launch_id);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_with_context(&context, &app_launch_id);
} else {
// No auto-start, consider running system setup
if (!setup::isCompleted()) {
@ -310,7 +315,8 @@ extern const ::AppManifest manifest = {
// used by the old, unconverted CrashDiagnostics app to return to the launcher after a crash).
uint32_t start() {
uint32_t instance_id = 0;
app_start(manifest.id, 0, nullptr, &instance_id);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_with_context(&context, &instance_id);
return instance_id;
}

View File

@ -269,7 +269,9 @@ int32_t appMain(int argc, char* argv[]) {
void start(const std::string& filePath) {
const char* argv[] = { filePath.c_str() };
uint32_t instanceId = 0;
app_start(manifest.id, 1, argv, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, 1, argv);
app_start_with_context(&context, &instanceId);
}
extern const ::AppManifest manifest = {

View File

@ -134,7 +134,7 @@ int32_t appMain(int argc, char* argv[]) {
namespace {
// Builds argv = [title, items...] for app_start_for_result().
// Builds argv = [title, items...] for app_start_with_context().
std::vector<const char*> buildArgv(const std::string& title, const std::vector<std::string>& items) {
std::vector<const char*> argv { title.c_str() };
for (const auto& item: items) {
@ -148,7 +148,10 @@ std::vector<const char*> buildArgv(const std::string& title, const std::vector<s
AppInstanceId start(AppInstanceId callerAppInstanceId, const std::string& title, const std::vector<std::string>& items) {
auto argv = buildArgv(title, items);
AppInstanceId instanceId = 0;
app_start_for_result(manifest.id, static_cast<int>(argv.size()), argv.data(), callerAppInstanceId, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, static_cast<int>(argv.size()), argv.data());
app_start_context_set_parent(&context, callerAppInstanceId);
app_start_with_context(&context, &instanceId);
return instanceId;
}

View File

@ -29,7 +29,8 @@ void onAppPressed(lv_event_t* e) {
// Fire-and-forget top-level navigation, same as AppList's own app-launch buttons.
const auto* manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
uint32_t instanceId = 0;
app_start(manifest->id, 0, nullptr, &instanceId);
AppStartContext context = app_start_context_for_manifest(manifest);
app_start_with_context(&context, &instanceId);
}
void onBackPressed(lv_event_t* event) {

View File

@ -270,7 +270,8 @@ int32_t appMain(int argc, char* argv[]) {
void start() {
uint32_t instanceId = 0;
app_start(manifest.id, 0, nullptr, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_with_context(&context, &instanceId);
}
extern const ::AppManifest manifest = {

View File

@ -8,6 +8,7 @@
#include <app/io.h>
#include <app/manager.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h>
#include <tactility/error.h>
@ -273,10 +274,11 @@ int runElf(const char* resolvedPath, int argc, char** argv) {
AppLocation location { APP_LOCATION_PATH, const_cast<char*>(resolvedPath) };
AppInstanceId childId = 0;
error_t result = app_execute_for_result_with_streams(
location, AppStackConfig {}, passedArgc, rewritten,
bindings, sizeof(bindings) / sizeof(bindings[0]),
app_scheduler_current_app_id(), &childId);
AppStartContext context = app_start_context_for_location(location);
app_start_context_set_arguments_ext(&context, passedArgc, rewritten);
app_start_context_set_streams(&context, bindings, sizeof(bindings) / sizeof(bindings[0]));
app_start_context_set_parent(&context, app_scheduler_current_app_id());
error_t result = app_start_with_context(&context, &childId);
if (result == ERROR_NONE) {
app_stream_bind_alias_fd(&stdoutStream, STDERR_FILENO);
}

View File

@ -1,75 +1,20 @@
#include <Tactility/app/shell/commands/Commands.h>
#include <cstdint>
#include <tactility/memory.h>
#include <cstdio>
#include <unistd.h>
#ifdef ESP_PLATFORM
#include <esp_heap_caps.h>
#elif defined(__APPLE__)
#include <mach/mach.h>
#include <mach/mach_host.h>
#include <sys/sysctl.h>
#endif
#if defined(__APPLE__) && !defined(ESP_PLATFORM)
// No sysconf(_SC_PHYS_PAGES/_SC_AVPHYS_PAGES) on macOS: total comes from sysctl, free from the
// Mach host VM statistics.
inline uint64_t appleTotalMemory() {
uint64_t memSize = 0;
size_t memSizeLen = sizeof(memSize);
if (sysctlbyname("hw.memsize", &memSize, &memSizeLen, nullptr, 0) != 0) {
return 0;
}
return memSize;
}
inline uint64_t appleFreeMemory() {
mach_port_t host = mach_host_self();
vm_size_t pageSize = 0;
vm_statistics64_data_t vmStats {};
mach_msg_type_number_t vmStatsCount = HOST_VM_INFO64_COUNT;
if (host_page_size(host, &pageSize) != KERN_SUCCESS ||
host_statistics64(host, HOST_VM_INFO64, reinterpret_cast<host_info64_t>(&vmStats), &vmStatsCount) != KERN_SUCCESS) {
return 0;
}
return static_cast<uint64_t>(vmStats.free_count + vmStats.inactive_count) * pageSize;
}
#elif !defined(ESP_PLATFORM)
inline uint64_t pagesToBytes(int pagesParam) {
const long pages = sysconf(pagesParam);
const long pageSize = sysconf(_SC_PAGESIZE);
if (pages < 0 || pageSize < 0) {
return 0;
}
return static_cast<uint64_t>(pages) * static_cast<uint64_t>(pageSize);
}
#endif
inline uint64_t getHeapTotal() {
#ifdef ESP_PLATFORM
return heap_caps_get_total_size(MALLOC_CAP_INTERNAL);
#elif defined(__APPLE__)
return appleTotalMemory();
#else
return pagesToBytes(_SC_PHYS_PAGES);
#endif
}
inline uint64_t getHeapFree() {
#ifdef ESP_PLATFORM
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
#elif defined(__APPLE__)
return appleFreeMemory();
#else
return pagesToBytes(_SC_AVPHYS_PAGES);
#endif
}
int cmdFree(int, char**) {
int cmdFree(int /*unused*/, char** /*unused*/) {
puts(" total free");
printf("Heap %15llu %15llu\n", static_cast<unsigned long long>(getHeapTotal()), static_cast<unsigned long long>(getHeapFree()));
#ifdef ESP_PLATFORM
printf("Heap %15zu %15zu\n", memory_heap_total(), memory_heap_free());
printf("External %15zu %15zu\n", heap_caps_get_total_size(MALLOC_CAP_SPIRAM), heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
#else
// Not the process's own heap capacity - see memory_heap_total()'s doc comment.
printf("System %15zu %15zu\n", memory_heap_total(), memory_heap_free());
#endif
return 0;
}

View File

@ -51,9 +51,14 @@ void runShell(int columns, volatile bool* stopRequested) {
const char* argv[] = { "shell", columnsArg };
AppInstanceId shellId = 0;
error_t result = app_start_for_result_with_streams(
"shell", 2, argv, bindings, sizeof(bindings) / sizeof(bindings[0]),
app_scheduler_current_app_id(), &shellId);
AppStartContext context;
error_t result = app_start_context_from_id("shell", &context);
if (result == ERROR_NONE) {
app_start_context_set_arguments_ext(&context, 2, argv);
app_start_context_set_streams(&context, bindings, sizeof(bindings) / sizeof(bindings[0]));
app_start_context_set_parent(&context, app_scheduler_current_app_id());
result = app_start_with_context(&context, &shellId);
}
if (result != ERROR_NONE) {
LOG_E(TAG, "Failed to start shell app");
app_event_unsubscribe(&eventSub);

View File

@ -5,23 +5,20 @@
#include <font/fonts.h>
#include <font/render.h>
#include <graphics/pixel_buffer.h>
// The font struct to use, e.g. ibmplexmono_14_font (see font/fonts.h) - set per device/build via
// a compile definition; that font's Kconfig entry (Modules/font-module/Kconfig) must be enabled.
// a compile definition; that font's Kconfig entry (Modules/graphics-module/Kconfig) must be enabled.
#ifndef TT_TERMINAL_FONT_SYMBOL
#error TT_TERMINAL_FONT_SYMBOL is not set
#endif
#include <tactility/drivers/display.h>
#include <tactility/log.h>
#include <tactility/memory.h>
#include <tactility/time.h>
#ifdef ESP_PLATFORM
#include <esp_cache.h>
#endif
#include <algorithm>
#include <cstdlib>
#include <cstring>
extern "C" {
#include <Tactility/app/terminal/vterm/vterm.h>
@ -34,13 +31,9 @@ namespace {
// Half-period of the cursor blink: on for this long, then off for this long.
constexpr uint32_t CURSOR_BLINK_INTERVAL_MS = 500;
/*
* The 16 ANSI colours come from vterm rather than being duplicated here, so vterm's palette stays
* the single source of truth: a program that recolours the terminal (plasma sets a 16-entry RGB565
* ramp) actually affects what appears.
*
* Index is vterm's 4-bit colour: 0-7 normal, 8-15 bright.
*/
// Palette comes from vterm rather than being duplicated here, so a program that recolours the
// terminal (e.g. plasma) actually affects what appears. Index is vterm's 4-bit colour: 0-7
// normal, 8-15 bright.
inline uint16_t paletteColour(uint8_t index) {
return vterm_get_palette()[index & 0x0F];
}
@ -49,6 +42,7 @@ inline uint16_t paletteColour(uint8_t index) {
bool TerminalRenderer::allocateCommon(Device* displayDevice) {
display = displayDevice;
monochrome = display_get_color_format(display) == DISPLAY_COLOR_FORMAT_MONOCHROME;
cellWidth = TT_TERMINAL_FONT_SYMBOL.glyph_width;
cellHeight = TT_TERMINAL_FONT_SYMBOL.glyph_height;
@ -66,29 +60,22 @@ bool TerminalRenderer::allocateCommon(Device* displayDevice) {
return false;
}
/*
* The grid rarely divides the panel exactly (720 pixels of height at 32 pixels a cell leaves
* half a cell over), so the slack is split between the two edges rather than all left at the
* bottom, where it shows as a strip of dead space below the last row.
*/
// The grid rarely divides the panel exactly, so the leftover is split between both edges
// rather than left as dead space on one side.
originX = (frameWidth - cols * cellWidth) / 2;
originY = (frameHeight - rowCount * cellHeight) / 2;
const size_t frameBytes = static_cast<size_t>(frameWidth) * frameHeight * sizeof(uint16_t);
// 64-byte aligned for DMA; PSRAM is fine at this size and internal RAM is scarce.
const MemoryPolicy frameBufferPolicy { .required = MEMORY_CAPABILITY_EXTERNAL, .desired = 0, .alignment = 64 };
frameBuffer = static_cast<uint16_t*>(memory_alloc_with_policy(frameBytes, &frameBufferPolicy));
const enum DisplayColorFormat colorFormat = display_get_color_format(display);
frameBuffer = pixel_buffer_create(colorFormat, frameWidth, cellHeight);
if (frameBuffer == nullptr) {
LOG_E(TAG, "Failed to allocate %u byte frame buffer", (unsigned)frameBytes);
LOG_E(TAG, "Failed to allocate frame buffer");
return false;
}
memset(frameBuffer, 0, frameBytes);
shadow = static_cast<Cell*>(calloc(static_cast<size_t>(cols) * rowCount, sizeof(Cell)));
if (shadow == nullptr) {
LOG_E(TAG, "Failed to allocate shadow grid");
memory_free(frameBuffer);
pixel_buffer_free(frameBuffer);
frameBuffer = nullptr;
return false;
}
@ -98,12 +85,20 @@ bool TerminalRenderer::allocateCommon(Device* displayDevice) {
void TerminalRenderer::freeCommon() {
if (frameBuffer != nullptr) {
memory_free(frameBuffer);
pixel_buffer_free(frameBuffer);
frameBuffer = nullptr;
}
if (fullFrameBuffer != nullptr) {
pixel_buffer_free(fullFrameBuffer);
fullFrameBuffer = nullptr;
}
fullFrameRequired = false;
fullFrameDirty = false;
free(shadow);
shadow = nullptr;
usingHwFrameBuffer = false;
pixel_buffer_free(hwFrameBuffers[0]);
pixel_buffer_free(hwFrameBuffers[1]);
hwFrameBuffers[0] = nullptr;
hwFrameBuffers[1] = nullptr;
display = nullptr;
@ -121,28 +116,100 @@ bool TerminalRenderer::acquireHwDoubleBuffer() {
return false;
}
hwFrameBuffers[0] = static_cast<uint16_t*>(fb0);
hwFrameBuffers[1] = static_cast<uint16_t*>(fb1);
const enum DisplayColorFormat colorFormat = display_get_color_format(display);
hwFrameBuffers[0] = pixel_buffer_wrap(fb0, colorFormat, panelWidth, panelHeight, 0);
hwFrameBuffers[1] = pixel_buffer_wrap(fb1, colorFormat, panelWidth, panelHeight, 0);
if (hwFrameBuffers[0] == nullptr || hwFrameBuffers[1] == nullptr) {
pixel_buffer_free(hwFrameBuffers[0]);
pixel_buffer_free(hwFrameBuffers[1]);
hwFrameBuffers[0] = nullptr;
hwFrameBuffers[1] = nullptr;
return false;
}
usingHwFrameBuffer = true;
backBufferIndex = 1;
const size_t panelBytes = static_cast<size_t>(panelWidth) * panelHeight * sizeof(uint16_t);
memset(hwFrameBuffers[0], 0, panelBytes);
memset(hwFrameBuffers[1], 0, panelBytes);
#ifdef ESP_PLATFORM
esp_cache_msync(hwFrameBuffers[0], panelBytes, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
esp_cache_msync(hwFrameBuffers[1], panelBytes, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
#endif
pixel_buffer_clear(hwFrameBuffers[0]);
pixel_buffer_clear(hwFrameBuffers[1]);
pixel_buffer_msync(hwFrameBuffers[0], 0, 0, panelWidth, panelHeight);
pixel_buffer_msync(hwFrameBuffers[1], 0, 0, panelWidth, panelHeight);
LOG_I(TAG, "Using hardware double buffering");
return true;
}
void TerminalRenderer::clearPanelOnce() {
if (frameBuffer == nullptr) {
return;
}
pixel_buffer_clear(frameBuffer);
for (int y = 0; y < frameHeight; y += cellHeight) {
currentRowYOffset = y;
present(y, std::min(frameHeight, y + cellHeight));
}
currentRowYOffset = 0;
finishFrame();
}
bool TerminalRenderer::allocateFullFrameBufferIfNeeded() {
fullFrameRequired = monochrome || display_has_capability(display, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME);
if (!fullFrameRequired || usingHwFrameBuffer) {
return true;
}
fullFrameBuffer = pixel_buffer_create(display_get_color_format(display), panelWidth, panelHeight);
if (fullFrameBuffer == nullptr) {
LOG_E(TAG, "Failed to allocate full-frame buffer");
return false;
}
return true;
}
void TerminalRenderer::ensureBackBufferSeeded() {
if (!usingHwFrameBuffer || hwBufferDirty) {
return;
}
pixel_buffer_blit(hwFrameBuffers[backBufferIndex], 0, 0, hwFrameBuffers[1 - backBufferIndex], 0, 0, panelWidth, panelHeight, PIXEL_BUFFER_CONVERSION_EXACT_BLACK);
pixel_buffer_msync(hwFrameBuffers[backBufferIndex], 0, 0, panelWidth, panelHeight);
}
void TerminalRenderer::presentRegion(PixelBuffer* src, int regionX, int regionY, int regionW, int regionH) {
if (usingHwFrameBuffer) {
ensureBackBufferSeeded();
pixel_buffer_blit(hwFrameBuffers[backBufferIndex], regionX, regionY, src, 0, 0, regionW, regionH, PIXEL_BUFFER_CONVERSION_EXACT_BLACK);
pixel_buffer_msync(hwFrameBuffers[backBufferIndex], regionX, regionY, regionW, regionH);
hwBufferDirty = true;
} else if (fullFrameRequired) {
// No seeding needed here, unlike the hw double buffer: there's only one such buffer, so
// it stays correct incrementally across calls.
pixel_buffer_blit(fullFrameBuffer, regionX, regionY, src, 0, 0, regionW, regionH, PIXEL_BUFFER_CONVERSION_EXACT_BLACK);
fullFrameDirty = true;
} else {
display_draw_bitmap(display, regionX, regionY, regionX + regionW, regionY + regionH, pixel_buffer_get_data(src));
}
}
void TerminalRenderer::finishFrame() {
if (usingHwFrameBuffer && hwBufferDirty) {
display_draw_bitmap(display, 0, 0, panelWidth, panelHeight, pixel_buffer_get_data(hwFrameBuffers[backBufferIndex]));
backBufferIndex = 1 - backBufferIndex;
hwBufferDirty = false;
} else if (fullFrameRequired && fullFrameDirty) {
display_draw_bitmap(display, 0, 0, panelWidth, panelHeight, pixel_buffer_get_data(fullFrameBuffer));
fullFrameDirty = false;
}
}
void TerminalRenderer::paintCell(int row, int col, char ch, uint8_t attr) {
const uint16_t fg = paletteColour(VTERM_ATTR_FG(attr));
const uint16_t bg = paletteColour(VTERM_ATTR_BG(attr));
uint16_t* out = &frameBuffer[(originY + row * cellHeight) * frameWidth + originX + col * cellWidth];
font_render_char(&TT_TERMINAL_FONT_SYMBOL, ch, fg, bg, out, frameWidth);
const int pixelY = originY + row * cellHeight - currentRowYOffset;
const int pixelX = originX + col * cellWidth;
font_render_char_pixel_buffer_rgb565(frameBuffer, pixelX, pixelY, &TT_TERMINAL_FONT_SYMBOL, ch, fg, bg, PIXEL_BUFFER_CONVERSION_EXACT_BLACK);
}
void TerminalRenderer::paintCursor(int row, int col) {
@ -150,11 +217,11 @@ void TerminalRenderer::paintCursor(int row, int col) {
return;
}
// The cell underneath comes from the shadow rather than from vterm's grid directly, since it
// reflects what render() actually drew this frame (e.g. a row served from scrollback).
// From the shadow, not vterm's grid directly, since it reflects what render() actually drew
// this frame (e.g. a row served from scrollback).
const Cell& cell = shadow[row * cols + col];
// Foreground and background swapped, which reads as a block cursor over whatever is there.
// Swapped fg/bg reads as a block cursor over whatever is there.
const uint8_t inverted = VTERM_ATTR(VTERM_ATTR_BG(cell.attr), VTERM_ATTR_FG(cell.attr));
paintCell(row, col, cell.ch, inverted);
}
@ -165,52 +232,17 @@ void TerminalRenderer::render(bool force) {
return;
}
bool changed = force;
// vterm tracks whether the active VT's cells or cursor changed since the last call; when
// neither that nor a forced repaint applies, rows the cursor doesn't touch skip the
// comparison scan below entirely.
const bool textDirty = force || vterm_take_dirty();
// Scratch row used when a line comes from scrollback rather than from the live terminal.
vterm_cell_t historyRow[VTERM_COLS];
/*
* Undo the cursor before comparing against the shadow.
*
* The cursor is drawn with inverted attributes that were never in the shadow, so leaving it
* there would make its cell compare equal and strand an inverted block on screen.
*/
if (cursorDrawn && cursorRow >= 0 && cursorRow < rowCount && cursorCol >= 0 && cursorCol < cols) {
char ch = ' ';
uint8_t attr = VTERM_DEFAULT_ATTR;
if (cells != nullptr) {
const vterm_cell_t& under = cells[cursorRow * VTERM_COLS + cursorCol];
ch = under.ch;
attr = under.attr;
}
paintCell(cursorRow, cursorCol, ch, attr);
cursorDrawn = false;
changed = true;
}
for (int row = 0; row < rowCount; row++) {
// While the view is scrolled back, the upper rows are served from history and the rest
// still come from the live screen, so the two meet seamlessly mid-scroll.
const bool fromHistory = Scrollback::rowForDisplay(row, rowCount, historyRow);
const vterm_cell_t* source = fromHistory ? historyRow : &cells[row * VTERM_COLS];
for (int col = 0; col < cols; col++) {
const char ch = source[col].ch;
const uint8_t attr = source[col].attr;
Cell& previous = shadow[row * cols + col];
if (!force && ch == previous.ch && attr == previous.attr) {
continue;
}
paintCell(row, col, ch, attr);
previous = Cell { ch, attr };
changed = true;
}
}
// Where to undo the cursor, if drawn: its position last frame, captured before this frame's
// blink/move below can change it. Its attributes were never written to the shadow, so
// leaving it would strand an inverted block that always compares equal.
const bool undrawCursor = cursorDrawn && cursorRow >= 0 && cursorRow < rowCount && cursorCol >= 0 && cursorCol < cols;
const int undrawRow = cursorRow;
const int undrawCol = cursorCol;
// Advance the blink on wall-clock time, so the rate doesn't depend on how often render() runs.
const uint32_t now = get_micros_since_boot() / 1000;
@ -219,16 +251,16 @@ void TerminalRenderer::render(bool force) {
blinkOn = !blinkOn;
}
int col = 0;
int row = 0;
int redrawCol = 0;
int redrawRow = 0;
int visible = 0;
vterm_get_cursor(vterm_get_active(), &col, &row, &visible);
vterm_get_cursor(vterm_get_active(), &redrawCol, &redrawRow, &visible);
// Typing should not make the cursor flicker off mid-keystroke, so restart the blink cycle
// whenever it moves: the cursor is then solid for a full interval at its new position.
if (row != cursorRow || col != cursorCol) {
cursorRow = row;
cursorCol = col;
if (redrawRow != cursorRow || redrawCol != cursorCol) {
cursorRow = redrawRow;
cursorCol = redrawCol;
blinkOn = true;
lastBlinkMs = now;
}
@ -238,15 +270,65 @@ void TerminalRenderer::render(bool force) {
visible = 0;
}
if (visible && blinkOn && row >= 0 && row < rowCount && col >= 0 && col < cols) {
paintCursor(row, col);
cursorDrawn = true;
changed = true;
const bool redrawCursor = visible && blinkOn && redrawRow >= 0 && redrawRow < rowCount && redrawCol >= 0 && redrawCol < cols;
if (!textDirty && !undrawCursor && !redrawCursor) {
return;
}
// Presenting is a whole-frame operation, so it only runs when something actually changed; an
// idle prompt costs only the comparison loop and the blink toggle.
if (changed) {
present();
// Scratch row used when a line comes from scrollback rather than from the live terminal.
vterm_cell_t historyRow[VTERM_COLS];
for (int row = 0; row < rowCount; row++) {
const bool touchesCursor = (undrawCursor && undrawRow == row) || (redrawCursor && redrawRow == row);
if (!textDirty && !touchesCursor) {
continue;
}
// While the view is scrolled back, the upper rows are served from history and the rest
// still come from the live screen, so the two meet seamlessly mid-scroll.
const bool fromHistory = Scrollback::rowForDisplay(row, rowCount, historyRow);
const vterm_cell_t* source = fromHistory ? historyRow : &cells[row * VTERM_COLS];
bool rowChanged = force || touchesCursor;
if (!rowChanged) {
// Cheap comparison-only pass: an idle row costs just this, no painting.
for (int col = 0; col < cols; col++) {
const Cell& previous = shadow[row * cols + col];
if (source[col].ch != previous.ch || source[col].attr != previous.attr) {
rowChanged = true;
break;
}
}
}
if (!rowChanged) {
continue;
}
currentRowYOffset = originY + row * cellHeight;
if (undrawCursor && undrawRow == row) {
const vterm_cell_t& under = cells[undrawRow * VTERM_COLS + undrawCol];
paintCell(undrawRow, undrawCol, under.ch, under.attr);
cursorDrawn = false;
}
// frameBuffer is reused for every row, so every cell must be repainted here, not just the
// ones that differ from shadow, or an unpainted cell presents the previous row's pixels.
for (int col = 0; col < cols; col++) {
const char ch = source[col].ch;
const uint8_t attr = source[col].attr;
paintCell(row, col, ch, attr);
shadow[row * cols + col] = Cell { ch, attr };
}
if (redrawCursor && redrawRow == row) {
paintCursor(redrawRow, redrawCol);
cursorDrawn = true;
}
present(currentRowYOffset, currentRowYOffset + cellHeight);
}
finishFrame();
}

View File

@ -1,14 +1,10 @@
#include <Tactility/app/terminal/TerminalRendererGeneric.h>
#include <graphics/pixel_buffer.h>
#include <tactility/drivers/display.h>
#include <tactility/log.h>
#ifdef ESP_PLATFORM
#include <esp_cache.h>
#endif
#include <cstring>
constexpr auto* TAG = "TermRenderGen";
TerminalRendererGeneric::~TerminalRendererGeneric() {
@ -19,7 +15,6 @@ bool TerminalRendererGeneric::begin(Device* displayDevice) {
panelWidth = display_get_resolution_x(displayDevice);
panelHeight = display_get_resolution_y(displayDevice);
// No rotation hardware here, so the terminal is drawn in the panel's own orientation.
frameWidth = panelWidth;
frameHeight = panelHeight;
@ -27,8 +22,14 @@ bool TerminalRendererGeneric::begin(Device* displayDevice) {
return false;
}
// Best-effort: falls back to pushing frameBuffer directly in present() if unavailable.
acquireHwDoubleBuffer();
acquireHwDoubleBuffer(); // best-effort; present() falls back to pushing frameBuffer directly
if (!allocateFullFrameBufferIfNeeded()) {
freeCommon();
return false;
}
clearPanelOnce();
LOG_I(TAG, "Terminal %dx%d cells (%dx%d px) on %dx%d panel",
cols, rowCount, cellWidth, cellHeight, panelWidth, panelHeight);
@ -39,21 +40,7 @@ void TerminalRendererGeneric::end() {
freeCommon();
}
void TerminalRendererGeneric::present() {
const size_t frameBytes = static_cast<size_t>(frameWidth) * frameHeight * sizeof(uint16_t);
#ifdef ESP_PLATFORM
esp_cache_msync(frameBuffer, frameBytes, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
#endif
if (usingHwFrameBuffer) {
uint16_t* out = hwFrameBuffers[backBufferIndex];
memcpy(out, frameBuffer, frameBytes);
#ifdef ESP_PLATFORM
esp_cache_msync(out, frameBytes, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
#endif
display_draw_bitmap(display, 0, 0, frameWidth, frameHeight, out);
backBufferIndex = 1 - backBufferIndex;
} else {
display_draw_bitmap(display, 0, 0, frameWidth, frameHeight, frameBuffer);
}
void TerminalRendererGeneric::present(int yStart, int yEnd) {
pixel_buffer_msync(frameBuffer, 0, 0, frameWidth, yEnd - yStart);
presentRegion(frameBuffer, 0, yStart, frameWidth, yEnd - yStart);
}

View File

@ -1,5 +1,7 @@
#include <Tactility/app/terminal/TerminalRendererPpa.h>
#include <graphics/pixel_buffer.h>
#include <tactility/drivers/display.h>
#include <tactility/log.h>
@ -11,11 +13,6 @@
#include <driver/ppa.h>
#include <esp_cache.h>
#include <esp_heap_caps.h>
#include <cstring>
constexpr auto* TAG = "TermRenderPpa";
bool TerminalRendererPpa::isSupported() {
@ -27,7 +24,6 @@ TerminalRendererPpa::~TerminalRendererPpa() {
}
bool TerminalRendererPpa::begin(Device* displayDevice) {
// These report the panel's native orientation, which on Tab5 is portrait.
panelWidth = display_get_resolution_x(displayDevice);
panelHeight = display_get_resolution_y(displayDevice);
@ -39,16 +35,21 @@ bool TerminalRendererPpa::begin(Device* displayDevice) {
return false;
}
// Output: either the panel's own double buffers, or one PSRAM buffer we push manually.
if (monochrome) {
// No PPA hardware here rotates packed 1bpp data - refuse rather than feed it bits it was
// never designed for.
LOG_E(TAG, "Monochrome displays are not supported by the PPA-rotated renderer");
freeCommon();
return false;
}
if (!acquireHwDoubleBuffer()) {
const size_t frameBytes = static_cast<size_t>(frameWidth) * frameHeight * sizeof(uint16_t);
rotatedBuffer = static_cast<uint16_t*>(heap_caps_aligned_alloc(64, frameBytes, MALLOC_CAP_SPIRAM));
if (rotatedBuffer == nullptr) {
rotatedRowBuffer = pixel_buffer_create(display_get_color_format(displayDevice), cellHeight, panelHeight);
if (rotatedRowBuffer == nullptr) {
LOG_E(TAG, "Failed to allocate rotation output buffer");
end();
return false;
}
memset(rotatedBuffer, 0, frameBytes);
}
ppa_client_config_t ppaConfig = {
@ -65,6 +66,13 @@ bool TerminalRendererPpa::begin(Device* displayDevice) {
}
ppaClient = client;
if (!allocateFullFrameBufferIfNeeded()) {
end();
return false;
}
clearPanelOnce();
LOG_I(TAG, "Terminal %dx%d cells (%dx%d px), %dx%d landscape on %dx%d panel",
cols, rowCount, cellWidth, cellHeight,
frameWidth, frameHeight, panelWidth, panelHeight);
@ -76,28 +84,37 @@ void TerminalRendererPpa::end() {
ppa_unregister_client(static_cast<ppa_client_handle_t>(ppaClient));
ppaClient = nullptr;
}
if (rotatedBuffer != nullptr) {
heap_caps_free(rotatedBuffer);
rotatedBuffer = nullptr;
if (rotatedRowBuffer != nullptr) {
pixel_buffer_free(rotatedRowBuffer);
rotatedRowBuffer = nullptr;
}
freeCommon();
}
void TerminalRendererPpa::present() {
const size_t frameBytes = static_cast<size_t>(frameWidth) * frameHeight * sizeof(uint16_t);
esp_cache_msync(frameBuffer, frameBytes, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
void TerminalRendererPpa::present(int yStart, int yEnd) {
const int rowPixelHeight = yEnd - yStart;
pixel_buffer_msync(frameBuffer, 0, 0, frameWidth, rowPixelHeight);
void* data = pixel_buffer_get_data(frameBuffer);
uint16_t* out = usingHwFrameBuffer ? hwFrameBuffers[backBufferIndex] : rotatedBuffer;
// Rotates straight into hwFrameBuffers[backBufferIndex] below, bypassing presentRegion() - so
// it must seed that buffer itself before this row overwrites part of it.
if (usingHwFrameBuffer) {
ensureBackBufferSeeded();
}
// Rotate the landscape frame 90 degrees into the panel's portrait orientation. No scaling:
// the buffer was sized as the panel's transpose, so it maps 1:1.
// A 90-degree rotation swaps axes: this source y-row becomes an output x-strip.
// UNVERIFIED on hardware: assumes increasing source y maps to increasing output x. If rows
// land mirrored/reversed, flip this single line to `frameHeight - yEnd` instead.
const int rotatedX = yStart;
// frameBuffer holds only one row, so it's described to PPA as its own self-contained picture.
ppa_srm_oper_config_t srmConfig = {
.in = {
.buffer = frameBuffer,
.buffer = data,
.pic_w = static_cast<uint32_t>(frameWidth),
.pic_h = static_cast<uint32_t>(frameHeight),
.pic_h = static_cast<uint32_t>(rowPixelHeight),
.block_w = static_cast<uint32_t>(frameWidth),
.block_h = static_cast<uint32_t>(frameHeight),
.block_h = static_cast<uint32_t>(rowPixelHeight),
.block_offset_x = 0,
.block_offset_y = 0,
.srm_cm = PPA_SRM_COLOR_MODE_RGB565,
@ -105,11 +122,15 @@ void TerminalRendererPpa::present() {
.yuv_std = PPA_COLOR_CONV_STD_RGB_YUV_BT601,
},
.out = {
.buffer = out,
.buffer_size = static_cast<uint32_t>(panelWidth * panelHeight * 2),
.pic_w = static_cast<uint32_t>(panelWidth),
.buffer = usingHwFrameBuffer ? pixel_buffer_get_data(hwFrameBuffers[backBufferIndex]) : pixel_buffer_get_data(rotatedRowBuffer),
.buffer_size = usingHwFrameBuffer
? static_cast<uint32_t>(panelWidth * panelHeight * 2)
: static_cast<uint32_t>(rowPixelHeight * panelHeight * 2),
// hw case: sub-block of the full panel-sized buffer. fallback: rotatedRowBuffer is
// its own small canvas, starting at (0,0).
.pic_w = usingHwFrameBuffer ? static_cast<uint32_t>(panelWidth) : static_cast<uint32_t>(rowPixelHeight),
.pic_h = static_cast<uint32_t>(panelHeight),
.block_offset_x = 0,
.block_offset_x = usingHwFrameBuffer ? static_cast<uint32_t>(rotatedX) : 0u,
.block_offset_y = 0,
.srm_cm = PPA_SRM_COLOR_MODE_RGB565,
.yuv_range = PPA_COLOR_RANGE_LIMIT,
@ -133,10 +154,11 @@ void TerminalRendererPpa::present() {
return;
}
display_draw_bitmap(display, 0, 0, panelWidth, panelHeight, out);
if (usingHwFrameBuffer) {
backBufferIndex = 1 - backBufferIndex;
// PPA already wrote straight into hwFrameBuffers[backBufferIndex] at the right offset.
hwBufferDirty = true;
} else {
presentRegion(rotatedRowBuffer, rotatedX, 0, rowPixelHeight, panelHeight);
}
}
@ -155,7 +177,7 @@ bool TerminalRendererPpa::begin(Device*) {
void TerminalRendererPpa::end() {
}
void TerminalRendererPpa::present() {
void TerminalRendererPpa::present(int, int) {
}
#endif

View File

@ -1,3 +1,6 @@
#include "tactility/memory.h"
#include <Tactility/app/terminal/Terminal.h>
#include <tactility/log.h>
@ -48,7 +51,7 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(main) },
.flags = 0,
.stack = {},
.stack = { .depth = 4096, .desired_memory_capability = 0 },
};
}

View File

@ -66,6 +66,12 @@ typedef struct {
static vterm_t *s_vterms = NULL;
volatile int s_active_vt = 0;
// Set whenever a write or switch touches the active VT's visible content; vterm_take_dirty()
// reads and clears it. Best-effort (no lock): a write racing a read can at worst cost one extra,
// harmless render pass next tick, never a missed one, since a set is never cleared except by the
// reader consuming it.
static volatile bool s_dirty = true;
/* Tactility: called with the top line still intact, just before it scrolls off. Lets the
* shell keep a scrollback history without vterm having to own one. */
static void (*s_scroll_callback)(void) = NULL;
@ -629,6 +635,7 @@ error_t vterm_init(void)
// 3. Set up initial active VT (0)
s_active_vt = 0;
s_dirty = true;
#if VTERM_COUNT > 1
// Load VT0 storage into IRAM
memcpy(s_iram_buffer, s_vterms[0].storage_cells, BUFFER_SIZE_BYTES);
@ -673,6 +680,7 @@ void vterm_deinit(void)
}
s_active_vt = 0;
s_dirty = true;
s_scroll_callback = NULL;
s_on_switch_cb = NULL;
@ -707,6 +715,7 @@ void vterm_switch(int vt_id)
new_vt->cells = s_iram_buffer; // New now points to IRAM
s_active_vt = vt_id;
s_dirty = true;
xSemaphoreGive(new_vt->mutex);
xSemaphoreGive(old_vt->mutex);
@ -723,6 +732,7 @@ void vterm_write(int vt_id, const char *data, size_t len)
vterm_t *vt = &s_vterms[vt_id];
xSemaphoreTake(vt->mutex, portMAX_DELAY);
const char *p = data;
const char *end = data + len;
@ -804,6 +814,10 @@ void vterm_write(int vt_id, const char *data, size_t len)
vt->escape_state = escape_mode;
vt->pending_wrap = pending_wrap;
// Set only after the cells are final, so a reader that consumes the flag sees complete content.
if (len > 0 && vt->cells == s_iram_buffer) {
__atomic_store_n(&s_dirty, true, __ATOMIC_RELEASE);
}
xSemaphoreGive(vt->mutex);
}
@ -871,6 +885,10 @@ void vterm_get_cursor(int vt_id, int *col, int *row, int *visible) {
}
}
bool vterm_take_dirty(void) {
return __atomic_exchange_n(&s_dirty, false, __ATOMIC_ACQ_REL);
}
int vterm_getchar(int vt_id, int timeout_ms) {
if (vt_id < 0 || vt_id >= VTERM_COUNT) return -1;
char c;

View File

@ -207,7 +207,8 @@ int32_t appMain(int argc, char* argv[]) {
uint32_t start() {
uint32_t instanceId = 0;
app_start(manifest.id, 0, nullptr, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_with_context(&context, &instanceId);
return instanceId;
}

View File

@ -274,7 +274,10 @@ int32_t appMain(int argc, char* argv[]) {
uint32_t start(uint32_t callerAppInstanceId, bool saveTimeZone) {
const char* argv[] = { saveTimeZone ? "1" : "0" };
uint32_t instanceId = 0;
app_start_for_result(manifest.id, 1, argv, callerAppInstanceId, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, 1, argv);
app_start_context_set_parent(&context, callerAppInstanceId);
app_start_with_context(&context, &instanceId);
return instanceId;
}

View File

@ -286,7 +286,9 @@ int32_t appMain(int argc, char* argv[]) {
uint32_t start(uint32_t callerAppInstanceId) {
uint32_t instanceId = 0;
app_start_for_result(manifest.id, 0, nullptr, callerAppInstanceId, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_parent(&context, callerAppInstanceId);
app_start_with_context(&context, &instanceId);
return instanceId;
}

View File

@ -318,7 +318,9 @@ int32_t appMain(int argc, char* argv[]) {
void start(const std::string& ssid) {
const char* argv[] = { ssid.c_str() };
uint32_t instanceId = 0;
app_start(manifest.id, 1, argv, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, 1, argv);
app_start_with_context(&context, &instanceId);
}
extern const ::AppManifest manifest = {

View File

@ -378,7 +378,9 @@ int32_t appMain(int argc, char* argv[]) {
void start(const std::string& ssid, const std::string& password) {
const char* argv[] = { ssid.c_str(), password.c_str() };
uint32_t instanceId = 0;
app_start(manifest.id, 2, argv, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_arguments_ext(&context, 2, argv);
app_start_with_context(&context, &instanceId);
}
extern const ::AppManifest manifest = {

View File

@ -241,7 +241,9 @@ int32_t appMain(int argc, char* argv[]) {
uint32_t start(uint32_t callerAppInstanceId) {
uint32_t instanceId = 0;
app_start_for_result(manifest.id, 0, nullptr, callerAppInstanceId, &instanceId);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_context_set_parent(&context, callerAppInstanceId);
app_start_with_context(&context, &instanceId);
return instanceId;
}

View File

@ -146,7 +146,10 @@ error_t DevelopmentService::handleAppRun(HttpServerRequest* request, void*) {
}
}
app_start(id_key_pos->second.c_str(), 0, nullptr, &instance_id);
AppStartContext context;
if (app_start_context_from_id(id_key_pos->second.c_str(), &context) == ERROR_NONE) {
app_start_with_context(&context, &instance_id);
}
LOG_I(TAG, "[200] /app/run %s", id_key_pos->second.c_str());
http_server_request_send(request, nullptr, 0);

View File

@ -1294,7 +1294,8 @@ error_t WebServerService::handleApiAppsRun(HttpServerRequest* request, void*) {
// Every app instance gets its own task now, so there's no "stop the existing one first" -
// this just starts a fresh instance alongside whatever's already running.
AppInstanceId instance_id = 0;
app_start(appId.c_str(), 0, nullptr, &instance_id);
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_with_context(&context, &instance_id);
LOG_I(TAG, "[200] /api/apps/run %s", appId.c_str());
http_server_request_send_string(request, "ok");

View File

@ -45,9 +45,9 @@ target_link_libraries(TactilityTests PRIVATE
cjson-module
cpp-symbols-module
crypt-module
font-module
freertos-module
gps-module
graphics-module
http-module
mbedtls-module
posix-symbols-module

View File

@ -46,6 +46,23 @@ extern const struct MemoryPolicy MEMORY_POLICY_DEFAULT;
*/
void memory_log_stats(void);
/**
* @brief Total size of the main heap, in bytes.
* @note On ESP-IDF this is the actual capacity memory_alloc_with_policy() draws from. On other
* platforms (no heap capability tracking), this returns system-wide physical memory instead -
* not the process's own allocatable heap - so it can report plenty of "free" memory while
* memory_alloc_with_policy() still fails under a process limit.
* @return the heap's total size, or 0 if unavailable on this platform
*/
size_t memory_heap_total(void);
/**
* @brief Currently free space in the main heap, in bytes.
* @note See memory_heap_total()'s own note - the same system-wide-vs-process-heap caveat applies.
* @return the heap's free size, or 0 if unavailable on this platform
*/
size_t memory_heap_free(void);
/**
* @brief Allocates memory that satisfies the given policy.
* @param[in] size number of bytes to allocate

View File

@ -24,42 +24,63 @@ const struct MemoryPolicy MEMORY_POLICY_DEFAULT = {
.alignment = 0,
};
void memory_log_stats() {
size_t memory_heap_total() {
#ifdef ESP_PLATFORM
size_t heap_free = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
size_t heap_total = heap_caps_get_total_size(MALLOC_CAP_INTERNAL);
LOG_I(TAG, "Heap: %zu / %zu available", heap_free, heap_total);
size_t ext_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
size_t ext_total = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
LOG_I(TAG, "External: %zu / %zu available", ext_free, ext_total);
return heap_caps_get_total_size(MALLOC_CAP_INTERNAL);
#elif defined(__APPLE__)
// No sysconf(_SC_PHYS_PAGES/_SC_AVPHYS_PAGES) on macOS: total comes from sysctl, free from
// the Mach host VM statistics.
uint64_t mem_size = 0;
size_t mem_size_len = sizeof(mem_size);
if (sysctlbyname("hw.memsize", &mem_size, &mem_size_len, nullptr, 0) != 0) {
return 0;
}
return static_cast<size_t>(mem_size);
#else
const long phys_pages = sysconf(_SC_PHYS_PAGES);
const long page_size = sysconf(_SC_PAGESIZE);
if (phys_pages < 0 || page_size < 0) {
return 0;
}
return static_cast<size_t>(static_cast<uint64_t>(phys_pages) * static_cast<uint64_t>(page_size));
#endif
}
size_t memory_heap_free() {
#ifdef ESP_PLATFORM
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
#elif defined(__APPLE__)
// No sysconf(_SC_AVPHYS_PAGES) on macOS: free space comes from the Mach host VM statistics.
mach_port_t host = mach_host_self();
vm_size_t page_size = 0;
vm_statistics64_data_t vm_stats {};
mach_msg_type_number_t vm_stats_count = HOST_VM_INFO64_COUNT;
mach_port_t host = mach_host_self();
if (sysctlbyname("hw.memsize", &mem_size, &mem_size_len, nullptr, 0) != 0 ||
host_page_size(host, &page_size) != KERN_SUCCESS ||
host_statistics64(host, HOST_VM_INFO64, reinterpret_cast<host_info64_t>(&vm_stats), &vm_stats_count) != KERN_SUCCESS) {
LOG_W(TAG, "Heap: host memory stats unavailable");
} else {
const uint64_t heap_free = static_cast<uint64_t>(vm_stats.free_count + vm_stats.inactive_count) * page_size;
LOG_I(TAG, "Heap: %llu / %llu available", static_cast<unsigned long long>(heap_free), static_cast<unsigned long long>(mem_size));
kern_return_t result = host_page_size(host, &page_size);
if (result == KERN_SUCCESS) {
result = host_statistics64(host, HOST_VM_INFO64, reinterpret_cast<host_info64_t>(&vm_stats), &vm_stats_count);
}
mach_port_deallocate(mach_task_self(), host);
if (result != KERN_SUCCESS) {
return 0;
}
return static_cast<size_t>(static_cast<uint64_t>(vm_stats.free_count + vm_stats.inactive_count) * page_size);
#else
const long phys_pages = sysconf(_SC_PHYS_PAGES);
const long avphys_pages = sysconf(_SC_AVPHYS_PAGES);
const long page_size = sysconf(_SC_PAGESIZE);
if (phys_pages < 0 || avphys_pages < 0 || page_size < 0) {
LOG_W(TAG, "Heap: sysconf() unavailable");
} else {
const uint64_t heap_total = static_cast<uint64_t>(phys_pages) * static_cast<uint64_t>(page_size);
const uint64_t heap_free = static_cast<uint64_t>(avphys_pages) * static_cast<uint64_t>(page_size);
LOG_I(TAG, "Heap: %llu / %llu available", static_cast<unsigned long long>(heap_free), static_cast<unsigned long long>(heap_total));
if (avphys_pages < 0 || page_size < 0) {
return 0;
}
return static_cast<size_t>(static_cast<uint64_t>(avphys_pages) * static_cast<uint64_t>(page_size));
#endif
}
void memory_log_stats() {
#ifdef ESP_PLATFORM
LOG_I(TAG, "Heap: %zu / %zu available", memory_heap_free(), memory_heap_total());
size_t ext_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
size_t ext_total = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
LOG_I(TAG, "External: %zu / %zu available", ext_free, ext_total);
#else
// Not the process's own heap capacity - see memory_heap_total()'s doc comment.
LOG_I(TAG, "System memory: %zu / %zu available", memory_heap_free(), memory_heap_total());
#endif
}

Some files were not shown because too many files have changed in this diff Show More