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
90 changed files with 2670 additions and 929 deletions
+120 -14
View File
@@ -1,22 +1,128 @@
#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) {
App app = {
static AppFlags app_get_flags_default(AppType type);
// region Alloc/free
App app_alloc(const AppManifest* manifest, Bundle* _Nullable parameters) {
AppData* data = malloc(sizeof(AppData));
*data = (AppData) {
.mutex = furi_mutex_alloc(FuriMutexTypeRecursive),
.state = APP_STATE_INITIAL,
.flags = app_get_flags_default(manifest->type),
.manifest = manifest,
.context = {
.data = NULL
}
.parameters = parameters,
.data = NULL
};
App* app_ptr = malloc(sizeof(App));
return memcpy(app_ptr, &app, sizeof(App));
return (App*)data;
}
void furi_app_free(App* app) {
furi_assert(app);
free(app);
void app_free(App app) {
AppData* data = (AppData*)app;
if (data->parameters) {
bundle_free(data->parameters);
}
furi_mutex_free(data->mutex);
free(data);
}
// endregion
// region Internal
static void app_lock(AppData* data) {
furi_mutex_acquire(data->mutex, FuriMutexTypeRecursive);
}
static void app_unlock(AppData* data) {
furi_mutex_release(data->mutex);
}
static AppFlags app_get_flags_default(AppType type) {
static const AppFlags DEFAULT_DESKTOP_FLAGS = {
.show_toolbar = false,
.show_statusbar = true
};
static const AppFlags DEFAULT_APP_FLAGS = {
.show_toolbar = true,
.show_statusbar = true
};
return type == AppTypeDesktop
? DEFAULT_DESKTOP_FLAGS
: DEFAULT_APP_FLAGS;
}
// endregion Internal
// region Public getters & setters
void app_set_state(App app, AppState state) {
AppData* data = (AppData*)app;
app_lock(data);
data->state = state;
app_unlock(data);
}
AppState app_get_state(App app) {
AppData* data = (AppData*)app;
app_lock(data);
AppState state = data->state;
app_unlock(data);
return state;
}
const AppManifest* app_get_manifest(App app) {
AppData* data = (AppData*)app;
// No need to lock const data;
return data->manifest;
}
AppFlags app_get_flags(App app) {
AppData* data = (AppData*)app;
app_lock(data);
AppFlags flags = data->flags;
app_unlock(data);
return flags;
}
void app_set_flags(App app, AppFlags flags) {
AppData* data = (AppData*)app;
app_lock(data);
data->flags = flags;
app_unlock(data);
}
void* app_get_data(App app) {
AppData* data = (AppData*)app;
app_lock(data);
void* value = data->data;
app_unlock(data);
return value;
}
void app_set_data(App app, void* value) {
AppData* data = (AppData*)app;
app_lock(data);
data->data = value;
app_unlock(data);
}
/** TODO: Make this thread-safe.
* In practice, the bundle is writeable, so someone could be writing to it
* while it is being accessed from another thread.
* Consider creating MutableBundle vs Bundle.
* Consider not exposing bundle, but expose `app_get_bundle_int(key)` methods with locking in it.
*/
Bundle* _Nullable app_get_parameters(App app) {
AppData* data = (AppData*)app;
app_lock(data);
Bundle* bundle = data->parameters;
app_unlock(data);
return bundle;
}
// endregion Public getters & setters
+51
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
+20 -6
View File
@@ -1,19 +1,33 @@
#pragma once
#include "app.h"
#include "app_manifest.h"
#include "thread.h"
#include "context.h"
#include "mutex.h"
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
FuriMutex* mutex;
const AppManifest* manifest;
Context context;
} App;
App* furi_app_alloc(const AppManifest* _Nonnull manifest);
void furi_app_free(App* _Nonnull app);
AppState state;
AppFlags flags;
/** @brief Optional parameters to start the app with
* When these are stored in the app struct, the struct takes ownership.
* Do not mutate after app creation.
*/
Bundle* _Nullable parameters;
/** @brief @brief Contextual data related to the running app's instance
* The app can attach its data to this.
* The lifecycle is determined by the on_start and on_stop methods in the AppManifest.
* These manifest methods can optionally allocate/free data that is attached here.
*/
void* _Nullable data;
} AppData;
#ifdef __cplusplus
}
+11 -4
View File
@@ -1,6 +1,5 @@
#pragma once
#include "context.h"
#include <stdio.h>
#ifdef __cplusplus
@@ -9,16 +8,19 @@ extern "C" {
// Forward declarations
typedef struct _lv_obj_t lv_obj_t;
typedef void* App;
typedef enum {
AppTypeDesktop,
AppTypeSystem,
AppTypeSettings,
AppTypeUser
} AppType;
typedef void (*AppOnStart)(Context* context);
typedef void (*AppOnStop)(Context* context);
typedef void (*AppOnShow)(Context* context, lv_obj_t* parent);
typedef void (*AppOnStart)(App app);
typedef void (*AppOnStop)(App app);
typedef void (*AppOnShow)(App app, lv_obj_t* parent);
typedef void (*AppOnHide)(App app);
typedef struct {
/**
@@ -55,6 +57,11 @@ typedef struct {
* Non-blocking method to create the GUI
*/
const AppOnShow _Nullable on_show;
/**
* Non-blocking method, called before gui is destroyed
*/
const AppOnHide _Nullable on_hide;
} AppManifest;
#ifdef __cplusplus
@@ -1,4 +1,5 @@
#include "app_manifest_registry.h"
#include "furi_core.h"
#include "m-dict.h"
#include "m_cstr_dup.h"
+205
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
+34
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
+1 -7
View File
@@ -1,11 +1,5 @@
#pragma once
typedef struct {
/** Contextual data related to the running app's instance
*
* The app can attach its data to this.
* The lifecycle is determined by the on_start and on_stop methods in the AppManifest.
* These manifest methods can optionally allocate/free data that is attached here.
*/
void* data;
} Context;
-6
View File
@@ -11,12 +11,6 @@ void furi_init() {
FURI_LOG_I(TAG, "init start");
furi_assert(!furi_kernel_is_irq());
if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
vTaskSuspendAll();
}
xTaskResumeAll();
#if defined(__ARM_ARCH_7A__) && (__ARM_ARCH_7A__ == 0U)
/* Service Call interrupt might be configured before kernel start */
/* and when its priority is lower or equal to BASEPRI, svc instruction */
+4 -4
View File
@@ -59,7 +59,7 @@ furi_message_queue_put(FuriMessageQueue* instance, const void* msg_ptr, uint32_t
return (stat);
}
FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uint32_t timeout) {
FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uint32_t timeout_ticks) {
QueueHandle_t hQueue = (QueueHandle_t)instance;
FuriStatus stat;
BaseType_t yield;
@@ -67,7 +67,7 @@ FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uin
stat = FuriStatusOk;
if (furi_kernel_is_irq() != 0U) {
if ((hQueue == NULL) || (msg_ptr == NULL) || (timeout != 0U)) {
if ((hQueue == NULL) || (msg_ptr == NULL) || (timeout_ticks != 0U)) {
stat = FuriStatusErrorParameter;
} else {
yield = pdFALSE;
@@ -82,8 +82,8 @@ FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uin
if ((hQueue == NULL) || (msg_ptr == NULL)) {
stat = FuriStatusErrorParameter;
} else {
if (xQueueReceive(hQueue, msg_ptr, (TickType_t)timeout) != pdPASS) {
if (timeout != 0U) {
if (xQueueReceive(hQueue, msg_ptr, (TickType_t)timeout_ticks) != pdPASS) {
if (timeout_ticks != 0U) {
stat = FuriStatusErrorTimeout;
} else {
stat = FuriStatusErrorResource;
+2 -2
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 msg_ptr The message pointer
* @param msg_prio The message prioority
* @param[in] timeout The timeout
* @param[in] timeout_ticks The timeout
*
* @return The furi status.
*/
FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uint32_t timeout);
FuriStatus furi_message_queue_get(FuriMessageQueue* instance, void* msg_ptr, uint32_t timeout_ticks);
/** Get queue capacity
*
+1
View File
@@ -4,6 +4,7 @@
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "log.h"
FuriMutex* furi_mutex_alloc(FuriMutexType type) {
furi_assert(!FURI_IS_IRQ_MODE());
+53 -11
View File
@@ -2,20 +2,62 @@
#include "furi_core.h"
#include "log.h"
#define TAG "service"
// region Alloc/free
Service* furi_service_alloc(const ServiceManifest* _Nonnull manifest) {
Service app = {
Service service_alloc(const ServiceManifest* _Nonnull manifest) {
ServiceData* data = malloc(sizeof(ServiceData));
*data = (ServiceData) {
.manifest = manifest,
.context = {
.data = NULL
}
.mutex = furi_mutex_alloc(FuriMutexTypeRecursive),
.data = NULL
};
Service* app_ptr = malloc(sizeof(Service));
return memcpy(app_ptr, &app, sizeof(Service));
return data;
}
void furi_service_free(Service* app) {
furi_assert(app);
free(app);
void service_free(Service service) {
ServiceData* data = (ServiceData*)service;
furi_assert(service);
furi_mutex_free(data->mutex);
free(data);
}
// endregion Alloc/free
// region Internal
static void service_lock(ServiceData * data) {
furi_mutex_acquire(data->mutex, FuriMutexTypeRecursive);
}
static void service_unlock(ServiceData* data) {
furi_mutex_release(data->mutex);
}
// endregion Internal
// region Getters & Setters
const ServiceManifest* service_get_manifest(Service service) {
ServiceData* data = (ServiceData*)service;
service_lock(data);
const ServiceManifest* manifest = data->manifest;
service_unlock(data);
return manifest;
}
void service_set_data(Service service, void* value) {
ServiceData* data = (ServiceData*)service;
service_lock(data);
data->data = value;
service_unlock(data);
}
void* _Nullable service_get_data(Service service) {
ServiceData* data = (ServiceData*)service;
service_lock(data);
void* value = data->data;
service_unlock(data);
return value;
}
// endregion Getters & Setters
+18
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
+9 -5
View File
@@ -1,12 +1,16 @@
#pragma once
#include "service_manifest.h"
#include "service.h"
#include "context.h"
#include "mutex.h"
#include "service_manifest.h"
typedef struct {
FuriMutex* mutex;
const ServiceManifest* manifest;
Context context;
} Service;
void* data;
} ServiceData;
Service* furi_service_alloc(const ServiceManifest* _Nonnull manifest);
void furi_service_free(Service* _Nonnull service);
Service service_alloc(const ServiceManifest* _Nonnull manifest);
void service_free(Service _Nonnull service);
+4 -2
View File
@@ -7,8 +7,10 @@
extern "C" {
#endif
typedef void (*ServiceOnStart)(Context* context);
typedef void (*ServiceOnStop)(Context* context);
typedef void* Service;
typedef void (*ServiceOnStart)(Service service);
typedef void (*ServiceOnStop)(Service service);
typedef struct {
/**
+19 -19
View File
@@ -9,17 +9,17 @@
#define TAG "service_registry"
DICT_DEF2(ServiceManifestDict, const char*, M_CSTR_DUP_OPLIST, const ServiceManifest*, M_PTR_OPLIST)
DICT_DEF2(ServiceInstanceDict, const char*, M_CSTR_DUP_OPLIST, const Service*, M_PTR_OPLIST)
DICT_DEF2(ServiceInstanceDict, const char*, M_CSTR_DUP_OPLIST, const ServiceData*, M_PTR_OPLIST)
#define APP_REGISTRY_FOR_EACH(manifest_var_name, code_to_execute) \
{ \
service_registry_manifest_lock(); \
ServiceManifestDict_it_t it; \
#define APP_REGISTRY_FOR_EACH(manifest_var_name, code_to_execute) \
{ \
service_registry_manifest_lock(); \
ServiceManifestDict_it_t it; \
for (ServiceManifestDict_it(it, service_manifest_dict); !ServiceManifestDict_end_p(it); ServiceManifestDict_next(it)) { \
const ServiceManifest*(manifest_var_name) = ServiceManifestDict_cref(it)->value; \
code_to_execute; \
} \
service_registry_manifest_unlock(); \
const ServiceManifest*(manifest_var_name) = ServiceManifestDict_cref(it)->value; \
code_to_execute; \
} \
service_registry_manifest_unlock(); \
}
ServiceManifestDict_t service_manifest_dict;
@@ -78,13 +78,13 @@ const ServiceManifest* _Nullable service_registry_find_manifest_by_id(const char
return (manifest != NULL) ? *manifest : NULL;
}
Service* _Nullable service_registry_find_instance_by_id(const char* id) {
ServiceData* _Nullable service_registry_find_instance_by_id(const char* id) {
service_registry_instance_lock();
const Service** _Nullable service_ptr = ServiceInstanceDict_get(service_instance_dict, id);
const ServiceData** _Nullable service_ptr = ServiceInstanceDict_get(service_instance_dict, id);
if (service_ptr == NULL) {
return NULL;
}
Service* service = (Service*) *service_ptr;
ServiceData* service = (ServiceData*)*service_ptr;
service_registry_instance_unlock();
return service;
}
@@ -100,12 +100,12 @@ bool service_registry_start(const char* service_id) {
FURI_LOG_I(TAG, "starting %s", service_id);
const ServiceManifest* manifest = service_registry_find_manifest_by_id(service_id);
if (manifest == NULL) {
FURI_LOG_I(TAG, "manifest not found for %s", service_id);
FURI_LOG_E(TAG, "manifest not found for service %s", service_id);
return false;
}
Service* service = furi_service_alloc(manifest);
service->manifest->on_start(&service->context);
Service service = service_alloc(manifest);
manifest->on_start(service);
service_registry_instance_lock();
ServiceInstanceDict_set_at(service_instance_dict, manifest->id, service);
@@ -117,14 +117,14 @@ bool service_registry_start(const char* service_id) {
bool service_registry_stop(const char* service_id) {
FURI_LOG_I(TAG, "stopping %s", service_id);
Service* service = service_registry_find_instance_by_id(service_id);
ServiceData* service = service_registry_find_instance_by_id(service_id);
if (service == NULL) {
FURI_LOG_I(TAG, "service not running: %s", service_id);
FURI_LOG_W(TAG, "service not running: %s", service_id);
return false;
}
service->manifest->on_stop(&service->context);
furi_service_free(service);
service->manifest->on_stop(service);
service_free(service);
service_registry_instance_lock();
ServiceInstanceDict_erase(service_instance_dict, service_id);