committed by
GitHub
parent
3b9986fcef
commit
e842e30ab3
@@ -0,0 +1,48 @@
|
||||
#include "app_manifest_registry.h"
|
||||
#include "check.h"
|
||||
#include "lvgl.h"
|
||||
#include "services/gui/gui.h"
|
||||
#include "services/gui/view_port.h"
|
||||
#include "services/loader/loader.h"
|
||||
|
||||
static void on_open_app(lv_event_t* e) {
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
if (code == LV_EVENT_CLICKED) {
|
||||
const AppManifest* manifest = lv_event_get_user_data(e);
|
||||
loader_start_app_nonblocking(manifest->id, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void add_app_to_list(const AppManifest* manifest, void* _Nullable parent) {
|
||||
furi_check(parent);
|
||||
lv_obj_t* list = (lv_obj_t*)parent;
|
||||
lv_obj_t* btn = lv_list_add_btn(list, LV_SYMBOL_FILE, manifest->name);
|
||||
lv_obj_add_event_cb(btn, &on_open_app, LV_EVENT_CLICKED, (void*)manifest);
|
||||
}
|
||||
|
||||
static void desktop_show(lv_obj_t* parent, void* context) {
|
||||
lv_obj_t* list = lv_list_create(parent);
|
||||
lv_obj_set_size(list, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_center(list);
|
||||
|
||||
lv_list_add_text(list, "System");
|
||||
app_manifest_registry_for_each_of_type(AppTypeSystem, list, add_app_to_list);
|
||||
lv_list_add_text(list, "User");
|
||||
app_manifest_registry_for_each_of_type(AppTypeUser, list, add_app_to_list);
|
||||
}
|
||||
|
||||
static void desktop_start() {
|
||||
ViewPort* view_port = view_port_alloc();
|
||||
view_port_draw_callback_set(view_port, &desktop_show, NULL);
|
||||
gui_add_view_port(view_port, GuiLayerDesktop);
|
||||
}
|
||||
|
||||
static void desktop_stop() {
|
||||
furi_crash("desktop_stop is not implemented");
|
||||
}
|
||||
|
||||
const ServiceManifest desktop_service = {
|
||||
.id = "desktop",
|
||||
.on_start = &desktop_start,
|
||||
.on_stop = &desktop_stop
|
||||
};
|
||||
@@ -0,0 +1,173 @@
|
||||
#include "check.h"
|
||||
#include "esp_lvgl_port.h"
|
||||
#include "furi_extra_defines.h"
|
||||
#include "gui_i.h"
|
||||
#include "log.h"
|
||||
#include "kernel.h"
|
||||
|
||||
#define TAG "gui"
|
||||
|
||||
// Forward declarations
|
||||
bool gui_redraw_fs(Gui*);
|
||||
void gui_redraw(Gui*);
|
||||
static int32_t gui_main(void*);
|
||||
|
||||
static Gui* gui = NULL;
|
||||
|
||||
Gui* gui_alloc() {
|
||||
Gui* instance = malloc(sizeof(Gui));
|
||||
memset(instance, 0, sizeof(Gui));
|
||||
furi_check(instance != NULL);
|
||||
instance->thread = furi_thread_alloc_ex(
|
||||
"gui",
|
||||
4096, // Last known minimum was 2800 for launching desktop
|
||||
&gui_main,
|
||||
NULL
|
||||
);
|
||||
|
||||
instance->mutex = xSemaphoreCreateRecursiveMutex();
|
||||
|
||||
furi_check(lvgl_port_lock(100));
|
||||
instance->lvgl_parent = lv_scr_act();
|
||||
lvgl_port_unlock();
|
||||
|
||||
for (size_t i = 0; i < GuiLayerMAX; i++) {
|
||||
instance->layers[i] = NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
// Input
|
||||
gui->input_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
|
||||
gui->input_events = furi_record_open(RECORD_INPUT_EVENTS);
|
||||
|
||||
furi_check(gui->input_events);
|
||||
furi_pubsub_subscribe(gui->input_events, gui_input_events_callback, gui);
|
||||
*/
|
||||
return instance;
|
||||
}
|
||||
|
||||
void gui_free(Gui* instance) {
|
||||
furi_assert(instance != NULL);
|
||||
furi_thread_free(instance->thread);
|
||||
furi_mutex_free(instance->mutex);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
void gui_lock() {
|
||||
furi_assert(gui);
|
||||
furi_assert(gui->mutex);
|
||||
furi_check(xSemaphoreTakeRecursive(gui->mutex, portMAX_DELAY) == pdPASS);
|
||||
}
|
||||
|
||||
void gui_unlock() {
|
||||
furi_assert(gui);
|
||||
furi_assert(gui->mutex);
|
||||
furi_check(xSemaphoreGiveRecursive(gui->mutex) == pdPASS);
|
||||
}
|
||||
|
||||
void gui_request_draw() {
|
||||
furi_assert(gui);
|
||||
|
||||
FuriThreadId thread_id = furi_thread_get_id(gui->thread);
|
||||
furi_thread_flags_set(thread_id, GUI_THREAD_FLAG_DRAW);
|
||||
}
|
||||
|
||||
void gui_add_view_port(ViewPort* view_port, GuiLayer layer) {
|
||||
furi_assert(gui);
|
||||
furi_assert(view_port);
|
||||
furi_check(layer < GuiLayerMAX);
|
||||
|
||||
gui_lock();
|
||||
furi_check(gui->layers[layer] == NULL, "layer in use");
|
||||
gui->layers[layer] = view_port;
|
||||
view_port_gui_set(view_port, gui);
|
||||
gui_unlock();
|
||||
|
||||
gui_request_draw();
|
||||
}
|
||||
|
||||
void gui_remove_view_port(ViewPort* view_port) {
|
||||
furi_assert(gui);
|
||||
furi_assert(view_port);
|
||||
|
||||
gui_lock();
|
||||
|
||||
view_port_gui_set(view_port, NULL);
|
||||
for (size_t i = 0; i < GuiLayerMAX; i++) {
|
||||
if (gui->layers[i] == view_port) {
|
||||
gui->layers[i] = NULL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
gui_unlock();
|
||||
|
||||
gui_request_draw();
|
||||
}
|
||||
|
||||
static int32_t gui_main(void* p) {
|
||||
UNUSED(p);
|
||||
furi_check(gui);
|
||||
Gui* local_gui = gui;
|
||||
|
||||
while (1) {
|
||||
uint32_t flags = furi_thread_flags_wait(
|
||||
GUI_THREAD_FLAG_ALL,
|
||||
FuriFlagWaitAny,
|
||||
FuriWaitForever
|
||||
);
|
||||
// Process and dispatch input
|
||||
/*if (flags & GUI_THREAD_FLAG_INPUT) {
|
||||
// Process till queue become empty
|
||||
InputEvent input_event;
|
||||
while(furi_message_queue_get(gui->input_queue, &input_event, 0) == FuriStatusOk) {
|
||||
gui_input(gui, &input_event);
|
||||
}
|
||||
}*/
|
||||
// Process and dispatch draw call
|
||||
if (flags & GUI_THREAD_FLAG_DRAW) {
|
||||
furi_thread_flags_clear(GUI_THREAD_FLAG_DRAW);
|
||||
gui_lock();
|
||||
gui_redraw(local_gui);
|
||||
gui_unlock();
|
||||
}
|
||||
|
||||
if (flags & GUI_THREAD_FLAG_EXIT) {
|
||||
furi_thread_flags_clear(GUI_THREAD_FLAG_EXIT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// region AppManifest
|
||||
|
||||
static void gui_start(void* parameter) {
|
||||
UNUSED(parameter);
|
||||
|
||||
gui = gui_alloc();
|
||||
|
||||
furi_thread_set_priority(gui->thread, FuriThreadPriorityNormal);
|
||||
furi_thread_start(gui->thread);
|
||||
}
|
||||
|
||||
static void gui_stop() {
|
||||
gui_lock();
|
||||
|
||||
FuriThreadId thread_id = furi_thread_get_id(gui->thread);
|
||||
furi_thread_flags_set(thread_id, GUI_THREAD_FLAG_EXIT);
|
||||
furi_thread_join(gui->thread);
|
||||
|
||||
gui_unlock();
|
||||
|
||||
gui_free(gui);
|
||||
}
|
||||
|
||||
const ServiceManifest gui_service = {
|
||||
.id = "gui",
|
||||
.on_start = &gui_start,
|
||||
.on_stop = &gui_stop
|
||||
};
|
||||
|
||||
// endregion
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "service_manifest.h"
|
||||
#include "view_port.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Gui layers */
|
||||
typedef enum {
|
||||
GuiLayerDesktop, /**< Desktop layer for internal use. Like fullscreen but with status bar */
|
||||
GuiLayerWindow, /**< Window layer, status bar is shown */
|
||||
GuiLayerFullscreen, /**< Fullscreen layer, no status bar */
|
||||
GuiLayerMAX /**< Don't use or move, special value */
|
||||
} GuiLayer;
|
||||
|
||||
typedef struct Gui Gui;
|
||||
|
||||
/** Add view_port to view_port tree
|
||||
*
|
||||
* @remark thread safe
|
||||
*
|
||||
* @param gui Gui instance
|
||||
* @param view_port ViewPort instance
|
||||
* @param[in] layer GuiLayer where to place view_port
|
||||
*/
|
||||
void gui_add_view_port(ViewPort* view_port, GuiLayer layer);
|
||||
|
||||
/** Remove view_port from rendering tree
|
||||
*
|
||||
* @remark thread safe
|
||||
*
|
||||
* @param gui Gui instance
|
||||
* @param view_port ViewPort instance
|
||||
*/
|
||||
void gui_remove_view_port(ViewPort* view_port);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "check.h"
|
||||
#include "esp_lvgl_port.h"
|
||||
#include "gui_i.h"
|
||||
#include "log.h"
|
||||
#include "services/gui/widgets/widgets.h"
|
||||
#include "services/loader/loader.h"
|
||||
|
||||
#define TAG "gui"
|
||||
|
||||
static lv_obj_t* screen_with_top_bar(lv_obj_t* parent) {
|
||||
lv_obj_set_style_bg_blacken(parent);
|
||||
|
||||
lv_obj_t* vertical_container = lv_obj_create(parent);
|
||||
lv_obj_set_size(vertical_container, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_no_padding(vertical_container);
|
||||
lv_obj_set_style_bg_blacken(vertical_container);
|
||||
|
||||
top_bar(vertical_container);
|
||||
|
||||
lv_obj_t* child_container = lv_obj_create(vertical_container);
|
||||
lv_obj_set_width(child_container, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(child_container, 1);
|
||||
lv_obj_set_style_no_padding(vertical_container);
|
||||
lv_obj_set_style_bg_blacken(vertical_container);
|
||||
|
||||
return child_container;
|
||||
}
|
||||
|
||||
static lv_obj_t* screen_with_top_bar_and_toolbar(lv_obj_t* parent) {
|
||||
lv_obj_set_style_bg_blacken(parent);
|
||||
|
||||
lv_obj_t* vertical_container = lv_obj_create(parent);
|
||||
lv_obj_set_size(vertical_container, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_no_padding(vertical_container);
|
||||
lv_obj_set_style_bg_blacken(vertical_container);
|
||||
|
||||
top_bar(vertical_container);
|
||||
|
||||
const AppManifest* manifest = loader_get_current_app();
|
||||
if (manifest != NULL) {
|
||||
toolbar(vertical_container, TOP_BAR_HEIGHT, manifest);
|
||||
}
|
||||
|
||||
lv_obj_t* spacer = lv_obj_create(vertical_container);
|
||||
lv_obj_set_size(spacer, 2, 2);
|
||||
lv_obj_set_style_bg_blacken(spacer);
|
||||
|
||||
lv_obj_t* child_container = lv_obj_create(vertical_container);
|
||||
lv_obj_set_width(child_container, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(child_container, 1);
|
||||
lv_obj_set_style_no_padding(vertical_container);
|
||||
lv_obj_set_style_bg_blacken(vertical_container);
|
||||
|
||||
return child_container;
|
||||
}
|
||||
|
||||
static bool gui_redraw_window(Gui* gui) {
|
||||
ViewPort* view_port = gui->layers[GuiLayerWindow];
|
||||
if (view_port) {
|
||||
lv_obj_t* container = screen_with_top_bar_and_toolbar(gui->lvgl_parent);
|
||||
view_port_draw(view_port, container);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool gui_redraw_desktop(Gui* gui) {
|
||||
ViewPort* view_port = gui->layers[GuiLayerDesktop];
|
||||
if (view_port) {
|
||||
lv_obj_t* container = screen_with_top_bar(gui->lvgl_parent);
|
||||
view_port_draw(view_port, container);
|
||||
return true;
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "no desktop layer found");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool gui_redraw_fs(Gui* gui) {
|
||||
ViewPort* view_port = gui->layers[GuiLayerFullscreen];
|
||||
if (view_port) {
|
||||
view_port_draw(view_port, gui->lvgl_parent);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void gui_redraw(Gui* gui) {
|
||||
furi_assert(gui);
|
||||
|
||||
furi_check(lvgl_port_lock(100));
|
||||
lv_obj_clean(gui->lvgl_parent);
|
||||
|
||||
if (!gui_redraw_fs(gui)) {
|
||||
if (!gui_redraw_window(gui)) {
|
||||
gui_redraw_desktop(gui);
|
||||
}
|
||||
}
|
||||
|
||||
lvgl_port_unlock();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "gui.h"
|
||||
#include "message_queue.h"
|
||||
#include "mutex.h"
|
||||
#include "pubsub.h"
|
||||
#include "view_port.h"
|
||||
#include "view_port_i.h"
|
||||
#include <stdio.h>
|
||||
|
||||
#define GUI_THREAD_FLAG_DRAW (1 << 0)
|
||||
#define GUI_THREAD_FLAG_INPUT (1 << 1)
|
||||
#define GUI_THREAD_FLAG_EXIT (1 << 2)
|
||||
#define GUI_THREAD_FLAG_ALL (GUI_THREAD_FLAG_DRAW | GUI_THREAD_FLAG_INPUT | GUI_THREAD_FLAG_EXIT)
|
||||
|
||||
/** Gui structure */
|
||||
struct Gui {
|
||||
// Thread and lock
|
||||
FuriThread* thread;
|
||||
SemaphoreHandle_t mutex;
|
||||
|
||||
// Layers and Canvas
|
||||
ViewPort* layers[GuiLayerMAX];
|
||||
lv_obj_t* lvgl_parent;
|
||||
|
||||
// Input
|
||||
/*
|
||||
FuriMessageQueue* input_queue;
|
||||
FuriPubSub* input_events;
|
||||
uint8_t ongoing_input;
|
||||
ViewPort* ongoing_input_view_port;
|
||||
*/
|
||||
};
|
||||
|
||||
/** Update GUI, request redraw
|
||||
*
|
||||
* @param gui Gui instance
|
||||
*/
|
||||
void gui_request_draw();
|
||||
|
||||
///** Input event callback
|
||||
// *
|
||||
// * Used to receive input from input service or to inject new input events
|
||||
// *
|
||||
// * @param[in] value The value pointer (InputEvent*)
|
||||
// * @param ctx The context (Gui instance)
|
||||
// */
|
||||
//void gui_input_events_callback(const void* value, void* ctx);
|
||||
|
||||
/** Lock GUI
|
||||
*
|
||||
* @param gui The Gui instance
|
||||
*/
|
||||
void gui_lock();
|
||||
|
||||
/** Unlock GUI
|
||||
*
|
||||
* @param gui The Gui instance
|
||||
*/
|
||||
void gui_unlock();
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "gui_i.h"
|
||||
|
||||
/*
|
||||
void gui_input_events_callback(const void* value, void* ctx) {
|
||||
furi_assert(value);
|
||||
furi_assert(ctx);
|
||||
|
||||
Gui* gui = ctx;
|
||||
|
||||
furi_message_queue_put(gui->input_queue, value, FuriWaitForever);
|
||||
furi_thread_flags_set(gui->thread_id, GUI_THREAD_FLAG_INPUT);
|
||||
}
|
||||
|
||||
static void gui_input(Gui* gui, InputEvent* input_event) {
|
||||
furi_assert(gui);
|
||||
furi_assert(input_event);
|
||||
|
||||
// Check input complementarity
|
||||
uint8_t key_bit = (1 << input_event->key);
|
||||
if(input_event->type == InputTypeRelease) {
|
||||
gui->ongoing_input &= ~key_bit;
|
||||
} else if(input_event->type == InputTypePress) {
|
||||
gui->ongoing_input |= key_bit;
|
||||
} else if(!(gui->ongoing_input & key_bit)) {
|
||||
FURI_LOG_D(
|
||||
TAG,
|
||||
"non-complementary input, discarding key: %s type: %s, sequence: %p",
|
||||
input_get_key_name(input_event->key),
|
||||
input_get_type_name(input_event->type),
|
||||
(void*)input_event->sequence);
|
||||
return;
|
||||
}
|
||||
|
||||
gui_lock(gui);
|
||||
|
||||
do {
|
||||
if(gui->direct_draw && !gui->ongoing_input_view_port) {
|
||||
break;
|
||||
}
|
||||
|
||||
ViewPort* view_port = NULL;
|
||||
|
||||
if(gui->lockdown) {
|
||||
view_port = gui_view_port_find_enabled(gui->layers[GuiLayerDesktop]);
|
||||
} else {
|
||||
view_port = gui_view_port_find_enabled(gui->layers[GuiLayerFullscreen]);
|
||||
if(!view_port) view_port = gui_view_port_find_enabled(gui->layers[GuiLayerWindow]);
|
||||
if(!view_port) view_port = gui_view_port_find_enabled(gui->layers[GuiLayerDesktop]);
|
||||
}
|
||||
|
||||
if(!(gui->ongoing_input & ~key_bit) && input_event->type == InputTypePress) {
|
||||
gui->ongoing_input_view_port = view_port;
|
||||
}
|
||||
|
||||
if(view_port && view_port == gui->ongoing_input_view_port) {
|
||||
view_port_input(view_port, input_event);
|
||||
} else if(gui->ongoing_input_view_port && input_event->type == InputTypeRelease) {
|
||||
FURI_LOG_D(
|
||||
TAG,
|
||||
"ViewPort changed while key press %p -> %p. Sending key: %s, type: %s, sequence: %p to previous view port",
|
||||
gui->ongoing_input_view_port,
|
||||
view_port,
|
||||
input_get_key_name(input_event->key),
|
||||
input_get_type_name(input_event->type),
|
||||
(void*)input_event->sequence);
|
||||
view_port_input(gui->ongoing_input_view_port, input_event);
|
||||
} else {
|
||||
FURI_LOG_D(
|
||||
TAG,
|
||||
"ViewPort changed while key press %p -> %p. Discarding key: %s, type: %s, sequence: %p",
|
||||
gui->ongoing_input_view_port,
|
||||
view_port,
|
||||
input_get_key_name(input_event->key),
|
||||
input_get_type_name(input_event->type),
|
||||
(void*)input_event->sequence);
|
||||
}
|
||||
} while(false);
|
||||
|
||||
gui_unlock(gui);
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,13 @@
|
||||
#include "icon_i.h"
|
||||
|
||||
uint8_t icon_get_width(const Icon* instance) {
|
||||
return instance->width;
|
||||
}
|
||||
|
||||
uint8_t icon_get_height(const Icon* instance) {
|
||||
return instance->height;
|
||||
}
|
||||
|
||||
const uint8_t* icon_get_data(const Icon* instance) {
|
||||
return instance->frames[0];
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
struct Icon {
|
||||
const uint8_t width;
|
||||
const uint8_t height;
|
||||
const uint8_t frame_count;
|
||||
const uint8_t frame_rate;
|
||||
const uint8_t* const* frames;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include "icon.h"
|
||||
|
||||
typedef struct {
|
||||
const uint8_t width;
|
||||
const uint8_t height;
|
||||
const uint8_t frame_count;
|
||||
const uint8_t frame_rate;
|
||||
const uint8_t* const* frames;
|
||||
} Icon;
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "check.h"
|
||||
#include "gui.h"
|
||||
#include "gui_i.h"
|
||||
#include "services/gui/widgets/widgets.h"
|
||||
#include "view_port_i.h"
|
||||
|
||||
#define TAG "viewport"
|
||||
|
||||
_Static_assert(ViewPortOrientationMAX == 4, "Incorrect ViewPortOrientation count");
|
||||
_Static_assert(
|
||||
(ViewPortOrientationHorizontal == 0 && ViewPortOrientationHorizontalFlip == 1 &&
|
||||
ViewPortOrientationVertical == 2 && ViewPortOrientationVerticalFlip == 3),
|
||||
"Incorrect ViewPortOrientation order"
|
||||
);
|
||||
|
||||
ViewPort* view_port_alloc() {
|
||||
ViewPort* view_port = malloc(sizeof(ViewPort));
|
||||
view_port->gui = NULL;
|
||||
view_port->is_enabled = true;
|
||||
view_port->mutex = furi_mutex_alloc(FuriMutexTypeRecursive);
|
||||
return view_port;
|
||||
}
|
||||
|
||||
void view_port_free(ViewPort* view_port) {
|
||||
furi_assert(view_port);
|
||||
furi_check(furi_mutex_acquire(view_port->mutex, FuriWaitForever) == FuriStatusOk);
|
||||
furi_check(view_port->gui == NULL);
|
||||
furi_check(furi_mutex_release(view_port->mutex) == FuriStatusOk);
|
||||
furi_mutex_free(view_port->mutex);
|
||||
free(view_port);
|
||||
}
|
||||
|
||||
void view_port_enabled_set(ViewPort* view_port, bool enabled) {
|
||||
furi_assert(view_port);
|
||||
furi_check(furi_mutex_acquire(view_port->mutex, FuriWaitForever) == FuriStatusOk);
|
||||
if (view_port->is_enabled != enabled) {
|
||||
view_port->is_enabled = enabled;
|
||||
if (view_port->gui) gui_request_draw();
|
||||
}
|
||||
furi_check(furi_mutex_release(view_port->mutex) == FuriStatusOk);
|
||||
}
|
||||
|
||||
bool view_port_is_enabled(const ViewPort* view_port) {
|
||||
furi_assert(view_port);
|
||||
furi_check(furi_mutex_acquire(view_port->mutex, FuriWaitForever) == FuriStatusOk);
|
||||
bool is_enabled = view_port->is_enabled;
|
||||
furi_check(furi_mutex_release(view_port->mutex) == FuriStatusOk);
|
||||
return is_enabled;
|
||||
}
|
||||
|
||||
void view_port_draw_callback_set(ViewPort* view_port, ViewPortDrawCallback callback, void* context) {
|
||||
furi_assert(view_port);
|
||||
furi_check(furi_mutex_acquire(view_port->mutex, FuriWaitForever) == FuriStatusOk);
|
||||
view_port->draw_callback = callback;
|
||||
view_port->draw_callback_context = context;
|
||||
furi_check(furi_mutex_release(view_port->mutex) == FuriStatusOk);
|
||||
}
|
||||
|
||||
void view_port_update(ViewPort* view_port) {
|
||||
furi_assert(view_port);
|
||||
|
||||
// We are not going to lockup system, but will notify you instead
|
||||
// Make sure that you don't call viewport methods inside another mutex, especially one that is used in draw call
|
||||
if (furi_mutex_acquire(view_port->mutex, 2) != FuriStatusOk) {
|
||||
ESP_LOGW(TAG, "ViewPort lockup: see %s:%d", __FILE__, __LINE__ - 3);
|
||||
}
|
||||
|
||||
if (view_port->gui && view_port->is_enabled) gui_request_draw();
|
||||
furi_mutex_release(view_port->mutex);
|
||||
}
|
||||
|
||||
void view_port_gui_set(ViewPort* view_port, Gui* gui) {
|
||||
furi_assert(view_port);
|
||||
furi_check(furi_mutex_acquire(view_port->mutex, FuriWaitForever) == FuriStatusOk);
|
||||
view_port->gui = gui;
|
||||
furi_check(furi_mutex_release(view_port->mutex) == FuriStatusOk);
|
||||
}
|
||||
|
||||
void view_port_draw(ViewPort* view_port, lv_obj_t* parent) {
|
||||
furi_assert(view_port);
|
||||
furi_assert(parent);
|
||||
|
||||
// We are not going to lockup system, but will notify you instead
|
||||
// Make sure that you don't call viewport methods inside another mutex, especially one that is used in draw call
|
||||
if (furi_mutex_acquire(view_port->mutex, 2) != FuriStatusOk) {
|
||||
ESP_LOGW(TAG, "ViewPort lockup: see %s:%d", __FILE__, __LINE__ - 3);
|
||||
}
|
||||
|
||||
furi_check(view_port->gui);
|
||||
|
||||
if (view_port->draw_callback) {
|
||||
lv_obj_clean(parent);
|
||||
lv_obj_set_style_no_padding(parent);
|
||||
view_port->draw_callback(parent, view_port->draw_callback_context);
|
||||
}
|
||||
|
||||
furi_mutex_release(view_port->mutex);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
typedef struct ViewPort ViewPort;
|
||||
|
||||
typedef enum {
|
||||
ViewPortOrientationHorizontal,
|
||||
ViewPortOrientationHorizontalFlip,
|
||||
ViewPortOrientationVertical,
|
||||
ViewPortOrientationVerticalFlip,
|
||||
ViewPortOrientationMAX, /**< Special value, don't use it */
|
||||
} ViewPortOrientation;
|
||||
|
||||
/** ViewPort Draw callback
|
||||
* @warning called from GUI thread
|
||||
*/
|
||||
typedef void (*ViewPortDrawCallback)(lv_obj_t* parent, void* context);
|
||||
|
||||
/** ViewPort allocator
|
||||
*
|
||||
* always returns view_port or stops system if not enough memory.
|
||||
*
|
||||
* @return ViewPort instance
|
||||
*/
|
||||
ViewPort* view_port_alloc();
|
||||
|
||||
/** ViewPort deallocator
|
||||
*
|
||||
* Ensure that view_port was unregistered in GUI system before use.
|
||||
*
|
||||
* @param view_port ViewPort instance
|
||||
*/
|
||||
void view_port_free(ViewPort* view_port);
|
||||
|
||||
/** Enable or disable view_port rendering.
|
||||
*
|
||||
* @param view_port ViewPort instance
|
||||
* @param enabled Indicates if enabled
|
||||
* @warning automatically dispatches update event
|
||||
*/
|
||||
void view_port_enabled_set(ViewPort* view_port, bool enabled);
|
||||
bool view_port_is_enabled(const ViewPort* view_port);
|
||||
|
||||
/** ViewPort event callbacks
|
||||
*
|
||||
* @param view_port ViewPort instance
|
||||
* @param callback appropriate callback function
|
||||
* @param context context to pass to callback
|
||||
*/
|
||||
void view_port_draw_callback_set(ViewPort* view_port, ViewPortDrawCallback callback, void* context);
|
||||
/** Emit update signal to GUI system.
|
||||
*
|
||||
* Rendering will happen later after GUI system process signal.
|
||||
*
|
||||
* @param view_port ViewPort instance
|
||||
*/
|
||||
void view_port_update(ViewPort* view_port);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "gui_i.h"
|
||||
#include "mutex.h"
|
||||
#include "view_port.h"
|
||||
|
||||
struct ViewPort {
|
||||
Gui* gui;
|
||||
FuriMutex* mutex;
|
||||
bool is_enabled;
|
||||
|
||||
ViewPortDrawCallback draw_callback;
|
||||
void* draw_callback_context;
|
||||
|
||||
/*
|
||||
ViewPortInputCallback input_callback;
|
||||
void* input_callback_context;
|
||||
*/
|
||||
};
|
||||
|
||||
/** Set GUI reference.
|
||||
*
|
||||
* To be used by GUI, called upon view_port tree insert
|
||||
*
|
||||
* @param view_port ViewPort instance
|
||||
* @param gui gui instance pointer
|
||||
*/
|
||||
void view_port_gui_set(ViewPort* view_port, Gui* gui);
|
||||
|
||||
/** Process draw call. Calls draw callback.
|
||||
*
|
||||
* To be used by GUI, called on tree redraw.
|
||||
*
|
||||
* @param view_port ViewPort instance
|
||||
* @param canvas canvas to draw at
|
||||
*/
|
||||
void view_port_draw(ViewPort* view_port, lv_obj_t* parent);
|
||||
|
||||
/** Process input. Calls input callback.
|
||||
*
|
||||
* To be used by GUI, called on input dispatch.
|
||||
*
|
||||
* @param view_port ViewPort instance
|
||||
* @param event pointer to input event
|
||||
*/
|
||||
//void view_port_input(ViewPort* view_port, InputEvent* event);
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "view_port_input.h"
|
||||
/*
|
||||
_Static_assert(InputKeyMAX == 6, "Incorrect InputKey count");
|
||||
_Static_assert(
|
||||
(InputKeyUp == 0 && InputKeyDown == 1 && InputKeyRight == 2 && InputKeyLeft == 3 &&
|
||||
InputKeyOk == 4 && InputKeyBack == 5),
|
||||
"Incorrect InputKey order");
|
||||
*/
|
||||
/** InputKey directional keys mappings for different screen orientations
|
||||
*
|
||||
*/
|
||||
/*
|
||||
static const InputKey view_port_input_mapping[ViewPortOrientationMAX][InputKeyMAX] = {
|
||||
{InputKeyUp,
|
||||
InputKeyDown,
|
||||
InputKeyRight,
|
||||
InputKeyLeft,
|
||||
InputKeyOk,
|
||||
InputKeyBack}, //ViewPortOrientationHorizontal
|
||||
{InputKeyDown,
|
||||
InputKeyUp,
|
||||
InputKeyLeft,
|
||||
InputKeyRight,
|
||||
InputKeyOk,
|
||||
InputKeyBack}, //ViewPortOrientationHorizontalFlip
|
||||
{InputKeyRight,
|
||||
InputKeyLeft,
|
||||
InputKeyDown,
|
||||
InputKeyUp,
|
||||
InputKeyOk,
|
||||
InputKeyBack}, //ViewPortOrientationVertical
|
||||
{InputKeyLeft,
|
||||
InputKeyRight,
|
||||
InputKeyUp,
|
||||
InputKeyDown,
|
||||
InputKeyOk,
|
||||
InputKeyBack}, //ViewPortOrientationVerticalFlip
|
||||
};
|
||||
|
||||
static const InputKey view_port_left_hand_input_mapping[InputKeyMAX] =
|
||||
{InputKeyDown, InputKeyUp, InputKeyLeft, InputKeyRight, InputKeyOk, InputKeyBack};
|
||||
|
||||
static const CanvasOrientation view_port_orientation_mapping[ViewPortOrientationMAX] = {
|
||||
[ViewPortOrientationHorizontal] = CanvasOrientationHorizontal,
|
||||
[ViewPortOrientationHorizontalFlip] = CanvasOrientationHorizontalFlip,
|
||||
[ViewPortOrientationVertical] = CanvasOrientationVertical,
|
||||
[ViewPortOrientationVerticalFlip] = CanvasOrientationVerticalFlip,
|
||||
};
|
||||
|
||||
//// Remaps directional pad buttons on Flipper based on ViewPort orientation
|
||||
static void view_port_map_input(InputEvent* event, ViewPortOrientation orientation) {
|
||||
furi_assert(orientation < ViewPortOrientationMAX && event->key < InputKeyMAX);
|
||||
|
||||
if(event->sequence_source != INPUT_SEQUENCE_SOURCE_HARDWARE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(orientation == ViewPortOrientationHorizontal ||
|
||||
orientation == ViewPortOrientationHorizontalFlip) {
|
||||
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagHandOrient)) {
|
||||
event->key = view_port_left_hand_input_mapping[event->key];
|
||||
}
|
||||
}
|
||||
event->key = view_port_input_mapping[orientation][event->key];
|
||||
}
|
||||
|
||||
void view_port_input_callback_set(
|
||||
ViewPort* view_port,
|
||||
ViewPortInputCallback callback,
|
||||
void* context) {
|
||||
furi_assert(view_port);
|
||||
furi_check(furi_mutex_acquire(view_port->mutex, FuriWaitForever) == FuriStatusOk);
|
||||
view_port->input_callback = callback;
|
||||
view_port->input_callback_context = context;
|
||||
furi_check(furi_mutex_release(view_port->mutex) == FuriStatusOk);
|
||||
}
|
||||
|
||||
void view_port_input(ViewPort* view_port, InputEvent* event) {
|
||||
furi_assert(view_port);
|
||||
furi_assert(event);
|
||||
furi_check(furi_mutex_acquire(view_port->mutex, FuriWaitForever) == FuriStatusOk);
|
||||
furi_check(view_port->gui);
|
||||
|
||||
if(view_port->input_callback) {
|
||||
ViewPortOrientation orientation = view_port_get_orientation(view_port);
|
||||
view_port_map_input(event, orientation);
|
||||
view_port->input_callback(event, view_port->input_callback_context);
|
||||
}
|
||||
furi_check(furi_mutex_release(view_port->mutex) == FuriStatusOk);
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
/** ViewPort Input callback
|
||||
* @warning called from GUI thread
|
||||
*/
|
||||
/*
|
||||
typedef void (*ViewPortInputCallback)(InputEvent* event, void* context);
|
||||
|
||||
void view_port_input_callback_set(
|
||||
ViewPort* view_port,
|
||||
ViewPortInputCallback callback,
|
||||
void* context);
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "spacer.h"
|
||||
#include "widgets.h"
|
||||
|
||||
lv_obj_t* spacer(lv_obj_t* parent, lv_coord_t width, lv_coord_t height) {
|
||||
lv_obj_t* spacer = lv_obj_create(parent);
|
||||
lv_obj_set_size(spacer, width, height);
|
||||
lv_obj_set_style_bg_invisible(spacer);
|
||||
return spacer;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
lv_obj_t* spacer(lv_obj_t* parent, lv_coord_t width, lv_coord_t height);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,40 @@
|
||||
#include "toolbar.h"
|
||||
#include "services/gui/widgets/widgets.h"
|
||||
#include "services/loader/loader.h"
|
||||
|
||||
static void app_toolbar_close(lv_event_t* event) {
|
||||
loader_stop_app();
|
||||
}
|
||||
|
||||
void toolbar(lv_obj_t* parent, lv_coord_t offset_y, const AppManifest* manifest) {
|
||||
lv_obj_t* toolbar = lv_obj_create(parent);
|
||||
lv_obj_set_width(toolbar, LV_PCT(100));
|
||||
lv_obj_set_height(toolbar, TOOLBAR_HEIGHT);
|
||||
lv_obj_set_pos(toolbar, 0, offset_y);
|
||||
lv_obj_set_style_no_padding(toolbar);
|
||||
lv_obj_center(toolbar);
|
||||
lv_obj_set_flex_flow(toolbar, LV_FLEX_FLOW_ROW);
|
||||
|
||||
lv_obj_t* close_button = lv_btn_create(toolbar);
|
||||
lv_obj_set_size(close_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4);
|
||||
lv_obj_set_style_no_padding(close_button);
|
||||
lv_obj_add_event_cb(close_button, &app_toolbar_close, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_t* close_button_image = lv_img_create(close_button);
|
||||
lv_img_set_src(close_button_image, LV_SYMBOL_CLOSE);
|
||||
lv_obj_align(close_button_image, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
// Need spacer to avoid button press glitch animation
|
||||
spacer(toolbar, 2, 1);
|
||||
|
||||
lv_obj_t* label_container = lv_obj_create(toolbar);
|
||||
lv_obj_set_style_no_padding(label_container);
|
||||
lv_obj_set_height(label_container, LV_PCT(100)); // 2% less due to 4px translate (it's not great, but it works)
|
||||
lv_obj_set_flex_grow(label_container, 1);
|
||||
|
||||
lv_obj_t* label = lv_label_create(label_container);
|
||||
lv_label_set_text(label, manifest->name);
|
||||
lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0);
|
||||
lv_obj_set_size(label, LV_PCT(100), TOOLBAR_FONT_HEIGHT);
|
||||
lv_obj_set_pos(label, 0, (TOOLBAR_HEIGHT - TOOLBAR_FONT_HEIGHT - 10) / 2);
|
||||
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
#include "app_manifest.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define TOOLBAR_HEIGHT 40
|
||||
#define TOOLBAR_FONT_HEIGHT 18
|
||||
|
||||
void toolbar(lv_obj_t* parent, lv_coord_t offset_y, const AppManifest* manifest);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "top_bar.h"
|
||||
#include "widgets.h"
|
||||
|
||||
void top_bar(lv_obj_t* parent) {
|
||||
lv_obj_t* topbar_container = lv_obj_create(parent);
|
||||
lv_obj_set_width(topbar_container, LV_PCT(100));
|
||||
lv_obj_set_height(topbar_container, TOP_BAR_HEIGHT);
|
||||
lv_obj_set_style_no_padding(topbar_container);
|
||||
lv_obj_set_style_bg_blacken(topbar_container);
|
||||
lv_obj_center(topbar_container);
|
||||
lv_obj_set_flex_flow(topbar_container, LV_FLEX_FLOW_ROW);
|
||||
|
||||
lv_obj_t* spacer = lv_obj_create(topbar_container);
|
||||
lv_obj_set_height(spacer, LV_PCT(100));
|
||||
lv_obj_set_style_no_padding(spacer);
|
||||
lv_obj_set_style_bg_blacken(spacer);
|
||||
lv_obj_set_flex_grow(spacer, 1);
|
||||
|
||||
lv_obj_t* wifi = lv_img_create(topbar_container);
|
||||
lv_obj_set_size(wifi, TOP_BAR_ICON_SIZE, TOP_BAR_ICON_SIZE);
|
||||
lv_obj_set_style_no_padding(wifi);
|
||||
lv_obj_set_style_bg_blacken(wifi);
|
||||
lv_obj_set_style_img_recolor(wifi, lv_color_white(), 0);
|
||||
lv_obj_set_style_img_recolor_opa(wifi, 255, 0);
|
||||
lv_img_set_src(wifi, "A:/assets/ic_small_wifi_off.png");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define TOP_BAR_ICON_SIZE 18
|
||||
#define TOP_BAR_HEIGHT (TOP_BAR_ICON_SIZE + 4) // 4 extra pixels for border and outline
|
||||
|
||||
void top_bar(lv_obj_t* parent);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "widgets.h"
|
||||
|
||||
void lv_obj_set_style_bg_blacken(lv_obj_t* obj) {
|
||||
lv_obj_set_style_bg_color(obj, lv_color_black(), 0);
|
||||
lv_obj_set_style_border_color(obj, lv_color_black(), 0);
|
||||
}
|
||||
|
||||
void lv_obj_set_style_bg_invisible(lv_obj_t* obj) {
|
||||
lv_obj_set_style_bg_opa(obj, 0, 0);
|
||||
lv_obj_set_style_border_opa(obj, 0, 0);
|
||||
}
|
||||
|
||||
void lv_obj_set_style_no_padding(lv_obj_t* obj) {
|
||||
lv_obj_set_style_pad_all(obj, LV_STATE_DEFAULT, 0);
|
||||
lv_obj_set_style_pad_gap(obj, LV_STATE_DEFAULT, 0);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "top_bar.h"
|
||||
#include "toolbar.h"
|
||||
#include "spacer.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void lv_obj_set_style_bg_blacken(lv_obj_t* obj);
|
||||
void lv_obj_set_style_bg_invisible(lv_obj_t* obj);
|
||||
void lv_obj_set_style_no_padding(lv_obj_t* obj);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,317 @@
|
||||
#include "app_i.h"
|
||||
#include "app_manifest.h"
|
||||
#include "app_manifest_registry.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "loader_i.h"
|
||||
#include "service_manifest.h"
|
||||
#include "services/gui/gui.h"
|
||||
#include <sys/cdefs.h>
|
||||
|
||||
#define TAG "Loader"
|
||||
|
||||
// Forward declarations
|
||||
static int32_t loader_main(void* p);
|
||||
|
||||
static Loader* loader = NULL;
|
||||
|
||||
static Loader* loader_alloc() {
|
||||
furi_check(loader == NULL);
|
||||
loader = malloc(sizeof(Loader));
|
||||
loader->pubsub = furi_pubsub_alloc();
|
||||
loader->queue = furi_message_queue_alloc(1, sizeof(LoaderMessage));
|
||||
loader->thread = furi_thread_alloc_ex(
|
||||
"loader",
|
||||
4096, // Last known minimum was 2400 for starting Hello World app
|
||||
&loader_main,
|
||||
NULL
|
||||
);
|
||||
loader->app_data.args = NULL;
|
||||
loader->app_data.app = NULL;
|
||||
loader->app_data.view_port = NULL;
|
||||
loader->mutex = xSemaphoreCreateRecursiveMutex();
|
||||
return loader;
|
||||
}
|
||||
|
||||
static void loader_free() {
|
||||
furi_check(loader != NULL);
|
||||
furi_thread_free(loader->thread);
|
||||
furi_pubsub_free(loader->pubsub);
|
||||
furi_message_queue_free(loader->queue);
|
||||
furi_mutex_free(loader->mutex);
|
||||
free(loader);
|
||||
}
|
||||
|
||||
void loader_lock() {
|
||||
furi_assert(loader);
|
||||
furi_assert(loader->mutex);
|
||||
furi_check(xSemaphoreTakeRecursive(loader->mutex, portMAX_DELAY) == pdPASS);
|
||||
}
|
||||
|
||||
void loader_unlock() {
|
||||
furi_assert(loader);
|
||||
furi_assert(loader->mutex);
|
||||
furi_check(xSemaphoreGiveRecursive(loader->mutex) == pdPASS);
|
||||
}
|
||||
|
||||
LoaderStatus loader_start_app(const char* id, const char* args, FuriString* error_message) {
|
||||
LoaderMessage message;
|
||||
LoaderMessageLoaderStatusResult result;
|
||||
|
||||
message.type = LoaderMessageTypeStartByName;
|
||||
message.start.id = id;
|
||||
message.start.args = args;
|
||||
message.start.error_message = error_message;
|
||||
message.api_lock = api_lock_alloc_locked();
|
||||
message.status_value = &result;
|
||||
furi_message_queue_put(loader->queue, &message, FuriWaitForever);
|
||||
api_lock_wait_unlock_and_free(message.api_lock);
|
||||
return result.value;
|
||||
}
|
||||
|
||||
void loader_start_app_nonblocking(const char* id, const char* args) {
|
||||
LoaderMessage message;
|
||||
LoaderMessageLoaderStatusResult result;
|
||||
|
||||
message.type = LoaderMessageTypeStartByName;
|
||||
message.start.id = id;
|
||||
message.start.args = args;
|
||||
message.start.error_message = NULL;
|
||||
message.api_lock = NULL;
|
||||
message.status_value = &result;
|
||||
furi_message_queue_put(loader->queue, &message, FuriWaitForever);
|
||||
}
|
||||
|
||||
void loader_stop_app() {
|
||||
LoaderMessage message;
|
||||
message.type = LoaderMessageTypeAppStop;
|
||||
furi_message_queue_put(loader->queue, &message, FuriWaitForever);
|
||||
}
|
||||
|
||||
const AppManifest* _Nullable loader_get_current_app() {
|
||||
loader_lock();
|
||||
const App* app = loader->app_data.app;
|
||||
const AppManifest* manifest = app ? app->manifest : NULL;
|
||||
loader_unlock();
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
FuriPubSub* loader_get_pubsub() {
|
||||
furi_assert(loader);
|
||||
// it's safe to return pubsub without locking
|
||||
// because it's never freed and loader is never exited
|
||||
// also the loader instance cannot be obtained until the pubsub is created
|
||||
return loader->pubsub;
|
||||
}
|
||||
|
||||
static void loader_log_status_error(
|
||||
LoaderStatus status,
|
||||
FuriString* error_message,
|
||||
const char* format,
|
||||
va_list args
|
||||
) {
|
||||
if (error_message) {
|
||||
furi_string_vprintf(error_message, format, args);
|
||||
FURI_LOG_E(TAG, "Status [%d]: %s", status, furi_string_get_cstr(error_message));
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Status [%d]", status);
|
||||
}
|
||||
}
|
||||
|
||||
static LoaderStatus loader_make_status_error(
|
||||
LoaderStatus status,
|
||||
FuriString* _Nullable error_message,
|
||||
const char* format,
|
||||
...
|
||||
) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
loader_log_status_error(status, error_message, format, args);
|
||||
va_end(args);
|
||||
return status;
|
||||
}
|
||||
|
||||
static LoaderStatus loader_make_success_status(FuriString* error_message) {
|
||||
if (error_message) {
|
||||
furi_string_set(error_message, "App started");
|
||||
}
|
||||
|
||||
return LoaderStatusOk;
|
||||
}
|
||||
|
||||
static void loader_start_app_with_manifest(
|
||||
const AppManifest* _Nonnull manifest,
|
||||
const char* args
|
||||
) {
|
||||
FURI_LOG_I(TAG, "start with manifest %s", manifest->id);
|
||||
|
||||
if (manifest->type != AppTypeUser && manifest->type != AppTypeSystem) {
|
||||
furi_crash("App type not supported by loader");
|
||||
}
|
||||
|
||||
App* _Nonnull app = furi_app_alloc(manifest);
|
||||
|
||||
loader_lock();
|
||||
|
||||
loader->app_data.app = app;
|
||||
loader->app_data.args = (void*)args;
|
||||
|
||||
if (manifest->on_start != NULL) {
|
||||
manifest->on_start((void*)args);
|
||||
}
|
||||
|
||||
if (manifest->on_show != NULL) {
|
||||
ViewPort* view_port = view_port_alloc();
|
||||
loader->app_data.view_port = view_port;
|
||||
view_port_draw_callback_set(view_port, manifest->on_show, NULL);
|
||||
|
||||
gui_add_view_port(view_port, GuiLayerWindow);
|
||||
} else {
|
||||
loader->app_data.view_port = NULL;
|
||||
}
|
||||
|
||||
loader_unlock();
|
||||
}
|
||||
|
||||
static LoaderStatus loader_do_start_by_id(
|
||||
const char* id,
|
||||
const char* args,
|
||||
FuriString* _Nullable error_message
|
||||
) {
|
||||
FURI_LOG_I(TAG, "Start by id %s", id);
|
||||
|
||||
const AppManifest* manifest = app_manifest_registry_find_by_id(id);
|
||||
if (manifest == NULL) {
|
||||
return loader_make_status_error(
|
||||
LoaderStatusErrorUnknownApp,
|
||||
error_message,
|
||||
"Application \"%s\" not found",
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
loader_start_app_with_manifest(manifest, args);
|
||||
return loader_make_success_status(error_message);
|
||||
}
|
||||
|
||||
static void loader_do_stop_app() {
|
||||
loader_lock();
|
||||
|
||||
App* app = loader->app_data.app;
|
||||
if (app == NULL) {
|
||||
FURI_LOG_W(TAG, "Stop app: no app running");
|
||||
return;
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "Stopping %s", app->manifest->id);
|
||||
|
||||
ViewPort* view_port = loader->app_data.view_port;
|
||||
if (view_port) {
|
||||
gui_remove_view_port(view_port);
|
||||
view_port_free(view_port);
|
||||
loader->app_data.view_port = NULL;
|
||||
}
|
||||
|
||||
if (app->manifest->on_stop) {
|
||||
app->manifest->on_stop();
|
||||
}
|
||||
|
||||
if (loader->app_data.args) {
|
||||
free(loader->app_data.args);
|
||||
loader->app_data.args = NULL;
|
||||
}
|
||||
|
||||
furi_app_free(loader->app_data.app);
|
||||
loader->app_data.app = NULL;
|
||||
|
||||
loader_unlock();
|
||||
|
||||
FURI_LOG_I(
|
||||
TAG,
|
||||
"Application stopped. Free heap: %zu",
|
||||
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)
|
||||
);
|
||||
|
||||
LoaderEvent event;
|
||||
event.type = LoaderEventTypeApplicationStopped;
|
||||
furi_pubsub_publish(loader->pubsub, &event);
|
||||
}
|
||||
|
||||
bool loader_is_app_running() {
|
||||
loader_lock();
|
||||
bool is_running = loader->app_data.app != NULL;
|
||||
loader_unlock();
|
||||
return is_running;
|
||||
}
|
||||
|
||||
static int32_t loader_main(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
LoaderMessage message;
|
||||
bool exit_requested = false;
|
||||
while (!exit_requested) {
|
||||
furi_check(loader != NULL);
|
||||
if (furi_message_queue_get(loader->queue, &message, FuriWaitForever) == FuriStatusOk) {
|
||||
FURI_LOG_I(TAG, "Processing message of type %d", message.type);
|
||||
switch (message.type) {
|
||||
case LoaderMessageTypeStartByName:
|
||||
if (loader_is_app_running()) {
|
||||
loader_do_stop_app();
|
||||
}
|
||||
message.status_value->value = loader_do_start_by_id(
|
||||
message.start.id,
|
||||
message.start.args,
|
||||
message.start.error_message
|
||||
);
|
||||
if (message.api_lock) {
|
||||
api_lock_unlock(message.api_lock);
|
||||
}
|
||||
break;
|
||||
case LoaderMessageTypeAppStop:
|
||||
loader_do_stop_app();
|
||||
break;
|
||||
case LoaderMessageTypeExit:
|
||||
exit_requested = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// region AppManifest
|
||||
|
||||
static void loader_start(void* parameter) {
|
||||
UNUSED(parameter);
|
||||
furi_check(loader == NULL);
|
||||
loader = loader_alloc();
|
||||
|
||||
furi_thread_set_priority(loader->thread, FuriThreadPriorityNormal);
|
||||
furi_thread_start(loader->thread);
|
||||
}
|
||||
|
||||
static void loader_stop() {
|
||||
furi_check(loader != NULL);
|
||||
LoaderMessage message = {
|
||||
.api_lock = NULL,
|
||||
.type = LoaderMessageTypeExit
|
||||
};
|
||||
|
||||
// Send stop signal to thread and wait for thread to finish
|
||||
furi_message_queue_put(loader->queue, &message, FuriWaitForever);
|
||||
furi_thread_join(loader->thread);
|
||||
|
||||
loader_free();
|
||||
loader = NULL;
|
||||
}
|
||||
|
||||
const ServiceManifest loader_service = {
|
||||
.id = "loader",
|
||||
.on_start = &loader_start,
|
||||
.on_stop = &loader_stop
|
||||
};
|
||||
|
||||
// endregion
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
#include "app_manifest.h"
|
||||
#include "furi_core.h"
|
||||
#include "furi_string.h"
|
||||
#include "pubsub.h"
|
||||
#include "service_manifest.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct Loader Loader;
|
||||
|
||||
typedef enum {
|
||||
LoaderStatusOk,
|
||||
LoaderStatusErrorAppStarted,
|
||||
LoaderStatusErrorUnknownApp,
|
||||
LoaderStatusErrorInternal,
|
||||
} LoaderStatus;
|
||||
|
||||
typedef enum {
|
||||
LoaderEventTypeApplicationStarted,
|
||||
LoaderEventTypeApplicationStopped
|
||||
} LoaderEventType;
|
||||
|
||||
typedef struct {
|
||||
LoaderEventType type;
|
||||
} LoaderEvent;
|
||||
|
||||
/**
|
||||
* @brief Close any running app, then start new one. Blocking.
|
||||
* @param[in] loader loader instance
|
||||
* @param[in] id application name or id
|
||||
* @param[in] args application arguments
|
||||
* @param[out] error_message detailed error message, can be NULL
|
||||
* @return LoaderStatus
|
||||
*/
|
||||
LoaderStatus loader_start_app(const char* id, const char* args, FuriString* error_message);
|
||||
|
||||
/**
|
||||
* @brief Close any running app, then start new one. Non-blocking.
|
||||
* @param[in] loader loader instance
|
||||
* @param[in] id application name or id
|
||||
* @param[in] args application arguments
|
||||
*/
|
||||
void loader_start_app_nonblocking(const char* id, const char* args);
|
||||
|
||||
void loader_stop_app();
|
||||
|
||||
bool loader_is_app_running();
|
||||
|
||||
const AppManifest* _Nullable loader_get_current_app();
|
||||
/**
|
||||
* @brief Start application with GUI error message
|
||||
* @param[in] instance loader instance
|
||||
* @param[in] name application name or id
|
||||
* @param[in] args application arguments
|
||||
* @return LoaderStatus
|
||||
*/
|
||||
//LoaderStatus loader_start_with_gui_error(const char* name, const char* args);
|
||||
|
||||
/**
|
||||
* @brief Show loader menu
|
||||
* @param[in] instance loader instance
|
||||
*/
|
||||
void loader_show_menu();
|
||||
|
||||
/**
|
||||
* @brief Get loader pubsub
|
||||
* @param[in] instance loader instance
|
||||
* @return FuriPubSub*
|
||||
*/
|
||||
FuriPubSub* loader_get_pubsub();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "api_lock.h"
|
||||
#include "app_manifest.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "loader.h"
|
||||
#include "message_queue.h"
|
||||
#include "pubsub.h"
|
||||
#include "services/gui/view_port.h"
|
||||
#include "thread.h"
|
||||
|
||||
typedef struct {
|
||||
char* args;
|
||||
App* app;
|
||||
ViewPort* view_port;
|
||||
} LoaderAppData;
|
||||
|
||||
struct Loader {
|
||||
FuriThread* thread;
|
||||
FuriPubSub* pubsub;
|
||||
FuriMessageQueue* queue;
|
||||
LoaderAppData app_data;
|
||||
SemaphoreHandle_t mutex;
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
LoaderMessageTypeStartByName,
|
||||
LoaderMessageTypeAppStop,
|
||||
LoaderMessageTypeExit,
|
||||
} LoaderMessageType;
|
||||
|
||||
typedef struct {
|
||||
const char* id;
|
||||
const char* args;
|
||||
FuriString* error_message;
|
||||
} LoaderMessageStartById;
|
||||
|
||||
typedef struct {
|
||||
LoaderStatus value;
|
||||
} LoaderMessageLoaderStatusResult;
|
||||
|
||||
typedef struct {
|
||||
bool value;
|
||||
} LoaderMessageBoolResult;
|
||||
|
||||
typedef struct {
|
||||
// This lock blocks anyone from starting an app as long
|
||||
// as an app is already running via loader_start()
|
||||
FuriApiLock api_lock;
|
||||
LoaderMessageType type;
|
||||
|
||||
union {
|
||||
LoaderMessageStartById start;
|
||||
};
|
||||
|
||||
union {
|
||||
LoaderMessageLoaderStatusResult* status_value;
|
||||
LoaderMessageBoolResult* bool_value;
|
||||
};
|
||||
} LoaderMessage;
|
||||
Reference in New Issue
Block a user