Wifi support and much more (#9)

* add wifi service

* updates for service/app registry changes

* wifi wip

* basic wifi functionality

radio on/off is working
scanning state is working

* fix for wifi switch state

* reduce singleton usage

* various improvements

* improved error handling for low memory issues

* working scanning

* various improvements

* various improvements and fixes

+ added auto-start support in Config

* allow hardwareconfig customizations

* fix for rgb format

* increased lvgl fps

17ms works but 16ms makes the touch events hang for some reason

* layout improvements

* wip on multi-screen view

* basic connection dialog

* more connection logic

* created proper app stack and lifecycle

* cleanup

* cleanup

* cleanup lv widgets

* proper toolbar implementation

* split up wifi apps

* wip

* revert naming

* wip

* temp fix for internal disconnect

* added bundle

* app/service vs appdata/servicedata

* working wifi connect parameters
This commit is contained in:
Ken Van Hoeylandt 2024-01-13 14:15:53 +01:00 committed by GitHub
parent 83e226f696
commit 64a01df750
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 2670 additions and 929 deletions

View File

@ -4,7 +4,7 @@ BasedOnStyle: LLVM
AccessModifierOffset: -4 AccessModifierOffset: -4
AlignAfterOpenBracket: BlockIndent AlignAfterOpenBracket: BlockIndent
AlignConsecutiveAssignments: None AlignConsecutiveAssignments: None
AlignOperands: Align AlignOperands: DontAlign
AllowAllArgumentsOnNextLine: false AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: false AllowAllConstructorInitializersOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false AllowAllParametersOfDeclarationOnNextLine: false

View File

@ -27,7 +27,7 @@ and [esp_lcd_touch](https://components.espressif.com/components/espressif/esp_lc
### Devices ### Devices
Predefined configurations are available for: 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)) - LilyGo T-Deck (see [lilygo.cc](https://www.lilygo.cc/products/t-deck), [AliExpress](https://www.aliexpress.com/item/1005005692235592.html))
- (more will follow) - (more will follow)

View File

@ -104,7 +104,7 @@ static bool create_display_device(DisplayDevice* display) {
ESP_LOGI(TAG, "install driver"); ESP_LOGI(TAG, "install driver");
const esp_lcd_panel_dev_config_t panel_config = { const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = GPIO_NUM_NC, .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, .data_endian = LCD_RGB_DATA_ENDIAN_BIG,
.bits_per_pixel = LCD_BITS_PER_PIXEL, .bits_per_pixel = LCD_BITS_PER_PIXEL,
.flags = { .flags = {

View File

@ -1,9 +1,5 @@
#include "lilygo_tdeck.h" #include "lilygo_tdeck.h"
void lilygo_tdeck_bootstrap();
DisplayDriver lilygo_tdeck_display_driver();
TouchDriver lilygo_tdeck_touch_driver();
const HardwareConfig lilygo_tdeck = { const HardwareConfig lilygo_tdeck = {
.bootstrap = &lilygo_tdeck_bootstrap, .bootstrap = &lilygo_tdeck_bootstrap,
.display_driver = &lilygo_tdeck_display_driver, .display_driver = &lilygo_tdeck_display_driver,

View File

@ -2,4 +2,17 @@
#include "tactility.h" #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; extern const HardwareConfig lilygo_tdeck;
#ifdef __cplusplus
}
#endif

View File

@ -63,7 +63,7 @@ static bool create_display_device(DisplayDevice* display) {
ESP_LOGI(TAG, "install driver"); ESP_LOGI(TAG, "install driver");
const esp_lcd_panel_dev_config_t panel_config = { const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = GPIO_NUM_NC, .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, .bits_per_pixel = LCD_BITS_PER_PIXEL,
}; };

View File

@ -1,8 +1,5 @@
#include "yellow_board.h" #include "yellow_board.h"
DisplayDriver board_2432s024_create_display_driver();
TouchDriver board_2432s024_create_touch_driver();
const HardwareConfig yellow_board_24inch_cap = { const HardwareConfig yellow_board_24inch_cap = {
.bootstrap = NULL, .bootstrap = NULL,
.display_driver = &board_2432s024_create_display_driver, .display_driver = &board_2432s024_create_display_driver,

View File

@ -2,5 +2,17 @@
#include "tactility.h" #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 // Capacitive touch version of the 2.4" yellow board
extern const HardwareConfig yellow_board_24inch_cap; extern const HardwareConfig yellow_board_24inch_cap;
#ifdef __cplusplus
}
#endif

View File

@ -1,22 +1,128 @@
#include "app_i.h" #include "app_i.h"
#include "furi_core.h"
#include "log.h"
#include "furi_string.h"
#define TAG "app" #include <stdio.h>
App* furi_app_alloc(const AppManifest* _Nonnull manifest) { static AppFlags app_get_flags_default(AppType type);
App app = {
// 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, .manifest = manifest,
.context = { .parameters = parameters,
.data = NULL .data = NULL
}
}; };
App* app_ptr = malloc(sizeof(App)); return (App*)data;
return memcpy(app_ptr, &app, sizeof(App));
} }
void furi_app_free(App* app) { void app_free(App app) {
furi_assert(app); AppData* data = (AppData*)app;
free(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

51
components/furi/src/app.h Normal file
View File

@ -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

View File

@ -1,19 +1,33 @@
#pragma once #pragma once
#include "app.h"
#include "app_manifest.h" #include "app_manifest.h"
#include "thread.h" #include "context.h"
#include "mutex.h"
#include <stdbool.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
typedef struct { typedef struct {
FuriMutex* mutex;
const AppManifest* manifest; const AppManifest* manifest;
Context context; AppState state;
} App; AppFlags flags;
/** @brief Optional parameters to start the app with
App* furi_app_alloc(const AppManifest* _Nonnull manifest); * When these are stored in the app struct, the struct takes ownership.
void furi_app_free(App* _Nonnull app); * 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 #ifdef __cplusplus
} }

View File

@ -1,6 +1,5 @@
#pragma once #pragma once
#include "context.h"
#include <stdio.h> #include <stdio.h>
#ifdef __cplusplus #ifdef __cplusplus
@ -9,16 +8,19 @@ extern "C" {
// Forward declarations // Forward declarations
typedef struct _lv_obj_t lv_obj_t; typedef struct _lv_obj_t lv_obj_t;
typedef void* App;
typedef enum { typedef enum {
AppTypeDesktop,
AppTypeSystem, AppTypeSystem,
AppTypeSettings, AppTypeSettings,
AppTypeUser AppTypeUser
} AppType; } AppType;
typedef void (*AppOnStart)(Context* context); typedef void (*AppOnStart)(App app);
typedef void (*AppOnStop)(Context* context); typedef void (*AppOnStop)(App app);
typedef void (*AppOnShow)(Context* context, lv_obj_t* parent); typedef void (*AppOnShow)(App app, lv_obj_t* parent);
typedef void (*AppOnHide)(App app);
typedef struct { typedef struct {
/** /**
@ -55,6 +57,11 @@ typedef struct {
* Non-blocking method to create the GUI * Non-blocking method to create the GUI
*/ */
const AppOnShow _Nullable on_show; const AppOnShow _Nullable on_show;
/**
* Non-blocking method, called before gui is destroyed
*/
const AppOnHide _Nullable on_hide;
} AppManifest; } AppManifest;
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -1,4 +1,5 @@
#include "app_manifest_registry.h" #include "app_manifest_registry.h"
#include "furi_core.h" #include "furi_core.h"
#include "m-dict.h" #include "m-dict.h"
#include "m_cstr_dup.h" #include "m_cstr_dup.h"

View File

@ -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

View File

@ -0,0 +1,34 @@
/**
* @brief key-value storage for general purpose.
* Maps strings on a fixed set of data types.
*/
#pragma once
#include <stdio.h>
#include <stdbool.h>
#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

View File

@ -1,11 +1,5 @@
#pragma once #pragma once
typedef struct { 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; } Context;

View File

@ -11,12 +11,6 @@ void furi_init() {
FURI_LOG_I(TAG, "init start"); FURI_LOG_I(TAG, "init start");
furi_assert(!furi_kernel_is_irq()); furi_assert(!furi_kernel_is_irq());
if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
vTaskSuspendAll();
}
xTaskResumeAll();
#if defined(__ARM_ARCH_7A__) && (__ARM_ARCH_7A__ == 0U) #if defined(__ARM_ARCH_7A__) && (__ARM_ARCH_7A__ == 0U)
/* Service Call interrupt might be configured before kernel start */ /* Service Call interrupt might be configured before kernel start */
/* and when its priority is lower or equal to BASEPRI, svc instruction */ /* and when its priority is lower or equal to BASEPRI, svc instruction */

View File

@ -59,7 +59,7 @@ furi_message_queue_put(FuriMessageQueue* instance, const void* msg_ptr, uint32_t
return (stat); 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; QueueHandle_t hQueue = (QueueHandle_t)instance;
FuriStatus stat; FuriStatus stat;
BaseType_t yield; BaseType_t yield;
@ -67,7 +67,7 @@ FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uin
stat = FuriStatusOk; stat = FuriStatusOk;
if (furi_kernel_is_irq() != 0U) { 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; stat = FuriStatusErrorParameter;
} else { } else {
yield = pdFALSE; yield = pdFALSE;
@ -82,8 +82,8 @@ FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uin
if ((hQueue == NULL) || (msg_ptr == NULL)) { if ((hQueue == NULL) || (msg_ptr == NULL)) {
stat = FuriStatusErrorParameter; stat = FuriStatusErrorParameter;
} else { } else {
if (xQueueReceive(hQueue, msg_ptr, (TickType_t)timeout) != pdPASS) { if (xQueueReceive(hQueue, msg_ptr, (TickType_t)timeout_ticks) != pdPASS) {
if (timeout != 0U) { if (timeout_ticks != 0U) {
stat = FuriStatusErrorTimeout; stat = FuriStatusErrorTimeout;
} else { } else {
stat = FuriStatusErrorResource; stat = FuriStatusErrorResource;

View File

@ -44,11 +44,11 @@ furi_message_queue_put(FuriMessageQueue* instance, const void* msg_ptr, uint32_t
* @param instance pointer to FuriMessageQueue instance * @param instance pointer to FuriMessageQueue instance
* @param msg_ptr The message pointer * @param msg_ptr The message pointer
* @param msg_prio The message prioority * @param msg_prio The message prioority
* @param[in] timeout The timeout * @param[in] timeout_ticks The timeout
* *
* @return The furi status. * @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 /** Get queue capacity
* *

View File

@ -4,6 +4,7 @@
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/semphr.h" #include "freertos/semphr.h"
#include "log.h"
FuriMutex* furi_mutex_alloc(FuriMutexType type) { FuriMutex* furi_mutex_alloc(FuriMutexType type) {
furi_assert(!FURI_IS_IRQ_MODE()); furi_assert(!FURI_IS_IRQ_MODE());

View File

@ -2,20 +2,62 @@
#include "furi_core.h" #include "furi_core.h"
#include "log.h" #include "log.h"
#define TAG "service" // region Alloc/free
Service* furi_service_alloc(const ServiceManifest* _Nonnull manifest) { Service service_alloc(const ServiceManifest* _Nonnull manifest) {
Service app = { ServiceData* data = malloc(sizeof(ServiceData));
*data = (ServiceData) {
.manifest = manifest, .manifest = manifest,
.context = { .mutex = furi_mutex_alloc(FuriMutexTypeRecursive),
.data = NULL .data = NULL
}
}; };
Service* app_ptr = malloc(sizeof(Service)); return data;
return memcpy(app_ptr, &app, sizeof(Service));
} }
void furi_service_free(Service* app) { void service_free(Service service) {
furi_assert(app); ServiceData* data = (ServiceData*)service;
free(app); 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

View File

@ -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

View File

@ -1,12 +1,16 @@
#pragma once #pragma once
#include "service_manifest.h" #include "service.h"
#include "context.h" #include "context.h"
#include "mutex.h"
#include "service_manifest.h"
typedef struct { typedef struct {
FuriMutex* mutex;
const ServiceManifest* manifest; const ServiceManifest* manifest;
Context context; void* data;
} Service; } ServiceData;
Service* furi_service_alloc(const ServiceManifest* _Nonnull manifest); Service service_alloc(const ServiceManifest* _Nonnull manifest);
void furi_service_free(Service* _Nonnull service); void service_free(Service _Nonnull service);

View File

@ -7,8 +7,10 @@
extern "C" { extern "C" {
#endif #endif
typedef void (*ServiceOnStart)(Context* context); typedef void* Service;
typedef void (*ServiceOnStop)(Context* context);
typedef void (*ServiceOnStart)(Service service);
typedef void (*ServiceOnStop)(Service service);
typedef struct { typedef struct {
/** /**

View File

@ -9,17 +9,17 @@
#define TAG "service_registry" #define TAG "service_registry"
DICT_DEF2(ServiceManifestDict, const char*, M_CSTR_DUP_OPLIST, const ServiceManifest*, M_PTR_OPLIST) 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) \ #define APP_REGISTRY_FOR_EACH(manifest_var_name, code_to_execute) \
{ \ { \
service_registry_manifest_lock(); \ service_registry_manifest_lock(); \
ServiceManifestDict_it_t it; \ ServiceManifestDict_it_t it; \
for (ServiceManifestDict_it(it, service_manifest_dict); !ServiceManifestDict_end_p(it); ServiceManifestDict_next(it)) { \ for (ServiceManifestDict_it(it, service_manifest_dict); !ServiceManifestDict_end_p(it); ServiceManifestDict_next(it)) { \
const ServiceManifest*(manifest_var_name) = ServiceManifestDict_cref(it)->value; \ const ServiceManifest*(manifest_var_name) = ServiceManifestDict_cref(it)->value; \
code_to_execute; \ code_to_execute; \
} \ } \
service_registry_manifest_unlock(); \ service_registry_manifest_unlock(); \
} }
ServiceManifestDict_t service_manifest_dict; 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; 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(); 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) { if (service_ptr == NULL) {
return NULL; return NULL;
} }
Service* service = (Service*) *service_ptr; ServiceData* service = (ServiceData*)*service_ptr;
service_registry_instance_unlock(); service_registry_instance_unlock();
return service; return service;
} }
@ -100,12 +100,12 @@ bool service_registry_start(const char* service_id) {
FURI_LOG_I(TAG, "starting %s", service_id); FURI_LOG_I(TAG, "starting %s", service_id);
const ServiceManifest* manifest = service_registry_find_manifest_by_id(service_id); const ServiceManifest* manifest = service_registry_find_manifest_by_id(service_id);
if (manifest == NULL) { 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; return false;
} }
Service* service = furi_service_alloc(manifest); Service service = service_alloc(manifest);
service->manifest->on_start(&service->context); manifest->on_start(service);
service_registry_instance_lock(); service_registry_instance_lock();
ServiceInstanceDict_set_at(service_instance_dict, manifest->id, service); 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) { bool service_registry_stop(const char* service_id) {
FURI_LOG_I(TAG, "stopping %s", 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) { 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; return false;
} }
service->manifest->on_stop(&service->context); service->manifest->on_stop(service);
furi_service_free(service); service_free(service);
service_registry_instance_lock(); service_registry_instance_lock();
ServiceInstanceDict_erase(service_instance_dict, service_id); ServiceInstanceDict_erase(service_instance_dict, service_id);

View File

@ -1,10 +1,14 @@
idf_component_register( idf_component_register(
SRC_DIRS "src" SRC_DIRS "src"
"src/apps/desktop"
"src/apps/system/system_info" "src/apps/system/system_info"
"src/services/desktop" "src/apps/system/wifi_connect"
"src/apps/system/wifi_manage"
"src/services/loader" "src/services/loader"
"src/services/gui" "src/services/gui"
"src/services/gui/widgets" "src/services/gui/widgets"
"src/services/wifi"
"src/ui"
INCLUDE_DIRS "src" INCLUDE_DIRS "src"
@ -12,6 +16,7 @@ idf_component_register(
esp_lcd esp_lcd
esp_lcd_touch esp_lcd_touch
esp_lvgl_port esp_lvgl_port
esp_wifi
driver driver
fatfs fatfs
furi furi

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

View File

@ -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
};

View File

@ -2,9 +2,10 @@
#include "furi_extra_defines.h" #include "furi_extra_defines.h"
#include "thread.h" #include "thread.h"
#include "lvgl.h" #include "lvgl.h"
#include "esp_wifi.h"
static void app_show(Context* context, lv_obj_t* parent) { static void app_show(App app, lv_obj_t* parent) {
UNUSED(context); UNUSED(app);
lv_obj_t* heap_info = lv_label_create(parent); lv_obj_t* heap_info = lv_label_create(parent);
lv_label_set_recolor(heap_info, true); lv_label_set_recolor(heap_info, true);

View File

@ -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
};

View File

@ -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

View File

@ -0,0 +1,10 @@
#pragma once
#include <stdbool.h>
typedef void (*OnConnectSsid)(const char* ssid, const char* password, void* context);
typedef struct {
OnConnectSsid on_connect_ssid;
void* on_connect_ssid_context;
} WifiConnectBindings;

View File

@ -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

View File

@ -0,0 +1,21 @@
#pragma once
#include <stdbool.h>
#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

View File

@ -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);
}

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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
};

View File

@ -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

View File

@ -0,0 +1,13 @@
#pragma once
#include <stdbool.h>
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;

View File

@ -0,0 +1,25 @@
#pragma once
#include <stdbool.h>
#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

View File

@ -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);
}

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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
};

View File

@ -1,14 +1,14 @@
#include "gui_i.h"
#include "check.h" #include "check.h"
#include "esp_lvgl_port.h" #include "esp_lvgl_port.h"
#include "furi_extra_defines.h" #include "furi_extra_defines.h"
#include "gui_i.h"
#include "log.h" #include "log.h"
#include "kernel.h" #include "kernel.h"
#define TAG "gui" #define TAG "gui"
// Forward declarations // Forward declarations
bool gui_redraw_fs(Gui*);
void gui_redraw(Gui*); void gui_redraw(Gui*);
static int32_t gui_main(void*); static int32_t gui_main(void*);
@ -24,25 +24,12 @@ Gui* gui_alloc() {
&gui_main, &gui_main,
NULL NULL
); );
instance->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); instance->mutex = furi_mutex_alloc(FuriMutexTypeRecursive);
furi_check(lvgl_port_lock(100)); furi_check(lvgl_port_lock(100));
instance->lvgl_parent = lv_scr_act(); instance->lvgl_parent = lv_scr_act();
lvgl_port_unlock(); 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; return instance;
} }
@ -67,42 +54,26 @@ void gui_unlock() {
void gui_request_draw() { void gui_request_draw() {
furi_assert(gui); furi_assert(gui);
FuriThreadId thread_id = furi_thread_get_id(gui->thread); FuriThreadId thread_id = furi_thread_get_id(gui->thread);
furi_thread_flags_set(thread_id, GUI_THREAD_FLAG_DRAW); furi_thread_flags_set(thread_id, GUI_THREAD_FLAG_DRAW);
} }
void gui_add_view_port(ViewPort* view_port, GuiLayer layer) { void gui_show_app(App app, ViewPortShowCallback on_show, ViewPortHideCallback on_hide) {
furi_assert(gui);
furi_assert(view_port);
furi_check(layer < GuiLayerMAX);
gui_lock(); gui_lock();
furi_check(gui->layers[layer] == NULL, "layer in use"); furi_check(gui->app_view_port == NULL);
gui->layers[layer] = view_port; gui->app_view_port = view_port_alloc(app, on_show, on_hide);
view_port_gui_set(view_port, gui);
gui_unlock(); gui_unlock();
gui_request_draw(); gui_request_draw();
} }
void gui_remove_view_port(ViewPort* view_port) { void gui_hide_app() {
furi_assert(gui);
furi_assert(view_port);
gui_lock(); gui_lock();
ViewPort* view_port = gui->app_view_port;
view_port_gui_set(view_port, NULL); furi_check(view_port != NULL);
for (size_t i = 0; i < GuiLayerMAX; i++) { view_port_hide(view_port);
if (gui->layers[i] == view_port) { view_port_free(view_port);
gui->layers[i] = NULL; gui->app_view_port = NULL;
break;
}
}
gui_unlock(); gui_unlock();
gui_request_draw();
} }
static int32_t gui_main(void* p) { static int32_t gui_main(void* p) {
@ -116,20 +87,10 @@ static int32_t gui_main(void* p) {
FuriFlagWaitAny, FuriFlagWaitAny,
FuriWaitForever 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 // Process and dispatch draw call
if (flags & GUI_THREAD_FLAG_DRAW) { if (flags & GUI_THREAD_FLAG_DRAW) {
furi_thread_flags_clear(GUI_THREAD_FLAG_DRAW); furi_thread_flags_clear(GUI_THREAD_FLAG_DRAW);
gui_lock();
gui_redraw(local_gui); gui_redraw(local_gui);
gui_unlock();
} }
if (flags & GUI_THREAD_FLAG_EXIT) { if (flags & GUI_THREAD_FLAG_EXIT) {
@ -143,8 +104,8 @@ static int32_t gui_main(void* p) {
// region AppManifest // region AppManifest
static void gui_start(Context* context) { static void gui_start(Service service) {
UNUSED(context); UNUSED(service);
gui = gui_alloc(); gui = gui_alloc();
@ -152,8 +113,8 @@ static void gui_start(Context* context) {
furi_thread_start(gui->thread); furi_thread_start(gui->thread);
} }
static void gui_stop(Context* context) { static void gui_stop(Service service) {
UNUSED(context); UNUSED(service);
gui_lock(); gui_lock();

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "app.h"
#include "service_manifest.h" #include "service_manifest.h"
#include "view_port.h" #include "view_port.h"
@ -7,32 +8,11 @@
extern "C" { extern "C" {
#endif #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; typedef struct Gui Gui;
/** Add view_port to view_port tree void gui_show_app(App app, ViewPortShowCallback on_show, ViewPortHideCallback on_hide);
*
* @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);
/** Remove view_port from rendering tree void gui_hide_app();
*
* @remark thread safe
*
* @param view_port ViewPort instance
*/
void gui_remove_view_port(ViewPort* view_port);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@ -2,105 +2,74 @@
#include "esp_lvgl_port.h" #include "esp_lvgl_port.h"
#include "gui_i.h" #include "gui_i.h"
#include "log.h" #include "log.h"
#include "services/gui/widgets/widgets.h" #include "services/gui/widgets/statusbar.h"
#include "services/loader/loader.h" #include "services/loader/loader.h"
#include "ui/spacer.h"
#include "ui/style.h"
#include "ui/toolbar.h"
#define TAG "gui" #define TAG "gui"
static lv_obj_t* screen_with_top_bar(lv_obj_t* parent) { static lv_obj_t* create_app_views(lv_obj_t* parent, App app) {
lv_obj_set_style_bg_blacken(parent); tt_lv_obj_set_style_bg_blacken(parent);
lv_obj_t* vertical_container = lv_obj_create(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_size(vertical_container, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_no_padding(vertical_container); tt_lv_obj_set_style_no_padding(vertical_container);
lv_obj_set_style_bg_blacken(vertical_container); tt_lv_obj_set_style_bg_blacken(vertical_container);
top_bar(vertical_container); // TODO: Move statusbar into separate ViewPort
AppFlags flags = app_get_flags(app);
if (flags.show_statusbar) {
tt_lv_statusbar_create(vertical_container);
}
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_t* child_container = lv_obj_create(vertical_container);
lv_obj_set_width(child_container, LV_PCT(100)); lv_obj_set_width(child_container, LV_PCT(100));
lv_obj_set_flex_grow(child_container, 1); 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; return child_container;
} }
static lv_obj_t* screen_with_top_bar_and_toolbar(lv_obj_t* parent) {
lv_obj_set_style_bg_blacken(parent);
lv_obj_t* vertical_container = lv_obj_create(parent);
lv_obj_set_size(vertical_container, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_no_padding(vertical_container);
lv_obj_set_style_bg_blacken(vertical_container);
top_bar(vertical_container);
const AppManifest* manifest = loader_get_current_app();
if (manifest != NULL) {
toolbar(vertical_container, TOP_BAR_HEIGHT, manifest);
}
lv_obj_t* spacer = lv_obj_create(vertical_container);
lv_obj_set_size(spacer, 2, 2);
lv_obj_set_style_bg_blacken(spacer);
lv_obj_t* child_container = lv_obj_create(vertical_container);
lv_obj_set_width(child_container, LV_PCT(100));
lv_obj_set_flex_grow(child_container, 1);
lv_obj_set_style_no_padding(vertical_container);
lv_obj_set_style_bg_blacken(vertical_container);
return child_container;
}
static bool gui_redraw_window(Gui* gui) {
ViewPort* view_port = gui->layers[GuiLayerWindow];
if (view_port) {
lv_obj_t* container = screen_with_top_bar_and_toolbar(gui->lvgl_parent);
view_port_draw(view_port, container);
return true;
} else {
return false;
}
}
static bool gui_redraw_desktop(Gui* gui) {
ViewPort* view_port = gui->layers[GuiLayerDesktop];
if (view_port) {
lv_obj_t* container = screen_with_top_bar(gui->lvgl_parent);
view_port_draw(view_port, container);
return true;
} else {
FURI_LOG_E(TAG, "no desktop layer found");
}
return false;
}
bool gui_redraw_fs(Gui* gui) {
ViewPort* view_port = gui->layers[GuiLayerFullscreen];
if (view_port) {
view_port_draw(view_port, gui->lvgl_parent);
return true;
} else {
return false;
}
}
void gui_redraw(Gui* gui) { void gui_redraw(Gui* gui) {
furi_assert(gui); furi_assert(gui);
// Lock GUI and LVGL
gui_lock();
furi_check(lvgl_port_lock(100)); furi_check(lvgl_port_lock(100));
lv_obj_clean(gui->lvgl_parent); lv_obj_clean(gui->lvgl_parent);
if (!gui_redraw_fs(gui)) { if (gui->app_view_port != NULL) {
if (!gui_redraw_window(gui)) { ViewPort* view_port = gui->app_view_port;
gui_redraw_desktop(gui); 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(); lvgl_port_unlock();
gui_unlock();
} }

View File

@ -20,31 +20,16 @@ struct Gui {
FuriMutex* mutex; FuriMutex* mutex;
// Layers and Canvas // Layers and Canvas
ViewPort* layers[GuiLayerMAX];
lv_obj_t* lvgl_parent; lv_obj_t* lvgl_parent;
// Input // App-specific
/* ViewPort* app_view_port;
FuriMessageQueue* input_queue;
FuriPubSub* input_events;
uint8_t ongoing_input;
ViewPort* ongoing_input_view_port;
*/
}; };
/** Update GUI, request redraw /** Update GUI, request redraw
*/ */
void gui_request_draw(); 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 /** Lock GUI
*/ */
void gui_lock(); void gui_lock();

View File

@ -1,98 +1,40 @@
#include "view_port.h"
#include "check.h" #include "check.h"
#include "gui.h" #include "ui/style.h"
#include "gui_i.h"
#include "services/gui/widgets/widgets.h"
#include "view_port_i.h" #include "view_port_i.h"
#define TAG "viewport" #define TAG "viewport"
_Static_assert(ViewPortOrientationMAX == 4, "Incorrect ViewPortOrientation count"); ViewPort* view_port_alloc(
_Static_assert( App app,
(ViewPortOrientationHorizontal == 0 && ViewPortOrientationHorizontalFlip == 1 && ViewPortShowCallback on_show,
ViewPortOrientationVertical == 2 && ViewPortOrientationVerticalFlip == 3), ViewPortHideCallback on_hide
"Incorrect ViewPortOrientation order" ) {
);
ViewPort* view_port_alloc() {
ViewPort* view_port = malloc(sizeof(ViewPort)); ViewPort* view_port = malloc(sizeof(ViewPort));
view_port->gui = NULL; view_port->app = app;
view_port->is_enabled = true; view_port->on_show = on_show;
view_port->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); view_port->on_hide = on_hide;
return view_port; return view_port;
} }
void view_port_free(ViewPort* view_port) { void view_port_free(ViewPort* view_port) {
furi_assert(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); free(view_port);
} }
void view_port_enabled_set(ViewPort* view_port, bool enabled) { void view_port_show(ViewPort* view_port, lv_obj_t* parent) {
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) {
furi_assert(view_port);
// We are not going to lockup system, but will notify you instead
// Make sure that you don't call viewport methods inside another mutex, especially one that is used in draw call
if (furi_mutex_acquire(view_port->mutex, 2) != FuriStatusOk) {
ESP_LOGW(TAG, "ViewPort lockup: see %s:%d", __FILE__, __LINE__ - 3);
}
if (view_port->gui && view_port->is_enabled) gui_request_draw();
furi_mutex_release(view_port->mutex);
}
void view_port_gui_set(ViewPort* view_port, Gui* gui) {
furi_assert(view_port);
furi_check(furi_mutex_acquire(view_port->mutex, FuriWaitForever) == FuriStatusOk);
view_port->gui = gui;
furi_check(furi_mutex_release(view_port->mutex) == FuriStatusOk);
}
void view_port_draw(ViewPort* view_port, lv_obj_t* parent) {
furi_assert(view_port); furi_assert(view_port);
furi_assert(parent); furi_assert(parent);
if (view_port->on_show) {
// We are not going to lockup system, but will notify you instead tt_lv_obj_set_style_no_padding(parent);
// Make sure that you don't call viewport methods inside another mutex, especially one that is used in draw call view_port->on_show(view_port->app, parent);
if (furi_mutex_acquire(view_port->mutex, 2) != FuriStatusOk) { }
ESP_LOGW(TAG, "ViewPort lockup: see %s:%d", __FILE__, __LINE__ - 3); }
void view_port_hide(ViewPort* view_port) {
furi_assert(view_port);
if (view_port->on_hide) {
view_port->on_hide(view_port->app);
} }
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);
}
furi_mutex_release(view_port->mutex);
} }

View File

@ -4,31 +4,39 @@
extern "C" { extern "C" {
#endif #endif
#include "app.h"
#include "lvgl.h" #include "lvgl.h"
#include "context.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 /** ViewPort Draw callback
* @warning called from GUI thread * @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 /** ViewPort allocator
* *
* always returns view_port or stops system if not enough memory. * 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 * @return ViewPort instance
*/ */
ViewPort* view_port_alloc(); ViewPort* view_port_alloc(
App app,
ViewPortShowCallback on_show,
ViewPortHideCallback on_hide
);
/** ViewPort deallocator /** ViewPort deallocator
* *
@ -38,30 +46,6 @@ ViewPort* view_port_alloc();
*/ */
void view_port_free(ViewPort* view_port); 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 #ifdef __cplusplus
} }
#endif #endif

View File

@ -1,47 +1,19 @@
#pragma once #pragma once
#include "context.h"
#include "gui_i.h"
#include "mutex.h"
#include "view_port.h" #include "view_port.h"
struct ViewPort { /** Process draw call. Calls on_show callback.
Gui* gui; * To be used by GUI, called on redraw.
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.
* *
* @param view_port ViewPort instance * @param view_port ViewPort instance
* @param canvas canvas to draw at * @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
*
* @param view_port ViewPort instance
* @param event pointer to input event
*/ */
//void view_port_input(ViewPort* view_port, InputEvent* event); void view_port_hide(ViewPort* view_port);

View File

@ -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);
}
*/

View File

@ -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);
*/

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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");
}

View File

@ -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

View File

@ -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

View File

@ -14,235 +14,243 @@
// Forward declarations // Forward declarations
static int32_t loader_main(void* p); static int32_t loader_main(void* p);
static Loader* loader = NULL; static Loader* loader_singleton = NULL;
static Loader* loader_alloc() { static Loader* loader_alloc() {
furi_check(loader == NULL); furi_check(loader_singleton == NULL);
loader = malloc(sizeof(Loader)); loader_singleton = malloc(sizeof(Loader));
loader->pubsub = furi_pubsub_alloc(); loader_singleton->pubsub = furi_pubsub_alloc();
loader->queue = furi_message_queue_alloc(1, sizeof(LoaderMessage)); loader_singleton->queue = furi_message_queue_alloc(1, sizeof(LoaderMessage));
loader->thread = furi_thread_alloc_ex( loader_singleton->thread = furi_thread_alloc_ex(
"loader", "loader",
4096, // Last known minimum was 2400 for starting Hello World app 4096, // Last known minimum was 2400 for starting Hello World app
&loader_main, &loader_main,
NULL NULL
); );
loader->app_data.app = NULL; loader_singleton->mutex = xSemaphoreCreateRecursiveMutex();
loader->app_data.view_port = NULL; loader_singleton->app_stack_index = -1;
loader->mutex = xSemaphoreCreateRecursiveMutex(); memset(loader_singleton->app_stack, 0, sizeof(App) * APP_STACK_SIZE);
return loader; return loader_singleton;
} }
static void loader_free() { static void loader_free() {
furi_check(loader != NULL); furi_check(loader_singleton != NULL);
furi_thread_free(loader->thread); furi_thread_free(loader_singleton->thread);
furi_pubsub_free(loader->pubsub); furi_pubsub_free(loader_singleton->pubsub);
furi_message_queue_free(loader->queue); furi_message_queue_free(loader_singleton->queue);
furi_mutex_free(loader->mutex); furi_mutex_free(loader_singleton->mutex);
free(loader); free(loader_singleton);
} }
void loader_lock() { void loader_lock() {
furi_assert(loader); furi_assert(loader_singleton);
furi_assert(loader->mutex); furi_assert(loader_singleton->mutex);
furi_check(xSemaphoreTakeRecursive(loader->mutex, portMAX_DELAY) == pdPASS); furi_check(xSemaphoreTakeRecursive(loader_singleton->mutex, portMAX_DELAY) == pdPASS);
} }
void loader_unlock() { void loader_unlock() {
furi_assert(loader); furi_assert(loader_singleton);
furi_assert(loader->mutex); furi_assert(loader_singleton->mutex);
furi_check(xSemaphoreGiveRecursive(loader->mutex) == pdPASS); furi_check(xSemaphoreGiveRecursive(loader_singleton->mutex) == pdPASS);
} }
LoaderStatus loader_start_app(const char* id, FuriString* error_message) { LoaderStatus loader_start_app(const char* id, bool blocking, Bundle* _Nullable bundle) {
LoaderMessage message; LoaderMessageLoaderStatusResult result = {
LoaderMessageLoaderStatusResult result; .value = LoaderStatusOk
};
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);
}
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; return result.value;
} }
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);
}
void loader_stop_app() { void loader_stop_app() {
LoaderMessage message; LoaderMessage message = {.type = LoaderMessageTypeAppStop};
message.type = LoaderMessageTypeAppStop; furi_message_queue_put(loader_singleton->queue, &message, FuriWaitForever);
furi_message_queue_put(loader->queue, &message, FuriWaitForever);
} }
const AppManifest* _Nullable loader_get_current_app() { App _Nullable loader_get_current_app() {
loader_lock(); loader_lock();
const App* app = loader->app_data.app; App app = (loader_singleton->app_stack_index >= 0)
const AppManifest* manifest = app ? app->manifest : NULL; ? loader_singleton->app_stack[loader_singleton->app_stack_index]
: NULL;
loader_unlock(); loader_unlock();
return app;
return manifest;
} }
FuriPubSub* loader_get_pubsub() { FuriPubSub* loader_get_pubsub() {
furi_assert(loader); furi_assert(loader_singleton);
// it's safe to return pubsub without locking // it's safe to return pubsub without locking
// because it's never freed and loader is never exited // because it's never freed and loader is never exited
// also the loader instance cannot be obtained until the pubsub is created // 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( static const char* app_state_to_string(AppState state) {
LoaderStatus status, switch (state) {
FuriString* error_message, case APP_STATE_INITIAL:
const char* format, return "initial";
va_list args case APP_STATE_STARTED:
) { return "started";
if (error_message) { case APP_STATE_SHOWING:
furi_string_vprintf(error_message, format, args); return "showing";
FURI_LOG_E(TAG, "Status [%d]: %s", status, furi_string_get_cstr(error_message)); case APP_STATE_HIDING:
} else { return "hiding";
FURI_LOG_E(TAG, "Status [%d]", status); case APP_STATE_STOPPED:
return "stopped";
default:
return "?";
} }
} }
static LoaderStatus loader_make_status_error( static void app_transition_to_state(App app, AppState state) {
LoaderStatus status, const AppManifest* manifest = app_get_manifest(app);
FuriString* _Nullable error_message, const AppState old_state = app_get_state(app);
const char* format,
... FURI_LOG_I(
) { TAG,
va_list args; "app \"%s\" state: %s -> %s",
va_start(args, format); manifest->id,
loader_log_status_error(status, error_message, format, args); app_state_to_string(old_state),
va_end(args); app_state_to_string(state)
return status; );
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 LoaderStatus loader_make_success_status(FuriString* error_message) { LoaderStatus loader_do_start_app_with_manifest(
if (error_message) { const AppManifest* _Nonnull manifest,
furi_string_set(error_message, "App started"); Bundle* _Nullable bundle
) {
FURI_LOG_I(TAG, "start with manifest %s", manifest->id);
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;
} }
int8_t previous_index = loader_singleton->app_stack_index;
loader_singleton->app_stack_index++;
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);
// 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);
}
app_transition_to_state(app, APP_STATE_SHOWING);
loader_unlock();
LoaderEvent event = {.type = LoaderEventTypeApplicationStarted};
furi_pubsub_publish(loader_singleton->pubsub, &event);
return LoaderStatusOk; return LoaderStatusOk;
} }
static void loader_start_app_with_manifest(
const AppManifest* _Nonnull manifest
) {
FURI_LOG_I(TAG, "start with manifest %s", manifest->id);
if (manifest->type != AppTypeUser && manifest->type != AppTypeSystem) {
furi_crash("App type not supported by loader");
}
App* _Nonnull app = furi_app_alloc(manifest);
loader_lock();
loader->app_data.app = app;
if (manifest->on_start != NULL) {
manifest->on_start(&loader->app_data.app->context);
}
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;
}
loader_unlock();
LoaderEvent event = {
.type = LoaderEventTypeApplicationStarted
};
furi_pubsub_publish(loader->pubsub, &event);
}
static LoaderStatus loader_do_start_by_id( static LoaderStatus loader_do_start_by_id(
const char* id, const char* id,
FuriString* _Nullable error_message Bundle* _Nullable bundle
) { ) {
FURI_LOG_I(TAG, "Start by id %s", id); FURI_LOG_I(TAG, "Start by id %s", id);
const AppManifest* manifest = app_manifest_registry_find_by_id(id); const AppManifest* manifest = app_manifest_registry_find_by_id(id);
if (manifest == NULL) { if (manifest == NULL) {
return loader_make_status_error( return LoaderStatusErrorUnknownApp;
LoaderStatusErrorUnknownApp, } else {
error_message, return loader_do_start_app_with_manifest(manifest, bundle);
"Application \"%s\" not found",
id
);
} }
loader_start_app_with_manifest(manifest);
return loader_make_success_status(error_message);
} }
static void loader_do_stop_app() { static void loader_do_stop_app() {
loader_lock(); loader_lock();
App* app = loader->app_data.app; int8_t current_app_index = loader_singleton->app_stack_index;
if (app == NULL) {
FURI_LOG_W(TAG, "Stop app: no app running"); if (current_app_index == -1) {
loader_unlock();
FURI_LOG_E(TAG, "Stop app: no app running");
return; return;
} }
FURI_LOG_I(TAG, "Stopping %s", app->manifest->id); if (current_app_index == 0) {
loader_unlock();
ViewPort* view_port = loader->app_data.view_port; FURI_LOG_E(TAG, "Stop app: can't stop root app");
if (view_port) { return;
gui_remove_view_port(view_port);
view_port_free(view_port);
loader->app_data.view_port = NULL;
} }
if (app->manifest->on_stop) { // Stop current app
app->manifest->on_stop(&app->context); 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);
furi_app_free(loader->app_data.app); app_free(app_to_stop);
loader->app_data.app = NULL; loader_singleton->app_stack[current_app_index] = NULL;
loader_singleton->app_stack_index--;
FURI_LOG_I(TAG, "Free heap: %zu", heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
// 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);
loader_unlock(); loader_unlock();
FURI_LOG_I( LoaderEvent event = {.type = LoaderEventTypeApplicationStopped};
TAG, furi_pubsub_publish(loader_singleton->pubsub, &event);
"Application stopped. Free heap: %zu",
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)
);
LoaderEvent event = {
.type = LoaderEventTypeApplicationStopped
};
furi_pubsub_publish(loader->pubsub, &event);
} }
bool loader_is_app_running() {
loader_lock();
bool is_running = loader->app_data.app != NULL;
loader_unlock();
return is_running;
}
static int32_t loader_main(void* p) { static int32_t loader_main(void* p) {
UNUSED(p); UNUSED(p);
@ -250,17 +258,15 @@ static int32_t loader_main(void* p) {
LoaderMessage message; LoaderMessage message;
bool exit_requested = false; bool exit_requested = false;
while (!exit_requested) { while (!exit_requested) {
furi_check(loader != NULL); furi_check(loader_singleton != NULL);
if (furi_message_queue_get(loader->queue, &message, FuriWaitForever) == FuriStatusOk) { if (furi_message_queue_get(loader_singleton->queue, &message, FuriWaitForever) == FuriStatusOk) {
FURI_LOG_I(TAG, "Processing message of type %d", message.type); FURI_LOG_I(TAG, "Processing message of type %d", message.type);
switch (message.type) { switch (message.type) {
case LoaderMessageTypeAppStart: case LoaderMessageTypeAppStart:
if (loader_is_app_running()) { // TODO: add bundle
loader_do_stop_app();
}
message.status_value->value = loader_do_start_by_id( message.status_value->value = loader_do_start_by_id(
message.start.id, message.start.id,
message.start.error_message message.start.bundle
); );
if (message.api_lock) { if (message.api_lock) {
api_lock_unlock(message.api_lock); api_lock_unlock(message.api_lock);
@ -281,29 +287,29 @@ static int32_t loader_main(void* p) {
// region AppManifest // region AppManifest
static void loader_start(Context* context) { static void loader_start(Service service) {
UNUSED(context); UNUSED(service);
furi_check(loader == NULL); furi_check(loader_singleton == NULL);
loader = loader_alloc(); loader_singleton = loader_alloc();
furi_thread_set_priority(loader->thread, FuriThreadPriorityNormal); furi_thread_set_priority(loader_singleton->thread, FuriThreadPriorityNormal);
furi_thread_start(loader->thread); furi_thread_start(loader_singleton->thread);
} }
static void loader_stop(Context* context) { static void loader_stop(Service service) {
UNUSED(context); UNUSED(service);
furi_check(loader != NULL); furi_check(loader_singleton != NULL);
// Send stop signal to thread and wait for thread to finish
LoaderMessage message = { LoaderMessage message = {
.api_lock = NULL, .api_lock = NULL,
.type = LoaderMessageTypeServiceStop .type = LoaderMessageTypeServiceStop
}; };
furi_message_queue_put(loader_singleton->queue, &message, FuriWaitForever);
// Send stop signal to thread and wait for thread to finish furi_thread_join(loader_singleton->thread);
furi_message_queue_put(loader->queue, &message, FuriWaitForever);
furi_thread_join(loader->thread);
loader_free(); loader_free();
loader = NULL; loader_singleton = NULL;
} }
const ServiceManifest loader_service = { const ServiceManifest loader_service = {

View File

@ -1,5 +1,7 @@
#pragma once #pragma once
#include "app_manifest.h" #include "app_manifest.h"
#include "bundle.h"
#include "furi_core.h" #include "furi_core.h"
#include "furi_string.h" #include "furi_string.h"
#include "pubsub.h" #include "pubsub.h"
@ -30,36 +32,15 @@ typedef struct {
/** /**
* @brief Close any running app, then start new one. Blocking. * @brief Close any running app, then start new one. Blocking.
* @param[in] id application name or id * @param[in] id application name or id
* @param[in] args application arguments * @param[in] blocking application arguments
* @param[out] error_message detailed error message, can be NULL * @param[in] bundle optional bundle. Ownership is transferred to Loader.
* @return LoaderStatus * @return LoaderStatus
*/ */
LoaderStatus loader_start_app(const char* id, FuriString* error_message); LoaderStatus loader_start_app(const char* id, bool blocking, Bundle* _Nullable bundle);
/**
* @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);
void loader_stop_app(); void loader_stop_app();
bool loader_is_app_running(); App _Nullable loader_get_current_app();
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();
/** /**
* @brief Get loader pubsub * @brief Get loader pubsub

View File

@ -10,17 +10,16 @@
#include "services/gui/view_port.h" #include "services/gui/view_port.h"
#include "thread.h" #include "thread.h"
typedef struct { #define APP_STACK_SIZE 32
App* app;
ViewPort* view_port;
} LoaderAppData;
struct Loader { struct Loader {
FuriThread* thread; FuriThread* thread;
FuriPubSub* pubsub; FuriPubSub* pubsub;
FuriMessageQueue* queue; FuriMessageQueue* queue;
LoaderAppData app_data; // TODO: replace with FuriMutex
SemaphoreHandle_t mutex; SemaphoreHandle_t mutex;
int8_t app_stack_index;
App app_stack[APP_STACK_SIZE];
}; };
typedef enum { typedef enum {
@ -31,7 +30,7 @@ typedef enum {
typedef struct { typedef struct {
const char* id; const char* id;
FuriString* error_message; Bundle* _Nullable bundle;
} LoaderMessageAppStart; } LoaderMessageAppStart;
typedef struct { typedef struct {

View File

@ -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 <sys/cdefs.h>
#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
};

View File

@ -0,0 +1,98 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "esp_wifi.h"
#include "pubsub.h"
#include <stdbool.h>
#include <stdio.h>
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

View File

@ -1,25 +1,37 @@
#include "tactility.h" #include "tactility.h"
#include <sys/cdefs.h>
#include "app_manifest_registry.h" #include "app_manifest_registry.h"
#include "devices_i.h" #include "devices_i.h"
#include "furi.h" #include "furi.h"
#include "graphics_i.h" #include "graphics_i.h"
#include "partitions.h" #include "partitions.h"
#include "service_registry.h" #include "service_registry.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
#include "services/loader/loader.h"
#define TAG "tactility" #define TAG "tactility"
// System services // System services
extern const ServiceManifest gui_service; extern const ServiceManifest gui_service;
extern const ServiceManifest loader_service; extern const ServiceManifest loader_service;
extern const ServiceManifest desktop_service; extern const ServiceManifest wifi_service;
// System apps // System apps
extern const AppManifest desktop_app;
extern const AppManifest system_info_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() { static void register_system_apps() {
FURI_LOG_I(TAG, "Registering default 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(&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) { 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"); FURI_LOG_I(TAG, "Registering system services");
service_registry_add(&gui_service); service_registry_add(&gui_service);
service_registry_add(&loader_service); service_registry_add(&loader_service);
service_registry_add(&desktop_service); service_registry_add(&wifi_service);
} }
static void start_system_services() { static void start_system_services() {
FURI_LOG_I(TAG, "Starting system services"); FURI_LOG_I(TAG, "Starting system services");
service_registry_start(gui_service.id); service_registry_start(gui_service.id);
service_registry_start(loader_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) { static void register_and_start_user_services(const Config* _Nonnull config) {
FURI_LOG_I(TAG, "Starting user services"); FURI_LOG_I(TAG, "Registering and starting user services");
for (size_t i = 0; i < CONFIG_SERVICES_LIMIT; i++) { for (size_t i = 0; i < CONFIG_SERVICES_LIMIT; i++) {
const ServiceManifest* manifest = config->services[i]; const ServiceManifest* manifest = config->services[i];
if (manifest != NULL) { if (manifest != NULL) {
// TODO: keep track of running services service_registry_add(manifest);
manifest->on_start(NULL); service_registry_start(manifest->id);
} else { } else {
// reached end of list // reached end of list
break; break;
@ -66,6 +78,14 @@ static void start_user_services(const Config* _Nonnull config) {
__attribute__((unused)) extern void tactility_start(const Config* _Nonnull config) { __attribute__((unused)) extern void tactility_start(const Config* _Nonnull config) {
furi_init(); 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(); tt_partitions_init();
Hardware hardware = tt_hardware_init(config->hardware); 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 all apps
register_system_services(); register_system_services();
register_system_apps(); register_system_apps();
// TODO: move this after start_system_services, but desktop must subscribe to app registry events first.
register_user_apps(config); register_user_apps(config);
// Start all services // Start all services
start_system_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);
} }

View File

@ -32,6 +32,7 @@ typedef struct {
// List of user applications // List of user applications
const AppManifest* const apps[CONFIG_APPS_LIMIT]; const AppManifest* const apps[CONFIG_APPS_LIMIT];
const ServiceManifest* const services[CONFIG_SERVICES_LIMIT]; const ServiceManifest* const services[CONFIG_SERVICES_LIMIT];
const char* auto_start_app_id;
} Config; } Config;
__attribute__((unused)) extern void tactility_start(const Config _Nonnull* config); __attribute__((unused)) extern void tactility_start(const Config _Nonnull* config);

View File

@ -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;
}

View File

@ -6,7 +6,7 @@
extern "C" { extern "C" {
#endif #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 #ifdef __cplusplus
} }

View File

@ -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_bg_color(obj, lv_color_black(), 0);
lv_obj_set_style_border_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_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_all(obj, LV_STATE_DEFAULT, 0);
lv_obj_set_style_pad_gap(obj, LV_STATE_DEFAULT, 0); lv_obj_set_style_pad_gap(obj, LV_STATE_DEFAULT, 0);
} }

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -2,8 +2,8 @@
#include "services/gui/gui.h" #include "services/gui/gui.h"
#include "services/loader/loader.h" #include "services/loader/loader.h"
static void app_show(Context* context, lv_obj_t* parent) { static void app_show(App app, lv_obj_t* parent) {
UNUSED(context); UNUSED(app);
lv_obj_t* label = lv_label_create(parent); lv_obj_t* label = lv_label_create(parent);
lv_label_set_recolor(label, true); lv_label_set_recolor(label, true);

View File

@ -15,6 +15,7 @@ __attribute__((unused)) void app_main(void) {
&hello_world_app &hello_world_app
}, },
.services = { }, .services = { },
.auto_start_app_id = NULL
}; };
tactility_start(&config); tactility_start(&config);

View File

@ -2,6 +2,6 @@
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, 0x9000, 0x6000, nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000, phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M, factory, app, factory, 0x10000, 3M,
assets, data, spiffs, , 128k, assets, data, spiffs, , 128k,
config, data, spiffs, , 64k, config, data, spiffs, , 64k,

1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 factory, app, factory, 0x10000, 1M, factory, app, factory, 0x10000, 3M,
6 assets, data, spiffs, , 128k,
7 config, data, spiffs, , 64k,

View File

@ -13,16 +13,19 @@ CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
# Hardware defaults # Hardware: Main
CONFIG_TT_BOARD_LILYGO_TDECK=y CONFIG_TT_BOARD_LILYGO_TDECK=y
CONFIG_IDF_TARGET="esp32s3" CONFIG_IDF_TARGET="esp32s3"
CONFIG_LV_COLOR_16_SWAP=y
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_FLASHMODE_QIO=y CONFIG_FLASHMODE_QIO=y
# SPI RAM # Hardware: SPI RAM
CONFIG_ESP32S3_SPIRAM_SUPPORT=y CONFIG_ESP32S3_SPIRAM_SUPPORT=y
CONFIG_SPIRAM_MODE_OCT=y CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=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

View File

@ -13,11 +13,15 @@ CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
# Hardware defaults # Hardware: Main
CONFIG_TT_BOARD_YELLOW_BOARD_24_CAP=y CONFIG_TT_BOARD_YELLOW_BOARD_24_CAP=y
CONFIG_IDF_TARGET="esp32" CONFIG_IDF_TARGET="esp32"
CONFIG_LV_COLOR_16_SWAP=y
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_FLASHMODE_QIO=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

View File

@ -12,8 +12,9 @@ CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2
CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
# Work-around for Furi issue
CONFIG_FREERTOS_UNICORE=y
# Hardware defaults # Hardware defaults
CONFIG_TT_BOARD_CUSTOM=y CONFIG_TT_BOARD_CUSTOM=y
# LVGL
CONFIG_LV_DISP_DEF_REFR_PERIOD=17
CONFIG_LV_INDEV_DEF_READ_PERIOD=17