mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
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:
parent
4279a8ed72
commit
86010186d8
@ -7,3 +7,4 @@ http-module
|
||||
lvgl-module
|
||||
lvgl-window-manager-module
|
||||
service-module
|
||||
graphics-module
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
62
Modules/app-module/include/app/env.h
Normal file
62
Modules/app-module/include/app/env.h
Normal 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
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
14
Modules/app-module/private/app/private/env_internal.h
Normal file
14
Modules/app-module/private/app/private/env_internal.h
Normal 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);
|
||||
@ -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). */
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
134
Modules/app-module/source/env.cpp
Normal file
134
Modules/app-module/source/env.cpp
Normal 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"
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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"
|
||||
|
||||
314
Modules/app-module/tests/source/env_test.cpp
Normal file
314
Modules/app-module/tests/source/env_test.cpp
Normal 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);
|
||||
}
|
||||
@ -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, ®istered_id), ERROR_NONE);
|
||||
AppStartContext registered_context = app_start_context_for_manifest(&manifest);
|
||||
REQUIRE_EQ(app_start_with_context(®istered_context, ®istered_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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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
|
||||
)
|
||||
@ -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
|
||||
@ -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,
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
21
Modules/graphics-module/CMakeLists.txt
Normal file
21
Modules/graphics-module/CMakeLists.txt
Normal 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}
|
||||
)
|
||||
43
Modules/graphics-module/include/font/render.h
Normal file
43
Modules/graphics-module/include/font/render.h
Normal 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
|
||||
@ -7,7 +7,7 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module font_module;
|
||||
extern struct Module graphics_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
101
Modules/graphics-module/include/graphics/pixel_buffer.h
Normal file
101
Modules/graphics-module/include/graphics/pixel_buffer.h
Normal 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
|
||||
@ -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")
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
37
Modules/graphics-module/source/module.c
Normal file
37
Modules/graphics-module/source/module.c
Normal 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,
|
||||
};
|
||||
334
Modules/graphics-module/source/pixel_buffer.c
Normal file
334
Modules/graphics-module/source/pixel_buffer.c
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Modules/graphics-module/source/render.cpp
Normal file
33
Modules/graphics-module/source/render.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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")
|
||||
|
||||
91
Tactility/Private/Tactility/app/applist/Favourites.h
Normal file
91
Tactility/Private/Tactility/app/applist/Favourites.h
Normal 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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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) + "\".");
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 },
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user