diff --git a/.clang-format b/.clang-format index fc70e419..5fd46b99 100644 --- a/.clang-format +++ b/.clang-format @@ -4,7 +4,7 @@ BasedOnStyle: LLVM AccessModifierOffset: -4 AlignAfterOpenBracket: BlockIndent AlignConsecutiveAssignments: None -AlignOperands: Align +AlignOperands: DontAlign AllowAllArgumentsOnNextLine: false AllowAllConstructorInitializersOnNextLine: false AllowAllParametersOfDeclarationOnNextLine: false diff --git a/README.md b/README.md index 17f72e31..77cb6fa8 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ and [esp_lcd_touch](https://components.espressif.com/components/espressif/esp_lc ### Devices Predefined configurations are available for: -- Yellow Board: 2.4" with capacitive touch (2432S024) (see AliExpress [1](https://www.aliexpress.com/item/1005005902429049.html), [2](https://www.aliexpress.com/item/1005005865107357.html)) +- Yellow Board: 2.4" with capacitive touch (2432S024C) (see AliExpress [1](https://www.aliexpress.com/item/1005005902429049.html), [2](https://www.aliexpress.com/item/1005005865107357.html)) - LilyGo T-Deck (see [lilygo.cc](https://www.lilygo.cc/products/t-deck), [AliExpress](https://www.aliexpress.com/item/1005005692235592.html)) - (more will follow) diff --git a/boards/lilygo_tdeck/display.c b/boards/lilygo_tdeck/display.c index 4e58ba7e..cc3637e7 100644 --- a/boards/lilygo_tdeck/display.c +++ b/boards/lilygo_tdeck/display.c @@ -104,7 +104,7 @@ static bool create_display_device(DisplayDevice* display) { ESP_LOGI(TAG, "install driver"); const esp_lcd_panel_dev_config_t panel_config = { .reset_gpio_num = GPIO_NUM_NC, - .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, .data_endian = LCD_RGB_DATA_ENDIAN_BIG, .bits_per_pixel = LCD_BITS_PER_PIXEL, .flags = { diff --git a/boards/lilygo_tdeck/lilygo_tdeck.c b/boards/lilygo_tdeck/lilygo_tdeck.c index 6a66dd14..3d868456 100644 --- a/boards/lilygo_tdeck/lilygo_tdeck.c +++ b/boards/lilygo_tdeck/lilygo_tdeck.c @@ -1,9 +1,5 @@ #include "lilygo_tdeck.h" -void lilygo_tdeck_bootstrap(); -DisplayDriver lilygo_tdeck_display_driver(); -TouchDriver lilygo_tdeck_touch_driver(); - const HardwareConfig lilygo_tdeck = { .bootstrap = &lilygo_tdeck_bootstrap, .display_driver = &lilygo_tdeck_display_driver, diff --git a/boards/lilygo_tdeck/lilygo_tdeck.h b/boards/lilygo_tdeck/lilygo_tdeck.h index f26f9001..4443fe8b 100644 --- a/boards/lilygo_tdeck/lilygo_tdeck.h +++ b/boards/lilygo_tdeck/lilygo_tdeck.h @@ -2,4 +2,17 @@ #include "tactility.h" +#ifdef __cplusplus +extern "C" { +#endif + +// Available for HardwareConfig customizations +void lilygo_tdeck_bootstrap(); +DisplayDriver lilygo_tdeck_display_driver(); +TouchDriver lilygo_tdeck_touch_driver(); + extern const HardwareConfig lilygo_tdeck; + +#ifdef __cplusplus +} +#endif diff --git a/boards/yellow_board/display.c b/boards/yellow_board/display.c index 3ea1ba0e..d9554459 100644 --- a/boards/yellow_board/display.c +++ b/boards/yellow_board/display.c @@ -63,7 +63,7 @@ static bool create_display_device(DisplayDevice* display) { ESP_LOGI(TAG, "install driver"); const esp_lcd_panel_dev_config_t panel_config = { .reset_gpio_num = GPIO_NUM_NC, - .rgb_endian = LCD_RGB_ENDIAN_RGB, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, .bits_per_pixel = LCD_BITS_PER_PIXEL, }; diff --git a/boards/yellow_board/yellow_board.c b/boards/yellow_board/yellow_board.c index 4543ef60..82d74b5f 100644 --- a/boards/yellow_board/yellow_board.c +++ b/boards/yellow_board/yellow_board.c @@ -1,8 +1,5 @@ #include "yellow_board.h" -DisplayDriver board_2432s024_create_display_driver(); -TouchDriver board_2432s024_create_touch_driver(); - const HardwareConfig yellow_board_24inch_cap = { .bootstrap = NULL, .display_driver = &board_2432s024_create_display_driver, diff --git a/boards/yellow_board/yellow_board.h b/boards/yellow_board/yellow_board.h index ca527aee..0c24b6bb 100644 --- a/boards/yellow_board/yellow_board.h +++ b/boards/yellow_board/yellow_board.h @@ -2,5 +2,17 @@ #include "tactility.h" +#ifdef __cplusplus +extern "C" { +#endif + +// Available for HardwareConfig customizations +DisplayDriver board_2432s024_create_display_driver(); +TouchDriver board_2432s024_create_touch_driver(); + // Capacitive touch version of the 2.4" yellow board extern const HardwareConfig yellow_board_24inch_cap; + +#ifdef __cplusplus +} +#endif diff --git a/components/furi/src/app.c b/components/furi/src/app.c index 6728aaf1..6c85c40f 100644 --- a/components/furi/src/app.c +++ b/components/furi/src/app.c @@ -1,22 +1,128 @@ #include "app_i.h" -#include "furi_core.h" -#include "log.h" -#include "furi_string.h" -#define TAG "app" +#include -App* furi_app_alloc(const AppManifest* _Nonnull manifest) { - App app = { +static AppFlags app_get_flags_default(AppType type); + +// region Alloc/free + +App app_alloc(const AppManifest* manifest, Bundle* _Nullable parameters) { + AppData* data = malloc(sizeof(AppData)); + *data = (AppData) { + .mutex = furi_mutex_alloc(FuriMutexTypeRecursive), + .state = APP_STATE_INITIAL, + .flags = app_get_flags_default(manifest->type), .manifest = manifest, - .context = { - .data = NULL - } + .parameters = parameters, + .data = NULL }; - App* app_ptr = malloc(sizeof(App)); - return memcpy(app_ptr, &app, sizeof(App)); + return (App*)data; } -void furi_app_free(App* app) { - furi_assert(app); - free(app); +void app_free(App app) { + AppData* data = (AppData*)app; + if (data->parameters) { + bundle_free(data->parameters); + } + furi_mutex_free(data->mutex); + free(data); } + +// endregion + +// region Internal + +static void app_lock(AppData* data) { + furi_mutex_acquire(data->mutex, FuriMutexTypeRecursive); +} + +static void app_unlock(AppData* data) { + furi_mutex_release(data->mutex); +} + +static AppFlags app_get_flags_default(AppType type) { + static const AppFlags DEFAULT_DESKTOP_FLAGS = { + .show_toolbar = false, + .show_statusbar = true + }; + + static const AppFlags DEFAULT_APP_FLAGS = { + .show_toolbar = true, + .show_statusbar = true + }; + + return type == AppTypeDesktop + ? DEFAULT_DESKTOP_FLAGS + : DEFAULT_APP_FLAGS; +} + +// endregion Internal + +// region Public getters & setters + +void app_set_state(App app, AppState state) { + AppData* data = (AppData*)app; + app_lock(data); + data->state = state; + app_unlock(data); +} + +AppState app_get_state(App app) { + AppData* data = (AppData*)app; + app_lock(data); + AppState state = data->state; + app_unlock(data); + return state; +} + +const AppManifest* app_get_manifest(App app) { + AppData* data = (AppData*)app; + // No need to lock const data; + return data->manifest; +} + +AppFlags app_get_flags(App app) { + AppData* data = (AppData*)app; + app_lock(data); + AppFlags flags = data->flags; + app_unlock(data); + return flags; +} + +void app_set_flags(App app, AppFlags flags) { + AppData* data = (AppData*)app; + app_lock(data); + data->flags = flags; + app_unlock(data); +} + +void* app_get_data(App app) { + AppData* data = (AppData*)app; + app_lock(data); + void* value = data->data; + app_unlock(data); + return value; +} + +void app_set_data(App app, void* value) { + AppData* data = (AppData*)app; + app_lock(data); + data->data = value; + app_unlock(data); +} + +/** TODO: Make this thread-safe. + * In practice, the bundle is writeable, so someone could be writing to it + * while it is being accessed from another thread. + * Consider creating MutableBundle vs Bundle. + * Consider not exposing bundle, but expose `app_get_bundle_int(key)` methods with locking in it. + */ +Bundle* _Nullable app_get_parameters(App app) { + AppData* data = (AppData*)app; + app_lock(data); + Bundle* bundle = data->parameters; + app_unlock(data); + return bundle; +} + +// endregion Public getters & setters diff --git a/components/furi/src/app.h b/components/furi/src/app.h new file mode 100644 index 00000000..b22a331e --- /dev/null +++ b/components/furi/src/app.h @@ -0,0 +1,51 @@ +#pragma once + +#include "app_manifest.h" +#include "bundle.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + APP_STATE_INITIAL, // App is being activated in loader + APP_STATE_STARTED, // App is in memory + APP_STATE_SHOWING, // App view is created + APP_STATE_HIDING, // App view is destroyed + APP_STATE_STOPPED // App is not in memory +} AppState; + +typedef union { + struct { + bool show_statusbar : 1; + bool show_toolbar : 1; + }; + unsigned char flags; +} AppFlags; + +typedef void* App; + +/** @brief Create an app + * @param manifest + * @param parameters optional bundle. memory ownership is transferred to App + * @return + */ +App app_alloc(const AppManifest* manifest, Bundle* _Nullable parameters); +void app_free(App app); + +void app_set_state(App app, AppState state); +AppState app_get_state(App app); + +const AppManifest* app_get_manifest(App app); + +AppFlags app_get_flags(App app); +void app_set_flags(App app, AppFlags flags); + +void* _Nullable app_get_data(App app); +void app_set_data(App app, void* data); + +Bundle* _Nullable app_get_parameters(App app); + +#ifdef __cplusplus +} +#endif diff --git a/components/furi/src/app_i.h b/components/furi/src/app_i.h index c5b08eaa..44544b03 100644 --- a/components/furi/src/app_i.h +++ b/components/furi/src/app_i.h @@ -1,19 +1,33 @@ #pragma once +#include "app.h" + #include "app_manifest.h" -#include "thread.h" +#include "context.h" +#include "mutex.h" +#include #ifdef __cplusplus extern "C" { #endif typedef struct { + FuriMutex* mutex; const AppManifest* manifest; - Context context; -} App; - -App* furi_app_alloc(const AppManifest* _Nonnull manifest); -void furi_app_free(App* _Nonnull app); + AppState state; + AppFlags flags; + /** @brief Optional parameters to start the app with + * When these are stored in the app struct, the struct takes ownership. + * Do not mutate after app creation. + */ + Bundle* _Nullable parameters; + /** @brief @brief Contextual data related to the running app's instance + * The app can attach its data to this. + * The lifecycle is determined by the on_start and on_stop methods in the AppManifest. + * These manifest methods can optionally allocate/free data that is attached here. + */ + void* _Nullable data; +} AppData; #ifdef __cplusplus } diff --git a/components/furi/src/app_manifest.h b/components/furi/src/app_manifest.h index 1ae4804f..980075fb 100644 --- a/components/furi/src/app_manifest.h +++ b/components/furi/src/app_manifest.h @@ -1,6 +1,5 @@ #pragma once -#include "context.h" #include #ifdef __cplusplus @@ -9,16 +8,19 @@ extern "C" { // Forward declarations typedef struct _lv_obj_t lv_obj_t; +typedef void* App; typedef enum { + AppTypeDesktop, AppTypeSystem, AppTypeSettings, AppTypeUser } AppType; -typedef void (*AppOnStart)(Context* context); -typedef void (*AppOnStop)(Context* context); -typedef void (*AppOnShow)(Context* context, lv_obj_t* parent); +typedef void (*AppOnStart)(App app); +typedef void (*AppOnStop)(App app); +typedef void (*AppOnShow)(App app, lv_obj_t* parent); +typedef void (*AppOnHide)(App app); typedef struct { /** @@ -55,6 +57,11 @@ typedef struct { * Non-blocking method to create the GUI */ const AppOnShow _Nullable on_show; + + /** + * Non-blocking method, called before gui is destroyed + */ + const AppOnHide _Nullable on_hide; } AppManifest; #ifdef __cplusplus diff --git a/components/furi/src/app_manifest_registry.c b/components/furi/src/app_manifest_registry.c index 94f57912..80d8bce7 100644 --- a/components/furi/src/app_manifest_registry.c +++ b/components/furi/src/app_manifest_registry.c @@ -1,4 +1,5 @@ #include "app_manifest_registry.h" + #include "furi_core.h" #include "m-dict.h" #include "m_cstr_dup.h" diff --git a/components/furi/src/bundle.c b/components/furi/src/bundle.c new file mode 100644 index 00000000..da6a4fb6 --- /dev/null +++ b/components/furi/src/bundle.c @@ -0,0 +1,205 @@ +#include "bundle.h" + +#include "m-dict.h" +#include "m_cstr_dup.h" +#include "check.h" + +// region BundleEntry + +typedef enum { + BUNDLE_ENTRY_TYPE_BOOL, + BUNDLE_ENTRY_TYPE_INT, + BUNDLE_ENTRY_TYPE_STRING, +} BundleEntryType; + +typedef struct { + BundleEntryType type; + union { + bool bool_value; + int int_value; + char* string_ptr; + }; +} BundleEntry; + +BundleEntry* bundle_entry_alloc_bool(bool value) { + BundleEntry* entry = malloc(sizeof(BundleEntry)); + entry->type = BUNDLE_ENTRY_TYPE_BOOL; + entry->bool_value = value; + return entry; +} + +BundleEntry* bundle_entry_alloc_int(int value) { + BundleEntry* entry = malloc(sizeof(BundleEntry)); + entry->type = BUNDLE_ENTRY_TYPE_INT; + entry->int_value = value; + return entry; +} + +BundleEntry* bundle_entry_alloc_string(const char* text) { + BundleEntry* entry = malloc(sizeof(BundleEntry)); + entry->type = BUNDLE_ENTRY_TYPE_STRING; + entry->string_ptr = malloc(strlen(text) + 1); + strcpy(entry->string_ptr, text); + return entry; +} + +BundleEntry* bundle_entry_alloc_copy(BundleEntry* source) { + BundleEntry* entry = malloc(sizeof(BundleEntry)); + entry->type = source->type; + if (source->type == BUNDLE_ENTRY_TYPE_STRING) { + entry->string_ptr = malloc(strlen(source->string_ptr) + 1); + strcpy(entry->string_ptr, source->string_ptr); + } else { + entry->int_value = source->int_value; + } + return entry; +} + +void bundle_entry_free(BundleEntry* entry) { + if (entry->type == BUNDLE_ENTRY_TYPE_STRING) { + free(entry->string_ptr); + } + free(entry); +} + +// endregion BundleEntry + +// region Bundle + +DICT_DEF2(BundleDict, const char*, M_CSTR_DUP_OPLIST, BundleEntry*, M_PTR_OPLIST) + +typedef struct { + BundleDict_t dict; +} BundleData; + +Bundle bundle_alloc() { + BundleData* bundle = malloc(sizeof(BundleData)); + BundleDict_init(bundle->dict); + return bundle; +} + +Bundle bundle_alloc_copy(Bundle source) { + BundleData* source_data = (BundleData*)source; + BundleData* target_data = bundle_alloc(); + + BundleDict_it_t it; + for (BundleDict_it(it, source_data->dict); !BundleDict_end_p(it); BundleDict_next(it)) { + const char* key = BundleDict_cref(it)->key; + BundleEntry* entry = BundleDict_cref(it)->value; + BundleEntry* entry_copy = bundle_entry_alloc_copy(entry); + BundleDict_set_at(target_data->dict, key, entry_copy); + } + + return target_data; +} + +void bundle_free(Bundle bundle) { + BundleData* data = (BundleData*)bundle; + + BundleDict_it_t it; + for (BundleDict_it(it, data->dict); !BundleDict_end_p(it); BundleDict_next(it)) { + bundle_entry_free(BundleDict_cref(it)->value); + } + + BundleDict_clear(data->dict); + free(data); +} + +bool bundle_get_bool(Bundle bundle, const char* key) { + BundleData* data = (BundleData*)bundle; + BundleEntry** entry = BundleDict_get(data->dict, key); + furi_check(entry != NULL); + return (*entry)->bool_value; +} + +int bundle_get_int(Bundle bundle, const char* key) { + BundleData* data = (BundleData*)bundle; + BundleEntry** entry = BundleDict_get(data->dict, key); + furi_check(entry != NULL); + return (*entry)->int_value; +} + +const char* bundle_get_string(Bundle bundle, const char* key) { + BundleData* data = (BundleData*)bundle; + BundleEntry** entry = BundleDict_get(data->dict, key); + furi_check(entry != NULL); + return (*entry)->string_ptr; +} + +bool bundle_opt_bool(Bundle bundle, const char* key, bool* out) { + BundleData* data = (BundleData*)bundle; + BundleEntry** entry = BundleDict_get(data->dict, key); + if (entry != NULL) { + *out = (*entry)->bool_value; + return true; + } else { + return false; + } +} + +bool bundle_opt_int(Bundle bundle, const char* key, int* out) { + BundleData* data = (BundleData*)bundle; + BundleEntry** entry = BundleDict_get(data->dict, key); + if (entry != NULL) { + *out = (*entry)->int_value; + return true; + } else { + return false; + } +} + +bool bundle_opt_string(Bundle bundle, const char* key, char** out) { + BundleData* data = (BundleData*)bundle; + BundleEntry** entry = BundleDict_get(data->dict, key); + if (entry != NULL) { + *out = (*entry)->string_ptr; + return true; + } else { + return false; + } +} + +void bundle_put_bool(Bundle bundle, const char* key, bool value) { + BundleData* data = (BundleData*)bundle; + BundleEntry** entry_handle = BundleDict_get(data->dict, key); + if (entry_handle != NULL) { + BundleEntry* entry = *entry_handle; + furi_assert(entry->type == BUNDLE_ENTRY_TYPE_BOOL); + entry->bool_value = value; + } else { + BundleEntry* entry = bundle_entry_alloc_bool(value); + BundleDict_set_at(data->dict, key, entry); + } +} + +void bundle_put_int(Bundle bundle, const char* key, int value) { + BundleData* data = (BundleData*)bundle; + BundleEntry** entry_handle = BundleDict_get(data->dict, key); + if (entry_handle != NULL) { + BundleEntry* entry = *entry_handle; + furi_assert(entry->type == BUNDLE_ENTRY_TYPE_INT); + entry->int_value = value; + } else { + BundleEntry* entry = bundle_entry_alloc_int(value); + BundleDict_set_at(data->dict, key, entry); + } +} + +void bundle_put_string(Bundle bundle, const char* key, const char* value) { + BundleData* data = (BundleData*)bundle; + BundleEntry** entry_handle = BundleDict_get(data->dict, key); + if (entry_handle != NULL) { + BundleEntry* entry = *entry_handle; + furi_assert(entry->type == BUNDLE_ENTRY_TYPE_STRING); + if (entry->string_ptr != NULL) { + free(entry->string_ptr); + } + entry->string_ptr = malloc(strlen(value) + 1); + strcpy(entry->string_ptr, value); + } else { + BundleEntry* entry = bundle_entry_alloc_string(value); + BundleDict_set_at(data->dict, key, entry); + } +} + +// endregion Bundle \ No newline at end of file diff --git a/components/furi/src/bundle.h b/components/furi/src/bundle.h new file mode 100644 index 00000000..b33c2c35 --- /dev/null +++ b/components/furi/src/bundle.h @@ -0,0 +1,34 @@ +/** + * @brief key-value storage for general purpose. + * Maps strings on a fixed set of data types. + */ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void* Bundle; + +Bundle bundle_alloc(); +Bundle bundle_alloc_copy(Bundle source); +void bundle_free(Bundle bundle); + +bool bundle_get_bool(Bundle bundle, const char* key); +int bundle_get_int(Bundle bundle, const char* key); +const char* bundle_get_string(Bundle bundle, const char* key); + +bool bundle_opt_bool(Bundle bundle, const char* key, bool* out); +bool bundle_opt_int(Bundle bundle, const char* key, int* out); +bool bundle_opt_string(Bundle bundle, const char* key, char** out); + +void bundle_put_bool(Bundle bundle, const char* key, bool value); +void bundle_put_int(Bundle bundle, const char* key, int value); +void bundle_put_string(Bundle bundle, const char* key, const char* value); + +#ifdef __cplusplus +} +#endif diff --git a/components/furi/src/context.h b/components/furi/src/context.h index 620571d4..654ec6ce 100644 --- a/components/furi/src/context.h +++ b/components/furi/src/context.h @@ -1,11 +1,5 @@ #pragma once typedef struct { - /** Contextual data related to the running app's instance - * - * The app can attach its data to this. - * The lifecycle is determined by the on_start and on_stop methods in the AppManifest. - * These manifest methods can optionally allocate/free data that is attached here. - */ - void* data; + } Context; diff --git a/components/furi/src/furi.c b/components/furi/src/furi.c index 4444c7cd..6822d167 100644 --- a/components/furi/src/furi.c +++ b/components/furi/src/furi.c @@ -11,12 +11,6 @@ void furi_init() { FURI_LOG_I(TAG, "init start"); furi_assert(!furi_kernel_is_irq()); - if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) { - vTaskSuspendAll(); - } - - xTaskResumeAll(); - #if defined(__ARM_ARCH_7A__) && (__ARM_ARCH_7A__ == 0U) /* Service Call interrupt might be configured before kernel start */ /* and when its priority is lower or equal to BASEPRI, svc instruction */ diff --git a/components/furi/src/message_queue.c b/components/furi/src/message_queue.c index 3b792575..5b50e740 100644 --- a/components/furi/src/message_queue.c +++ b/components/furi/src/message_queue.c @@ -59,7 +59,7 @@ furi_message_queue_put(FuriMessageQueue* instance, const void* msg_ptr, uint32_t return (stat); } -FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uint32_t timeout) { +FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uint32_t timeout_ticks) { QueueHandle_t hQueue = (QueueHandle_t)instance; FuriStatus stat; BaseType_t yield; @@ -67,7 +67,7 @@ FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uin stat = FuriStatusOk; if (furi_kernel_is_irq() != 0U) { - if ((hQueue == NULL) || (msg_ptr == NULL) || (timeout != 0U)) { + if ((hQueue == NULL) || (msg_ptr == NULL) || (timeout_ticks != 0U)) { stat = FuriStatusErrorParameter; } else { yield = pdFALSE; @@ -82,8 +82,8 @@ FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uin if ((hQueue == NULL) || (msg_ptr == NULL)) { stat = FuriStatusErrorParameter; } else { - if (xQueueReceive(hQueue, msg_ptr, (TickType_t)timeout) != pdPASS) { - if (timeout != 0U) { + if (xQueueReceive(hQueue, msg_ptr, (TickType_t)timeout_ticks) != pdPASS) { + if (timeout_ticks != 0U) { stat = FuriStatusErrorTimeout; } else { stat = FuriStatusErrorResource; diff --git a/components/furi/src/message_queue.h b/components/furi/src/message_queue.h index f9cb9dae..4b17e7ee 100644 --- a/components/furi/src/message_queue.h +++ b/components/furi/src/message_queue.h @@ -44,11 +44,11 @@ furi_message_queue_put(FuriMessageQueue* instance, const void* msg_ptr, uint32_t * @param instance pointer to FuriMessageQueue instance * @param msg_ptr The message pointer * @param msg_prio The message prioority - * @param[in] timeout The timeout + * @param[in] timeout_ticks The timeout * * @return The furi status. */ -FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uint32_t timeout); +FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uint32_t timeout_ticks); /** Get queue capacity * diff --git a/components/furi/src/mutex.c b/components/furi/src/mutex.c index 45500a18..ee190caf 100644 --- a/components/furi/src/mutex.c +++ b/components/furi/src/mutex.c @@ -4,6 +4,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" +#include "log.h" FuriMutex* furi_mutex_alloc(FuriMutexType type) { furi_assert(!FURI_IS_IRQ_MODE()); diff --git a/components/furi/src/service.c b/components/furi/src/service.c index 19a3727e..f71988d4 100644 --- a/components/furi/src/service.c +++ b/components/furi/src/service.c @@ -2,20 +2,62 @@ #include "furi_core.h" #include "log.h" -#define TAG "service" +// region Alloc/free -Service* furi_service_alloc(const ServiceManifest* _Nonnull manifest) { - Service app = { +Service service_alloc(const ServiceManifest* _Nonnull manifest) { + ServiceData* data = malloc(sizeof(ServiceData)); + *data = (ServiceData) { .manifest = manifest, - .context = { - .data = NULL - } + .mutex = furi_mutex_alloc(FuriMutexTypeRecursive), + .data = NULL }; - Service* app_ptr = malloc(sizeof(Service)); - return memcpy(app_ptr, &app, sizeof(Service)); + return data; } -void furi_service_free(Service* app) { - furi_assert(app); - free(app); +void service_free(Service service) { + ServiceData* data = (ServiceData*)service; + furi_assert(service); + furi_mutex_free(data->mutex); + free(data); } + +// endregion Alloc/free + +// region Internal + +static void service_lock(ServiceData * data) { + furi_mutex_acquire(data->mutex, FuriMutexTypeRecursive); +} + +static void service_unlock(ServiceData* data) { + furi_mutex_release(data->mutex); +} + +// endregion Internal + +// region Getters & Setters + +const ServiceManifest* service_get_manifest(Service service) { + ServiceData* data = (ServiceData*)service; + service_lock(data); + const ServiceManifest* manifest = data->manifest; + service_unlock(data); + return manifest; +} + +void service_set_data(Service service, void* value) { + ServiceData* data = (ServiceData*)service; + service_lock(data); + data->data = value; + service_unlock(data); +} + +void* _Nullable service_get_data(Service service) { + ServiceData* data = (ServiceData*)service; + service_lock(data); + void* value = data->data; + service_unlock(data); + return value; +} + +// endregion Getters & Setters \ No newline at end of file diff --git a/components/furi/src/service.h b/components/furi/src/service.h new file mode 100644 index 00000000..f08d471d --- /dev/null +++ b/components/furi/src/service.h @@ -0,0 +1,18 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "service_manifest.h" + +typedef void* Service; + +const ServiceManifest* service_get_manifest(Service service); + +void service_set_data(Service service, void* value); +void* _Nullable service_get_data(Service service); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/components/furi/src/service_i.h b/components/furi/src/service_i.h index fb34348f..d836e6dd 100644 --- a/components/furi/src/service_i.h +++ b/components/furi/src/service_i.h @@ -1,12 +1,16 @@ #pragma once -#include "service_manifest.h" +#include "service.h" + #include "context.h" +#include "mutex.h" +#include "service_manifest.h" typedef struct { + FuriMutex* mutex; const ServiceManifest* manifest; - Context context; -} Service; + void* data; +} ServiceData; -Service* furi_service_alloc(const ServiceManifest* _Nonnull manifest); -void furi_service_free(Service* _Nonnull service); +Service service_alloc(const ServiceManifest* _Nonnull manifest); +void service_free(Service _Nonnull service); diff --git a/components/furi/src/service_manifest.h b/components/furi/src/service_manifest.h index b065ba13..3db1fead 100644 --- a/components/furi/src/service_manifest.h +++ b/components/furi/src/service_manifest.h @@ -7,8 +7,10 @@ extern "C" { #endif -typedef void (*ServiceOnStart)(Context* context); -typedef void (*ServiceOnStop)(Context* context); +typedef void* Service; + +typedef void (*ServiceOnStart)(Service service); +typedef void (*ServiceOnStop)(Service service); typedef struct { /** diff --git a/components/furi/src/service_registry.c b/components/furi/src/service_registry.c index 61c4ef8d..c7ef891a 100644 --- a/components/furi/src/service_registry.c +++ b/components/furi/src/service_registry.c @@ -9,17 +9,17 @@ #define TAG "service_registry" DICT_DEF2(ServiceManifestDict, const char*, M_CSTR_DUP_OPLIST, const ServiceManifest*, M_PTR_OPLIST) -DICT_DEF2(ServiceInstanceDict, const char*, M_CSTR_DUP_OPLIST, const Service*, M_PTR_OPLIST) +DICT_DEF2(ServiceInstanceDict, const char*, M_CSTR_DUP_OPLIST, const ServiceData*, M_PTR_OPLIST) -#define APP_REGISTRY_FOR_EACH(manifest_var_name, code_to_execute) \ - { \ - service_registry_manifest_lock(); \ - ServiceManifestDict_it_t it; \ +#define APP_REGISTRY_FOR_EACH(manifest_var_name, code_to_execute) \ + { \ + service_registry_manifest_lock(); \ + ServiceManifestDict_it_t it; \ for (ServiceManifestDict_it(it, service_manifest_dict); !ServiceManifestDict_end_p(it); ServiceManifestDict_next(it)) { \ - const ServiceManifest*(manifest_var_name) = ServiceManifestDict_cref(it)->value; \ - code_to_execute; \ - } \ - service_registry_manifest_unlock(); \ + const ServiceManifest*(manifest_var_name) = ServiceManifestDict_cref(it)->value; \ + code_to_execute; \ + } \ + service_registry_manifest_unlock(); \ } ServiceManifestDict_t service_manifest_dict; @@ -78,13 +78,13 @@ const ServiceManifest* _Nullable service_registry_find_manifest_by_id(const char return (manifest != NULL) ? *manifest : NULL; } -Service* _Nullable service_registry_find_instance_by_id(const char* id) { +ServiceData* _Nullable service_registry_find_instance_by_id(const char* id) { service_registry_instance_lock(); - const Service** _Nullable service_ptr = ServiceInstanceDict_get(service_instance_dict, id); + const ServiceData** _Nullable service_ptr = ServiceInstanceDict_get(service_instance_dict, id); if (service_ptr == NULL) { return NULL; } - Service* service = (Service*) *service_ptr; + ServiceData* service = (ServiceData*)*service_ptr; service_registry_instance_unlock(); return service; } @@ -100,12 +100,12 @@ bool service_registry_start(const char* service_id) { FURI_LOG_I(TAG, "starting %s", service_id); const ServiceManifest* manifest = service_registry_find_manifest_by_id(service_id); if (manifest == NULL) { - FURI_LOG_I(TAG, "manifest not found for %s", service_id); + FURI_LOG_E(TAG, "manifest not found for service %s", service_id); return false; } - Service* service = furi_service_alloc(manifest); - service->manifest->on_start(&service->context); + Service service = service_alloc(manifest); + manifest->on_start(service); service_registry_instance_lock(); ServiceInstanceDict_set_at(service_instance_dict, manifest->id, service); @@ -117,14 +117,14 @@ bool service_registry_start(const char* service_id) { bool service_registry_stop(const char* service_id) { FURI_LOG_I(TAG, "stopping %s", service_id); - Service* service = service_registry_find_instance_by_id(service_id); + ServiceData* service = service_registry_find_instance_by_id(service_id); if (service == NULL) { - FURI_LOG_I(TAG, "service not running: %s", service_id); + FURI_LOG_W(TAG, "service not running: %s", service_id); return false; } - service->manifest->on_stop(&service->context); - furi_service_free(service); + service->manifest->on_stop(service); + service_free(service); service_registry_instance_lock(); ServiceInstanceDict_erase(service_instance_dict, service_id); diff --git a/components/tactility/CMakeLists.txt b/components/tactility/CMakeLists.txt index 31c9e8c4..4f2e1f27 100644 --- a/components/tactility/CMakeLists.txt +++ b/components/tactility/CMakeLists.txt @@ -1,10 +1,14 @@ idf_component_register( SRC_DIRS "src" + "src/apps/desktop" "src/apps/system/system_info" - "src/services/desktop" + "src/apps/system/wifi_connect" + "src/apps/system/wifi_manage" "src/services/loader" "src/services/gui" "src/services/gui/widgets" + "src/services/wifi" + "src/ui" INCLUDE_DIRS "src" @@ -12,6 +16,7 @@ idf_component_register( esp_lcd esp_lcd_touch esp_lvgl_port + esp_wifi driver fatfs furi diff --git a/components/tactility/assets/network_wifi.png b/components/tactility/assets/network_wifi.png new file mode 100644 index 00000000..00c107a1 Binary files /dev/null and b/components/tactility/assets/network_wifi.png differ diff --git a/components/tactility/assets/network_wifi_1_bar.png b/components/tactility/assets/network_wifi_1_bar.png new file mode 100644 index 00000000..a181c191 Binary files /dev/null and b/components/tactility/assets/network_wifi_1_bar.png differ diff --git a/components/tactility/assets/network_wifi_1_bar_locked.png b/components/tactility/assets/network_wifi_1_bar_locked.png new file mode 100644 index 00000000..1b59d794 Binary files /dev/null and b/components/tactility/assets/network_wifi_1_bar_locked.png differ diff --git a/components/tactility/assets/network_wifi_2_bar.png b/components/tactility/assets/network_wifi_2_bar.png new file mode 100644 index 00000000..82b45eb4 Binary files /dev/null and b/components/tactility/assets/network_wifi_2_bar.png differ diff --git a/components/tactility/assets/network_wifi_2_bar_locked.png b/components/tactility/assets/network_wifi_2_bar_locked.png new file mode 100644 index 00000000..ca2c8f67 Binary files /dev/null and b/components/tactility/assets/network_wifi_2_bar_locked.png differ diff --git a/components/tactility/assets/network_wifi_3_bar.png b/components/tactility/assets/network_wifi_3_bar.png new file mode 100644 index 00000000..c07627de Binary files /dev/null and b/components/tactility/assets/network_wifi_3_bar.png differ diff --git a/components/tactility/assets/network_wifi_3_bar_locked.png b/components/tactility/assets/network_wifi_3_bar_locked.png new file mode 100644 index 00000000..b9596cfe Binary files /dev/null and b/components/tactility/assets/network_wifi_3_bar_locked.png differ diff --git a/components/tactility/assets/network_wifi_locked.png b/components/tactility/assets/network_wifi_locked.png new file mode 100644 index 00000000..cfd4f35c Binary files /dev/null and b/components/tactility/assets/network_wifi_locked.png differ diff --git a/components/tactility/src/apps/desktop/desktop.c b/components/tactility/src/apps/desktop/desktop.c new file mode 100644 index 00000000..4de5f5bf --- /dev/null +++ b/components/tactility/src/apps/desktop/desktop.c @@ -0,0 +1,43 @@ +#include "app_manifest_registry.h" +#include "check.h" +#include "lvgl.h" +#include "services/loader/loader.h" + +static void on_app_pressed(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(manifest->id, false, NULL); + } +} + +static void create_app_widget(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_app_pressed, LV_EVENT_CLICKED, (void*)manifest); +} + +static void desktop_show(App app, lv_obj_t* parent) { + UNUSED(app); + + 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, create_app_widget); + lv_list_add_text(list, "User"); + app_manifest_registry_for_each_of_type(AppTypeUser, list, create_app_widget); +} + +const AppManifest desktop_app = { + .id = "desktop", + .name = "Desktop", + .icon = NULL, + .type = AppTypeDesktop, + .on_start = NULL, + .on_stop = NULL, + .on_show = &desktop_show, + .on_hide = NULL +}; diff --git a/components/tactility/src/apps/system/system_info/system_info.c b/components/tactility/src/apps/system/system_info/system_info.c index a14d3a5e..4091906d 100644 --- a/components/tactility/src/apps/system/system_info/system_info.c +++ b/components/tactility/src/apps/system/system_info/system_info.c @@ -2,9 +2,10 @@ #include "furi_extra_defines.h" #include "thread.h" #include "lvgl.h" +#include "esp_wifi.h" -static void app_show(Context* context, lv_obj_t* parent) { - UNUSED(context); +static void app_show(App app, lv_obj_t* parent) { + UNUSED(app); lv_obj_t* heap_info = lv_label_create(parent); lv_label_set_recolor(heap_info, true); diff --git a/components/tactility/src/apps/system/wifi_connect/wifi_connect.c b/components/tactility/src/apps/system/wifi_connect/wifi_connect.c new file mode 100644 index 00000000..5d1184fe --- /dev/null +++ b/components/tactility/src/apps/system/wifi_connect/wifi_connect.c @@ -0,0 +1,116 @@ +#include "wifi_connect.h" + +#include "app.h" +#include "esp_lvgl_port.h" +#include "furi_core.h" +#include "services/wifi/wifi.h" +#include "wifi_connect_state_updating.h" + +// Forward declarations +static void wifi_connect_event_callback(const void* message, void* context); + +static void on_connect(const char* ssid, const char* password, void* parameter) { + UNUSED(parameter); + wifi_connect(ssid, password); +} + +static WifiConnect* wifi_connect_alloc() { + WifiConnect* wifi = malloc(sizeof(WifiConnect)); + + FuriPubSub* wifi_pubsub = wifi_get_pubsub(); + wifi->wifi_subscription = furi_pubsub_subscribe(wifi_pubsub, &wifi_connect_event_callback, wifi); + wifi->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + wifi->state = (WifiConnectState) { + .radio_state = wifi_get_radio_state() + }; + wifi->bindings = (WifiConnectBindings) { + .on_connect_ssid = &on_connect, + .on_connect_ssid_context = wifi, + }; + wifi->view_enabled = false; + + return wifi; +} + +static void wifi_connect_free(WifiConnect* wifi) { + FuriPubSub* wifi_pubsub = wifi_get_pubsub(); + furi_pubsub_unsubscribe(wifi_pubsub, wifi->wifi_subscription); + furi_mutex_free(wifi->mutex); + + free(wifi); +} + +void wifi_connect_lock(WifiConnect* wifi) { + furi_assert(wifi); + furi_assert(wifi->mutex); + furi_mutex_acquire(wifi->mutex, FuriWaitForever); +} + +void wifi_connect_unlock(WifiConnect* wifi) { + furi_assert(wifi); + furi_assert(wifi->mutex); + furi_mutex_release(wifi->mutex); +} + +void wifi_connect_request_view_update(WifiConnect* wifi) { + wifi_connect_lock(wifi); + if (wifi->view_enabled) { + lvgl_port_lock(100); + wifi_connect_view_update(&wifi->view, &wifi->bindings, &wifi->state); + lvgl_port_unlock(); + } + wifi_connect_unlock(wifi); +} + +static void wifi_connect_event_callback(const void* message, void* context) { + const WifiEvent* event = (const WifiEvent*)message; + WifiConnect* wifi = (WifiConnect*)context; + wifi_connect_state_set_radio_state(wifi, wifi_get_radio_state()); + switch (event->type) { + case WifiEventTypeRadioStateOn: + wifi_scan(); + break; + default: + break; + } +} + +static void app_show(App app, lv_obj_t* parent) { + WifiConnect* wifi = (WifiConnect*)app_get_data(app); + + wifi_connect_lock(wifi); + wifi->view_enabled = true; + wifi_connect_view_create(app, wifi, parent); + wifi_connect_view_update(&wifi->view, &wifi->bindings, &wifi->state); + wifi_connect_unlock(wifi); +} + +static void app_hide(App app) { + WifiConnect* wifi = (WifiConnect*)app_get_data(app); + wifi_connect_lock(wifi); + wifi->view_enabled = false; + wifi_connect_unlock(wifi); +} + +static void app_start(App app) { + WifiConnect* wifi_connect = wifi_connect_alloc(app); + app_set_data(app, wifi_connect); +} + +static void app_stop(App app) { + WifiConnect* wifi = app_get_data(app); + furi_assert(wifi != NULL); + wifi_connect_free(wifi); + app_set_data(app, NULL); +} + +AppManifest wifi_connect_app = { + .id = "wifi_connect", + .name = "Wi-Fi Connect", + .icon = NULL, + .type = AppTypeSystem, + .on_start = &app_start, + .on_stop = &app_stop, + .on_show = &app_show, + .on_hide = &app_hide +}; diff --git a/components/tactility/src/apps/system/wifi_connect/wifi_connect.h b/components/tactility/src/apps/system/wifi_connect/wifi_connect.h new file mode 100644 index 00000000..4c6a52c4 --- /dev/null +++ b/components/tactility/src/apps/system/wifi_connect/wifi_connect.h @@ -0,0 +1,30 @@ +#pragma once + +#include "mutex.h" +#include "services/wifi/wifi.h" +#include "wifi_connect_bindings.h" +#include "wifi_connect_state.h" +#include "wifi_connect_view.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + FuriPubSubSubscription* wifi_subscription; + FuriMutex* mutex; + WifiConnectState state; + WifiConnectView view; + bool view_enabled; + WifiConnectBindings bindings; +} WifiConnect; + +void wifi_connect_lock(WifiConnect* wifi); + +void wifi_connect_unlock(WifiConnect* wifi); + +void wifi_connect_request_view_update(WifiConnect* wifi); + +#ifdef __cplusplus +} +#endif diff --git a/components/tactility/src/apps/system/wifi_connect/wifi_connect_bindings.h b/components/tactility/src/apps/system/wifi_connect/wifi_connect_bindings.h new file mode 100644 index 00000000..51678559 --- /dev/null +++ b/components/tactility/src/apps/system/wifi_connect/wifi_connect_bindings.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +typedef void (*OnConnectSsid)(const char* ssid, const char* password, void* context); + +typedef struct { + OnConnectSsid on_connect_ssid; + void* on_connect_ssid_context; +} WifiConnectBindings; diff --git a/components/tactility/src/apps/system/wifi_connect/wifi_connect_bundle.h b/components/tactility/src/apps/system/wifi_connect/wifi_connect_bundle.h new file mode 100644 index 00000000..c7c0864f --- /dev/null +++ b/components/tactility/src/apps/system/wifi_connect/wifi_connect_bundle.h @@ -0,0 +1,12 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#define WIFI_CONNECT_PARAM_SSID "ssid" // String +#define WIFI_CONNECT_PARAM_PASSWORD "password" // String + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/components/tactility/src/apps/system/wifi_connect/wifi_connect_state.h b/components/tactility/src/apps/system/wifi_connect/wifi_connect_state.h new file mode 100644 index 00000000..e8cc430f --- /dev/null +++ b/components/tactility/src/apps/system/wifi_connect/wifi_connect_state.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "app.h" +#include "services/wifi/wifi.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * View's state + */ +typedef struct { + WifiRadioState radio_state; + bool connection_error; +} WifiConnectState; + +#ifdef __cplusplus +} +#endif diff --git a/components/tactility/src/apps/system/wifi_connect/wifi_connect_state_updating.c b/components/tactility/src/apps/system/wifi_connect/wifi_connect_state_updating.c new file mode 100644 index 00000000..d2884cc1 --- /dev/null +++ b/components/tactility/src/apps/system/wifi_connect/wifi_connect_state_updating.c @@ -0,0 +1,11 @@ +#include "wifi_connect_state_updating.h" + +#include "esp_lvgl_port.h" + +void wifi_connect_state_set_radio_state(WifiConnect* wifi, WifiRadioState state) { + wifi_connect_lock(wifi); + wifi->state.radio_state = state; + wifi_connect_unlock(wifi); + + wifi_connect_request_view_update(wifi); +} diff --git a/components/tactility/src/apps/system/wifi_connect/wifi_connect_state_updating.h b/components/tactility/src/apps/system/wifi_connect/wifi_connect_state_updating.h new file mode 100644 index 00000000..4a4e0c3d --- /dev/null +++ b/components/tactility/src/apps/system/wifi_connect/wifi_connect_state_updating.h @@ -0,0 +1,14 @@ +#pragma once + +#include "wifi_connect.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void wifi_connect_state_set_scanning(WifiConnect* wifi, bool is_scanning); +void wifi_connect_state_set_radio_state(WifiConnect* wifi, WifiRadioState state); + +#ifdef __cplusplus +} +#endif diff --git a/components/tactility/src/apps/system/wifi_connect/wifi_connect_view.c b/components/tactility/src/apps/system/wifi_connect/wifi_connect_view.c new file mode 100644 index 00000000..3f562dca --- /dev/null +++ b/components/tactility/src/apps/system/wifi_connect/wifi_connect_view.c @@ -0,0 +1,85 @@ +#include "wifi_connect_view.h" + +#include "lvgl.h" +#include "ui/spacer.h" +#include "ui/style.h" +#include "wifi_connect.h" +#include "wifi_connect_bundle.h" +#include "wifi_connect_state.h" + +static void on_connect(lv_event_t* event) { + WifiConnect* wifi = (WifiConnect*)event->user_data; + WifiConnectView* view = &wifi->view; + const char* ssid = lv_textarea_get_text(view->ssid_textarea); + const char* password = lv_textarea_get_text(view->password_textarea); + + WifiConnectBindings* bindings = &wifi->bindings; + bindings->on_connect_ssid( + ssid, + password, + bindings->on_connect_ssid_context + ); +} + +// TODO: Standardize dialogs +void wifi_connect_view_create(App app, void* wifi, lv_obj_t* parent) { + WifiConnect* wifi_connect = (WifiConnect*)wifi; + WifiConnectView* view = &wifi_connect->view; + // TODO: Standardize this into "window content" function? + // TODO: It can then be dynamically determined based on screen res and size + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_top(parent, 8, 0); + lv_obj_set_style_pad_bottom(parent, 8, 0); + lv_obj_set_style_pad_left(parent, 16, 0); + lv_obj_set_style_pad_right(parent, 16, 0); + + view->root = parent; + + lv_obj_t* ssid_label = lv_label_create(parent); + lv_label_set_text(ssid_label, "Network:"); + view->ssid_textarea = lv_textarea_create(parent); + lv_textarea_set_one_line(view->ssid_textarea, true); + + lv_obj_t* password_label = lv_label_create(parent); + lv_label_set_text(password_label, "Password:"); + view->password_textarea = lv_textarea_create(parent); + lv_textarea_set_one_line(view->password_textarea, true); + lv_textarea_set_password_show_time(view->password_textarea, 0); + lv_textarea_set_password_mode(view->password_textarea, true); + + lv_obj_t* button_container = lv_obj_create(parent); + lv_obj_set_width(button_container, LV_PCT(100)); + lv_obj_set_height(button_container, LV_SIZE_CONTENT); + tt_lv_obj_set_style_no_padding(button_container); + lv_obj_set_style_border_width(button_container, 0, 0); + lv_obj_set_flex_flow(button_container, LV_FLEX_FLOW_ROW); + + lv_obj_t* spacer_left = tt_lv_spacer_create(button_container, 1, 1); + lv_obj_set_flex_grow(spacer_left, 1); + + view->connect_button = lv_btn_create(button_container); + lv_obj_t* connect_label = lv_label_create(view->connect_button); + lv_label_set_text(connect_label, "Connect"); + lv_obj_center(connect_label); + lv_obj_add_event_cb(view->connect_button, &on_connect, LV_EVENT_CLICKED, wifi); + + // Init from app parameters + Bundle* _Nullable bundle = app_get_parameters(app); + if (bundle) { + char* ssid; + if (bundle_opt_string(bundle, WIFI_CONNECT_PARAM_SSID, &ssid)) { + lv_textarea_set_text(view->ssid_textarea, ssid); + } + + char* password; + if (bundle_opt_string(bundle, WIFI_CONNECT_PARAM_PASSWORD, &password)) { + lv_textarea_set_text(view->password_textarea, password); + } + } +} + +void wifi_connect_view_update(WifiConnectView* view, WifiConnectBindings* bindings, WifiConnectState* state) { + UNUSED(view); + UNUSED(bindings); + UNUSED(state); +} diff --git a/components/tactility/src/apps/system/wifi_connect/wifi_connect_view.h b/components/tactility/src/apps/system/wifi_connect/wifi_connect_view.h new file mode 100644 index 00000000..59595652 --- /dev/null +++ b/components/tactility/src/apps/system/wifi_connect/wifi_connect_view.h @@ -0,0 +1,24 @@ +#pragma once + +#include "lvgl.h" +#include "wifi_connect_state.h" +#include "wifi_connect_bindings.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + lv_obj_t* root; + lv_obj_t* ssid_textarea; + lv_obj_t* password_textarea; + lv_obj_t* connect_button; + lv_obj_t* cancel_button; +} WifiConnectView; + +void wifi_connect_view_create(App app, void* wifi, lv_obj_t* parent); +void wifi_connect_view_update(WifiConnectView* view, WifiConnectBindings* bindings, WifiConnectState* state); + +#ifdef __cplusplus +} +#endif diff --git a/components/tactility/src/apps/system/wifi_manage/wifi_manage.c b/components/tactility/src/apps/system/wifi_manage/wifi_manage.c new file mode 100644 index 00000000..4eb86509 --- /dev/null +++ b/components/tactility/src/apps/system/wifi_manage/wifi_manage.c @@ -0,0 +1,150 @@ +#include "wifi_manage.h" + +#include "app.h" +#include "apps/system/wifi_connect/wifi_connect_bundle.h" +#include "esp_lvgl_port.h" +#include "furi_core.h" +#include "services/loader/loader.h" +#include "wifi_manage_state_updating.h" +#include "wifi_manage_view.h" + +// Forward declarations +static void wifi_manage_event_callback(const void* message, void* context); + +static void on_connect(const char* ssid) { + Bundle bundle = bundle_alloc(); + bundle_put_string(bundle, WIFI_CONNECT_PARAM_SSID, ssid); + bundle_put_string(bundle, WIFI_CONNECT_PARAM_PASSWORD, ""); // TODO: Implement from cache + loader_start_app("wifi_connect", false, bundle); +} + +static void on_disconnect() { + wifi_disconnect(); +} + +static void on_wifi_toggled(bool enabled) { + wifi_set_enabled(enabled); +} + +static WifiManage* wifi_manage_alloc() { + WifiManage* wifi = malloc(sizeof(WifiManage)); + + FuriPubSub* wifi_pubsub = wifi_get_pubsub(); + wifi->wifi_subscription = furi_pubsub_subscribe(wifi_pubsub, &wifi_manage_event_callback, wifi); + wifi->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + wifi->state = (WifiManageState) { + .scanning = wifi_is_scanning(), + .radio_state = wifi_get_radio_state() + }; + wifi->view_enabled = false; + wifi->bindings = (WifiManageBindings) { + .on_wifi_toggled = &on_wifi_toggled, + .on_connect_ssid = &on_connect, + .on_disconnect = &on_disconnect + }; + + return wifi; +} + +static void wifi_manage_free(WifiManage* wifi) { + FuriPubSub* wifi_pubsub = wifi_get_pubsub(); + furi_pubsub_unsubscribe(wifi_pubsub, wifi->wifi_subscription); + furi_mutex_free(wifi->mutex); + + free(wifi); +} + +void wifi_manage_lock(WifiManage* wifi) { + furi_assert(wifi); + furi_assert(wifi->mutex); + furi_mutex_acquire(wifi->mutex, FuriWaitForever); +} + +void wifi_manage_unlock(WifiManage* wifi) { + furi_assert(wifi); + furi_assert(wifi->mutex); + furi_mutex_release(wifi->mutex); +} + +void wifi_manage_request_view_update(WifiManage* wifi) { + wifi_manage_lock(wifi); + if (wifi->view_enabled) { + lvgl_port_lock(100); + wifi_manage_view_update(&wifi->view, &wifi->bindings, &wifi->state); + lvgl_port_unlock(); + } + wifi_manage_unlock(wifi); +} + +static void wifi_manage_event_callback(const void* message, void* context) { + const WifiEvent* event = (const WifiEvent*)message; + WifiManage* wifi = (WifiManage*)context; + wifi_manage_state_set_radio_state(wifi, wifi_get_radio_state()); + switch (event->type) { + case WifiEventTypeScanStarted: + wifi_manage_state_set_scanning(wifi, true); + break; + case WifiEventTypeScanFinished: + wifi_manage_state_set_scanning(wifi, false); + wifi_manage_state_update_scanned_records(wifi); + break; + case WifiEventTypeRadioStateOn: + if (!wifi_is_scanning()) { + wifi_scan(); + } + break; + default: + break; + } +} + +static void app_show(App app, lv_obj_t* parent) { + WifiManage* wifi = (WifiManage*)app_get_data(app); + + // State update (it has its own locking) + wifi_manage_state_set_radio_state(wifi, wifi_get_radio_state()); + wifi_manage_state_set_scanning(wifi, wifi_is_scanning()); + wifi_manage_state_update_scanned_records(wifi); + + // View update + wifi_manage_lock(wifi); + wifi->view_enabled = true; + wifi_manage_view_create(&wifi->view, &wifi->bindings, parent); + wifi_manage_view_update(&wifi->view, &wifi->bindings, &wifi->state); + wifi_manage_unlock(wifi); + + WifiRadioState radio_state = wifi_get_radio_state(); + if (radio_state == WIFI_RADIO_ON && !wifi_is_scanning()) { + wifi_scan(); + } +} + +static void app_hide(App app) { + WifiManage* wifi = (WifiManage*)app_get_data(app); + wifi_manage_lock(wifi); + wifi->view_enabled = false; + wifi_manage_unlock(wifi); +} + +static void app_start(App app) { + WifiManage* wifi = wifi_manage_alloc(); + app_set_data(app, wifi); +} + +static void app_stop(App app) { + WifiManage* wifi = (WifiManage*)app_get_data(app); + furi_assert(wifi != NULL); + wifi_manage_free(wifi); + app_set_data(app, NULL); +} + +AppManifest wifi_manage_app = { + .id = "wifi_manage", + .name = "Wi-Fi", + .icon = NULL, + .type = AppTypeSystem, + .on_start = &app_start, + .on_stop = &app_stop, + .on_show = &app_show, + .on_hide = &app_hide +}; diff --git a/components/tactility/src/apps/system/wifi_manage/wifi_manage.h b/components/tactility/src/apps/system/wifi_manage/wifi_manage.h new file mode 100644 index 00000000..536953b5 --- /dev/null +++ b/components/tactility/src/apps/system/wifi_manage/wifi_manage.h @@ -0,0 +1,28 @@ +#pragma once + +#include "mutex.h" +#include "services/wifi/wifi.h" +#include "wifi_manage_view.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + FuriPubSubSubscription* wifi_subscription; + FuriMutex* mutex; + WifiManageState state; + WifiManageView view; + bool view_enabled; + WifiManageBindings bindings; +} WifiManage; + +void wifi_manage_lock(WifiManage* wifi); + +void wifi_manage_unlock(WifiManage* wifi); + +void wifi_manage_request_view_update(WifiManage* wifi); + +#ifdef __cplusplus +} +#endif diff --git a/components/tactility/src/apps/system/wifi_manage/wifi_manage_bindings.h b/components/tactility/src/apps/system/wifi_manage/wifi_manage_bindings.h new file mode 100644 index 00000000..e79ef1fe --- /dev/null +++ b/components/tactility/src/apps/system/wifi_manage/wifi_manage_bindings.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +typedef void (*OnWifiToggled)(bool enable); +typedef void (*OnConnectSsid)(const char* ssid); +typedef void (*OnDisconnect)(); + +typedef struct { + OnWifiToggled on_wifi_toggled; + OnConnectSsid on_connect_ssid; + OnDisconnect on_disconnect; +} WifiManageBindings; diff --git a/components/tactility/src/apps/system/wifi_manage/wifi_manage_state.h b/components/tactility/src/apps/system/wifi_manage/wifi_manage_state.h new file mode 100644 index 00000000..e9d36a78 --- /dev/null +++ b/components/tactility/src/apps/system/wifi_manage/wifi_manage_state.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "services/wifi/wifi.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define WIFI_SCAN_AP_RECORD_COUNT 16 + +/** + * View's state + */ +typedef struct { + bool scanning; + WifiRadioState radio_state; + uint8_t connect_ssid[33]; + WifiApRecord ap_records[WIFI_SCAN_AP_RECORD_COUNT]; + uint16_t ap_records_count; +} WifiManageState; + +#ifdef __cplusplus +} +#endif diff --git a/components/tactility/src/apps/system/wifi_manage/wifi_manage_state_updating.c b/components/tactility/src/apps/system/wifi_manage/wifi_manage_state_updating.c new file mode 100644 index 00000000..a99b7ed1 --- /dev/null +++ b/components/tactility/src/apps/system/wifi_manage/wifi_manage_state_updating.c @@ -0,0 +1,30 @@ +#include "wifi_manage.h" +#include "esp_lvgl_port.h" + +void wifi_manage_state_set_scanning(WifiManage* wifi, bool is_scanning) { + wifi_manage_lock(wifi); + wifi->state.scanning = is_scanning; + wifi_manage_unlock(wifi); + + wifi_manage_request_view_update(wifi); +} + +void wifi_manage_state_set_radio_state(WifiManage* wifi, WifiRadioState state) { + wifi_manage_lock(wifi); + wifi->state.radio_state = state; + wifi_manage_unlock(wifi); + + wifi_manage_request_view_update(wifi); +} + +void wifi_manage_state_update_scanned_records(WifiManage* wifi) { + wifi_manage_lock(wifi); + wifi_get_scan_results( + wifi->state.ap_records, + WIFI_SCAN_AP_RECORD_COUNT, + &wifi->state.ap_records_count + ); + wifi_manage_unlock(wifi); + + wifi_manage_request_view_update(wifi); +} \ No newline at end of file diff --git a/components/tactility/src/apps/system/wifi_manage/wifi_manage_state_updating.h b/components/tactility/src/apps/system/wifi_manage/wifi_manage_state_updating.h new file mode 100644 index 00000000..e939979d --- /dev/null +++ b/components/tactility/src/apps/system/wifi_manage/wifi_manage_state_updating.h @@ -0,0 +1,15 @@ +#pragma once + +#include "wifi_manage.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void wifi_manage_state_set_scanning(WifiManage* wifi, bool is_scanning); +void wifi_manage_state_set_radio_state(WifiManage* wifi, WifiRadioState state); +void wifi_manage_state_update_scanned_records(WifiManage* wifi); + +#ifdef __cplusplus +} +#endif diff --git a/components/tactility/src/apps/system/wifi_manage/wifi_manage_view.c b/components/tactility/src/apps/system/wifi_manage/wifi_manage_view.c new file mode 100644 index 00000000..882390d1 --- /dev/null +++ b/components/tactility/src/apps/system/wifi_manage/wifi_manage_view.c @@ -0,0 +1,220 @@ +#include "wifi_manage_view.h" + +#include "log.h" +#include "services/wifi/wifi.h" +#include "ui/style.h" +#include "wifi_manage_state.h" + +#define TAG "wifi_main_view" +#define SPINNER_HEIGHT 40 + +static void on_enable_switch_changed(lv_event_t* event) { + lv_event_code_t code = lv_event_get_code(event); + lv_obj_t* enable_switch = lv_event_get_target(event); + if (code == LV_EVENT_VALUE_CHANGED) { + bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED); + WifiManageBindings* bindings = (WifiManageBindings*)event->user_data; + bindings->on_wifi_toggled(is_on); + } +} + +static void on_disconnect_pressed(lv_event_t* event) { + WifiManageBindings* bindings = (WifiManageBindings*)event->user_data; + bindings->on_disconnect(); +} + +// region Secondary updates + +static const char* get_network_icon(int8_t rssi, wifi_auth_mode_t auth_mode) { + if (rssi > -67) { + if (auth_mode == WIFI_AUTH_OPEN) + return "A:/assets/network_wifi.png"; + else + return "A:/assets/network_wifi_locked.png"; + } else if (rssi > -70) { + if (auth_mode == WIFI_AUTH_OPEN) + return "A:/assets/network_wifi_3_bar.png"; + else + return "A:/assets/network_wifi_3_bar_locked.png"; + } else if (rssi > -80) { + if (auth_mode == WIFI_AUTH_OPEN) + return "A:/assets/network_wifi_2_bar.png"; + else + return "A:/assets/network_wifi_2_bar_locked.png"; + } else { + if (auth_mode == WIFI_AUTH_OPEN) + return "A:/assets/network_wifi_1_bar.png"; + else + return "A:/assets/network_wifi_1_bar_locked.png"; + } +} + +static void connect(lv_event_t* event) { + lv_obj_t* button = event->current_target; + // Assumes that the second child of the button is a label ... risky + lv_obj_t* label = lv_obj_get_child(button, 1); + // We get the SSID from the button label because it's safer than alloc'ing + // our own and passing it as the event data + const char* ssid = lv_label_get_text(label); + FURI_LOG_I(TAG, "Clicked AP: %s", ssid); + WifiManageBindings* bindings = (WifiManageBindings*)event->user_data; + bindings->on_connect_ssid(ssid); +} + +static void create_network_button(WifiManageView* view, WifiManageBindings* bindings, WifiApRecord* record) { + const char* ssid = (const char*)record->ssid; + const char* icon = get_network_icon(record->rssi, record->auth_mode); + lv_obj_t* ap_button = lv_list_add_btn( + view->networks_list, + icon, + ssid + ); + lv_obj_add_event_cb(ap_button, &connect, LV_EVENT_CLICKED, bindings); +} + +static void update_network_list(WifiManageView* view, WifiManageState* state, WifiManageBindings* bindings) { + lv_obj_clean(view->networks_list); + switch (state->radio_state) { + case WIFI_RADIO_ON_PENDING: + case WIFI_RADIO_ON: + case WIFI_RADIO_CONNECTION_PENDING: + case WIFI_RADIO_CONNECTION_ACTIVE: { + lv_obj_clear_flag(view->networks_label, LV_OBJ_FLAG_HIDDEN); + if (state->ap_records_count > 0) { + for (int i = 0; i < state->ap_records_count; ++i) { + create_network_button(view, bindings, &state->ap_records[i]); + } + lv_obj_clear_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN); + } else if (state->scanning) { + lv_obj_add_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_clear_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN); + lv_obj_t* label = lv_label_create(view->networks_list); + lv_label_set_text(label, "No networks found."); + } + break; + } + case WIFI_RADIO_OFF_PENDING: + case WIFI_RADIO_OFF: { + lv_obj_add_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(view->networks_label, LV_OBJ_FLAG_HIDDEN); + break; + } + } +} + +void update_scanning(WifiManageView* view, WifiManageState* state) { + if (state->radio_state == WIFI_RADIO_ON && state->scanning) { + lv_obj_clear_flag(view->scanning_spinner, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(view->scanning_spinner, LV_OBJ_FLAG_HIDDEN); + } +} + +static void update_wifi_toggle(WifiManageView* view, WifiManageState* state) { + lv_obj_clear_state(view->enable_switch, LV_STATE_ANY); + switch (state->radio_state) { + case WIFI_RADIO_ON: + case WIFI_RADIO_CONNECTION_PENDING: + case WIFI_RADIO_CONNECTION_ACTIVE: + lv_obj_add_state(view->enable_switch, LV_STATE_CHECKED); + break; + case WIFI_RADIO_ON_PENDING: + lv_obj_add_state(view->enable_switch, LV_STATE_CHECKED | LV_STATE_DISABLED); + break; + case WIFI_RADIO_OFF: + break; + case WIFI_RADIO_OFF_PENDING: + lv_obj_add_state(view->enable_switch, LV_STATE_DISABLED); + break; + } +} + +static void update_connected_ap(WifiManageView* view, WifiManageState* state, WifiManageBindings* bindings) { + switch (state->radio_state) { + case WIFI_RADIO_CONNECTION_PENDING: + case WIFI_RADIO_CONNECTION_ACTIVE: + lv_obj_clear_flag(view->connected_ap_container, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(view->connected_ap_label, (const char*)state->connect_ssid); + break; + default: + lv_obj_add_flag(view->connected_ap_container, LV_OBJ_FLAG_HIDDEN); + break; + } +} + +// endregion Secondary updates + +// region Main + +void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent) { + view->root = parent; + + // TODO: Standardize this into "window content" function? + // TODO: It can then be dynamically determined based on screen res and size + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_top(parent, 8, 0); + lv_obj_set_style_pad_bottom(parent, 8, 0); + lv_obj_set_style_pad_left(parent, 16, 0); + lv_obj_set_style_pad_right(parent, 16, 0); + + // Top row: enable/disable + lv_obj_t* switch_container = lv_obj_create(parent); + lv_obj_set_width(switch_container, LV_PCT(100)); + lv_obj_set_height(switch_container, LV_SIZE_CONTENT); + tt_lv_obj_set_style_no_padding(switch_container); + tt_lv_obj_set_style_bg_invisible(switch_container); + + lv_obj_t* enable_label = lv_label_create(switch_container); + lv_label_set_text(enable_label, "Wi-Fi"); + lv_obj_set_align(enable_label, LV_ALIGN_LEFT_MID); + + view->enable_switch = lv_switch_create(switch_container); + lv_obj_add_event_cb(view->enable_switch, on_enable_switch_changed, LV_EVENT_ALL, bindings); + lv_obj_set_align(view->enable_switch, LV_ALIGN_RIGHT_MID); + + view->connected_ap_container = lv_obj_create(parent); + lv_obj_set_width(view->connected_ap_container, LV_PCT(100)); + lv_obj_set_height(view->connected_ap_container, LV_SIZE_CONTENT); + tt_lv_obj_set_style_no_padding(view->connected_ap_container); + tt_lv_obj_set_style_bg_invisible(view->connected_ap_container); + + view->connected_ap_label = lv_label_create(view->connected_ap_container); + lv_label_set_text(view->connected_ap_label, ""); + lv_obj_set_align(view->connected_ap_label, LV_ALIGN_LEFT_MID); + + lv_obj_t* disconnect_button = lv_btn_create(view->connected_ap_container); + lv_obj_add_event_cb(disconnect_button, &on_disconnect_pressed, LV_EVENT_CLICKED, bindings); + lv_obj_t* disconnect_label = lv_label_create(disconnect_button); + lv_label_set_text(disconnect_label, "Disconnect"); + lv_obj_center(disconnect_label); + + // Networks + + view->networks_label = lv_label_create(parent); + lv_label_set_text(view->networks_label, "Networks"); + lv_obj_set_style_text_align(view->networks_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_pad_top(view->networks_label, 8, 0); + lv_obj_set_style_pad_bottom(view->networks_label, 8, 0); + lv_obj_set_style_pad_left(view->networks_label, 2, 0); + lv_obj_set_align(view->networks_label, LV_ALIGN_LEFT_MID); + + view->scanning_spinner = lv_spinner_create(parent, 1000, 60); + lv_obj_set_size(view->scanning_spinner, SPINNER_HEIGHT, SPINNER_HEIGHT); + lv_obj_set_style_pad_top(view->scanning_spinner, 4, 0); + lv_obj_set_style_pad_bottom(view->scanning_spinner, 4, 0); + + view->networks_list = lv_obj_create(parent); + lv_obj_set_flex_flow(view->networks_list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_width(view->networks_list, LV_PCT(100)); + lv_obj_set_height(view->networks_list, LV_SIZE_CONTENT); + lv_obj_set_style_pad_top(view->networks_list, 8, 0); + lv_obj_set_style_pad_bottom(view->networks_list, 8, 0); +} + +void wifi_manage_view_update(WifiManageView* view, WifiManageBindings* bindings, WifiManageState* state) { + update_wifi_toggle(view, state); + update_scanning(view, state); + update_network_list(view, state, bindings); + update_connected_ap(view, state, bindings); +} diff --git a/components/tactility/src/apps/system/wifi_manage/wifi_manage_view.h b/components/tactility/src/apps/system/wifi_manage/wifi_manage_view.h new file mode 100644 index 00000000..76b9cd88 --- /dev/null +++ b/components/tactility/src/apps/system/wifi_manage/wifi_manage_view.h @@ -0,0 +1,26 @@ +#pragma once + +#include "lvgl.h" +#include "wifi_manage_bindings.h" +#include "wifi_manage_state.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + lv_obj_t* root; + lv_obj_t* enable_switch; + lv_obj_t* scanning_spinner; + lv_obj_t* networks_label; + lv_obj_t* networks_list; + lv_obj_t* connected_ap_container; + lv_obj_t* connected_ap_label; +} WifiManageView; + +void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent); +void wifi_manage_view_update(WifiManageView* view, WifiManageBindings* bindings, WifiManageState* state); + +#ifdef __cplusplus +} +#endif diff --git a/components/tactility/src/services/desktop/desktop.c b/components/tactility/src/services/desktop/desktop.c deleted file mode 100644 index 6b19bb05..00000000 --- a/components/tactility/src/services/desktop/desktop.c +++ /dev/null @@ -1,48 +0,0 @@ -#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); - } -} - -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(Context* context, lv_obj_t* parent) { - 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 -}; diff --git a/components/tactility/src/services/gui/gui.c b/components/tactility/src/services/gui/gui.c index 23d80134..51248774 100644 --- a/components/tactility/src/services/gui/gui.c +++ b/components/tactility/src/services/gui/gui.c @@ -1,14 +1,14 @@ +#include "gui_i.h" + #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*); @@ -24,25 +24,12 @@ Gui* gui_alloc() { &gui_main, NULL ); - instance->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); 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; } @@ -67,42 +54,26 @@ void gui_unlock() { 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); - +void gui_show_app(App app, ViewPortShowCallback on_show, ViewPortHideCallback on_hide) { gui_lock(); - furi_check(gui->layers[layer] == NULL, "layer in use"); - gui->layers[layer] = view_port; - view_port_gui_set(view_port, gui); + furi_check(gui->app_view_port == NULL); + gui->app_view_port = view_port_alloc(app, on_show, on_hide); gui_unlock(); - gui_request_draw(); } -void gui_remove_view_port(ViewPort* view_port) { - furi_assert(gui); - furi_assert(view_port); - +void gui_hide_app() { 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; - } - } - + ViewPort* view_port = gui->app_view_port; + furi_check(view_port != NULL); + view_port_hide(view_port); + view_port_free(view_port); + gui->app_view_port = NULL; gui_unlock(); - - gui_request_draw(); } static int32_t gui_main(void* p) { @@ -116,20 +87,10 @@ static int32_t gui_main(void* p) { 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) { @@ -143,8 +104,8 @@ static int32_t gui_main(void* p) { // region AppManifest -static void gui_start(Context* context) { - UNUSED(context); +static void gui_start(Service service) { + UNUSED(service); gui = gui_alloc(); @@ -152,8 +113,8 @@ static void gui_start(Context* context) { furi_thread_start(gui->thread); } -static void gui_stop(Context* context) { - UNUSED(context); +static void gui_stop(Service service) { + UNUSED(service); gui_lock(); diff --git a/components/tactility/src/services/gui/gui.h b/components/tactility/src/services/gui/gui.h index 0757dd6d..02388e2c 100644 --- a/components/tactility/src/services/gui/gui.h +++ b/components/tactility/src/services/gui/gui.h @@ -1,5 +1,6 @@ #pragma once +#include "app.h" #include "service_manifest.h" #include "view_port.h" @@ -7,32 +8,11 @@ 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 view_port ViewPort instance - * @param[in] layer GuiLayer where to place view_port - */ -void gui_add_view_port(ViewPort* view_port, GuiLayer layer); +void gui_show_app(App app, ViewPortShowCallback on_show, ViewPortHideCallback on_hide); -/** Remove view_port from rendering tree - * - * @remark thread safe - * - * @param view_port ViewPort instance - */ -void gui_remove_view_port(ViewPort* view_port); +void gui_hide_app(); #ifdef __cplusplus } diff --git a/components/tactility/src/services/gui/gui_draw.c b/components/tactility/src/services/gui/gui_draw.c index 05f117c0..b968457f 100644 --- a/components/tactility/src/services/gui/gui_draw.c +++ b/components/tactility/src/services/gui/gui_draw.c @@ -2,105 +2,74 @@ #include "esp_lvgl_port.h" #include "gui_i.h" #include "log.h" -#include "services/gui/widgets/widgets.h" +#include "services/gui/widgets/statusbar.h" #include "services/loader/loader.h" +#include "ui/spacer.h" +#include "ui/style.h" +#include "ui/toolbar.h" #define TAG "gui" -static lv_obj_t* screen_with_top_bar(lv_obj_t* parent) { - lv_obj_set_style_bg_blacken(parent); +static lv_obj_t* create_app_views(lv_obj_t* parent, App app) { + tt_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); + tt_lv_obj_set_style_no_padding(vertical_container); + tt_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); + // TODO: Move statusbar into separate ViewPort + AppFlags flags = app_get_flags(app); + if (flags.show_statusbar) { + tt_lv_statusbar_create(vertical_container); } - lv_obj_t* spacer = lv_obj_create(vertical_container); - lv_obj_set_size(spacer, 2, 2); - lv_obj_set_style_bg_blacken(spacer); + if (flags.show_toolbar) { + const AppManifest* manifest = app_get_manifest(app); + if (manifest != NULL) { + // TODO: Keep toolbar on app level so app can update it (app_set_toolbar() etc?) + Toolbar toolbar = { + .nav_action = &loader_stop_app, + .nav_icon = LV_SYMBOL_CLOSE, + .title = manifest->name + }; + lv_obj_t* toolbar_widget = tt_lv_toolbar_create(vertical_container, &toolbar); + lv_obj_set_pos(toolbar_widget, 0, STATUSBAR_HEIGHT); + + // Black area between toolbar and content below + lv_obj_t* spacer = tt_lv_spacer_create(vertical_container, 1, 2); + tt_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); + // Lock GUI and LVGL + gui_lock(); 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); - } + if (gui->app_view_port != NULL) { + ViewPort* view_port = gui->app_view_port; + furi_assert(view_port); + App app = gui->app_view_port->app; + lv_obj_t* container = create_app_views(gui->lvgl_parent, app); + view_port_show(view_port, container); + } else { + FURI_LOG_W(TAG, "nothing to draw"); } + // Unlock GUI and LVGL lvgl_port_unlock(); + gui_unlock(); } diff --git a/components/tactility/src/services/gui/gui_i.h b/components/tactility/src/services/gui/gui_i.h index 6a2db07c..6704c3cb 100644 --- a/components/tactility/src/services/gui/gui_i.h +++ b/components/tactility/src/services/gui/gui_i.h @@ -20,31 +20,16 @@ struct Gui { FuriMutex* 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; - */ + // App-specific + ViewPort* app_view_port; }; /** Update GUI, request redraw */ 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 */ void gui_lock(); diff --git a/components/tactility/src/services/gui/view_port.c b/components/tactility/src/services/gui/view_port.c index bf009d73..4e337c42 100644 --- a/components/tactility/src/services/gui/view_port.c +++ b/components/tactility/src/services/gui/view_port.c @@ -1,98 +1,40 @@ +#include "view_port.h" + #include "check.h" -#include "gui.h" -#include "gui_i.h" -#include "services/gui/widgets/widgets.h" +#include "ui/style.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_alloc( + App app, + ViewPortShowCallback on_show, + ViewPortHideCallback on_hide +) { ViewPort* view_port = malloc(sizeof(ViewPort)); - view_port->gui = NULL; - view_port->is_enabled = true; - view_port->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); + view_port->app = app; + view_port->on_show = on_show; + view_port->on_hide = on_hide; 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, Context* 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) { +void view_port_show(ViewPort* view_port, lv_obj_t* parent) { 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); + furi_assert(parent); + if (view_port->on_show) { + tt_lv_obj_set_style_no_padding(parent); + view_port->on_show(view_port->app, parent); } - - 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) { +void view_port_hide(ViewPort* view_port) { 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(view_port->draw_callback_context, parent); + if (view_port->on_hide) { + view_port->on_hide(view_port->app); } - - furi_mutex_release(view_port->mutex); } diff --git a/components/tactility/src/services/gui/view_port.h b/components/tactility/src/services/gui/view_port.h index afde4466..13dd74c1 100644 --- a/components/tactility/src/services/gui/view_port.h +++ b/components/tactility/src/services/gui/view_port.h @@ -4,31 +4,39 @@ extern "C" { #endif +#include "app.h" #include "lvgl.h" #include "context.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)(Context* context, lv_obj_t* parent); +typedef void (*ViewPortShowCallback)(App app, lv_obj_t* parent); +typedef void (*ViewPortHideCallback)(App app); + +// TODO: Move internally, use handle publicly + +typedef struct { + App app; + ViewPortShowCallback on_show; + ViewPortHideCallback _Nullable on_hide; + bool app_toolbar; +} ViewPort; /** ViewPort allocator * * always returns view_port or stops system if not enough memory. + * @param app + * @param on_show Called to create LVGL widgets + * @param on_hide Called before clearing the LVGL widget parent * * @return ViewPort instance */ -ViewPort* view_port_alloc(); +ViewPort* view_port_alloc( + App app, + ViewPortShowCallback on_show, + ViewPortHideCallback on_hide +); /** ViewPort deallocator * @@ -38,30 +46,6 @@ ViewPort* view_port_alloc(); */ 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, Context* 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 diff --git a/components/tactility/src/services/gui/view_port_i.h b/components/tactility/src/services/gui/view_port_i.h index 4eddebfa..f8e24742 100644 --- a/components/tactility/src/services/gui/view_port_i.h +++ b/components/tactility/src/services/gui/view_port_i.h @@ -1,47 +1,19 @@ #pragma once -#include "context.h" -#include "gui_i.h" -#include "mutex.h" #include "view_port.h" -struct ViewPort { - Gui* gui; - FuriMutex* mutex; - bool is_enabled; - - ViewPortDrawCallback draw_callback; - Context* 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. +/** Process draw call. Calls on_show callback. + * To be used by GUI, called on redraw. * * @param view_port ViewPort instance * @param canvas canvas to draw at */ -void view_port_draw(ViewPort* view_port, lv_obj_t* parent); +void view_port_show(ViewPort* view_port, lv_obj_t* parent); -/** Process input. Calls input callback. +/** + * Process draw clearing call. Calls on_hdie callback. + * To be used by GUI, called on redraw. * - * To be used by GUI, called on input dispatch. - * - * @param view_port ViewPort instance - * @param event pointer to input event + * @param view_port */ -//void view_port_input(ViewPort* view_port, InputEvent* event); +void view_port_hide(ViewPort* view_port); diff --git a/components/tactility/src/services/gui/view_port_input.c b/components/tactility/src/services/gui/view_port_input.c deleted file mode 100644 index 144177ac..00000000 --- a/components/tactility/src/services/gui/view_port_input.c +++ /dev/null @@ -1,91 +0,0 @@ -#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); -} -*/ diff --git a/components/tactility/src/services/gui/view_port_input.h b/components/tactility/src/services/gui/view_port_input.h deleted file mode 100644 index 243687ab..00000000 --- a/components/tactility/src/services/gui/view_port_input.h +++ /dev/null @@ -1,14 +0,0 @@ -#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); - -*/ \ No newline at end of file diff --git a/components/tactility/src/services/gui/widgets/spacer.c b/components/tactility/src/services/gui/widgets/spacer.c deleted file mode 100644 index 3b3aafb2..00000000 --- a/components/tactility/src/services/gui/widgets/spacer.c +++ /dev/null @@ -1,9 +0,0 @@ -#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; -} diff --git a/components/tactility/src/services/gui/widgets/statusbar.c b/components/tactility/src/services/gui/widgets/statusbar.c new file mode 100644 index 00000000..b5fa9713 --- /dev/null +++ b/components/tactility/src/services/gui/widgets/statusbar.c @@ -0,0 +1,27 @@ +#include "statusbar.h" + +#include "ui/spacer.h" +#include "ui/style.h" + +lv_obj_t* tt_lv_statusbar_create(lv_obj_t* parent) { + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_height(wrapper, STATUSBAR_HEIGHT); + tt_lv_obj_set_style_no_padding(wrapper); + tt_lv_obj_set_style_bg_blacken(wrapper); + lv_obj_center(wrapper); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); + + lv_obj_t* left_spacer = tt_lv_spacer_create(wrapper, 1, 1); + lv_obj_set_flex_grow(left_spacer, 1); + + lv_obj_t* wifi = lv_img_create(wrapper); + lv_obj_set_size(wifi, STATUSBAR_ICON_SIZE, STATUSBAR_ICON_SIZE); + tt_lv_obj_set_style_no_padding(wifi); + tt_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"); + + return wrapper; +} \ No newline at end of file diff --git a/components/tactility/src/services/gui/widgets/statusbar.h b/components/tactility/src/services/gui/widgets/statusbar.h new file mode 100644 index 00000000..871d7c8e --- /dev/null +++ b/components/tactility/src/services/gui/widgets/statusbar.h @@ -0,0 +1,16 @@ +#pragma once + +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define STATUSBAR_ICON_SIZE 18 +#define STATUSBAR_HEIGHT (STATUSBAR_ICON_SIZE + 4) // 4 extra pixels for border and outline + +lv_obj_t* tt_lv_statusbar_create(lv_obj_t* parent); + +#ifdef __cplusplus +} +#endif diff --git a/components/tactility/src/services/gui/widgets/toolbar.c b/components/tactility/src/services/gui/widgets/toolbar.c deleted file mode 100644 index 0040b9a8..00000000 --- a/components/tactility/src/services/gui/widgets/toolbar.c +++ /dev/null @@ -1,40 +0,0 @@ -#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); -} diff --git a/components/tactility/src/services/gui/widgets/toolbar.h b/components/tactility/src/services/gui/widgets/toolbar.h deleted file mode 100644 index e64138c7..00000000 --- a/components/tactility/src/services/gui/widgets/toolbar.h +++ /dev/null @@ -1,17 +0,0 @@ -#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 diff --git a/components/tactility/src/services/gui/widgets/top_bar.c b/components/tactility/src/services/gui/widgets/top_bar.c deleted file mode 100644 index 5ed49a8c..00000000 --- a/components/tactility/src/services/gui/widgets/top_bar.c +++ /dev/null @@ -1,26 +0,0 @@ -#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"); -} \ No newline at end of file diff --git a/components/tactility/src/services/gui/widgets/top_bar.h b/components/tactility/src/services/gui/widgets/top_bar.h deleted file mode 100644 index a1588610..00000000 --- a/components/tactility/src/services/gui/widgets/top_bar.h +++ /dev/null @@ -1,16 +0,0 @@ -#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 diff --git a/components/tactility/src/services/gui/widgets/widgets.h b/components/tactility/src/services/gui/widgets/widgets.h deleted file mode 100644 index d59dd3c7..00000000 --- a/components/tactility/src/services/gui/widgets/widgets.h +++ /dev/null @@ -1,17 +0,0 @@ -#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 diff --git a/components/tactility/src/services/loader/loader.c b/components/tactility/src/services/loader/loader.c index a0bf75c5..43fdf10a 100644 --- a/components/tactility/src/services/loader/loader.c +++ b/components/tactility/src/services/loader/loader.c @@ -14,253 +14,259 @@ // Forward declarations static int32_t loader_main(void* p); -static Loader* loader = NULL; +static Loader* loader_singleton = 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( + furi_check(loader_singleton == NULL); + loader_singleton = malloc(sizeof(Loader)); + loader_singleton->pubsub = furi_pubsub_alloc(); + loader_singleton->queue = furi_message_queue_alloc(1, sizeof(LoaderMessage)); + loader_singleton->thread = furi_thread_alloc_ex( "loader", 4096, // Last known minimum was 2400 for starting Hello World app &loader_main, NULL ); - loader->app_data.app = NULL; - loader->app_data.view_port = NULL; - loader->mutex = xSemaphoreCreateRecursiveMutex(); - return loader; + loader_singleton->mutex = xSemaphoreCreateRecursiveMutex(); + loader_singleton->app_stack_index = -1; + memset(loader_singleton->app_stack, 0, sizeof(App) * APP_STACK_SIZE); + return loader_singleton; } 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); + furi_check(loader_singleton != NULL); + furi_thread_free(loader_singleton->thread); + furi_pubsub_free(loader_singleton->pubsub); + furi_message_queue_free(loader_singleton->queue); + furi_mutex_free(loader_singleton->mutex); + free(loader_singleton); } void loader_lock() { - furi_assert(loader); - furi_assert(loader->mutex); - furi_check(xSemaphoreTakeRecursive(loader->mutex, portMAX_DELAY) == pdPASS); + furi_assert(loader_singleton); + furi_assert(loader_singleton->mutex); + furi_check(xSemaphoreTakeRecursive(loader_singleton->mutex, portMAX_DELAY) == pdPASS); } void loader_unlock() { - furi_assert(loader); - furi_assert(loader->mutex); - furi_check(xSemaphoreGiveRecursive(loader->mutex) == pdPASS); + furi_assert(loader_singleton); + furi_assert(loader_singleton->mutex); + furi_check(xSemaphoreGiveRecursive(loader_singleton->mutex) == pdPASS); } -LoaderStatus loader_start_app(const char* id, FuriString* error_message) { - LoaderMessage message; - LoaderMessageLoaderStatusResult result; - - message.type = LoaderMessageTypeAppStart; - message.start.id = id; - 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; -} +LoaderStatus loader_start_app(const char* id, bool blocking, Bundle* _Nullable bundle) { + LoaderMessageLoaderStatusResult result = { + .value = LoaderStatusOk + }; -void loader_start_app_nonblocking(const char* id) { - LoaderMessage message; - LoaderMessageLoaderStatusResult result; - - message.type = LoaderMessageTypeAppStart; - message.start.id = id; - message.start.error_message = NULL; - message.api_lock = NULL; - message.status_value = &result; - furi_message_queue_put(loader->queue, &message, FuriWaitForever); + LoaderMessage message = { + .type = LoaderMessageTypeAppStart, + .start.id = id, + .start.bundle = bundle, + .status_value = &result, + .api_lock = blocking ? api_lock_alloc_locked() : NULL + }; + + furi_message_queue_put(loader_singleton->queue, &message, FuriWaitForever); + + if (blocking) { + api_lock_wait_unlock_and_free(message.api_lock); + } + + return result.value; } void loader_stop_app() { - LoaderMessage message; - message.type = LoaderMessageTypeAppStop; - furi_message_queue_put(loader->queue, &message, FuriWaitForever); + LoaderMessage message = {.type = LoaderMessageTypeAppStop}; + furi_message_queue_put(loader_singleton->queue, &message, FuriWaitForever); } -const AppManifest* _Nullable loader_get_current_app() { +App _Nullable loader_get_current_app() { loader_lock(); - const App* app = loader->app_data.app; - const AppManifest* manifest = app ? app->manifest : NULL; + App app = (loader_singleton->app_stack_index >= 0) + ? loader_singleton->app_stack[loader_singleton->app_stack_index] + : NULL; loader_unlock(); - - return manifest; + return app; } FuriPubSub* loader_get_pubsub() { - furi_assert(loader); + furi_assert(loader_singleton); // 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; + return loader_singleton->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 const char* app_state_to_string(AppState state) { + switch (state) { + case APP_STATE_INITIAL: + return "initial"; + case APP_STATE_STARTED: + return "started"; + case APP_STATE_SHOWING: + return "showing"; + case APP_STATE_HIDING: + return "hiding"; + case APP_STATE_STOPPED: + return "stopped"; + default: + return "?"; } } -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 void app_transition_to_state(App app, AppState state) { + const AppManifest* manifest = app_get_manifest(app); + const AppState old_state = app_get_state(app); -static LoaderStatus loader_make_success_status(FuriString* error_message) { - if (error_message) { - furi_string_set(error_message, "App started"); - } + FURI_LOG_I( + TAG, + "app \"%s\" state: %s -> %s", + manifest->id, + app_state_to_string(old_state), + app_state_to_string(state) + ); - return LoaderStatusOk; + switch (state) { + case APP_STATE_INITIAL: + app_set_state(app, APP_STATE_INITIAL); + break; + case APP_STATE_STARTED: + if (manifest->on_start != NULL) { + manifest->on_start(app); + } + app_set_state(app, APP_STATE_STARTED); + break; + case APP_STATE_SHOWING: + gui_show_app( + app, + manifest->on_show, + manifest->on_hide + ); + app_set_state(app, APP_STATE_SHOWING); + break; + case APP_STATE_HIDING: + gui_hide_app(); + app_set_state(app, APP_STATE_HIDING); + break; + case APP_STATE_STOPPED: + if (manifest->on_stop) { + manifest->on_stop(app); + } + app_set_data(app, NULL); + app_set_state(app, APP_STATE_STOPPED); + break; + } } -static void loader_start_app_with_manifest( - const AppManifest* _Nonnull manifest +LoaderStatus loader_do_start_app_with_manifest( + const AppManifest* _Nonnull manifest, + Bundle* _Nullable bundle ) { 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"); + loader_lock(); + + if (loader_singleton->app_stack_index >= (APP_STACK_SIZE - 1)) { + FURI_LOG_E(TAG, "failed to start app: stack limit of %d reached", APP_STACK_SIZE); + return LoaderStatusErrorInternal; } - App* _Nonnull app = furi_app_alloc(manifest); + int8_t previous_index = loader_singleton->app_stack_index; + loader_singleton->app_stack_index++; - loader_lock(); + App app = app_alloc(manifest, bundle); + furi_assert(loader_singleton->app_stack[loader_singleton->app_stack_index] == NULL); + loader_singleton->app_stack[loader_singleton->app_stack_index] = app; + app_transition_to_state(app, APP_STATE_INITIAL); + app_transition_to_state(app, APP_STATE_STARTED); - loader->app_data.app = app; - - if (manifest->on_start != NULL) { - manifest->on_start(&loader->app_data.app->context); + // We might have to hide the previous app first + if (previous_index != -1) { + App previous_app = loader_singleton->app_stack[previous_index]; + app_transition_to_state(previous_app, APP_STATE_HIDING); } - 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, - &loader->app_data.app->context - ); - gui_add_view_port(view_port, GuiLayerWindow); - } else { - loader->app_data.view_port = NULL; - } + app_transition_to_state(app, APP_STATE_SHOWING); loader_unlock(); - LoaderEvent event = { - .type = LoaderEventTypeApplicationStarted - }; - furi_pubsub_publish(loader->pubsub, &event); + LoaderEvent event = {.type = LoaderEventTypeApplicationStarted}; + furi_pubsub_publish(loader_singleton->pubsub, &event); + + return LoaderStatusOk; } static LoaderStatus loader_do_start_by_id( const char* id, - FuriString* _Nullable error_message + Bundle* _Nullable bundle ) { 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 - ); + return LoaderStatusErrorUnknownApp; + } else { + return loader_do_start_app_with_manifest(manifest, bundle); } - - loader_start_app_with_manifest(manifest); - 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; - } + int8_t current_app_index = loader_singleton->app_stack_index; - 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 (current_app_index == -1) { + loader_unlock(); + FURI_LOG_E(TAG, "Stop app: no app running"); + return; } - if (app->manifest->on_stop) { - app->manifest->on_stop(&app->context); + if (current_app_index == 0) { + loader_unlock(); + FURI_LOG_E(TAG, "Stop app: can't stop root app"); + return; } - furi_app_free(loader->app_data.app); - loader->app_data.app = NULL; + // Stop current app + App app_to_stop = loader_singleton->app_stack[current_app_index]; + app_transition_to_state(app_to_stop, APP_STATE_HIDING); + app_transition_to_state(app_to_stop, APP_STATE_STOPPED); - loader_unlock(); + app_free(app_to_stop); + loader_singleton->app_stack[current_app_index] = NULL; + loader_singleton->app_stack_index--; - FURI_LOG_I( - TAG, - "Application stopped. Free heap: %zu", - heap_caps_get_free_size(MALLOC_CAP_INTERNAL) - ); + FURI_LOG_I(TAG, "Free heap: %zu", heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); - LoaderEvent event = { - .type = LoaderEventTypeApplicationStopped - }; - furi_pubsub_publish(loader->pubsub, &event); -} + // Resume previous app + furi_assert(loader_singleton->app_stack[loader_singleton->app_stack_index] != NULL); + App app_to_resume = loader_singleton->app_stack[loader_singleton->app_stack_index]; + app_transition_to_state(app_to_resume, APP_STATE_SHOWING); -bool loader_is_app_running() { - loader_lock(); - bool is_running = loader->app_data.app != NULL; loader_unlock(); - return is_running; + + LoaderEvent event = {.type = LoaderEventTypeApplicationStopped}; + furi_pubsub_publish(loader_singleton->pubsub, &event); } + 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_check(loader_singleton != NULL); + if (furi_message_queue_get(loader_singleton->queue, &message, FuriWaitForever) == FuriStatusOk) { FURI_LOG_I(TAG, "Processing message of type %d", message.type); switch (message.type) { case LoaderMessageTypeAppStart: - if (loader_is_app_running()) { - loader_do_stop_app(); - } + // TODO: add bundle message.status_value->value = loader_do_start_by_id( message.start.id, - message.start.error_message + message.start.bundle ); if (message.api_lock) { api_lock_unlock(message.api_lock); @@ -281,29 +287,29 @@ static int32_t loader_main(void* p) { // region AppManifest -static void loader_start(Context* context) { - UNUSED(context); - furi_check(loader == NULL); - loader = loader_alloc(); +static void loader_start(Service service) { + UNUSED(service); + furi_check(loader_singleton == NULL); + loader_singleton = loader_alloc(); - furi_thread_set_priority(loader->thread, FuriThreadPriorityNormal); - furi_thread_start(loader->thread); + furi_thread_set_priority(loader_singleton->thread, FuriThreadPriorityNormal); + furi_thread_start(loader_singleton->thread); } -static void loader_stop(Context* context) { - UNUSED(context); - furi_check(loader != NULL); +static void loader_stop(Service service) { + UNUSED(service); + furi_check(loader_singleton != NULL); + + // Send stop signal to thread and wait for thread to finish LoaderMessage message = { .api_lock = NULL, .type = LoaderMessageTypeServiceStop }; - - // Send stop signal to thread and wait for thread to finish - furi_message_queue_put(loader->queue, &message, FuriWaitForever); - furi_thread_join(loader->thread); + furi_message_queue_put(loader_singleton->queue, &message, FuriWaitForever); + furi_thread_join(loader_singleton->thread); loader_free(); - loader = NULL; + loader_singleton = NULL; } const ServiceManifest loader_service = { diff --git a/components/tactility/src/services/loader/loader.h b/components/tactility/src/services/loader/loader.h index 4be0df0b..58adaba5 100644 --- a/components/tactility/src/services/loader/loader.h +++ b/components/tactility/src/services/loader/loader.h @@ -1,5 +1,7 @@ #pragma once + #include "app_manifest.h" +#include "bundle.h" #include "furi_core.h" #include "furi_string.h" #include "pubsub.h" @@ -30,36 +32,15 @@ typedef struct { /** * @brief Close any running app, then start new one. Blocking. * @param[in] id application name or id - * @param[in] args application arguments - * @param[out] error_message detailed error message, can be NULL + * @param[in] blocking application arguments + * @param[in] bundle optional bundle. Ownership is transferred to Loader. * @return LoaderStatus */ -LoaderStatus loader_start_app(const char* id, FuriString* error_message); - -/** - * @brief Close any running app, then start new one. Non-blocking. - * @param[in] id application name or id - * @param[in] args application arguments - */ -void loader_start_app_nonblocking(const char* id); +LoaderStatus loader_start_app(const char* id, bool blocking, Bundle* _Nullable bundle); 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] 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 - */ -void loader_show_menu(); +App _Nullable loader_get_current_app(); /** * @brief Get loader pubsub diff --git a/components/tactility/src/services/loader/loader_i.h b/components/tactility/src/services/loader/loader_i.h index 0fc5023d..51b0c791 100644 --- a/components/tactility/src/services/loader/loader_i.h +++ b/components/tactility/src/services/loader/loader_i.h @@ -10,17 +10,16 @@ #include "services/gui/view_port.h" #include "thread.h" -typedef struct { - App* app; - ViewPort* view_port; -} LoaderAppData; +#define APP_STACK_SIZE 32 struct Loader { FuriThread* thread; FuriPubSub* pubsub; FuriMessageQueue* queue; - LoaderAppData app_data; + // TODO: replace with FuriMutex SemaphoreHandle_t mutex; + int8_t app_stack_index; + App app_stack[APP_STACK_SIZE]; }; typedef enum { @@ -31,7 +30,7 @@ typedef enum { typedef struct { const char* id; - FuriString* error_message; + Bundle* _Nullable bundle; } LoaderMessageAppStart; typedef struct { diff --git a/components/tactility/src/services/wifi/wifi.c b/components/tactility/src/services/wifi/wifi.c new file mode 100644 index 00000000..b82cfd7b --- /dev/null +++ b/components/tactility/src/services/wifi/wifi.c @@ -0,0 +1,591 @@ +#include "wifi.h" + +#include "check.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "log.h" +#include "message_queue.h" +#include "mutex.h" +#include "pubsub.h" +#include "service.h" +#include + +#define TAG "wifi" +#define WIFI_SCAN_RECORD_LIMIT 16 // default, can be overridden +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +typedef struct { + /** @brief Locking mechanism for modifying the Wifi instance */ + FuriMutex* mutex; + /** @brief The public event bus */ + FuriPubSub* pubsub; + /** @brief The internal message queue */ + FuriMessageQueue* queue; + /** @brief The network interface when wifi is started */ + esp_netif_t* _Nullable netif; + /** @brief Scanning results */ + wifi_ap_record_t* _Nullable scan_list; + /** @brief The current item count in scan_list (-1 when scan_list is NULL) */ + uint16_t scan_list_count; + /** @brief Maximum amount of records to scan (value > 0) */ + uint16_t scan_list_limit; + bool scan_active; + esp_event_handler_instance_t event_handler_any_id; + esp_event_handler_instance_t event_handler_got_ip; + EventGroupHandle_t event_group; + WifiRadioState radio_state; +} Wifi; + +typedef enum { + WifiMessageTypeRadioOn, + WifiMessageTypeRadioOff, + WifiMessageTypeScan, + WifiMessageTypeConnect, + WifiMessageTypeDisconnect +} WifiMessageType; + +typedef struct { + uint8_t ssid[32]; + uint8_t password[64]; +} WifiConnectMessage; + +typedef struct { + WifiMessageType type; + union { + WifiConnectMessage connect_message; + }; +} WifiMessage; + +static Wifi* wifi_singleton = NULL; + +// Forward declarations +static void wifi_scan_list_free_safely(Wifi* wifi); +static void wifi_disconnect_internal(Wifi* wifi); +static void wifi_lock(Wifi* wifi); +static void wifi_unlock(Wifi* wifi); + +// region Alloc + +static Wifi* wifi_alloc() { + Wifi* instance = malloc(sizeof(Wifi)); + instance->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); + instance->pubsub = furi_pubsub_alloc(); + // TODO: Deal with messages that come in while an action is ongoing + // for example: when scanning and you turn off the radio, the scan should probably stop or turning off + // the radio should disable the on/off button in the app as it is pending. + instance->queue = furi_message_queue_alloc(1, sizeof(WifiMessage)); + instance->netif = NULL; + instance->scan_active = false; + instance->scan_list = NULL; + instance->scan_list_count = 0; + instance->scan_list_limit = WIFI_SCAN_RECORD_LIMIT; + instance->event_handler_any_id = NULL; + instance->event_handler_got_ip = NULL; + instance->event_group = xEventGroupCreate(); + instance->radio_state = WIFI_RADIO_OFF; + return instance; +} + +static void wifi_free(Wifi* instance) { + furi_mutex_free(instance->mutex); + furi_pubsub_free(instance->pubsub); + furi_message_queue_free(instance->queue); + free(instance); +} + +// endregion Alloc + +// region Public functions + +FuriPubSub* wifi_get_pubsub() { + furi_assert(wifi_singleton); + return wifi_singleton->pubsub; +} + +WifiRadioState wifi_get_radio_state() { + return wifi_singleton->radio_state; +} + +void wifi_scan() { + furi_assert(wifi_singleton); + WifiMessage message = {.type = WifiMessageTypeScan}; + // No need to lock for queue + furi_message_queue_put(wifi_singleton->queue, &message, 100 / portTICK_PERIOD_MS); +} + +bool wifi_is_scanning() { + furi_assert(wifi_singleton); + return wifi_singleton->scan_active; +} + +void wifi_connect(const char* ssid, const char* _Nullable password) { + furi_assert(wifi_singleton); + furi_check(strlen(ssid) <= 32); + furi_check(password == NULL || strlen(password) <= 64); + WifiMessage message = {.type = WifiMessageTypeConnect}; + memcpy(message.connect_message.ssid, ssid, 32); + if (password != NULL) { + memcpy(message.connect_message.password, password, 32); + } else { + message.connect_message.password[0] = 0; + } + furi_message_queue_put(wifi_singleton->queue, &message, 100 / portTICK_PERIOD_MS); +} + +void wifi_disconnect() { + furi_assert(wifi_singleton); + WifiMessage message = {.type = WifiMessageTypeDisconnect}; + furi_message_queue_put(wifi_singleton->queue, &message, 100 / portTICK_PERIOD_MS); +} + +void wifi_set_scan_records(uint16_t records) { + furi_assert(wifi_singleton); + if (records != wifi_singleton->scan_list_limit) { + wifi_scan_list_free_safely(wifi_singleton); + wifi_singleton->scan_list_limit = records; + } +} + +void wifi_get_scan_results(WifiApRecord records[], uint16_t limit, uint16_t* result_count) { + furi_check(wifi_singleton); + furi_check(result_count); + + if (wifi_singleton->scan_list_count == 0) { + *result_count = 0; + } else { + uint16_t i = 0; + FURI_LOG_I(TAG, "processing up to %d APs", wifi_singleton->scan_list_count); + uint16_t last_index = MIN(wifi_singleton->scan_list_count, limit); + for (; i < last_index; ++i) { + memcpy(records[i].ssid, wifi_singleton->scan_list[i].ssid, 33); + records[i].rssi = wifi_singleton->scan_list[i].rssi; + records[i].auth_mode = wifi_singleton->scan_list[i].authmode; + } + // The index already overflowed right before the for-loop was terminated, + // so it effectively became the list count: + *result_count = i; + } +} + +void wifi_set_enabled(bool enabled) { + if (enabled) { + WifiMessage message = {.type = WifiMessageTypeRadioOn}; + // No need to lock for queue + furi_message_queue_put(wifi_singleton->queue, &message, 100 / portTICK_PERIOD_MS); + } else { + WifiMessage message = {.type = WifiMessageTypeRadioOff}; + // No need to lock for queue + furi_message_queue_put(wifi_singleton->queue, &message, 100 / portTICK_PERIOD_MS); + } +} + +// endregion Public functions + +static void wifi_lock(Wifi* wifi) { + furi_crash("this fails for now"); + furi_assert(wifi); + furi_assert(wifi->mutex); + furi_check(xSemaphoreTakeRecursive(wifi->mutex, portMAX_DELAY) == pdPASS); +} + +static void wifi_unlock(Wifi* wifi) { + furi_assert(wifi); + furi_assert(wifi->mutex); + furi_check(xSemaphoreGiveRecursive(wifi->mutex) == pdPASS); +} + +static void wifi_scan_list_alloc(Wifi* wifi) { + furi_check(wifi->scan_list == NULL); + wifi->scan_list = malloc(sizeof(wifi_ap_record_t) * wifi->scan_list_limit); + wifi->scan_list_count = 0; +} + +static void wifi_scan_list_alloc_safely(Wifi* wifi) { + if (wifi->scan_list == NULL) { + wifi_scan_list_alloc(wifi); + } +} + +static void wifi_scan_list_free(Wifi* wifi) { + furi_check(wifi->scan_list != NULL); + free(wifi->scan_list); + wifi->scan_list = NULL; + wifi->scan_list_count = 0; +} + +static void wifi_scan_list_free_safely(Wifi* wifi) { + if (wifi->scan_list != NULL) { + wifi_scan_list_free(wifi); + } +} + +static void wifi_publish_event_simple(Wifi* wifi, WifiEventType type) { + WifiEvent turning_on_event = {.type = type}; + furi_pubsub_publish(wifi->pubsub, &turning_on_event); +} + +static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { + UNUSED(arg); + + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + xEventGroupSetBits(wifi_singleton->event_group, WIFI_FAIL_BIT); + ESP_LOGI(TAG, "disconnected"); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data; + ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip)); + xEventGroupSetBits(wifi_singleton->event_group, WIFI_CONNECTED_BIT); + } +} + +static void wifi_enable(Wifi* wifi) { + WifiRadioState state = wifi->radio_state; + if ( + state == WIFI_RADIO_ON || + state == WIFI_RADIO_ON_PENDING || + state == WIFI_RADIO_OFF_PENDING + ) { + FURI_LOG_W(TAG, "Can't enable from current state"); + return; + } + + FURI_LOG_I(TAG, "Enabling"); + wifi->radio_state = WIFI_RADIO_ON_PENDING; + wifi_publish_event_simple(wifi, WifiEventTypeRadioStateOnPending); + + if (wifi->netif != NULL) { + esp_netif_destroy(wifi->netif); + } + wifi->netif = esp_netif_create_default_wifi_sta(); + + // Warning: this is the memory-intensive operation + // It uses over 117kB of RAM with default settings for S3 on IDF v5.1.2 + wifi_init_config_t config = WIFI_INIT_CONFIG_DEFAULT(); + esp_err_t init_result = esp_wifi_init(&config); + if (init_result != ESP_OK) { + FURI_LOG_E(TAG, "Wifi init failed"); + if (init_result == ESP_ERR_NO_MEM) { + FURI_LOG_E(TAG, "Insufficient memory"); + } + wifi->radio_state = WIFI_RADIO_OFF; + wifi_publish_event_simple(wifi, WifiEventTypeRadioStateOff); + return; + } + + esp_wifi_set_storage(WIFI_STORAGE_RAM); + + // TODO: don't crash on check failure + ESP_ERROR_CHECK(esp_event_handler_instance_register( + WIFI_EVENT, + ESP_EVENT_ANY_ID, + &event_handler, + NULL, + &wifi->event_handler_any_id + )); + + // TODO: don't crash on check failure + ESP_ERROR_CHECK(esp_event_handler_instance_register( + IP_EVENT, + IP_EVENT_STA_GOT_IP, + &event_handler, + NULL, + &wifi->event_handler_got_ip + )); + + if (esp_wifi_set_mode(WIFI_MODE_STA) != ESP_OK) { + FURI_LOG_E(TAG, "Wifi mode setting failed"); + wifi->radio_state = WIFI_RADIO_OFF; + esp_wifi_deinit(); + wifi_publish_event_simple(wifi, WifiEventTypeRadioStateOff); + return; + } + + esp_err_t start_result = esp_wifi_start(); + if (start_result != ESP_OK) { + FURI_LOG_E(TAG, "Wifi start failed"); + if (start_result == ESP_ERR_NO_MEM) { + FURI_LOG_E(TAG, "Insufficient memory"); + } + wifi->radio_state = WIFI_RADIO_OFF; + esp_wifi_set_mode(WIFI_MODE_NULL); + esp_wifi_deinit(); + wifi_publish_event_simple(wifi, WifiEventTypeRadioStateOff); + return; + } + + wifi->radio_state = WIFI_RADIO_ON; + wifi_publish_event_simple(wifi, WifiEventTypeRadioStateOn); + FURI_LOG_I(TAG, "Enabled"); +} + +static void wifi_disable(Wifi* wifi) { + WifiRadioState state = wifi->radio_state; + if ( + state == WIFI_RADIO_OFF || + state == WIFI_RADIO_OFF_PENDING || + state == WIFI_RADIO_ON_PENDING + ) { + FURI_LOG_W(TAG, "Can't disable from current state"); + return; + } + + FURI_LOG_I(TAG, "Disabling"); + wifi->radio_state = WIFI_RADIO_OFF_PENDING; + wifi_publish_event_simple(wifi, WifiEventTypeRadioStateOffPending); + + // Free up scan list memory + wifi_scan_list_free_safely(wifi_singleton); + + if (esp_wifi_stop() != ESP_OK) { + FURI_LOG_E(TAG, "Failed to stop radio"); + wifi->radio_state = WIFI_RADIO_ON; + wifi_publish_event_simple(wifi, WifiEventTypeRadioStateOn); + return; + } + + if (esp_wifi_set_mode(WIFI_MODE_NULL) != ESP_OK) { + FURI_LOG_E(TAG, "Failed to unset mode"); + } + + if (esp_event_handler_instance_unregister( + WIFI_EVENT, + ESP_EVENT_ANY_ID, + wifi->event_handler_any_id + ) != ESP_OK) { + FURI_LOG_E(TAG, "Failed to unregister id event handler"); + } + + if (esp_event_handler_instance_unregister( + IP_EVENT, + IP_EVENT_STA_GOT_IP, + wifi->event_handler_got_ip + ) != ESP_OK) { + FURI_LOG_E(TAG, "Failed to unregister ip event handler"); + } + + if (esp_wifi_deinit() != ESP_OK) { + FURI_LOG_E(TAG, "Failed to deinit"); + } + + furi_check(wifi->netif != NULL); + esp_netif_destroy(wifi->netif); + wifi->netif = NULL; + + wifi->radio_state = WIFI_RADIO_OFF; + wifi_publish_event_simple(wifi, WifiEventTypeRadioStateOff); + FURI_LOG_I(TAG, "Disabled"); +} + +static void wifi_scan_internal(Wifi* wifi) { + WifiRadioState state = wifi->radio_state; + if (state != WIFI_RADIO_ON && state != WIFI_RADIO_CONNECTION_ACTIVE) { + FURI_LOG_W(TAG, "Scan unavailable: wifi not enabled"); + return; + } + + FURI_LOG_I(TAG, "Starting scan"); + wifi->scan_active = true; + wifi_publish_event_simple(wifi, WifiEventTypeScanStarted); + + // Create scan list if it does not exist + wifi_scan_list_alloc_safely(wifi); + wifi->scan_list_count = 0; + + esp_wifi_scan_start(NULL, true); + uint16_t record_count = wifi->scan_list_limit; + ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&record_count, wifi->scan_list)); + uint16_t safe_record_count = MIN(wifi->scan_list_limit, record_count); + wifi->scan_list_count = safe_record_count; + FURI_LOG_I(TAG, "Scanned %u APs. Showing %u:", record_count, safe_record_count); + for (uint16_t i = 0; i < safe_record_count; i++) { + wifi_ap_record_t* record = &wifi->scan_list[i]; + FURI_LOG_I(TAG, " - SSID %s (RSSI %d, channel %d)", record->ssid, record->rssi, record->primary); + } + + esp_wifi_scan_stop(); + + wifi_publish_event_simple(wifi, WifiEventTypeScanFinished); + wifi->scan_active = false; + FURI_LOG_I(TAG, "Finished scan"); +} + +static void wifi_connect_internal(Wifi* wifi, WifiConnectMessage* connect_message) { + // TODO: only when connected! + wifi_disconnect_internal(wifi); + + wifi->radio_state = WIFI_RADIO_CONNECTION_PENDING; + + wifi_publish_event_simple(wifi, WifiEventTypeConnectionPending); + + wifi_config_t wifi_config = { + .sta = { + /* Authmode threshold resets to WPA2 as default if password matches WPA2 standards (pasword len => 8). + * If you want to connect the device to deprecated WEP/WPA networks, Please set the threshold value + * to WIFI_AUTH_WEP/WIFI_AUTH_WPA_PSK and set the password with length and format matching to + * WIFI_AUTH_WEP/WIFI_AUTH_WPA_PSK standards. + */ + .threshold.authmode = WIFI_AUTH_WPA2_WPA3_PSK, + .sae_pwe_h2e = WPA3_SAE_PWE_BOTH, + .sae_h2e_identifier = {0}, + }, + }; + memcpy(wifi_config.sta.ssid, connect_message->ssid, 32); + memcpy(wifi_config.sta.password, connect_message->password, 64); + + esp_err_t set_config_result = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); + if (set_config_result != ESP_OK) { + wifi->radio_state = WIFI_RADIO_ON; + FURI_LOG_E(TAG, "failed to set wifi config (%s)", esp_err_to_name(set_config_result)); + wifi_publish_event_simple(wifi, WifiEventTypeConnectionFailed); + return; + } + + esp_err_t wifi_start_result = esp_wifi_start(); + if (wifi_start_result != ESP_OK) { + wifi->radio_state = WIFI_RADIO_ON; + FURI_LOG_E(TAG, "failed to start wifi to begin connecting (%s)", esp_err_to_name(wifi_start_result)); + wifi_publish_event_simple(wifi, WifiEventTypeConnectionFailed); + return; + } + + /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) + * or connection failed for the maximum number of re-tries (WIFI_FAIL_BIT). + * The bits are set by wifi_event_handler() */ + EventBits_t bits = xEventGroupWaitBits( + wifi->event_group, + WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY + ); + + if (bits & WIFI_CONNECTED_BIT) { + wifi->radio_state = WIFI_RADIO_CONNECTION_ACTIVE; + wifi_publish_event_simple(wifi, WifiEventTypeConnectionSuccess); + ESP_LOGI(TAG, "Connected to %s", connect_message->ssid); + } else if (bits & WIFI_FAIL_BIT) { + wifi->radio_state = WIFI_RADIO_ON; + wifi_publish_event_simple(wifi, WifiEventTypeConnectionFailed); + ESP_LOGI(TAG, "Failed to connect to %s", connect_message->ssid); + } else { + wifi->radio_state = WIFI_RADIO_ON; + wifi_publish_event_simple(wifi, WifiEventTypeConnectionFailed); + ESP_LOGE(TAG, "UNEXPECTED EVENT"); + } +} + +static void wifi_disconnect_internal(Wifi* wifi) { + esp_err_t stop_result = esp_wifi_stop(); + if (stop_result != ESP_OK) { + FURI_LOG_E(TAG, "Failed to disconnect (%s)", esp_err_to_name(stop_result)); + } else { + wifi->radio_state = WIFI_RADIO_ON; + wifi_publish_event_simple(wifi, WifiEventTypeDisconnected); + FURI_LOG_I(TAG, "Disconnected"); + } +} + +static void wifi_disconnect_internal_but_keep_active(Wifi* wifi) { + esp_err_t stop_result = esp_wifi_stop(); + if (stop_result != ESP_OK) { + FURI_LOG_E(TAG, "Failed to disconnect (%s)", esp_err_to_name(stop_result)); + return; + } + + wifi_config_t wifi_config = { + .sta = { + .ssid = {0}, + .password = {0}, + .threshold.authmode = WIFI_AUTH_OPEN, + .sae_pwe_h2e = WPA3_SAE_PWE_UNSPECIFIED, + .sae_h2e_identifier = {0}, + }, + }; + + esp_err_t set_config_result = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); + if (set_config_result != ESP_OK) { + // TODO: disable radio, because radio state is in limbo between off and on + wifi->radio_state = WIFI_RADIO_OFF; + FURI_LOG_E(TAG, "failed to set wifi config (%s)", esp_err_to_name(set_config_result)); + wifi_publish_event_simple(wifi, WifiEventTypeRadioStateOff); + return; + } + + esp_err_t wifi_start_result = esp_wifi_start(); + if (wifi_start_result != ESP_OK) { + // TODO: disable radio, because radio state is in limbo between off and on + wifi->radio_state = WIFI_RADIO_OFF; + FURI_LOG_E(TAG, "failed to start wifi to begin connecting (%s)", esp_err_to_name(wifi_start_result)); + wifi_publish_event_simple(wifi, WifiEventTypeRadioStateOff); + return; + } + + wifi->radio_state = WIFI_RADIO_ON; + wifi_publish_event_simple(wifi, WifiEventTypeDisconnected); + FURI_LOG_I(TAG, "Disconnected"); +} + +// ESP wifi APIs need to run from the main task, so we can't just spawn a thread +_Noreturn int32_t wifi_main(void* p) { + UNUSED(p); + + FURI_LOG_I(TAG, "Started main loop"); + furi_check(wifi_singleton != NULL); + Wifi* wifi = wifi_singleton; + FuriMessageQueue* queue = wifi->queue; + + WifiMessage message; + while (true) { + if (furi_message_queue_get(queue, &message, 1000 / portTICK_PERIOD_MS) == FuriStatusOk) { + FURI_LOG_I(TAG, "Processing message of type %d", message.type); + switch (message.type) { + case WifiMessageTypeRadioOn: + wifi_enable(wifi); + break; + case WifiMessageTypeRadioOff: + wifi_disable(wifi); + break; + case WifiMessageTypeScan: + wifi_scan_internal(wifi); + break; + case WifiMessageTypeConnect: + wifi_connect_internal(wifi, &message.connect_message); + break; + case WifiMessageTypeDisconnect: + wifi_disconnect_internal_but_keep_active(wifi); + break; + } + } + } +} + +static void wifi_service_start(Service service) { + UNUSED(service); + furi_check(wifi_singleton == NULL); + wifi_singleton = wifi_alloc(); +} + +static void wifi_service_stop(Service service) { + UNUSED(service); + furi_check(wifi_singleton != NULL); + + WifiRadioState state = wifi_singleton->radio_state; + if (state != WIFI_RADIO_OFF) { + wifi_disable(wifi_singleton); + } + + wifi_free(wifi_singleton); + wifi_singleton = NULL; + + // wifi_main() cannot be stopped yet as it runs in the main task. + // We could theoretically exit it, but then we wouldn't be able to restart the service. + furi_crash("not fully implemented"); +} + +const ServiceManifest wifi_service = { + .id = "wifi", + .on_start = &wifi_service_start, + .on_stop = &wifi_service_stop +}; diff --git a/components/tactility/src/services/wifi/wifi.h b/components/tactility/src/services/wifi/wifi.h new file mode 100644 index 00000000..76de7454 --- /dev/null +++ b/components/tactility/src/services/wifi/wifi.h @@ -0,0 +1,98 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "esp_wifi.h" +#include "pubsub.h" +#include +#include + +typedef enum { + /** Radio was turned on */ + WifiEventTypeRadioStateOn, + /** Radio is turning on. */ + WifiEventTypeRadioStateOnPending, + /** Radio is turned off */ + WifiEventTypeRadioStateOff, + /** Radio is turning off */ + WifiEventTypeRadioStateOffPending, + /** Started scanning for access points */ + WifiEventTypeScanStarted, + /** Finished scanning for access points */ // TODO: 1 second validity + WifiEventTypeScanFinished, + WifiEventTypeDisconnected, + WifiEventTypeConnectionPending, + WifiEventTypeConnectionSuccess, + WifiEventTypeConnectionFailed +} WifiEventType; + +typedef enum { + WIFI_RADIO_ON_PENDING, + WIFI_RADIO_ON, + WIFI_RADIO_CONNECTION_PENDING, + WIFI_RADIO_CONNECTION_ACTIVE, + WIFI_RADIO_OFF_PENDING, + WIFI_RADIO_OFF +} WifiRadioState; + +typedef struct { + WifiEventType type; +} WifiEvent; + +typedef struct { + uint8_t ssid[33]; + int8_t rssi; + wifi_auth_mode_t auth_mode; +} WifiApRecord; + +/** + * @brief Get wifi pubsub + * @return FuriPubSub* + */ +FuriPubSub* wifi_get_pubsub(); + +WifiRadioState wifi_get_radio_state(); +/** + * @brief Request scanning update. Returns immediately. Results are through pubsub. + */ +void wifi_scan(); + +bool wifi_is_scanning(); + +/** + * @brief Returns the access points from the last scan (if any). It only contains public APs. + * @param records the allocated buffer to store the records in + * @param limit the maximum amount of records to store + */ +void wifi_get_scan_results(WifiApRecord records[], uint16_t limit, uint16_t* result_count); + +/** + * @brief Overrides the default scan result size of 16. + * @param records the record limit for the scan result (84 bytes per record!) + */ +void wifi_set_scan_records(uint16_t records); + +/** + * @brief Enable/disable the radio. Ignores input if desired state matches current state. + * @param enabled + */ +void wifi_set_enabled(bool enabled); + +/** + * @brief Connect to a network. Disconnects any existing connection. + * Returns immediately but runs in the background. Results are through pubsub. + * @param ssid + * @param password + */ +void wifi_connect(const char* ssid, const char* _Nullable password); + +/** + * @brief Disconnect from the access point. Doesn't have any effect when not connected. + */ +void wifi_disconnect(); + +#ifdef __cplusplus +} +#endif diff --git a/components/tactility/src/tactility.c b/components/tactility/src/tactility.c index 7948c6ad..43c285f5 100644 --- a/components/tactility/src/tactility.c +++ b/components/tactility/src/tactility.c @@ -1,25 +1,37 @@ #include "tactility.h" +#include #include "app_manifest_registry.h" #include "devices_i.h" #include "furi.h" #include "graphics_i.h" #include "partitions.h" #include "service_registry.h" +#include "esp_wifi.h" +#include "nvs_flash.h" +#include "services/loader/loader.h" #define TAG "tactility" // System services extern const ServiceManifest gui_service; extern const ServiceManifest loader_service; -extern const ServiceManifest desktop_service; +extern const ServiceManifest wifi_service; // System apps +extern const AppManifest desktop_app; extern const AppManifest system_info_app; +extern const AppManifest wifi_connect_app; +extern const AppManifest wifi_manage_app; + +_Noreturn int32_t wifi_main(void* p); static void register_system_apps() { FURI_LOG_I(TAG, "Registering default apps"); + app_manifest_registry_add(&desktop_app); app_manifest_registry_add(&system_info_app); + app_manifest_registry_add(&wifi_connect_app); + app_manifest_registry_add(&wifi_manage_app); } static void register_user_apps(const Config* _Nonnull config) { @@ -39,23 +51,23 @@ static void register_system_services() { FURI_LOG_I(TAG, "Registering system services"); service_registry_add(&gui_service); service_registry_add(&loader_service); - service_registry_add(&desktop_service); + service_registry_add(&wifi_service); } static void start_system_services() { FURI_LOG_I(TAG, "Starting system services"); service_registry_start(gui_service.id); service_registry_start(loader_service.id); - service_registry_start(desktop_service.id); + service_registry_start(wifi_service.id); } -static void start_user_services(const Config* _Nonnull config) { - FURI_LOG_I(TAG, "Starting user services"); +static void register_and_start_user_services(const Config* _Nonnull config) { + FURI_LOG_I(TAG, "Registering and starting user services"); for (size_t i = 0; i < CONFIG_SERVICES_LIMIT; i++) { const ServiceManifest* manifest = config->services[i]; if (manifest != NULL) { - // TODO: keep track of running services - manifest->on_start(NULL); + service_registry_add(manifest); + service_registry_start(manifest->id); } else { // reached end of list break; @@ -66,6 +78,14 @@ static void start_user_services(const Config* _Nonnull config) { __attribute__((unused)) extern void tactility_start(const Config* _Nonnull config) { furi_init(); + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + tt_partitions_init(); Hardware hardware = tt_hardware_init(config->hardware); @@ -74,9 +94,24 @@ __attribute__((unused)) extern void tactility_start(const Config* _Nonnull confi // Register all apps register_system_services(); register_system_apps(); + // TODO: move this after start_system_services, but desktop must subscribe to app registry events first. register_user_apps(config); // Start all services start_system_services(); - start_user_services(config); + register_and_start_user_services(config); + + // Network interface + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + loader_start_app(desktop_app.id, true, NULL); + + if (config->auto_start_app_id != NULL) { + loader_start_app(config->auto_start_app_id, false, NULL); + } + + // Wifi must run in the main task, or otherwise it will crash the app + // TODO: What if we need more functionality on the main task? + wifi_main(NULL); } diff --git a/components/tactility/src/tactility.h b/components/tactility/src/tactility.h index 1086ca90..43135fcf 100644 --- a/components/tactility/src/tactility.h +++ b/components/tactility/src/tactility.h @@ -32,6 +32,7 @@ typedef struct { // List of user applications const AppManifest* const apps[CONFIG_APPS_LIMIT]; const ServiceManifest* const services[CONFIG_SERVICES_LIMIT]; + const char* auto_start_app_id; } Config; __attribute__((unused)) extern void tactility_start(const Config _Nonnull* config); diff --git a/components/tactility/src/ui/spacer.c b/components/tactility/src/ui/spacer.c new file mode 100644 index 00000000..173bda87 --- /dev/null +++ b/components/tactility/src/ui/spacer.c @@ -0,0 +1,9 @@ +#include "spacer.h" +#include "style.h" + +lv_obj_t* tt_lv_spacer_create(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); + tt_lv_obj_set_style_bg_invisible(spacer); + return spacer; +} diff --git a/components/tactility/src/services/gui/widgets/spacer.h b/components/tactility/src/ui/spacer.h similarity index 55% rename from components/tactility/src/services/gui/widgets/spacer.h rename to components/tactility/src/ui/spacer.h index 1592f43f..b13453b6 100644 --- a/components/tactility/src/services/gui/widgets/spacer.h +++ b/components/tactility/src/ui/spacer.h @@ -6,7 +6,7 @@ extern "C" { #endif -lv_obj_t* spacer(lv_obj_t* parent, lv_coord_t width, lv_coord_t height); +lv_obj_t* tt_lv_spacer_create(lv_obj_t* parent, lv_coord_t width, lv_coord_t height); #ifdef __cplusplus } diff --git a/components/tactility/src/services/gui/widgets/widgets.c b/components/tactility/src/ui/style.c similarity index 55% rename from components/tactility/src/services/gui/widgets/widgets.c rename to components/tactility/src/ui/style.c index 93ab8c95..bc71aeec 100644 --- a/components/tactility/src/services/gui/widgets/widgets.c +++ b/components/tactility/src/ui/style.c @@ -1,16 +1,16 @@ -#include "widgets.h" +#include "style.h" -void lv_obj_set_style_bg_blacken(lv_obj_t* obj) { +void tt_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) { +void tt_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); + lv_obj_set_style_border_width(obj, 0, 0); } -void lv_obj_set_style_no_padding(lv_obj_t* obj) { +void tt_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); } diff --git a/components/tactility/src/ui/style.h b/components/tactility/src/ui/style.h new file mode 100644 index 00000000..b1f77870 --- /dev/null +++ b/components/tactility/src/ui/style.h @@ -0,0 +1,15 @@ +#pragma once + +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void tt_lv_obj_set_style_bg_blacken(lv_obj_t* obj); +void tt_lv_obj_set_style_bg_invisible(lv_obj_t* obj); +void tt_lv_obj_set_style_no_padding(lv_obj_t* obj); + +#ifdef __cplusplus +} +#endif diff --git a/components/tactility/src/ui/toolbar.c b/components/tactility/src/ui/toolbar.c new file mode 100644 index 00000000..e553d35f --- /dev/null +++ b/components/tactility/src/ui/toolbar.c @@ -0,0 +1,48 @@ +#include "toolbar.h" + +#include "services/loader/loader.h" +#include "ui/spacer.h" +#include "ui/style.h" + +#define TOOLBAR_HEIGHT 40 +#define TOOLBAR_FONT_HEIGHT 18 + +static void on_nav_pressed(lv_event_t* event) { + NavAction action = (NavAction)event->user_data; + action(); +} + +lv_obj_t* tt_lv_toolbar_create(lv_obj_t* parent, const Toolbar* toolbar) { + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_height(wrapper, TOOLBAR_HEIGHT); + tt_lv_obj_set_style_no_padding(wrapper); + lv_obj_center(wrapper); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); + + lv_obj_t* close_button = lv_btn_create(wrapper); + lv_obj_set_size(close_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4); + tt_lv_obj_set_style_no_padding(close_button); + lv_obj_add_event_cb(close_button, &on_nav_pressed, LV_EVENT_CLICKED, toolbar->nav_action); + lv_obj_t* close_button_image = lv_img_create(close_button); + lv_img_set_src(close_button_image, toolbar->nav_icon); // e.g. LV_SYMBOL_CLOSE + lv_obj_align(close_button_image, LV_ALIGN_CENTER, 0, 0); + + // Need spacer to avoid button press glitch animation + tt_lv_spacer_create(wrapper, 2, 1); + + lv_obj_t* label_container = lv_obj_create(wrapper); + tt_lv_obj_set_style_no_padding(label_container); + lv_obj_set_style_border_width(label_container, 0, 0); + 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* title_label = lv_label_create(label_container); + lv_label_set_text(title_label, toolbar->title); + lv_obj_set_style_text_font(title_label, &lv_font_montserrat_18, 0); + lv_obj_set_size(title_label, LV_PCT(100), TOOLBAR_FONT_HEIGHT); + lv_obj_set_pos(title_label, 0, (TOOLBAR_HEIGHT - TOOLBAR_FONT_HEIGHT - 10) / 2); + lv_obj_set_style_text_align(title_label, LV_TEXT_ALIGN_CENTER, 0); + + return wrapper; +} diff --git a/components/tactility/src/ui/toolbar.h b/components/tactility/src/ui/toolbar.h new file mode 100644 index 00000000..7e853c03 --- /dev/null +++ b/components/tactility/src/ui/toolbar.h @@ -0,0 +1,21 @@ +#pragma once + +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void(*NavAction)(); + +typedef struct { + const char* _Nullable title; + const char* _Nullable nav_icon; // LVGL compatible definition (e.g. local file or embedded icon from LVGL) + NavAction nav_action; +} Toolbar; + +lv_obj_t* tt_lv_toolbar_create(lv_obj_t* parent, const Toolbar* toolbar); + +#ifdef __cplusplus +} +#endif diff --git a/main/src/hello_world/hello_world.c b/main/src/hello_world/hello_world.c index 59d6c348..38e713ed 100644 --- a/main/src/hello_world/hello_world.c +++ b/main/src/hello_world/hello_world.c @@ -2,8 +2,8 @@ #include "services/gui/gui.h" #include "services/loader/loader.h" -static void app_show(Context* context, lv_obj_t* parent) { - UNUSED(context); +static void app_show(App app, lv_obj_t* parent) { + UNUSED(app); lv_obj_t* label = lv_label_create(parent); lv_label_set_recolor(label, true); diff --git a/main/src/main.c b/main/src/main.c index 62ab3bd3..92b641e4 100644 --- a/main/src/main.c +++ b/main/src/main.c @@ -15,6 +15,7 @@ __attribute__((unused)) void app_main(void) { &hello_world_app }, .services = { }, + .auto_start_app_id = NULL }; tactility_start(&config); diff --git a/partitions.csv b/partitions.csv index 2292c96e..6a8f899b 100644 --- a/partitions.csv +++ b/partitions.csv @@ -2,6 +2,6 @@ # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 1M, +factory, app, factory, 0x10000, 3M, assets, data, spiffs, , 128k, config, data, spiffs, , 64k, diff --git a/sdkconfig.board.lilygo_tdeck b/sdkconfig.board.lilygo_tdeck index cf96dfb1..999d0632 100644 --- a/sdkconfig.board.lilygo_tdeck +++ b/sdkconfig.board.lilygo_tdeck @@ -13,16 +13,19 @@ CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" -# Hardware defaults +# Hardware: Main CONFIG_TT_BOARD_LILYGO_TDECK=y CONFIG_IDF_TARGET="esp32s3" -CONFIG_LV_COLOR_16_SWAP=y CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y CONFIG_FLASHMODE_QIO=y -# SPI RAM +# Hardware: SPI RAM CONFIG_ESP32S3_SPIRAM_SUPPORT=y CONFIG_SPIRAM_MODE_OCT=y CONFIG_SPIRAM_SPEED_80M=y - +# LVGL +CONFIG_LV_COLOR_16_SWAP=y +CONFIG_LV_DISP_DEF_REFR_PERIOD=17 +CONFIG_LV_INDEV_DEF_READ_PERIOD=17 +CONFIG_LV_DPI_DEF=139 diff --git a/sdkconfig.board.yellow_board b/sdkconfig.board.yellow_board index 6a48f618..1a87d4eb 100644 --- a/sdkconfig.board.yellow_board +++ b/sdkconfig.board.yellow_board @@ -13,11 +13,15 @@ CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" -# Hardware defaults +# Hardware: Main CONFIG_TT_BOARD_YELLOW_BOARD_24_CAP=y CONFIG_IDF_TARGET="esp32" -CONFIG_LV_COLOR_16_SWAP=y CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_FLASHMODE_QIO=y +# LVGL +CONFIG_LV_COLOR_16_SWAP=y +CONFIG_LV_DISP_DEF_REFR_PERIOD=17 +CONFIG_LV_INDEV_DEF_READ_PERIOD=17 +CONFIG_LV_DPI_DEF=160 diff --git a/sdkconfig.defaults b/sdkconfig.defaults index e35b6c0b..9131ef19 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -12,8 +12,9 @@ CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" -# Work-around for Furi issue -CONFIG_FREERTOS_UNICORE=y # Hardware defaults CONFIG_TT_BOARD_CUSTOM=y +# LVGL +CONFIG_LV_DISP_DEF_REFR_PERIOD=17 +CONFIG_LV_INDEV_DEF_READ_PERIOD=17